mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): add image viewer overlay
- Works on txt2img, canvas and workflows tabs, img2img has its own side-by-side view - In workflow editor, the is closeable only if you are in edit mode, else it's always there - Press `i` to open - Press `esc` to close - Selecting an image or changing image selection opens the viewer - When generating, if auto-switch to new image is enabled, the viewer opens when an image comes in To support this change, I organized and restructured some tab stuff.
This commit is contained in:
parent
209ddc2037
commit
c9886796f6
@ -361,7 +361,8 @@
|
|||||||
"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",
|
||||||
|
"backToEditor": "Back to {{tab}} (Esc)"
|
||||||
},
|
},
|
||||||
"hotkeys": {
|
"hotkeys": {
|
||||||
"searchHotkeys": "Search Hotkeys",
|
"searchHotkeys": "Search Hotkeys",
|
||||||
@ -584,6 +585,14 @@
|
|||||||
"upscale": {
|
"upscale": {
|
||||||
"desc": "Upscale the current image",
|
"desc": "Upscale the current image",
|
||||||
"title": "Upscale"
|
"title": "Upscale"
|
||||||
|
},
|
||||||
|
"backToEditor": {
|
||||||
|
"desc": "Closes the Image Viewer and shows the Editor View (Text to Image tab only)",
|
||||||
|
"title": "Back to Editor"
|
||||||
|
},
|
||||||
|
"openImageViewer": {
|
||||||
|
"desc": "Opens the Image Viewer (Text to Image tab only)",
|
||||||
|
"title": "Open Image Viewer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
|
@ -0,0 +1,80 @@
|
|||||||
|
import { Button, Flex } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import CurrentImageButtons from 'features/gallery/components/CurrentImage/CurrentImageButtons';
|
||||||
|
import CurrentImagePreview from 'features/gallery/components/CurrentImage/CurrentImagePreview';
|
||||||
|
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
||||||
|
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
||||||
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiArrowLeftBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
const TAB_NAME_TO_TKEY: Record<InvokeTabName, string> = {
|
||||||
|
txt2img: 'common.txt2img',
|
||||||
|
img2img: 'common.img2img',
|
||||||
|
unifiedCanvas: 'common.unifiedCanvas',
|
||||||
|
nodes: 'common.nodes',
|
||||||
|
modelManager: 'modelManager.modelManager',
|
||||||
|
queue: 'queue.queue',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ImageViewer = memo(() => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const isOpen = useAppSelector((s) => s.gallery.isImageViewerOpen);
|
||||||
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
|
const activeTabLabel = useMemo(
|
||||||
|
() => t('gallery.backToEditor', { tab: t(TAB_NAME_TO_TKEY[activeTabName]) }),
|
||||||
|
[t, activeTabName]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onClose = useCallback(() => {
|
||||||
|
dispatch(isImageViewerOpenChanged(false));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const onOpen = useCallback(() => {
|
||||||
|
dispatch(isImageViewerOpenChanged(true));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
useHotkeys('esc', onClose, { enabled: isOpen }, [isOpen]);
|
||||||
|
useHotkeys('i', onOpen, { enabled: !isOpen }, [isOpen]);
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
>
|
||||||
|
<CurrentImageButtons />
|
||||||
|
<CurrentImagePreview />
|
||||||
|
<Button
|
||||||
|
aria-label={activeTabLabel}
|
||||||
|
tooltip={activeTabLabel}
|
||||||
|
onClick={onClose}
|
||||||
|
leftIcon={<PiArrowLeftBold />}
|
||||||
|
position="absolute"
|
||||||
|
top={2}
|
||||||
|
insetInlineEnd={2}
|
||||||
|
>
|
||||||
|
{t('common.back')}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ImageViewer.displayName = 'ImageViewer';
|
@ -21,6 +21,7 @@ const initialGalleryState: GalleryState = {
|
|||||||
boardSearchText: '',
|
boardSearchText: '',
|
||||||
limit: INITIAL_IMAGE_LIMIT,
|
limit: INITIAL_IMAGE_LIMIT,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
|
isImageViewerOpen: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const gallerySlice = createSlice({
|
export const gallerySlice = createSlice({
|
||||||
@ -29,9 +30,11 @@ export const gallerySlice = createSlice({
|
|||||||
reducers: {
|
reducers: {
|
||||||
imageSelected: (state, action: PayloadAction<ImageDTO | null>) => {
|
imageSelected: (state, action: PayloadAction<ImageDTO | null>) => {
|
||||||
state.selection = action.payload ? [action.payload] : [];
|
state.selection = action.payload ? [action.payload] : [];
|
||||||
|
state.isImageViewerOpen = true;
|
||||||
},
|
},
|
||||||
selectionChanged: (state, action: PayloadAction<ImageDTO[]>) => {
|
selectionChanged: (state, action: PayloadAction<ImageDTO[]>) => {
|
||||||
state.selection = uniqBy(action.payload, (i) => i.image_name);
|
state.selection = uniqBy(action.payload, (i) => i.image_name);
|
||||||
|
state.isImageViewerOpen = true;
|
||||||
},
|
},
|
||||||
shouldAutoSwitchChanged: (state, action: PayloadAction<boolean>) => {
|
shouldAutoSwitchChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
state.shouldAutoSwitch = action.payload;
|
state.shouldAutoSwitch = action.payload;
|
||||||
@ -75,6 +78,9 @@ export const gallerySlice = createSlice({
|
|||||||
alwaysShowImageSizeBadgeChanged: (state, action: PayloadAction<boolean>) => {
|
alwaysShowImageSizeBadgeChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
state.alwaysShowImageSizeBadge = action.payload;
|
state.alwaysShowImageSizeBadge = action.payload;
|
||||||
},
|
},
|
||||||
|
isImageViewerOpenChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.isImageViewerOpen = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder.addMatcher(isAnyBoardDeleted, (state, action) => {
|
builder.addMatcher(isAnyBoardDeleted, (state, action) => {
|
||||||
@ -112,6 +118,7 @@ export const {
|
|||||||
boardSearchTextChanged,
|
boardSearchTextChanged,
|
||||||
moreImagesLoaded,
|
moreImagesLoaded,
|
||||||
alwaysShowImageSizeBadgeChanged,
|
alwaysShowImageSizeBadgeChanged,
|
||||||
|
isImageViewerOpenChanged,
|
||||||
} = gallerySlice.actions;
|
} = gallerySlice.actions;
|
||||||
|
|
||||||
const isAnyBoardDeleted = isAnyOf(
|
const isAnyBoardDeleted = isAnyOf(
|
||||||
@ -133,5 +140,5 @@ export const galleryPersistConfig: PersistConfig<GalleryState> = {
|
|||||||
name: gallerySlice.name,
|
name: gallerySlice.name,
|
||||||
initialState: initialGalleryState,
|
initialState: initialGalleryState,
|
||||||
migrate: migrateGalleryState,
|
migrate: migrateGalleryState,
|
||||||
persistDenylist: ['selection', 'selectedBoardId', 'galleryView', 'offset', 'limit'],
|
persistDenylist: ['selection', 'selectedBoardId', 'galleryView', 'offset', 'limit', 'isImageViewerOpen'],
|
||||||
};
|
};
|
||||||
|
@ -20,4 +20,5 @@ export type GalleryState = {
|
|||||||
offset: number;
|
offset: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
alwaysShowImageSizeBadge: boolean;
|
alwaysShowImageSizeBadge: boolean;
|
||||||
|
isImageViewerOpen: boolean;
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import type { ControlNetConfig, IPAdapterConfig, T2IAdapterConfig } from 'features/controlAdapters/store/types';
|
import type { ControlNetConfig, IPAdapterConfig, T2IAdapterConfig } from 'features/controlAdapters/store/types';
|
||||||
import type { ControlNetConfigV2, IPAdapterConfigV2, T2IAdapterConfigV2 } from 'features/controlLayers/util/controlAdapters';
|
import type {
|
||||||
|
ControlNetConfigV2,
|
||||||
|
IPAdapterConfigV2,
|
||||||
|
T2IAdapterConfigV2,
|
||||||
|
} from 'features/controlLayers/util/controlAdapters';
|
||||||
import type { O } from 'ts-toolbelt';
|
import type { O } from 'ts-toolbelt';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -5,7 +5,7 @@ import NodeOpacitySlider from './NodeOpacitySlider';
|
|||||||
import ViewportControls from './ViewportControls';
|
import ViewportControls from './ViewportControls';
|
||||||
|
|
||||||
const BottomLeftPanel = () => (
|
const BottomLeftPanel = () => (
|
||||||
<Flex gap={2} position="absolute" bottom={2} insetInlineStart={2}>
|
<Flex gap={2} position="absolute" bottom={0} insetInlineStart={0}>
|
||||||
<ViewportControls />
|
<ViewportControls />
|
||||||
<NodeOpacitySlider />
|
<NodeOpacitySlider />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -19,7 +19,7 @@ const MinimapPanel = () => {
|
|||||||
const shouldShowMinimapPanel = useAppSelector((s) => s.nodes.shouldShowMinimapPanel);
|
const shouldShowMinimapPanel = useAppSelector((s) => s.nodes.shouldShowMinimapPanel);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex gap={2} position="absolute" bottom={2} insetInlineEnd={2}>
|
<Flex gap={2} position="absolute" bottom={0} insetInlineEnd={0}>
|
||||||
{shouldShowMinimapPanel && (
|
{shouldShowMinimapPanel && (
|
||||||
<ChakraMiniMap
|
<ChakraMiniMap
|
||||||
pannable
|
pannable
|
||||||
|
@ -11,7 +11,7 @@ import { memo } from 'react';
|
|||||||
const TopCenterPanel = () => {
|
const TopCenterPanel = () => {
|
||||||
const name = useAppSelector((s) => s.workflow.name);
|
const name = useAppSelector((s) => s.workflow.name);
|
||||||
return (
|
return (
|
||||||
<Flex gap={2} top={2} left={2} right={2} position="absolute" alignItems="flex-start" pointerEvents="none">
|
<Flex gap={2} top={0} left={0} right={0} position="absolute" alignItems="flex-start" pointerEvents="none">
|
||||||
<Flex gap="2">
|
<Flex gap="2">
|
||||||
<AddNodeButton />
|
<AddNodeButton />
|
||||||
<UpdateNodesButton />
|
<UpdateNodesButton />
|
||||||
|
@ -140,6 +140,16 @@ export const useHotkeyData = (): HotkeyGroup[] => {
|
|||||||
desc: t('hotkeys.nextImage.desc'),
|
desc: t('hotkeys.nextImage.desc'),
|
||||||
hotkeys: [['Arrow Right']],
|
hotkeys: [['Arrow Right']],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t('hotkeys.openImageViewer.title'),
|
||||||
|
desc: t('hotkeys.openImageViewer.desc'),
|
||||||
|
hotkeys: [['I']],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('hotkeys.backToEditor.title'),
|
||||||
|
desc: t('hotkeys.backToEditor.desc'),
|
||||||
|
hotkeys: [['Esc']],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
[t]
|
[t]
|
||||||
|
@ -12,10 +12,17 @@ import { selectConfigSlice } from 'features/system/store/configSlice';
|
|||||||
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
|
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
|
||||||
import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons';
|
import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons';
|
||||||
import ParametersPanelTextToImage from 'features/ui/components/ParametersPanelTextToImage';
|
import ParametersPanelTextToImage from 'features/ui/components/ParametersPanelTextToImage';
|
||||||
|
import ImageToImageTab from 'features/ui/components/tabs/ImageToImageTab';
|
||||||
|
import ModelManagerTab from 'features/ui/components/tabs/ModelManagerTab';
|
||||||
|
import NodesTab from 'features/ui/components/tabs/NodesTab';
|
||||||
|
import QueueTab from 'features/ui/components/tabs/QueueTab';
|
||||||
|
import TextToImageTab from 'features/ui/components/tabs/TextToImageTab';
|
||||||
|
import UnifiedCanvasTab from 'features/ui/components/tabs/UnifiedCanvasTab';
|
||||||
import type { UsePanelOptions } from 'features/ui/hooks/usePanel';
|
import type { UsePanelOptions } from 'features/ui/hooks/usePanel';
|
||||||
import { usePanel } from 'features/ui/hooks/usePanel';
|
import { usePanel } from 'features/ui/hooks/usePanel';
|
||||||
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
|
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
|
||||||
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
||||||
|
import { TAB_NUMBER_MAP } from 'features/ui/store/tabMap';
|
||||||
import { activeTabIndexSelector, activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
import { activeTabIndexSelector, activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||||
import type { CSSProperties, MouseEvent, ReactElement, ReactNode } from 'react';
|
import type { CSSProperties, MouseEvent, ReactElement, ReactNode } from 'react';
|
||||||
@ -28,62 +35,56 @@ import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
|
|||||||
import { Panel, PanelGroup } from 'react-resizable-panels';
|
import { Panel, PanelGroup } from 'react-resizable-panels';
|
||||||
|
|
||||||
import ParametersPanel from './ParametersPanel';
|
import ParametersPanel from './ParametersPanel';
|
||||||
import ImageTab from './tabs/ImageToImageTab';
|
|
||||||
import ModelManagerTab from './tabs/ModelManagerTab';
|
|
||||||
import NodesTab from './tabs/NodesTab';
|
|
||||||
import QueueTab from './tabs/QueueTab';
|
|
||||||
import ResizeHandle from './tabs/ResizeHandle';
|
import ResizeHandle from './tabs/ResizeHandle';
|
||||||
import TextToImageTab from './tabs/TextToImageTab';
|
|
||||||
import UnifiedCanvasTab from './tabs/UnifiedCanvasTab';
|
|
||||||
|
|
||||||
interface InvokeTabInfo {
|
type TabData = {
|
||||||
id: InvokeTabName;
|
id: InvokeTabName;
|
||||||
translationKey: string;
|
translationKey: string;
|
||||||
icon: ReactElement;
|
icon: ReactElement;
|
||||||
content: ReactNode;
|
content: ReactNode;
|
||||||
}
|
};
|
||||||
|
|
||||||
const tabs: InvokeTabInfo[] = [
|
const TAB_DATA: Record<InvokeTabName, TabData> = {
|
||||||
{
|
txt2img: {
|
||||||
id: 'txt2img',
|
id: 'txt2img',
|
||||||
translationKey: 'common.txt2img',
|
translationKey: 'common.txt2img',
|
||||||
icon: <RiInputMethodLine />,
|
icon: <RiInputMethodLine />,
|
||||||
content: <TextToImageTab />,
|
content: <TextToImageTab />,
|
||||||
},
|
},
|
||||||
{
|
img2img: {
|
||||||
id: 'img2img',
|
id: 'img2img',
|
||||||
translationKey: 'common.img2img',
|
translationKey: 'common.img2img',
|
||||||
icon: <RiImage2Line />,
|
icon: <RiImage2Line />,
|
||||||
content: <ImageTab />,
|
content: <ImageToImageTab />,
|
||||||
},
|
},
|
||||||
{
|
unifiedCanvas: {
|
||||||
id: 'unifiedCanvas',
|
id: 'unifiedCanvas',
|
||||||
translationKey: 'common.unifiedCanvas',
|
translationKey: 'common.unifiedCanvas',
|
||||||
icon: <RiBrushLine />,
|
icon: <RiBrushLine />,
|
||||||
content: <UnifiedCanvasTab />,
|
content: <UnifiedCanvasTab />,
|
||||||
},
|
},
|
||||||
{
|
nodes: {
|
||||||
id: 'nodes',
|
id: 'nodes',
|
||||||
translationKey: 'common.nodes',
|
translationKey: 'common.nodes',
|
||||||
icon: <PiFlowArrowBold />,
|
icon: <PiFlowArrowBold />,
|
||||||
content: <NodesTab />,
|
content: <NodesTab />,
|
||||||
},
|
},
|
||||||
{
|
modelManager: {
|
||||||
id: 'modelManager',
|
id: 'modelManager',
|
||||||
translationKey: 'modelManager.modelManager',
|
translationKey: 'modelManager.modelManager',
|
||||||
icon: <RiBox2Line />,
|
icon: <RiBox2Line />,
|
||||||
content: <ModelManagerTab />,
|
content: <ModelManagerTab />,
|
||||||
},
|
},
|
||||||
{
|
queue: {
|
||||||
id: 'queue',
|
id: 'queue',
|
||||||
translationKey: 'queue.queue',
|
translationKey: 'queue.queue',
|
||||||
icon: <RiPlayList2Fill />,
|
icon: <RiPlayList2Fill />,
|
||||||
content: <QueueTab />,
|
content: <QueueTab />,
|
||||||
},
|
},
|
||||||
];
|
};
|
||||||
|
|
||||||
const enabledTabsSelector = createMemoizedSelector(selectConfigSlice, (config) =>
|
const enabledTabsSelector = createMemoizedSelector(selectConfigSlice, (config) =>
|
||||||
tabs.filter((tab) => !config.disabledTabs.includes(tab.id))
|
TAB_NUMBER_MAP.map((tabName) => TAB_DATA[tabName]).filter((tab) => !config.disabledTabs.includes(tab.id))
|
||||||
);
|
);
|
||||||
|
|
||||||
const NO_GALLERY_PANEL_TABS: InvokeTabName[] = ['modelManager', 'queue'];
|
const NO_GALLERY_PANEL_TABS: InvokeTabName[] = ['modelManager', 'queue'];
|
||||||
|
@ -39,8 +39,8 @@ const ParametersPanelTextToImage = () => {
|
|||||||
{isSDXL ? <SDXLPrompts /> : <Prompts />}
|
{isSDXL ? <SDXLPrompts /> : <Prompts />}
|
||||||
<Tabs variant="line" isLazy={true} display="flex" flexDir="column" w="full" h="full">
|
<Tabs variant="line" isLazy={true} display="flex" flexDir="column" w="full" h="full">
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab>{t('common.settingsLabel')}</Tab>
|
<Tab flexGrow={1}>{t('common.settingsLabel')}</Tab>
|
||||||
<Tab>{controlLayersTitle}</Tab>
|
<Tab flexGrow={1}>{controlLayersTitle}</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
|
|
||||||
<TabPanels w="full" h="full">
|
<TabPanels w="full" h="full">
|
||||||
|
@ -29,7 +29,7 @@ const ImageToImageTab = () => {
|
|||||||
const panelStorage = usePanelStorage();
|
const panelStorage = usePanelStorage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box w="full" h="full">
|
<Box position="relative" w="full" h="full">
|
||||||
<PanelGroup
|
<PanelGroup
|
||||||
ref={panelGroupRef}
|
ref={panelGroupRef}
|
||||||
autoSaveId="imageTab.content"
|
autoSaveId="imageTab.content"
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import CurrentImageDisplay from 'features/gallery/components/CurrentImage/CurrentImageDisplay';
|
import CurrentImageDisplay from 'features/gallery/components/CurrentImage/CurrentImageDisplay';
|
||||||
|
import { ImageViewer } from 'features/gallery/components/ImageViewer';
|
||||||
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';
|
||||||
@ -9,20 +10,23 @@ const NodesTab = () => {
|
|||||||
const mode = useAppSelector((s) => s.workflow.mode);
|
const mode = useAppSelector((s) => s.workflow.mode);
|
||||||
|
|
||||||
if (mode === 'edit') {
|
if (mode === 'edit') {
|
||||||
return (
|
|
||||||
<ReactFlowProvider>
|
|
||||||
<NodeEditor />
|
|
||||||
</ReactFlowProvider>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
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">
|
||||||
<Flex w="full" h="full">
|
<ReactFlowProvider>
|
||||||
<CurrentImageDisplay />
|
<NodeEditor />
|
||||||
</Flex>
|
</ReactFlowProvider>
|
||||||
|
<ImageViewer />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
|
||||||
|
<Flex w="full" h="full">
|
||||||
|
<CurrentImageDisplay />
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(NodesTab);
|
export default memo(NodesTab);
|
||||||
|
@ -1,31 +1,13 @@
|
|||||||
import { Box, Tab, TabList, TabPanel, TabPanels, Tabs } 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 { useControlLayersTitle } from 'features/controlLayers/hooks/useControlLayersTitle';
|
import { ImageViewer } from 'features/gallery/components/ImageViewer';
|
||||||
import CurrentImageDisplay from 'features/gallery/components/CurrentImage/CurrentImageDisplay';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
const TextToImageTab = () => {
|
const TextToImageTab = () => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const controlLayersTitle = useControlLayersTitle();
|
|
||||||
|
|
||||||
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">
|
||||||
<Tabs variant="line" isLazy={true} display="flex" flexDir="column" w="full" h="full">
|
<ControlLayersEditor />
|
||||||
<TabList>
|
<ImageViewer />
|
||||||
<Tab>{t('common.viewer')}</Tab>
|
|
||||||
<Tab>{controlLayersTitle}</Tab>
|
|
||||||
</TabList>
|
|
||||||
|
|
||||||
<TabPanels w="full" h="full" minH={0} minW={0}>
|
|
||||||
<TabPanel>
|
|
||||||
<CurrentImageDisplay />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel>
|
|
||||||
<ControlLayersEditor />
|
|
||||||
</TabPanel>
|
|
||||||
</TabPanels>
|
|
||||||
</Tabs>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -6,6 +6,7 @@ import { CANVAS_TAB_TESTID } from 'features/canvas/store/constants';
|
|||||||
import { useDroppableTypesafe } from 'features/dnd/hooks/typesafeHooks';
|
import { useDroppableTypesafe } from 'features/dnd/hooks/typesafeHooks';
|
||||||
import type { CanvasInitialImageDropData } from 'features/dnd/types';
|
import type { CanvasInitialImageDropData } from 'features/dnd/types';
|
||||||
import { isValidDrop } from 'features/dnd/util/isValidDrop';
|
import { isValidDrop } from 'features/dnd/util/isValidDrop';
|
||||||
|
import { ImageViewer } from 'features/gallery/components/ImageViewer';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@ -27,6 +28,7 @@ const UnifiedCanvasTab = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
|
position="relative"
|
||||||
layerStyle="first"
|
layerStyle="first"
|
||||||
ref={setDroppableRef}
|
ref={setDroppableRef}
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
@ -40,6 +42,7 @@ const UnifiedCanvasTab = () => {
|
|||||||
>
|
>
|
||||||
<IAICanvasToolbar />
|
<IAICanvasToolbar />
|
||||||
<IAICanvas />
|
<IAICanvas />
|
||||||
|
<ImageViewer />
|
||||||
{isValidDrop(droppableData, active) && (
|
{isValidDrop(droppableData, active) && (
|
||||||
<IAIDropOverlay isOver={isOver} label={t('toast.setCanvasInitialImage')} />
|
<IAIDropOverlay isOver={isOver} label={t('toast.setCanvasInitialImage')} />
|
||||||
)}
|
)}
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
export const tabMap = ['txt2img', 'img2img', 'unifiedCanvas', 'nodes', 'modelManager', 'queue'] as const;
|
|
||||||
|
|
||||||
export type InvokeTabName = (typeof tabMap)[number];
|
|
3
invokeai/frontend/web/src/features/ui/store/tabMap.tsx
Normal file
3
invokeai/frontend/web/src/features/ui/store/tabMap.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const TAB_NUMBER_MAP = ['txt2img', 'img2img', 'unifiedCanvas', 'nodes', 'modelManager', 'queue'] as const;
|
||||||
|
|
||||||
|
export type InvokeTabName = (typeof TAB_NUMBER_MAP)[number];
|
@ -3,7 +3,7 @@ import { selectConfigSlice } from 'features/system/store/configSlice';
|
|||||||
import { selectUiSlice } from 'features/ui/store/uiSlice';
|
import { selectUiSlice } from 'features/ui/store/uiSlice';
|
||||||
import { isString } from 'lodash-es';
|
import { isString } from 'lodash-es';
|
||||||
|
|
||||||
import { tabMap } from './tabMap';
|
import { TAB_NUMBER_MAP } from './tabMap';
|
||||||
|
|
||||||
export const activeTabNameSelector = createSelector(
|
export const activeTabNameSelector = createSelector(
|
||||||
selectUiSlice,
|
selectUiSlice,
|
||||||
@ -15,7 +15,7 @@ export const activeTabNameSelector = createSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const activeTabIndexSelector = createSelector(selectUiSlice, selectConfigSlice, (ui, config) => {
|
export const activeTabIndexSelector = createSelector(selectUiSlice, selectConfigSlice, (ui, config) => {
|
||||||
const tabs = tabMap.filter((t) => !config.disabledTabs.includes(t));
|
const tabs = TAB_NUMBER_MAP.filter((t) => !config.disabledTabs.includes(t));
|
||||||
const idx = tabs.indexOf(ui.activeTab);
|
const idx = tabs.indexOf(ui.activeTab);
|
||||||
return idx === -1 ? 0 : idx;
|
return idx === -1 ? 0 : idx;
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user