mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): revise image viewer
- Viewer only exists on Generation tab - Viewer defaults to open - When clicking the Control Layers tab on the left panel, close the viewer (i.e. open the CL editor) - Do not switch to editor when adding layers (this is handled by clicking the Control Layers tab) - Do not open viewer when single-clicking images in gallery - _Do_ open viewer when _double_-clicking images in gallery - Do not change viewer state when switching between app tabs (this no longer makes sense; the viewer only exists on generation tab) - Change the button to a drop down menu that states what you are currently doing, e.g. Viewing vs Editing
This commit is contained in:
committed by
Kent Keirsey
parent
e8023c44b0
commit
e8e764be20
@ -142,9 +142,11 @@
|
|||||||
"blue": "Blue",
|
"blue": "Blue",
|
||||||
"alpha": "Alpha",
|
"alpha": "Alpha",
|
||||||
"selected": "Selected",
|
"selected": "Selected",
|
||||||
"viewer": "Viewer",
|
|
||||||
"tab": "Tab",
|
"tab": "Tab",
|
||||||
"close": "Close"
|
"viewing": "Viewing",
|
||||||
|
"viewingDesc": "Review images in a large gallery view",
|
||||||
|
"editing": "Editing",
|
||||||
|
"editingDesc": "Edit on the Control Layers canvas"
|
||||||
},
|
},
|
||||||
"controlnet": {
|
"controlnet": {
|
||||||
"controlAdapter_one": "Control Adapter",
|
"controlAdapter_one": "Control Adapter",
|
||||||
@ -365,10 +367,7 @@
|
|||||||
"bulkDownloadRequestFailed": "Problem Preparing Download",
|
"bulkDownloadRequestFailed": "Problem Preparing Download",
|
||||||
"bulkDownloadFailed": "Download Failed",
|
"bulkDownloadFailed": "Download Failed",
|
||||||
"problemDeletingImages": "Problem Deleting Images",
|
"problemDeletingImages": "Problem Deleting Images",
|
||||||
"problemDeletingImagesDesc": "One or more images could not be deleted",
|
"problemDeletingImagesDesc": "One or more images could not be deleted"
|
||||||
"switchTo": "Switch to {{ tab }} (Z)",
|
|
||||||
"openFloatingViewer": "Open Floating Viewer",
|
|
||||||
"closeFloatingViewer": "Close Floating Viewer"
|
|
||||||
},
|
},
|
||||||
"hotkeys": {
|
"hotkeys": {
|
||||||
"searchHotkeys": "Search Hotkeys",
|
"searchHotkeys": "Search Hotkeys",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { createAction } from '@reduxjs/toolkit';
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
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 { imagesApi } from 'services/api/endpoints/images';
|
||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
import { imagesSelectors } from 'services/api/util';
|
import { imagesSelectors } from 'services/api/util';
|
||||||
@ -62,7 +62,6 @@ export const addGalleryImageClickedListener = (startAppListening: AppStartListen
|
|||||||
} else {
|
} else {
|
||||||
dispatch(selectionChanged([imageDTO]));
|
dispatch(selectionChanged([imageDTO]));
|
||||||
}
|
}
|
||||||
dispatch(isImageViewerOpenChanged(true));
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -70,6 +70,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
onMouseOver,
|
onMouseOver,
|
||||||
onMouseOut,
|
onMouseOut,
|
||||||
dataTestId,
|
dataTestId,
|
||||||
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
@ -138,6 +139,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
minH={minSize ? minSize : undefined}
|
minH={minSize ? minSize : undefined}
|
||||||
userSelect="none"
|
userSelect="none"
|
||||||
cursor={isDragDisabled || !imageDTO ? 'default' : 'pointer'}
|
cursor={isDragDisabled || !imageDTO ? 'default' : 'pointer'}
|
||||||
|
{...rest}
|
||||||
>
|
>
|
||||||
{imageDTO && (
|
{imageDTO && (
|
||||||
<Flex
|
<Flex
|
||||||
|
@ -22,7 +22,6 @@ import {
|
|||||||
} from 'features/canvas/store/canvasSlice';
|
} from 'features/canvas/store/canvasSlice';
|
||||||
import type { CanvasLayer } from 'features/canvas/store/canvasTypes';
|
import type { CanvasLayer } from 'features/canvas/store/canvasTypes';
|
||||||
import { LAYER_NAMES_DICT } from 'features/canvas/store/canvasTypes';
|
import { LAYER_NAMES_DICT } from 'features/canvas/store/canvasTypes';
|
||||||
import { ViewerButton } from 'features/gallery/components/ImageViewer/ViewerButton';
|
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -221,106 +220,96 @@ const IAICanvasToolbar = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex w="full" gap={2} alignItems="center">
|
<Flex w="full" gap={2} alignItems="center">
|
||||||
<Flex flex={1} justifyContent="center">
|
<Tooltip label={`${t('unifiedCanvas.layer')} (Q)`}>
|
||||||
<Flex gap={2} marginInlineEnd="auto" />
|
<FormControl isDisabled={isStaging} w="5rem">
|
||||||
</Flex>
|
<Combobox value={value} options={LAYER_NAMES_DICT} onChange={handleChangeLayer} />
|
||||||
<Flex flex={1} gap={2} justifyContent="center">
|
</FormControl>
|
||||||
<Tooltip label={`${t('unifiedCanvas.layer')} (Q)`}>
|
</Tooltip>
|
||||||
<FormControl isDisabled={isStaging} w="5rem">
|
|
||||||
<Combobox value={value} options={LAYER_NAMES_DICT} onChange={handleChangeLayer} />
|
|
||||||
</FormControl>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<IAICanvasMaskOptions />
|
<IAICanvasMaskOptions />
|
||||||
<IAICanvasToolChooserOptions />
|
<IAICanvasToolChooserOptions />
|
||||||
|
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={`${t('unifiedCanvas.move')} (V)`}
|
aria-label={`${t('unifiedCanvas.move')} (V)`}
|
||||||
tooltip={`${t('unifiedCanvas.move')} (V)`}
|
tooltip={`${t('unifiedCanvas.move')} (V)`}
|
||||||
icon={<PiHandGrabbingBold />}
|
icon={<PiHandGrabbingBold />}
|
||||||
isChecked={tool === 'move' || isStaging}
|
isChecked={tool === 'move' || isStaging}
|
||||||
onClick={handleSelectMoveTool}
|
onClick={handleSelectMoveTool}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={`${shouldShowBoundingBox ? t('unifiedCanvas.hideBoundingBox') : t('unifiedCanvas.showBoundingBox')} (Shift + H)`}
|
aria-label={`${shouldShowBoundingBox ? t('unifiedCanvas.hideBoundingBox') : t('unifiedCanvas.showBoundingBox')} (Shift + H)`}
|
||||||
tooltip={`${shouldShowBoundingBox ? t('unifiedCanvas.hideBoundingBox') : t('unifiedCanvas.showBoundingBox')} (Shift + H)`}
|
tooltip={`${shouldShowBoundingBox ? t('unifiedCanvas.hideBoundingBox') : t('unifiedCanvas.showBoundingBox')} (Shift + H)`}
|
||||||
icon={shouldShowBoundingBox ? <PiEyeBold /> : <PiEyeSlashBold />}
|
icon={shouldShowBoundingBox ? <PiEyeBold /> : <PiEyeSlashBold />}
|
||||||
onClick={handleSetShouldShowBoundingBox}
|
onClick={handleSetShouldShowBoundingBox}
|
||||||
isDisabled={isStaging}
|
isDisabled={isStaging}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={`${t('unifiedCanvas.resetView')} (R)`}
|
aria-label={`${t('unifiedCanvas.resetView')} (R)`}
|
||||||
tooltip={`${t('unifiedCanvas.resetView')} (R)`}
|
tooltip={`${t('unifiedCanvas.resetView')} (R)`}
|
||||||
icon={<PiCrosshairSimpleBold />}
|
icon={<PiCrosshairSimpleBold />}
|
||||||
onClick={handleClickResetCanvasView}
|
onClick={handleClickResetCanvasView}
|
||||||
/>
|
/>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
|
<IconButton
|
||||||
|
aria-label={`${t('unifiedCanvas.mergeVisible')} (Shift+M)`}
|
||||||
|
tooltip={`${t('unifiedCanvas.mergeVisible')} (Shift+M)`}
|
||||||
|
icon={<PiStackBold />}
|
||||||
|
onClick={handleMergeVisible}
|
||||||
|
isDisabled={isStaging}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
aria-label={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
|
||||||
|
tooltip={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
|
||||||
|
icon={<PiFloppyDiskBold />}
|
||||||
|
onClick={handleSaveToGallery}
|
||||||
|
isDisabled={isStaging}
|
||||||
|
/>
|
||||||
|
{isClipboardAPIAvailable && (
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={`${t('unifiedCanvas.mergeVisible')} (Shift+M)`}
|
aria-label={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
|
||||||
tooltip={`${t('unifiedCanvas.mergeVisible')} (Shift+M)`}
|
tooltip={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
|
||||||
icon={<PiStackBold />}
|
icon={<PiCopyBold />}
|
||||||
onClick={handleMergeVisible}
|
onClick={handleCopyImageToClipboard}
|
||||||
isDisabled={isStaging}
|
isDisabled={isStaging}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
)}
|
||||||
aria-label={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
|
<IconButton
|
||||||
tooltip={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
|
aria-label={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
|
||||||
icon={<PiFloppyDiskBold />}
|
tooltip={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
|
||||||
onClick={handleSaveToGallery}
|
icon={<PiDownloadSimpleBold />}
|
||||||
isDisabled={isStaging}
|
onClick={handleDownloadAsImage}
|
||||||
/>
|
isDisabled={isStaging}
|
||||||
{isClipboardAPIAvailable && (
|
/>
|
||||||
<IconButton
|
</ButtonGroup>
|
||||||
aria-label={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
|
<ButtonGroup>
|
||||||
tooltip={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
|
<IAICanvasUndoButton />
|
||||||
icon={<PiCopyBold />}
|
<IAICanvasRedoButton />
|
||||||
onClick={handleCopyImageToClipboard}
|
</ButtonGroup>
|
||||||
isDisabled={isStaging}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<IconButton
|
|
||||||
aria-label={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
|
|
||||||
tooltip={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
|
|
||||||
icon={<PiDownloadSimpleBold />}
|
|
||||||
onClick={handleDownloadAsImage}
|
|
||||||
isDisabled={isStaging}
|
|
||||||
/>
|
|
||||||
</ButtonGroup>
|
|
||||||
<ButtonGroup>
|
|
||||||
<IAICanvasUndoButton />
|
|
||||||
<IAICanvasRedoButton />
|
|
||||||
</ButtonGroup>
|
|
||||||
|
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={`${t('common.upload')}`}
|
aria-label={`${t('common.upload')}`}
|
||||||
tooltip={`${t('common.upload')}`}
|
tooltip={`${t('common.upload')}`}
|
||||||
icon={<PiUploadSimpleBold />}
|
icon={<PiUploadSimpleBold />}
|
||||||
isDisabled={isStaging}
|
isDisabled={isStaging}
|
||||||
{...getUploadButtonProps()}
|
{...getUploadButtonProps()}
|
||||||
/>
|
/>
|
||||||
<input {...getUploadInputProps()} />
|
<input {...getUploadInputProps()} />
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={`${t('unifiedCanvas.clearCanvas')}`}
|
aria-label={`${t('unifiedCanvas.clearCanvas')}`}
|
||||||
tooltip={`${t('unifiedCanvas.clearCanvas')}`}
|
tooltip={`${t('unifiedCanvas.clearCanvas')}`}
|
||||||
icon={<PiTrashSimpleBold />}
|
icon={<PiTrashSimpleBold />}
|
||||||
onClick={handleResetCanvas}
|
onClick={handleResetCanvas}
|
||||||
colorScheme="error"
|
colorScheme="error"
|
||||||
isDisabled={isStaging}
|
isDisabled={isStaging}
|
||||||
/>
|
/>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<IAICanvasSettingsButtonPopover />
|
<IAICanvasSettingsButtonPopover />
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</Flex>
|
|
||||||
<Flex flex={1} justifyContent="center">
|
|
||||||
<Flex gap={2} marginInlineStart="auto">
|
|
||||||
<ViewerButton />
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -4,7 +4,7 @@ import { BrushSize } from 'features/controlLayers/components/BrushSize';
|
|||||||
import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover';
|
import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover';
|
||||||
import { ToolChooser } from 'features/controlLayers/components/ToolChooser';
|
import { ToolChooser } from 'features/controlLayers/components/ToolChooser';
|
||||||
import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup';
|
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';
|
import { memo } from 'react';
|
||||||
|
|
||||||
export const ControlLayersToolbar = memo(() => {
|
export const ControlLayersToolbar = memo(() => {
|
||||||
@ -21,7 +21,7 @@ export const ControlLayersToolbar = memo(() => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
<Flex flex={1} justifyContent="center">
|
<Flex flex={1} justifyContent="center">
|
||||||
<Flex gap={2} marginInlineStart="auto">
|
<Flex gap={2} marginInlineStart="auto">
|
||||||
<ViewerButton />
|
<ViewerToggleMenu />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -11,6 +11,7 @@ import type { GallerySelectionDraggableData, ImageDraggableData, TypesafeDraggab
|
|||||||
import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId';
|
import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId';
|
||||||
import { useMultiselect } from 'features/gallery/hooks/useMultiselect';
|
import { useMultiselect } from 'features/gallery/hooks/useMultiselect';
|
||||||
import { useScrollIntoView } from 'features/gallery/hooks/useScrollIntoView';
|
import { useScrollIntoView } from 'features/gallery/hooks/useScrollIntoView';
|
||||||
|
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
||||||
import type { MouseEvent } from 'react';
|
import type { MouseEvent } from 'react';
|
||||||
import { memo, useCallback, useMemo, useState } from 'react';
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -102,6 +103,10 @@ const GalleryImage = (props: HoverableImageProps) => {
|
|||||||
setIsHovered(true);
|
setIsHovered(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onDoubleClick = useCallback(() => {
|
||||||
|
dispatch(isImageViewerOpenChanged(true));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
const handleMouseOut = useCallback(() => {
|
const handleMouseOut = useCallback(() => {
|
||||||
setIsHovered(false);
|
setIsHovered(false);
|
||||||
}, []);
|
}, []);
|
||||||
@ -143,6 +148,7 @@ const GalleryImage = (props: HoverableImageProps) => {
|
|||||||
>
|
>
|
||||||
<IAIDndImage
|
<IAIDndImage
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
onDoubleClick={onDoubleClick}
|
||||||
imageDTO={imageDTO}
|
imageDTO={imageDTO}
|
||||||
draggableData={draggableData}
|
draggableData={draggableData}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
import { Button } from '@invoke-ai/ui-library';
|
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { PiArrowsDownUpBold } from 'react-icons/pi';
|
|
||||||
|
|
||||||
import { useImageViewer } from './useImageViewer';
|
|
||||||
|
|
||||||
const TAB_NAME_TO_TKEY_SHORT: Record<InvokeTabName, string> = {
|
|
||||||
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 (
|
|
||||||
<Button
|
|
||||||
aria-label={tooltip}
|
|
||||||
tooltip={tooltip}
|
|
||||||
onClick={onClose}
|
|
||||||
variant="outline"
|
|
||||||
leftIcon={<PiArrowsDownUpBold />}
|
|
||||||
>
|
|
||||||
{t(TAB_NAME_TO_TKEY_SHORT[activeTabName])}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
@ -10,7 +10,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
|||||||
|
|
||||||
import CurrentImageButtons from './CurrentImageButtons';
|
import CurrentImageButtons from './CurrentImageButtons';
|
||||||
import CurrentImagePreview from './CurrentImagePreview';
|
import CurrentImagePreview from './CurrentImagePreview';
|
||||||
import { EditorButton } from './EditorButton';
|
import { ViewerToggleMenu } from './ViewerToggleMenu';
|
||||||
|
|
||||||
const VIEWER_ENABLED_TABS: InvokeTabName[] = ['canvas', 'generation', 'workflows'];
|
const VIEWER_ENABLED_TABS: InvokeTabName[] = ['canvas', 'generation', 'workflows'];
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ export const ImageViewer = memo(() => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
<Flex flex={1} justifyContent="center">
|
<Flex flex={1} justifyContent="center">
|
||||||
<Flex gap={2} marginInlineStart="auto">
|
<Flex gap={2} marginInlineStart="auto">
|
||||||
<EditorButton />
|
<ViewerToggleMenu />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -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 (
|
||||||
|
<Flex
|
||||||
|
layerStyle="first"
|
||||||
|
borderRadius="base"
|
||||||
|
position="absolute"
|
||||||
|
flexDirection="column"
|
||||||
|
top={0}
|
||||||
|
right={0}
|
||||||
|
bottom={0}
|
||||||
|
left={0}
|
||||||
|
p={2}
|
||||||
|
rowGap={4}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
zIndex={10} // reactflow puts its minimap at 5, so we need to be above that
|
||||||
|
>
|
||||||
|
<Flex w="full" gap={2}>
|
||||||
|
<Flex flex={1} justifyContent="center">
|
||||||
|
<Flex gap={2} marginInlineEnd="auto">
|
||||||
|
<ToggleProgressButton />
|
||||||
|
<ToggleMetadataViewerButton />
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Flex flex={1} gap={2} justifyContent="center">
|
||||||
|
<CurrentImageButtons />
|
||||||
|
</Flex>
|
||||||
|
<Flex flex={1} justifyContent="center">
|
||||||
|
<Flex gap={2} marginInlineStart="auto" />
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<CurrentImagePreview />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ImageViewerWorkflows.displayName = 'ImageViewerWorkflows';
|
@ -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 (
|
|
||||||
<Button
|
|
||||||
aria-label={tooltip}
|
|
||||||
tooltip={tooltip}
|
|
||||||
onClick={onOpen}
|
|
||||||
variant="outline"
|
|
||||||
pointerEvents="auto"
|
|
||||||
leftIcon={<PiArrowsDownUpBold />}
|
|
||||||
>
|
|
||||||
{t('common.viewer')}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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 (
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button variant="outline">
|
||||||
|
<Flex gap={3} w="full" alignItems="center">
|
||||||
|
{isOpen ? <Icon as={PiEyeBold} /> : <Icon as={PiPencilBold} />}
|
||||||
|
<Text fontSize="md">{isOpen ? t('common.viewing') : t('common.editing')}</Text>
|
||||||
|
<Icon as={PiCaretDownBold} />
|
||||||
|
</Flex>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent p={2}>
|
||||||
|
<PopoverArrow />
|
||||||
|
<PopoverBody>
|
||||||
|
<Flex flexDir="column">
|
||||||
|
<Button onClick={onOpen} variant="ghost" h="auto" w="auto" p={2}>
|
||||||
|
<Flex gap={2} w="full">
|
||||||
|
<Icon as={PiCheckBold} visibility={isOpen ? 'visible' : 'hidden'} />
|
||||||
|
<Flex flexDir="column" gap={2} alignItems="flex-start">
|
||||||
|
<Text fontWeight="semibold" color="base.100">
|
||||||
|
{t('common.viewing')}
|
||||||
|
</Text>
|
||||||
|
<Text fontWeight="normal" color="base.300">
|
||||||
|
{t('common.viewingDesc')}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onClose} variant="ghost" h="auto" w="auto" p={2}>
|
||||||
|
<Flex gap={2} w="full">
|
||||||
|
<Icon as={PiCheckBold} visibility={isOpen ? 'hidden' : 'visible'} />
|
||||||
|
<Flex flexDir="column" gap={2} alignItems="flex-start">
|
||||||
|
<Text fontWeight="semibold" color="base.100">
|
||||||
|
{t('common.editing')}
|
||||||
|
</Text>
|
||||||
|
<Text fontWeight="normal" color="base.300">
|
||||||
|
{t('common.editingDesc')}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</PopoverBody>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
@ -1,8 +1,6 @@
|
|||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
|
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
|
||||||
import type { PersistConfig, RootState } from 'app/store/store';
|
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 { uniqBy } from 'lodash-es';
|
||||||
import { boardsApi } from 'services/api/endpoints/boards';
|
import { boardsApi } from 'services/api/endpoints/boards';
|
||||||
import { imagesApi } from 'services/api/endpoints/images';
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
@ -23,7 +21,7 @@ const initialGalleryState: GalleryState = {
|
|||||||
boardSearchText: '',
|
boardSearchText: '',
|
||||||
limit: INITIAL_IMAGE_LIMIT,
|
limit: INITIAL_IMAGE_LIMIT,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
isImageViewerOpen: false,
|
isImageViewerOpen: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const gallerySlice = createSlice({
|
export const gallerySlice = createSlice({
|
||||||
@ -83,12 +81,6 @@ export const gallerySlice = createSlice({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder.addCase(setActiveTab, (state) => {
|
|
||||||
state.isImageViewerOpen = false;
|
|
||||||
});
|
|
||||||
builder.addCase(rgLayerAdded, (state) => {
|
|
||||||
state.isImageViewerOpen = false;
|
|
||||||
});
|
|
||||||
builder.addMatcher(isAnyBoardDeleted, (state, action) => {
|
builder.addMatcher(isAnyBoardDeleted, (state, action) => {
|
||||||
const deletedBoardId = action.meta.arg.originalArgs;
|
const deletedBoardId = action.meta.arg.originalArgs;
|
||||||
if (deletedBoardId === state.selectedBoardId) {
|
if (deletedBoardId === state.selectedBoardId) {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
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 AddNodeButton from 'features/nodes/components/flow/panels/TopPanel/AddNodeButton';
|
||||||
import ClearFlowButton from 'features/nodes/components/flow/panels/TopPanel/ClearFlowButton';
|
import ClearFlowButton from 'features/nodes/components/flow/panels/TopPanel/ClearFlowButton';
|
||||||
import SaveWorkflowButton from 'features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton';
|
import SaveWorkflowButton from 'features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton';
|
||||||
@ -23,7 +22,6 @@ const TopCenterPanel = () => {
|
|||||||
<ClearFlowButton />
|
<ClearFlowButton />
|
||||||
<SaveWorkflowButton />
|
<SaveWorkflowButton />
|
||||||
<WorkflowLibraryMenu />
|
<WorkflowLibraryMenu />
|
||||||
<ViewerButton />
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -4,7 +4,6 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
|||||||
import { $customNavComponent } from 'app/store/nanostores/customNavComponent';
|
import { $customNavComponent } from 'app/store/nanostores/customNavComponent';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent';
|
import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent';
|
||||||
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
|
||||||
import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup';
|
import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup';
|
||||||
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
|
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
|
||||||
import SettingsMenu from 'features/system/components/SettingsModal/SettingsMenu';
|
import SettingsMenu from 'features/system/components/SettingsModal/SettingsMenu';
|
||||||
@ -255,9 +254,8 @@ const InvokeTabs = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Panel id="main-panel" order={1} minSize={20}>
|
<Panel id="main-panel" order={1} minSize={20}>
|
||||||
<TabPanels w="full" h="full" position="relative">
|
<TabPanels w="full" h="full">
|
||||||
{tabPanels}
|
{tabPanels}
|
||||||
<ImageViewer />
|
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
</Panel>
|
</Panel>
|
||||||
{shouldShowGalleryPanel && (
|
{shouldShowGalleryPanel && (
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import type { ChakraProps } from '@invoke-ai/ui-library';
|
import type { ChakraProps } from '@invoke-ai/ui-library';
|
||||||
import { Box, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } 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 { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
||||||
import { ControlLayersPanelContent } from 'features/controlLayers/components/ControlLayersPanelContent';
|
import { ControlLayersPanelContent } from 'features/controlLayers/components/ControlLayersPanelContent';
|
||||||
|
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
||||||
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
|
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
|
||||||
import QueueControls from 'features/queue/components/QueueControls';
|
import QueueControls from 'features/queue/components/QueueControls';
|
||||||
import { SDXLPrompts } from 'features/sdxl/components/SDXLPrompts/SDXLPrompts';
|
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 { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const overlayScrollbarsStyles: CSSProperties = {
|
const overlayScrollbarsStyles: CSSProperties = {
|
||||||
@ -37,6 +38,7 @@ const selectedStyles: ChakraProps['sx'] = {
|
|||||||
|
|
||||||
const ParametersPanelTextToImage = () => {
|
const ParametersPanelTextToImage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
const controlLayersCount = useAppSelector((s) => s.controlLayers.present.layers.length);
|
const controlLayersCount = useAppSelector((s) => s.controlLayers.present.layers.length);
|
||||||
const controlLayersTitle = useMemo(() => {
|
const controlLayersTitle = useMemo(() => {
|
||||||
@ -46,6 +48,14 @@ const ParametersPanelTextToImage = () => {
|
|||||||
return `${t('controlLayers.controlLayers')} (${controlLayersCount})`;
|
return `${t('controlLayers.controlLayers')} (${controlLayersCount})`;
|
||||||
}, [controlLayersCount, t]);
|
}, [controlLayersCount, t]);
|
||||||
const isSDXL = useAppSelector((s) => s.generation.model?.base === 'sdxl');
|
const isSDXL = useAppSelector((s) => s.generation.model?.base === 'sdxl');
|
||||||
|
const onChangeTabs = useCallback(
|
||||||
|
(i: number) => {
|
||||||
|
if (i === 1) {
|
||||||
|
dispatch(isImageViewerOpenChanged(false));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex w="full" h="full" flexDir="column" gap={2}>
|
<Flex w="full" h="full" flexDir="column" gap={2}>
|
||||||
@ -55,7 +65,15 @@ const ParametersPanelTextToImage = () => {
|
|||||||
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
|
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
|
||||||
<Flex gap={2} flexDirection="column" h="full" w="full">
|
<Flex gap={2} flexDirection="column" h="full" w="full">
|
||||||
{isSDXL ? <SDXLPrompts /> : <Prompts />}
|
{isSDXL ? <SDXLPrompts /> : <Prompts />}
|
||||||
<Tabs variant="enclosed" display="flex" flexDir="column" w="full" h="full" gap={2}>
|
<Tabs
|
||||||
|
variant="enclosed"
|
||||||
|
display="flex"
|
||||||
|
flexDir="column"
|
||||||
|
w="full"
|
||||||
|
h="full"
|
||||||
|
gap={2}
|
||||||
|
onChange={onChangeTabs}
|
||||||
|
>
|
||||||
<TabList gap={2} fontSize="sm" borderColor="base.800">
|
<TabList gap={2} fontSize="sm" borderColor="base.800">
|
||||||
<Tab sx={baseStyles} _selected={selectedStyles}>
|
<Tab sx={baseStyles} _selected={selectedStyles}>
|
||||||
{t('common.settingsLabel')}
|
{t('common.settingsLabel')}
|
||||||
|
@ -1,9 +1,20 @@
|
|||||||
import { Box } from '@invoke-ai/ui-library';
|
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 NodeEditor from 'features/nodes/components/NodeEditor';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { ReactFlowProvider } from 'reactflow';
|
import { ReactFlowProvider } from 'reactflow';
|
||||||
|
|
||||||
const NodesTab = () => {
|
const NodesTab = () => {
|
||||||
|
const mode = useAppSelector((s) => s.workflow.mode);
|
||||||
|
if (mode === 'view') {
|
||||||
|
return (
|
||||||
|
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
|
||||||
|
<ImageViewerWorkflows />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
|
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import { Box } from '@invoke-ai/ui-library';
|
import { Box } from '@invoke-ai/ui-library';
|
||||||
import { ControlLayersEditor } from 'features/controlLayers/components/ControlLayersEditor';
|
import { ControlLayersEditor } from 'features/controlLayers/components/ControlLayersEditor';
|
||||||
|
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
const TextToImageTab = () => {
|
const TextToImageTab = () => {
|
||||||
return (
|
return (
|
||||||
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
|
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
|
||||||
<ControlLayersEditor />
|
<ControlLayersEditor />
|
||||||
|
<ImageViewer />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user