mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-01 15:54:58 -05:00
feat(ui): reworked layout (wip)
This commit is contained in:
@@ -375,6 +375,7 @@
|
||||
"useCache": "Use Cache"
|
||||
},
|
||||
"gallery": {
|
||||
"gallery": "Gallery",
|
||||
"alwaysShowImageSizeBadge": "Always Show Image Size Badge",
|
||||
"assets": "Assets",
|
||||
"autoAssignBoardOnClick": "Auto-Assign Board on Click",
|
||||
@@ -387,11 +388,11 @@
|
||||
"deleteImage_one": "Delete Image",
|
||||
"deleteImage_other": "Delete {{count}} Images",
|
||||
"deleteImagePermanent": "Deleted images cannot be restored.",
|
||||
"displayBoardSearch": "Display Board Search",
|
||||
"displaySearch": "Display Search",
|
||||
"displayBoardSearch": "Board Search",
|
||||
"displaySearch": "Image Search",
|
||||
"download": "Download",
|
||||
"exitBoardSearch": "Exit Board Search",
|
||||
"exitSearch": "Exit Search",
|
||||
"exitSearch": "Exit Image Search",
|
||||
"featuresWillReset": "If you delete this image, those features will immediately be reset.",
|
||||
"galleryImageSize": "Image Size",
|
||||
"gallerySettings": "Gallery Settings",
|
||||
@@ -437,7 +438,8 @@
|
||||
"compareHelp1": "Hold <Kbd>Alt</Kbd> while clicking a gallery image or using the arrow keys to change the compare image.",
|
||||
"compareHelp2": "Press <Kbd>M</Kbd> to cycle through comparison modes.",
|
||||
"compareHelp3": "Press <Kbd>C</Kbd> to swap the compared images.",
|
||||
"compareHelp4": "Press <Kbd>Z</Kbd> or <Kbd>Esc</Kbd> to exit."
|
||||
"compareHelp4": "Press <Kbd>Z</Kbd> or <Kbd>Esc</Kbd> to exit.",
|
||||
"toggleMiniViewer": "Toggle Mini Viewer"
|
||||
},
|
||||
"hotkeys": {
|
||||
"searchHotkeys": "Search Hotkeys",
|
||||
@@ -1049,8 +1051,8 @@
|
||||
"scaledHeight": "Scaled H",
|
||||
"scaledWidth": "Scaled W",
|
||||
"scheduler": "Scheduler",
|
||||
"seamlessXAxis": "Seamless Tiling X Axis",
|
||||
"seamlessYAxis": "Seamless Tiling Y Axis",
|
||||
"seamlessXAxis": "Seamless X Axis",
|
||||
"seamlessYAxis": "Seamless Y Axis",
|
||||
"seed": "Seed",
|
||||
"imageActions": "Image Actions",
|
||||
"sendToCanvas": "Send To Canvas",
|
||||
@@ -1714,6 +1716,8 @@
|
||||
"inpaintMask": "Inpaint Mask",
|
||||
"regionalGuidance": "Regional Guidance",
|
||||
"ipAdapter": "IP Adapter",
|
||||
"sendingToCanvas": "Sending to Canvas",
|
||||
"sendingToGallery": "Sending to Gallery",
|
||||
"sendToGallery": "Send To Gallery",
|
||||
"sendToGalleryDesc": "Generations will be sent to the gallery.",
|
||||
"sendToCanvas": "Send To Canvas",
|
||||
|
||||
@@ -21,10 +21,16 @@ function ThemeLocaleProvider({ children }: ThemeLocaleProviderProps) {
|
||||
direction,
|
||||
shadows: {
|
||||
..._theme.shadows,
|
||||
selected:
|
||||
'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-500), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)',
|
||||
hoverSelected:
|
||||
'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-400), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)',
|
||||
hoverUnselected:
|
||||
'inset 0px 0px 0px 2px var(--invoke-colors-invokeBlue-300), inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-800)',
|
||||
selectedForCompare:
|
||||
'0px 0px 0px 1px var(--invoke-colors-base-900), 0px 0px 0px 4px var(--invoke-colors-green-400)',
|
||||
'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-300), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)',
|
||||
hoverSelectedForCompare:
|
||||
'0px 0px 0px 1px var(--invoke-colors-base-900), 0px 0px 0px 4px var(--invoke-colors-green-300)',
|
||||
'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-200), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)',
|
||||
},
|
||||
});
|
||||
}, [direction]);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { enqueueRequested } from 'app/store/actions';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
|
||||
import { buildMultidiffusionUpscaleGraph } from 'features/nodes/util/graph/buildMultidiffusionUpscaleGraph';
|
||||
import { queueApi } from 'services/api/endpoints/queue';
|
||||
@@ -11,7 +10,6 @@ export const addEnqueueRequestedUpscale = (startAppListening: AppStartListening)
|
||||
enqueueRequested.match(action) && action.payload.tabName === 'upscaling',
|
||||
effect: async (action, { getState, dispatch }) => {
|
||||
const state = getState();
|
||||
const { shouldShowProgressInViewer } = state.ui;
|
||||
const { prepend } = action.payload;
|
||||
|
||||
const { g, noise, posCond } = await buildMultidiffusionUpscaleGraph(state);
|
||||
@@ -25,9 +23,6 @@ export const addEnqueueRequestedUpscale = (startAppListening: AppStartListening)
|
||||
);
|
||||
try {
|
||||
await req.unwrap();
|
||||
if (shouldShowProgressInViewer) {
|
||||
dispatch(isImageViewerOpenChanged(true));
|
||||
}
|
||||
} finally {
|
||||
req.reset();
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { CanvasControlLayerState, CanvasRasterLayerState } from 'features/c
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
|
||||
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
|
||||
import { isValidDrop } from 'features/dnd/util/isValidDrop';
|
||||
import { imageToCompareChanged, isImageViewerOpenChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { imageToCompareChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
@@ -146,7 +146,6 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
|
||||
) {
|
||||
const { imageDTO } = activeData.payload;
|
||||
dispatch(imageToCompareChanged(imageDTO));
|
||||
dispatch(isImageViewerOpenChanged(true));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,18 +6,51 @@ import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
|
||||
import ImageContextMenu from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
|
||||
import type { MouseEvent, ReactElement, ReactNode, SyntheticEvent } from 'react';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { PiImageBold, PiUploadSimpleBold } from 'react-icons/pi';
|
||||
import type { ImageDTO, PostUploadAction } from 'services/api/types';
|
||||
|
||||
import IAIDraggable from './IAIDraggable';
|
||||
import IAIDroppable from './IAIDroppable';
|
||||
import SelectionOverlay from './SelectionOverlay';
|
||||
|
||||
const defaultUploadElement = <Icon as={PiUploadSimpleBold} boxSize={16} />;
|
||||
|
||||
const defaultNoContentFallback = <IAINoContentFallback icon={PiImageBold} />;
|
||||
|
||||
const sx: SystemStyleObject = {
|
||||
'.gallery-image-container::before': {
|
||||
content: '""',
|
||||
display: 'inline-block',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
pointerEvents: 'none',
|
||||
borderRadius: 'base',
|
||||
},
|
||||
'&[data-selected="selected"]>.gallery-image-container::before': {
|
||||
boxShadow:
|
||||
'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-500), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)',
|
||||
},
|
||||
'&[data-selected="selectedForCompare"]>.gallery-image-container::before': {
|
||||
boxShadow:
|
||||
'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-300), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)',
|
||||
},
|
||||
'&:hover>.gallery-image-container::before': {
|
||||
boxShadow:
|
||||
'inset 0px 0px 0px 2px var(--invoke-colors-invokeBlue-300), inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-800)',
|
||||
},
|
||||
'&:hover[data-selected="selected"]>.gallery-image-container::before': {
|
||||
boxShadow:
|
||||
'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-400), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)',
|
||||
},
|
||||
'&:hover[data-selected="selectedForCompare"]>.gallery-image-container::before': {
|
||||
boxShadow:
|
||||
'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-200), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)',
|
||||
},
|
||||
};
|
||||
|
||||
type IAIDndImageProps = FlexProps & {
|
||||
imageDTO: ImageDTO | undefined;
|
||||
onError?: (event: SyntheticEvent<HTMLImageElement>) => void;
|
||||
@@ -75,13 +108,11 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const handleMouseOver = useCallback(
|
||||
(e: MouseEvent<HTMLDivElement>) => {
|
||||
if (onMouseOver) {
|
||||
onMouseOver(e);
|
||||
}
|
||||
setIsHovered(true);
|
||||
},
|
||||
[onMouseOver]
|
||||
);
|
||||
@@ -90,7 +121,6 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
if (onMouseOut) {
|
||||
onMouseOut(e);
|
||||
}
|
||||
setIsHovered(false);
|
||||
},
|
||||
[onMouseOut]
|
||||
);
|
||||
@@ -141,10 +171,13 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
minH={minSize ? minSize : undefined}
|
||||
userSelect="none"
|
||||
cursor={isDragDisabled || !imageDTO ? 'default' : 'pointer'}
|
||||
sx={withHoverOverlay ? sx : undefined}
|
||||
data-selected={isSelectedForCompare ? 'selectedForCompare' : isSelected ? 'selected' : undefined}
|
||||
{...rest}
|
||||
>
|
||||
{imageDTO && (
|
||||
<Flex
|
||||
className="gallery-image-container"
|
||||
w="full"
|
||||
h="full"
|
||||
position={fitContainer ? 'absolute' : 'relative'}
|
||||
@@ -167,11 +200,6 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
data-testid={dataTestId}
|
||||
/>
|
||||
{withMetadataOverlay && <ImageMetadataOverlay imageDTO={imageDTO} />}
|
||||
<SelectionOverlay
|
||||
isSelected={isSelected}
|
||||
isSelectedForCompare={isSelectedForCompare}
|
||||
isHovered={withHoverOverlay ? isHovered : false}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
{!imageDTO && !isUploadDisabled && (
|
||||
|
||||
@@ -114,4 +114,13 @@ export const useGlobalHotkeys = () => {
|
||||
},
|
||||
[dispatch, isModelManagerEnabled]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
isModelManagerEnabled ? '6' : '5',
|
||||
() => {
|
||||
dispatch(setActiveTab('gallery'));
|
||||
setScopes([]);
|
||||
},
|
||||
[dispatch, isModelManagerEnabled]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import IAIDroppable from 'common/components/IAIDroppable';
|
||||
import type { AddControlLayerFromImageDropData, AddRasterLayerFromImageDropData } from 'features/dnd/types';
|
||||
import { useIsImageViewerOpen } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { memo } from 'react';
|
||||
|
||||
const addRasterLayerFromImageDropData: AddRasterLayerFromImageDropData = {
|
||||
@@ -15,12 +14,6 @@ const addControlLayerFromImageDropData: AddControlLayerFromImageDropData = {
|
||||
};
|
||||
|
||||
export const CanvasDropArea = memo(() => {
|
||||
const isImageViewerOpen = useIsImageViewerOpen();
|
||||
|
||||
if (isImageViewerOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex position="absolute" top={0} right={0} bottom="50%" left={0} gap={2} pointerEvents="none">
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { CanvasEditor } from 'features/controlLayers/components/CanvasEditor';
|
||||
|
||||
const meta: Meta<typeof CanvasEditor> = {
|
||||
title: 'Feature/ControlLayers',
|
||||
tags: ['autodocs'],
|
||||
component: CanvasEditor,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof CanvasEditor>;
|
||||
|
||||
const Component = () => {
|
||||
return (
|
||||
<Flex w={1500} h={1500}>
|
||||
<CanvasEditor />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
render: Component,
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import { memo } from 'react';
|
||||
|
||||
export const EntityListGlobalActionBar = memo(() => {
|
||||
return (
|
||||
<Flex w="full" py={1} px={1} gap={2} alignItems="center">
|
||||
<Flex w="full" gap={2} alignItems="center">
|
||||
<EntityListGlobalActionBarDenoisingStrength />
|
||||
<Spacer />
|
||||
<Flex>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { memo } from 'react';
|
||||
|
||||
export const EntityListSelectedEntityActionBar = memo(() => {
|
||||
return (
|
||||
<Flex w="full" py={1} px={1} gap={2} alignItems="center">
|
||||
<Flex w="full" gap={2} alignItems="center" ps={1}>
|
||||
<EntityListSelectedEntityActionBarOpacity />
|
||||
<Spacer />
|
||||
<EntityListSelectedEntityActionBarFill />
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Divider, Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasAddEntityButtons } from 'features/controlLayers/components/CanvasAddEntityButtons';
|
||||
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList';
|
||||
import { EntityListGlobalActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBar';
|
||||
import { EntityListSelectedEntityActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectHasEntities } from 'features/controlLayers/store/selectors';
|
||||
@@ -14,8 +13,6 @@ export const CanvasPanelContent = memo(() => {
|
||||
return (
|
||||
<CanvasManagerProviderGate>
|
||||
<Flex flexDir="column" gap={2} w="full" h="full">
|
||||
<EntityListGlobalActionBar />
|
||||
<Divider py={0} />
|
||||
<EntityListSelectedEntityActionBar />
|
||||
<Divider py={0} />
|
||||
{!hasEntities && <CanvasAddEntityButtons />}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { useDndContext } from '@dnd-kit/core';
|
||||
import { Box, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
|
||||
import { CanvasPanelContent } from 'features/controlLayers/components/CanvasPanelContent';
|
||||
import { CanvasSendToToggle } from 'features/controlLayers/components/CanvasSendToToggle';
|
||||
import { selectSendToCanvas } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
|
||||
import { memo, useCallback, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const CanvasRightPanelContent = memo(() => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [tab, setTab] = useState(0);
|
||||
useScopeOnFocus('gallery', ref);
|
||||
|
||||
return (
|
||||
<Tabs index={tab} onChange={setTab} w="full" h="full" display="flex" flexDir="column">
|
||||
<TabList alignItems="center">
|
||||
<PanelTabs setTab={setTab} />
|
||||
<Spacer />
|
||||
<CanvasSendToToggle />
|
||||
</TabList>
|
||||
<TabPanels w="full" h="full">
|
||||
<TabPanel w="full" h="full" p={0} pt={2}>
|
||||
<GalleryPanelContent />
|
||||
</TabPanel>
|
||||
<TabPanel w="full" h="full" p={0} pt={2}>
|
||||
<CanvasPanelContent />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasRightPanelContent.displayName = 'CanvasRightPanelContent';
|
||||
|
||||
const PanelTabs = memo(({ setTab }: { setTab: (val: number) => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const sendToCanvas = useAppSelector(selectSendToCanvas);
|
||||
const tabTimeout = useRef<number | null>(null);
|
||||
const dndCtx = useDndContext();
|
||||
|
||||
const onOnMouseOverLayersTab = useCallback(() => {
|
||||
tabTimeout.current = window.setTimeout(() => {
|
||||
if (dndCtx.active) {
|
||||
setTab(1);
|
||||
}
|
||||
}, 300);
|
||||
}, [dndCtx.active, setTab]);
|
||||
|
||||
const onOnMouseOverGalleryTab = useCallback(() => {
|
||||
tabTimeout.current = window.setTimeout(() => {
|
||||
if (dndCtx.active) {
|
||||
setTab(0);
|
||||
}
|
||||
}, 300);
|
||||
}, [dndCtx.active, setTab]);
|
||||
|
||||
const onMouseOut = useCallback(() => {
|
||||
if (tabTimeout.current) {
|
||||
clearTimeout(tabTimeout.current);
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<Tab position="relative" onMouseOver={onOnMouseOverGalleryTab} onMouseOut={onMouseOut}>
|
||||
{t('gallery.gallery')}
|
||||
{!sendToCanvas && (
|
||||
<Box position="absolute" top={2} right={2} h={2} w={2} bg="invokeYellow.300" borderRadius="full" />
|
||||
)}
|
||||
</Tab>
|
||||
<Tab position="relative" onMouseOver={onOnMouseOverLayersTab} onMouseOut={onMouseOut}>
|
||||
{t('controlLayers.layer_other')}
|
||||
{sendToCanvas && (
|
||||
<Box position="absolute" top={2} right={2} h={2} w={2} bg="invokeYellow.300" borderRadius="full" />
|
||||
)}
|
||||
</Tab>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
PanelTabs.displayName = 'PanelTabs';
|
||||
@@ -1,64 +1,76 @@
|
||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { IconSwitch } from 'common/components/IconSwitch';
|
||||
import {
|
||||
selectCanvasSettingsSlice,
|
||||
settingsSendToCanvasChanged,
|
||||
} from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
Button,
|
||||
Flex,
|
||||
Icon,
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverBody,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
Text,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectSendToCanvas, settingsSendToCanvasChanged } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiImageBold, PiPaintBrushBold } from 'react-icons/pi';
|
||||
|
||||
const TooltipSendToGallery = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Flex flexDir="column">
|
||||
<Text fontWeight="semibold">{t('controlLayers.sendToGallery')}</Text>
|
||||
<Text fontWeight="normal">{t('controlLayers.sendToGalleryDesc')}</Text>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
TooltipSendToGallery.displayName = 'TooltipSendToGallery';
|
||||
|
||||
const TooltipSendToCanvas = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Flex flexDir="column">
|
||||
<Text fontWeight="semibold">{t('controlLayers.sendToCanvas')}</Text>
|
||||
<Text fontWeight="normal">{t('controlLayers.sendToCanvasDesc')}</Text>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
TooltipSendToCanvas.displayName = 'TooltipSendToCanvas';
|
||||
|
||||
const selectSendToCanvas = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.sendToCanvas);
|
||||
import { PiCaretDownBold, PiCheckBold } from 'react-icons/pi';
|
||||
|
||||
export const CanvasSendToToggle = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const sendToCanvas = useAppSelector(selectSendToCanvas);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onChange = useCallback(
|
||||
(isChecked: boolean) => {
|
||||
dispatch(settingsSendToCanvasChanged(isChecked));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const enableSendToCanvas = useCallback(() => {
|
||||
dispatch(settingsSendToCanvasChanged(true));
|
||||
}, [dispatch]);
|
||||
|
||||
const disableSendToCanvas = useCallback(() => {
|
||||
dispatch(settingsSendToCanvasChanged(false));
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<IconSwitch
|
||||
isChecked={sendToCanvas}
|
||||
onChange={onChange}
|
||||
iconUnchecked={<PiImageBold />}
|
||||
tooltipUnchecked={<TooltipSendToGallery />}
|
||||
iconChecked={<PiPaintBrushBold />}
|
||||
tooltipChecked={<TooltipSendToCanvas />}
|
||||
ariaLabel="Toggle canvas mode"
|
||||
/>
|
||||
<Popover isLazy>
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="link"
|
||||
data-testid="toggle-viewer-menu-button"
|
||||
pointerEvents="auto"
|
||||
rightIcon={<PiCaretDownBold />}
|
||||
>
|
||||
{sendToCanvas ? t('controlLayers.sendingToCanvas') : t('controlLayers.sendingToGallery')}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent p={2} pointerEvents="auto">
|
||||
<PopoverArrow />
|
||||
<PopoverBody>
|
||||
<Flex flexDir="column">
|
||||
<Button onClick={disableSendToCanvas} variant="ghost" h="auto" w="auto" p={2}>
|
||||
<Flex gap={2} w="full">
|
||||
<Icon as={PiCheckBold} visibility={!sendToCanvas ? 'visible' : 'hidden'} />
|
||||
<Flex flexDir="column" gap={2} alignItems="flex-start">
|
||||
<Text fontWeight="semibold">{t('controlLayers.sendToGallery')}</Text>
|
||||
<Text fontWeight="normal" variant="subtext">
|
||||
{t('controlLayers.sendToGalleryDesc')}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Button>
|
||||
<Button onClick={enableSendToCanvas} variant="ghost" h="auto" w="auto" p={2}>
|
||||
<Flex gap={2} w="full">
|
||||
<Icon as={PiCheckBold} visibility={sendToCanvas ? 'visible' : 'hidden'} />
|
||||
<Flex flexDir="column" gap={2} alignItems="flex-start">
|
||||
<Text fontWeight="semibold">{t('controlLayers.sendToCanvas')}</Text>
|
||||
<Text fontWeight="normal" variant="subtext">
|
||||
{t('controlLayers.sendToCanvasDesc')}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Button>
|
||||
</Flex>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Transform } from 'features/controlLayers/components/Transform/Transform
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { memo, useRef } from 'react';
|
||||
|
||||
export const CanvasEditor = memo(() => {
|
||||
export const CanvasTabContent = memo(() => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useScopeOnFocus('canvas', ref);
|
||||
|
||||
@@ -48,4 +48,4 @@ export const CanvasEditor = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
CanvasEditor.displayName = 'CanvasEditor';
|
||||
CanvasTabContent.displayName = 'CanvasTabContent';
|
||||
@@ -85,7 +85,7 @@ export const IPAdapterImagePreview = memo(
|
||||
/>
|
||||
|
||||
{controlImage && (
|
||||
<Flex position="absolute" flexDir="column" top={1} insetInlineEnd={1} gap={1}>
|
||||
<Flex position="absolute" flexDir="column" top={2} insetInlineEnd={2} gap={1}>
|
||||
<IAIDndImageIcon
|
||||
onClick={handleResetControlImage}
|
||||
icon={<PiArrowCounterClockwiseBold size={16} />}
|
||||
|
||||
@@ -13,8 +13,6 @@ import { useCanvasEntityQuickSwitchHotkey } from 'features/controlLayers/hooks/u
|
||||
import { useCanvasResetLayerHotkey } from 'features/controlLayers/hooks/useCanvasResetLayerHotkey';
|
||||
import { useCanvasUndoRedoHotkeys } from 'features/controlLayers/hooks/useCanvasUndoRedoHotkeys';
|
||||
import { useNextPrevEntityHotkeys } from 'features/controlLayers/hooks/useNextPrevEntity';
|
||||
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
|
||||
import { ViewerToggle } from 'features/gallery/components/ImageViewer/ViewerToggleMenu';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const CanvasToolbar = memo(() => {
|
||||
@@ -27,7 +25,6 @@ export const CanvasToolbar = memo(() => {
|
||||
return (
|
||||
<CanvasManagerProviderGate>
|
||||
<Flex w="full" gap={2} alignItems="center">
|
||||
<ToggleProgressButton />
|
||||
<ToolChooser />
|
||||
<Spacer />
|
||||
<ToolSettings />
|
||||
@@ -38,7 +35,6 @@ export const CanvasToolbar = memo(() => {
|
||||
<ToolColorPicker />
|
||||
<CanvasToolbarSaveToGalleryButton />
|
||||
<CanvasSettingsPopover />
|
||||
<ViewerToggle />
|
||||
</Flex>
|
||||
</CanvasManagerProviderGate>
|
||||
);
|
||||
|
||||
@@ -160,3 +160,4 @@ export const selectDynamicGrid = createCanvasSettingsSelector((settings) => sett
|
||||
export const selectShowHUD = createCanvasSettingsSelector((settings) => settings.showHUD);
|
||||
export const selectAutoProcessFilter = createCanvasSettingsSelector((settings) => settings.autoProcessFilter);
|
||||
export const selectSnapToGrid = createCanvasSettingsSelector((settings) => settings.snapToGrid);
|
||||
export const selectSendToCanvas = createCanvasSettingsSelector((canvasSettings) => canvasSettings.sendToCanvas);
|
||||
|
||||
@@ -35,6 +35,7 @@ const dragOverlayStyles: CSSProperties = {
|
||||
width: 'min-content',
|
||||
height: 'min-content',
|
||||
cursor: 'grabbing',
|
||||
pointerEvents: 'none',
|
||||
userSelect: 'none',
|
||||
// expand overlay to prevent cursor from going outside it and displaying
|
||||
padding: '10rem',
|
||||
|
||||
@@ -14,12 +14,14 @@ import {
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useGallerySearchTerm } from 'features/gallery/components/ImageGrid/useGallerySearchTerm';
|
||||
import { selectSelectedBoardId } from 'features/gallery/store/gallerySelectors';
|
||||
import { galleryViewChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import CurrentImageButtons from 'features/gallery/components/ImageViewer/CurrentImageButtons';
|
||||
import CurrentImagePreview from 'features/gallery/components/ImageViewer/CurrentImagePreview';
|
||||
import { selectIsMiniViewerOpen, selectSelectedBoardId } from 'features/gallery/store/gallerySelectors';
|
||||
import { galleryViewChanged, isMiniViewerOpenToggled, selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiMagnifyingGlassBold } from 'react-icons/pi';
|
||||
import { PiEyeBold, PiEyeClosedBold, PiMagnifyingGlassBold } from 'react-icons/pi';
|
||||
import { useBoardName } from 'services/api/hooks/useBoardName';
|
||||
|
||||
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
|
||||
@@ -38,7 +40,7 @@ const SELECTED_STYLES: ChakraProps['sx'] = {
|
||||
color: 'invokeBlue.300',
|
||||
};
|
||||
|
||||
const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 };
|
||||
const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0, width: '100%' };
|
||||
|
||||
const selectGalleryView = createSelector(selectGallerySlice, (gallery) => gallery.galleryView);
|
||||
const selectSearchTerm = createSelector(selectGallerySlice, (gallery) => gallery.searchTerm);
|
||||
@@ -50,7 +52,11 @@ export const Gallery = () => {
|
||||
const initialSearchTerm = useAppSelector(selectSearchTerm);
|
||||
const searchDisclosure = useDisclosure({ defaultIsOpen: initialSearchTerm.length > 0 });
|
||||
const [searchTerm, onChangeSearchTerm, onResetSearchTerm] = useGallerySearchTerm();
|
||||
const isMiniViewerOpen = useAppSelector(selectIsMiniViewerOpen);
|
||||
|
||||
const toggleMiniViewer = useCallback(() => {
|
||||
dispatch(isMiniViewerOpenToggled());
|
||||
}, [dispatch]);
|
||||
const handleClickImages = useCallback(() => {
|
||||
dispatch(galleryViewChanged('images'));
|
||||
}, [dispatch]);
|
||||
@@ -68,7 +74,7 @@ export const Gallery = () => {
|
||||
const boardName = useBoardName(selectedBoardId);
|
||||
|
||||
return (
|
||||
<Flex flexDirection="column" alignItems="center" justifyContent="space-between" h="full" w="full" pt={1}>
|
||||
<Flex flexDirection="column" alignItems="center" justifyContent="space-between" h="full" w="full" pt={1} minH={0}>
|
||||
<Tabs index={galleryView === 'images' ? 0 : 1} variant="enclosed" display="flex" flexDir="column" w="full">
|
||||
<TabList gap={2} fontSize="sm" borderColor="base.800" alignItems="center" w="full">
|
||||
<Text fontSize="sm" fontWeight="semibold" noOfLines={1} px="2" wordBreak="break-all">
|
||||
@@ -81,28 +87,54 @@ export const Gallery = () => {
|
||||
<Tab sx={BASE_STYLES} _selected={SELECTED_STYLES} onClick={handleClickAssets} data-testid="assets-tab">
|
||||
{t('gallery.assets')}
|
||||
</Tab>
|
||||
<IconButton
|
||||
onClick={handleClickSearch}
|
||||
tooltip={searchDisclosure.isOpen ? `${t('gallery.exitSearch')}` : `${t('gallery.displaySearch')}`}
|
||||
aria-label={t('gallery.displaySearch')}
|
||||
icon={<PiMagnifyingGlassBold />}
|
||||
colorScheme={searchDisclosure.isOpen ? 'invokeBlue' : 'base'}
|
||||
variant="link"
|
||||
/>
|
||||
<Flex h="full">
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
onClick={toggleMiniViewer}
|
||||
tooltip={t('gallery.toggleMiniViewer')}
|
||||
aria-label={t('gallery.toggleMiniViewer')}
|
||||
icon={isMiniViewerOpen ? <PiEyeBold /> : <PiEyeClosedBold />}
|
||||
colorScheme={isMiniViewerOpen ? 'invokeBlue' : 'base'}
|
||||
/>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
onClick={handleClickSearch}
|
||||
tooltip={searchDisclosure.isOpen ? `${t('gallery.exitSearch')}` : `${t('gallery.displaySearch')}`}
|
||||
aria-label={t('gallery.displaySearch')}
|
||||
icon={<PiMagnifyingGlassBold />}
|
||||
/>
|
||||
</Flex>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
|
||||
<Box w="full">
|
||||
<Collapse in={searchDisclosure.isOpen} style={COLLAPSE_STYLES}>
|
||||
<Box w="full" pt={2}>
|
||||
<GallerySearch
|
||||
searchTerm={searchTerm}
|
||||
onChangeSearchTerm={onChangeSearchTerm}
|
||||
onResetSearchTerm={onResetSearchTerm}
|
||||
/>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Box>
|
||||
<Collapse in={searchDisclosure.isOpen} style={COLLAPSE_STYLES}>
|
||||
<Box w="full" pt={2}>
|
||||
<GallerySearch
|
||||
searchTerm={searchTerm}
|
||||
onChangeSearchTerm={onChangeSearchTerm}
|
||||
onResetSearchTerm={onResetSearchTerm}
|
||||
/>
|
||||
</Box>
|
||||
</Collapse>
|
||||
<Collapse in={isMiniViewerOpen} style={COLLAPSE_STYLES}>
|
||||
<Box position="relative" w="full" mt={2} aspectRatio="1/1">
|
||||
<CurrentImagePreview />
|
||||
<Flex
|
||||
position="absolute"
|
||||
top={2}
|
||||
gap={2}
|
||||
justifyContent="space-between"
|
||||
left="50%"
|
||||
transform="translateX(-50%)"
|
||||
>
|
||||
<CurrentImageButtons />
|
||||
</Flex>
|
||||
</Box>
|
||||
</Collapse>
|
||||
<GalleryImageGrid />
|
||||
<GalleryPagination />
|
||||
</Flex>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, Button, Collapse, Divider, Flex, IconButton, useDisclosure } from '@invoke-ai/ui-library';
|
||||
import { Box, Button, Collapse, Divider, Flex, IconButton, Spacer, useDisclosure } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
|
||||
import { GalleryHeader } from 'features/gallery/components/GalleryHeader';
|
||||
@@ -60,36 +60,31 @@ const GalleryPanelContent = () => {
|
||||
|
||||
return (
|
||||
<Flex ref={ref} position="relative" flexDirection="column" h="full" w="full" tabIndex={-1}>
|
||||
<Flex alignItems="center" gap={0}>
|
||||
<GalleryHeader />
|
||||
<Flex alignItems="center" justifyContent="space-between" w="full">
|
||||
<Button
|
||||
<GalleryHeader />
|
||||
<Flex alignItems="center" w="full">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={handleToggleBoardPanel}
|
||||
rightIcon={boardsListPanel.isCollapsed ? <PiCaretDownBold /> : <PiCaretUpBold />}
|
||||
>
|
||||
{boardsListPanel.isCollapsed ? t('boards.viewBoards') : t('boards.hideBoards')}
|
||||
</Button>
|
||||
<Spacer />
|
||||
<Flex h="full">
|
||||
<GallerySettingsPopover />
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={handleToggleBoardPanel}
|
||||
rightIcon={boardsListPanel.isCollapsed ? <PiCaretDownBold /> : <PiCaretUpBold />}
|
||||
>
|
||||
{boardsListPanel.isCollapsed ? t('boards.viewBoards') : t('boards.hideBoards')}
|
||||
</Button>
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<GallerySettingsPopover />
|
||||
<Flex>
|
||||
<IconButton
|
||||
w="full"
|
||||
h="full"
|
||||
onClick={handleClickBoardSearch}
|
||||
tooltip={
|
||||
boardSearchDisclosure.isOpen
|
||||
? `${t('gallery.exitBoardSearch')}`
|
||||
: `${t('gallery.displayBoardSearch')}`
|
||||
}
|
||||
aria-label={t('gallery.displayBoardSearch')}
|
||||
icon={<PiMagnifyingGlassBold />}
|
||||
colorScheme={boardSearchDisclosure.isOpen ? 'invokeBlue' : 'base'}
|
||||
variant="link"
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
onClick={handleClickBoardSearch}
|
||||
tooltip={
|
||||
boardSearchDisclosure.isOpen ? `${t('gallery.exitBoardSearch')}` : `${t('gallery.displayBoardSearch')}`
|
||||
}
|
||||
aria-label={t('gallery.displayBoardSearch')}
|
||||
icon={<PiMagnifyingGlassBold />}
|
||||
colorScheme={boardSearchDisclosure.isOpen ? 'invokeBlue' : 'base'}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
|
||||
@@ -17,7 +17,13 @@ const GallerySettingsPopover = () => {
|
||||
return (
|
||||
<Popover isLazy>
|
||||
<PopoverTrigger>
|
||||
<IconButton aria-label={t('gallery.gallerySettings')} icon={<RiSettings4Fill />} variant="link" h="full" />
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
aria-label={t('gallery.gallerySettings')}
|
||||
icon={<RiSettings4Fill />}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<PopoverBody>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { imageSelected, imageToCompareChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiEyeBold } from 'react-icons/pi';
|
||||
@@ -11,13 +11,12 @@ export const ImageMenuItemOpenInViewer = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const imageViewer = useImageViewer();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(imageToCompareChanged(null));
|
||||
dispatch(imageSelected(imageDTO));
|
||||
imageViewer.onOpen();
|
||||
}, [dispatch, imageDTO, imageViewer]);
|
||||
dispatch(setActiveTab('gallery'));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
return (
|
||||
<MenuItem icon={<PiEyeBold />} onClick={onClick}>
|
||||
|
||||
@@ -5,7 +5,6 @@ import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { sentImageToCanvas } from 'features/gallery/store/actions';
|
||||
import { toast } from 'features/toast/toast';
|
||||
@@ -21,7 +20,6 @@ export const ImageMenuItemSendToCanvas = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const bboxRect = useAppSelector(selectBboxRect);
|
||||
const imageViewer = useImageViewer();
|
||||
|
||||
const handleSendToCanvas = useCallback(() => {
|
||||
const imageObject = imageDTOToImageObject(imageDTO);
|
||||
@@ -32,13 +30,12 @@ export const ImageMenuItemSendToCanvas = memo(() => {
|
||||
dispatch(sentImageToCanvas());
|
||||
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
|
||||
dispatch(setActiveTab('generation'));
|
||||
imageViewer.onClose();
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [bboxRect.x, bboxRect.y, dispatch, imageDTO, imageViewer, t]);
|
||||
}, [bboxRect.x, bboxRect.y, dispatch, imageDTO, t]);
|
||||
|
||||
return (
|
||||
<MenuItem icon={<PiShareFatBold />} onClickCapture={handleSendToCanvas} id="send-to-canvas">
|
||||
|
||||
@@ -13,11 +13,8 @@ import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid
|
||||
import { useMultiselect } from 'features/gallery/hooks/useMultiselect';
|
||||
import { useScrollIntoView } from 'features/gallery/hooks/useScrollIntoView';
|
||||
import { selectSelectedBoardId } from 'features/gallery/store/gallerySelectors';
|
||||
import {
|
||||
imageToCompareChanged,
|
||||
isImageViewerOpenChanged,
|
||||
selectGallerySlice,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { imageToCompareChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import type { MouseEvent, MouseEventHandler } from 'react';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -116,7 +113,7 @@ const GalleryImage = ({ index, imageDTO }: HoverableImageProps) => {
|
||||
}, []);
|
||||
|
||||
const onDoubleClick = useCallback(() => {
|
||||
dispatch(isImageViewerOpenChanged(true));
|
||||
dispatch(setActiveTab('gallery'));
|
||||
dispatch(imageToCompareChanged(null));
|
||||
}, [dispatch]);
|
||||
|
||||
@@ -150,7 +147,7 @@ const GalleryImage = ({ index, imageDTO }: HoverableImageProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box w="full" h="full" p={1.5} className={GALLERY_IMAGE_CLASS_NAME} data-testid={dataTestId} sx={boxSx}>
|
||||
<Box w="full" h="full" className={GALLERY_IMAGE_CLASS_NAME} data-testid={dataTestId} sx={boxSx}>
|
||||
<Flex
|
||||
ref={imageContainerRef}
|
||||
userSelect="none"
|
||||
@@ -183,13 +180,12 @@ const GalleryImage = ({ index, imageDTO }: HoverableImageProps) => {
|
||||
color="base.50"
|
||||
fontSize="sm"
|
||||
fontWeight="semibold"
|
||||
bottom={0}
|
||||
left={0}
|
||||
bottom={1}
|
||||
left={1}
|
||||
opacity={0.7}
|
||||
px={2}
|
||||
lineHeight={1.25}
|
||||
borderTopEndRadius="base"
|
||||
borderBottomStartRadius="base"
|
||||
sx={badgeSx}
|
||||
pointerEvents="none"
|
||||
>{`${imageDTO.width}x${imageDTO.height}`}</Text>
|
||||
@@ -199,8 +195,8 @@ const GalleryImage = ({ index, imageDTO }: HoverableImageProps) => {
|
||||
icon={starIcon}
|
||||
tooltip={starTooltip}
|
||||
position="absolute"
|
||||
top={1}
|
||||
insetInlineEnd={1}
|
||||
top={2}
|
||||
insetInlineEnd={2}
|
||||
/>
|
||||
|
||||
{isHovered && <DeleteIcon onClick={handleDelete} />}
|
||||
@@ -227,8 +223,8 @@ const DeleteIcon = ({ onClick }: { onClick: MouseEventHandler }) => {
|
||||
icon={<PiTrashSimpleFill size="16px" />}
|
||||
tooltip={t('gallery.deleteImage_one')}
|
||||
position="absolute"
|
||||
bottom={1}
|
||||
insetInlineEnd={1}
|
||||
bottom={2}
|
||||
insetInlineEnd={2}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -78,24 +78,52 @@ const Content = () => {
|
||||
// Managing refs for dynamically rendered components is a bit tedious:
|
||||
// - https://react.dev/learn/manipulating-the-dom-with-refs#how-to-manage-a-list-of-refs-using-a-ref-callback
|
||||
// As a easy workaround, we can just grab the first gallery image element directly.
|
||||
const galleryImageEl = document.querySelector(`.${GALLERY_IMAGE_CLASS_NAME}`);
|
||||
if (!galleryImageEl) {
|
||||
const imageEl = document.querySelector(`.${GALLERY_IMAGE_CLASS_NAME}`);
|
||||
if (!imageEl) {
|
||||
// No images in gallery?
|
||||
return;
|
||||
}
|
||||
|
||||
const galleryImageRect = galleryImageEl.getBoundingClientRect();
|
||||
const gridEl = document.querySelector(`.${GALLERY_GRID_CLASS_NAME}`);
|
||||
|
||||
if (!gridEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageRect = imageEl.getBoundingClientRect();
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
|
||||
if (!galleryImageRect.width || !galleryImageRect.height || !containerRect.width || !containerRect.height) {
|
||||
// We need to account for the gap between images
|
||||
const gridElStyle = window.getComputedStyle(gridEl);
|
||||
const gap = parseFloat(gridElStyle.gap);
|
||||
|
||||
if (!imageRect.width || !imageRect.height || !containerRect.width || !containerRect.height) {
|
||||
// Gallery is too small to fit images or not rendered yet
|
||||
return;
|
||||
}
|
||||
|
||||
// Floating-point precision requires we round to get the correct number of images per row
|
||||
const imagesPerRow = Math.round(containerRect.width / galleryImageRect.width);
|
||||
// However, when calculating the number of images per column, we want to floor the value to not overflow the container
|
||||
const imagesPerColumn = Math.floor(containerRect.height / galleryImageRect.height);
|
||||
let imagesPerColumn = 0;
|
||||
let spaceUsed = 0;
|
||||
|
||||
while (spaceUsed + imageRect.height <= containerRect.height) {
|
||||
imagesPerColumn++; // Increment the number of images
|
||||
spaceUsed += imageRect.height; // Add image size to the used space
|
||||
if (spaceUsed + gap <= containerRect.height) {
|
||||
spaceUsed += gap; // Add gap size to the used space after each image except after the last image
|
||||
}
|
||||
}
|
||||
|
||||
let imagesPerRow = 0;
|
||||
spaceUsed = 0;
|
||||
|
||||
while (spaceUsed + imageRect.width <= containerRect.width) {
|
||||
imagesPerRow++; // Increment the number of images
|
||||
spaceUsed += imageRect.width; // Add image size to the used space
|
||||
if (spaceUsed + gap <= containerRect.width) {
|
||||
spaceUsed += gap; // Add gap size to the used space after each image except after the last image
|
||||
}
|
||||
}
|
||||
|
||||
// Always load at least 1 row of images
|
||||
const limit = Math.max(imagesPerRow, imagesPerRow * imagesPerColumn);
|
||||
dispatch(limitChanged(limit));
|
||||
@@ -139,6 +167,7 @@ const Content = () => {
|
||||
<Grid
|
||||
className={GALLERY_GRID_CLASS_NAME}
|
||||
gridTemplateColumns={`repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, 1fr))`}
|
||||
gap={1}
|
||||
>
|
||||
{imageDTOs.map((imageDTO, index) => (
|
||||
<GalleryImage key={imageDTO.image_name} imageDTO={imageDTO} index={index} />
|
||||
|
||||
@@ -4,13 +4,13 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { $activeScopes } from 'common/hooks/interactionScopes';
|
||||
import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages';
|
||||
import { selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { $isGalleryPanelOpen } from 'features/ui/store/uiSlice';
|
||||
import { $isRightPanelOpen } from 'features/ui/store/uiSlice';
|
||||
import { computed } from 'nanostores';
|
||||
import { useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const $isSelectAllEnabled = computed([$activeScopes, $isGalleryPanelOpen], (activeScopes, isGalleryPanelOpen) => {
|
||||
const $isSelectAllEnabled = computed([$activeScopes, $isRightPanelOpen], (activeScopes, isGalleryPanelOpen) => {
|
||||
return activeScopes.has('gallery') && !activeScopes.has('workflows') && isGalleryPanelOpen;
|
||||
});
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@ export const ImageViewer = memo(() => {
|
||||
<Flex
|
||||
ref={ref}
|
||||
tabIndex={-1}
|
||||
layerStyle="body"
|
||||
layerStyle="first"
|
||||
p={2}
|
||||
borderRadius="base"
|
||||
position="absolute"
|
||||
flexDirection="column"
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { IconSwitch } from 'common/components/IconSwitch';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiEyeBold, PiPencilBold } from 'react-icons/pi';
|
||||
|
||||
const TooltipEdit = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Flex flexDir="column">
|
||||
<Text fontWeight="semibold">{t('common.edit')}</Text>
|
||||
<Text fontWeight="normal">{t('common.editDesc')}</Text>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
TooltipEdit.displayName = 'TooltipEdit';
|
||||
|
||||
const TooltipView = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Flex flexDir="column">
|
||||
<Text fontWeight="semibold">{t('common.view')}</Text>
|
||||
<Text fontWeight="normal">{t('common.viewDesc')}</Text>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
TooltipView.displayName = 'TooltipView';
|
||||
|
||||
export const ViewerToggle = memo(() => {
|
||||
const imageViewer = useImageViewer();
|
||||
useHotkeys('z', imageViewer.onToggle, [imageViewer]);
|
||||
useHotkeys('esc', imageViewer.onClose, [imageViewer]);
|
||||
const onChange = useCallback(
|
||||
(isChecked: boolean) => {
|
||||
if (isChecked) {
|
||||
imageViewer.onClose();
|
||||
} else {
|
||||
imageViewer.onOpen();
|
||||
}
|
||||
},
|
||||
[imageViewer]
|
||||
);
|
||||
|
||||
return (
|
||||
<IconSwitch
|
||||
isChecked={!imageViewer.isOpen}
|
||||
onChange={onChange}
|
||||
iconUnchecked={<PiEyeBold />}
|
||||
tooltipUnchecked={<TooltipView />}
|
||||
iconChecked={<PiPencilBold />}
|
||||
tooltipChecked={<TooltipEdit />}
|
||||
ariaLabel="Toggle viewer"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
ViewerToggle.displayName = 'ViewerToggle';
|
||||
@@ -1,23 +1,11 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton';
|
||||
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo } from 'react';
|
||||
|
||||
import CurrentImageButtons from './CurrentImageButtons';
|
||||
import { ViewerToggle } from './ViewerToggleMenu';
|
||||
|
||||
const selectShowToggle = createSelector(selectActiveTab, (tab) => {
|
||||
if (tab === 'upscaling' || tab === 'workflows') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
export const ViewerToolbar = memo(() => {
|
||||
const showToggle = useAppSelector(selectShowToggle);
|
||||
return (
|
||||
<Flex w="full" gap={2}>
|
||||
<Flex flex={1} justifyContent="center">
|
||||
@@ -30,9 +18,7 @@ export const ViewerToolbar = memo(() => {
|
||||
<CurrentImageButtons />
|
||||
</Flex>
|
||||
<Flex flex={1} justifyContent="center">
|
||||
<Flex gap={2} marginInlineStart="auto">
|
||||
{showToggle && <ViewerToggle />}
|
||||
</Flex>
|
||||
<Flex gap={2} marginInlineStart="auto" />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectHasImageToCompare, selectIsImageViewerOpen } from 'features/gallery/store/gallerySelectors';
|
||||
import {
|
||||
imageToCompareChanged,
|
||||
isImageViewerOpenChanged,
|
||||
selectGallerySlice,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
|
||||
import { selectUiSlice } from 'features/ui/store/uiSlice';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
const selectIsOpen = createSelector(selectUiSlice, selectWorkflowSlice, selectGallerySlice, (ui, workflow, gallery) => {
|
||||
const tab = ui.activeTab;
|
||||
const workflowsMode = workflow.mode;
|
||||
if (tab === 'models' || tab === 'queue') {
|
||||
return false;
|
||||
}
|
||||
if (tab === 'workflows' && workflowsMode === 'edit') {
|
||||
return false;
|
||||
}
|
||||
if (tab === 'workflows' && workflowsMode === 'view') {
|
||||
return true;
|
||||
}
|
||||
if (tab === 'upscaling') {
|
||||
return true;
|
||||
}
|
||||
return gallery.isImageViewerOpen;
|
||||
});
|
||||
|
||||
export const useIsImageViewerOpen = () => {
|
||||
const isOpen = useAppSelector(selectIsOpen);
|
||||
return isOpen;
|
||||
};
|
||||
|
||||
const selectIsForcedOpen = createSelector(selectUiSlice, selectWorkflowSlice, (ui, workflow) => {
|
||||
return ui.activeTab === 'upscaling' || (ui.activeTab === 'workflows' && workflow.mode === 'view');
|
||||
});
|
||||
|
||||
export const useImageViewer = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const isComparing = useAppSelector(selectHasImageToCompare);
|
||||
const isNaturallyOpen = useAppSelector(selectIsImageViewerOpen);
|
||||
const isForcedOpen = useAppSelector(selectIsForcedOpen);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
if (isForcedOpen) {
|
||||
return;
|
||||
}
|
||||
if (isComparing && isNaturallyOpen) {
|
||||
dispatch(imageToCompareChanged(null));
|
||||
} else {
|
||||
dispatch(isImageViewerOpenChanged(false));
|
||||
}
|
||||
}, [dispatch, isComparing, isForcedOpen, isNaturallyOpen]);
|
||||
|
||||
const onOpen = useCallback(() => {
|
||||
dispatch(isImageViewerOpenChanged(true));
|
||||
}, [dispatch]);
|
||||
|
||||
const onToggle = useCallback(() => {
|
||||
if (isForcedOpen) {
|
||||
return;
|
||||
}
|
||||
if (isComparing && isNaturallyOpen) {
|
||||
dispatch(imageToCompareChanged(null));
|
||||
} else {
|
||||
dispatch(isImageViewerOpenChanged(!isNaturallyOpen));
|
||||
}
|
||||
}, [dispatch, isComparing, isForcedOpen, isNaturallyOpen]);
|
||||
|
||||
return { isOpen: isNaturallyOpen || isForcedOpen, onOpen, onClose, onToggle, isComparing };
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { useGalleryNavigation } from 'features/gallery/hooks/useGalleryNavigation';
|
||||
import { useGalleryPagination } from 'features/gallery/hooks/useGalleryPagination';
|
||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { $isGalleryPanelOpen } from 'features/ui/store/uiSlice';
|
||||
import { $isRightPanelOpen } from 'features/ui/store/uiSlice';
|
||||
import { computed } from 'nanostores';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useListImagesQuery } from 'services/api/endpoints/images';
|
||||
@@ -15,7 +15,7 @@ const $leftRightHotkeysEnabled = computed($activeScopes, (activeScopes) => {
|
||||
return !activeScopes.has('canvas') || activeScopes.has('imageViewer');
|
||||
});
|
||||
|
||||
const $upDownHotkeysEnabled = computed([$activeScopes, $isGalleryPanelOpen], (activeScopes, isGalleryPanelOpen) => {
|
||||
const $upDownHotkeysEnabled = computed([$activeScopes, $isRightPanelOpen], (activeScopes, isGalleryPanelOpen) => {
|
||||
// The up and down hotkeys can be used when the gallery is focused and the canvas is not focused, and the gallery panel is open.
|
||||
return !activeScopes.has('canvas') && isGalleryPanelOpen;
|
||||
});
|
||||
|
||||
@@ -57,4 +57,4 @@ export const selectImageToCompare = createSelector(selectGallerySlice, (gallery)
|
||||
export const selectHasImageToCompare = createSelector(selectImageToCompare, (imageToCompare) =>
|
||||
Boolean(imageToCompare)
|
||||
);
|
||||
export const selectIsImageViewerOpen = createSelector(selectGallerySlice, (gallery) => gallery.isImageViewerOpen);
|
||||
export const selectIsMiniViewerOpen = createSelector(selectGallerySlice, (gallery) => gallery.isMiniViewerOpen);
|
||||
|
||||
@@ -21,11 +21,11 @@ const initialGalleryState: GalleryState = {
|
||||
starredFirst: true,
|
||||
orderDir: 'DESC',
|
||||
searchTerm: '',
|
||||
isImageViewerOpen: true,
|
||||
imageToCompare: null,
|
||||
comparisonMode: 'slider',
|
||||
comparisonFit: 'fill',
|
||||
shouldShowArchivedBoards: false,
|
||||
isMiniViewerOpen: false,
|
||||
};
|
||||
|
||||
export const gallerySlice = createSlice({
|
||||
@@ -40,9 +40,6 @@ export const gallerySlice = createSlice({
|
||||
},
|
||||
imageToCompareChanged: (state, action: PayloadAction<ImageDTO | null>) => {
|
||||
state.imageToCompare = action.payload;
|
||||
if (action.payload) {
|
||||
state.isImageViewerOpen = true;
|
||||
}
|
||||
},
|
||||
comparisonModeChanged: (state, action: PayloadAction<ComparisonMode>) => {
|
||||
state.comparisonMode = action.payload;
|
||||
@@ -91,8 +88,8 @@ export const gallerySlice = createSlice({
|
||||
alwaysShowImageSizeBadgeChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.alwaysShowImageSizeBadge = action.payload;
|
||||
},
|
||||
isImageViewerOpenChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.isImageViewerOpen = action.payload;
|
||||
isMiniViewerOpenToggled: (state) => {
|
||||
state.isMiniViewerOpen = !state.isMiniViewerOpen;
|
||||
},
|
||||
comparedImagesSwapped: (state) => {
|
||||
if (state.imageToCompare) {
|
||||
@@ -138,7 +135,6 @@ export const {
|
||||
selectionChanged,
|
||||
boardSearchTextChanged,
|
||||
alwaysShowImageSizeBadgeChanged,
|
||||
isImageViewerOpenChanged,
|
||||
imageToCompareChanged,
|
||||
comparisonModeChanged,
|
||||
comparedImagesSwapped,
|
||||
@@ -150,6 +146,7 @@ export const {
|
||||
starredFirstChanged,
|
||||
shouldShowArchivedBoardsChanged,
|
||||
searchTermChanged,
|
||||
isMiniViewerOpenToggled,
|
||||
} = gallerySlice.actions;
|
||||
|
||||
export const selectGallerySlice = (state: RootState) => state.gallery;
|
||||
@@ -166,13 +163,5 @@ export const galleryPersistConfig: PersistConfig<GalleryState> = {
|
||||
name: gallerySlice.name,
|
||||
initialState: initialGalleryState,
|
||||
migrate: migrateGalleryState,
|
||||
persistDenylist: [
|
||||
'selection',
|
||||
'selectedBoardId',
|
||||
'galleryView',
|
||||
'offset',
|
||||
'limit',
|
||||
'isImageViewerOpen',
|
||||
'imageToCompare',
|
||||
],
|
||||
persistDenylist: ['selection', 'selectedBoardId', 'galleryView', 'offset', 'limit', 'imageToCompare'],
|
||||
};
|
||||
|
||||
@@ -27,6 +27,6 @@ export type GalleryState = {
|
||||
imageToCompare: ImageDTO | null;
|
||||
comparisonMode: ComparisonMode;
|
||||
comparisonFit: ComparisonFit;
|
||||
isImageViewerOpen: boolean;
|
||||
shouldShowArchivedBoards: boolean;
|
||||
isMiniViewerOpen: boolean;
|
||||
};
|
||||
|
||||
@@ -2,12 +2,13 @@ import 'reactflow/dist/style.css';
|
||||
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
|
||||
import { AddNodeCmdk } from 'features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk';
|
||||
import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel';
|
||||
import WorkflowEditorSettings from 'features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings';
|
||||
import { LoadWorkflowFromGraphModal } from 'features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal';
|
||||
import { SaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/SaveWorkflowAsDialog';
|
||||
import { memo } from 'react';
|
||||
import { memo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MdDeviceHub } from 'react-icons/md';
|
||||
import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
|
||||
@@ -19,8 +20,13 @@ import MinimapPanel from './flow/panels/MinimapPanel/MinimapPanel';
|
||||
const NodeEditor = () => {
|
||||
const { data, isLoading } = useGetOpenAPISchemaQuery();
|
||||
const { t } = useTranslation();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useScopeOnFocus('workflows', ref);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
tabIndex={-1}
|
||||
ref={ref}
|
||||
layerStyle="first"
|
||||
position="relative"
|
||||
width="full"
|
||||
|
||||
@@ -34,5 +34,5 @@ export const BboxSettings = memo(() => {
|
||||
BboxSettings.displayName = 'BboxSettings';
|
||||
|
||||
const formLabelProps: FormLabelProps = {
|
||||
minW: 14,
|
||||
minW: 10,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||
import { selectImg2imgStrength, setImg2imgStrength } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectImg2imgStrengthConfig } from 'features/system/store/configSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const marks = [0, 0.5, 1];
|
||||
|
||||
export const ParamDenoisingStrength = memo(() => {
|
||||
const img2imgStrength = useAppSelector(selectImg2imgStrength);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onChange = useCallback(
|
||||
(v: number) => {
|
||||
dispatch(setImg2imgStrength(v));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const config = useAppSelector(selectImg2imgStrengthConfig);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<FormControl>
|
||||
<InformationalPopover feature="paramDenoisingStrength">
|
||||
<FormLabel>{`${t('parameters.denoisingStrength')}`}</FormLabel>
|
||||
</InformationalPopover>
|
||||
<CompositeSlider
|
||||
step={config.coarseStep}
|
||||
fineStep={config.fineStep}
|
||||
min={config.sliderMin}
|
||||
max={config.sliderMax}
|
||||
defaultValue={config.initial}
|
||||
onChange={onChange}
|
||||
value={img2imgStrength}
|
||||
marks={marks}
|
||||
/>
|
||||
<CompositeNumberInput
|
||||
step={config.coarseStep}
|
||||
fineStep={config.fineStep}
|
||||
min={config.numberInputMin}
|
||||
max={config.numberInputMax}
|
||||
defaultValue={config.initial}
|
||||
onChange={onChange}
|
||||
value={img2imgStrength}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
});
|
||||
|
||||
ParamDenoisingStrength.displayName = 'ParamDenoisingStrength';
|
||||
@@ -18,7 +18,7 @@ export const ParamSeedRandomize = memo(() => {
|
||||
|
||||
return (
|
||||
<FormControl w="min-content">
|
||||
<FormLabel>{t('common.random')}</FormLabel>
|
||||
<FormLabel m={0}>{t('common.random')}</FormLabel>
|
||||
<Switch isChecked={shouldRandomizeSeed} onChange={handleChangeShouldRandomizeSeed} />
|
||||
</FormControl>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { Box, Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
|
||||
@@ -40,16 +40,18 @@ const ParamVAEModelSelect = () => {
|
||||
return (
|
||||
<FormControl isDisabled={!options.length} isInvalid={!options.length}>
|
||||
<InformationalPopover feature="paramVAE">
|
||||
<FormLabel>{t('modelManager.vae')}</FormLabel>
|
||||
<FormLabel m={0}>{t('modelManager.vae')}</FormLabel>
|
||||
</InformationalPopover>
|
||||
<Combobox
|
||||
isClearable
|
||||
value={value}
|
||||
placeholder={value ? value.value : t('models.defaultVAE')}
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
noOptionsMessage={noOptionsMessage}
|
||||
/>
|
||||
<Box w="full" minW={0}>
|
||||
<Combobox
|
||||
isClearable
|
||||
value={value}
|
||||
placeholder={value ? value.value : t('models.defaultVAE')}
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
noOptionsMessage={noOptionsMessage}
|
||||
/>
|
||||
</Box>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ const options = [
|
||||
{ label: 'FP32', value: 'fp32' },
|
||||
];
|
||||
|
||||
const ParamVAEModelSelect = () => {
|
||||
const ParamVAEPrecision = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const vaePrecision = useAppSelector(selectVAEPrecision);
|
||||
@@ -31,13 +31,13 @@ const ParamVAEModelSelect = () => {
|
||||
const value = useMemo(() => options.find((o) => o.value === vaePrecision), [vaePrecision]);
|
||||
|
||||
return (
|
||||
<FormControl w="14rem" flexShrink={0}>
|
||||
<FormControl w={24}>
|
||||
<InformationalPopover feature="paramVAEPrecision">
|
||||
<FormLabel>{t('modelManager.vaePrecision')}</FormLabel>
|
||||
<FormLabel m={0}>{t('modelManager.vaePrecision')}</FormLabel>
|
||||
</InformationalPopover>
|
||||
<Combobox value={value} options={options} onChange={onChange} />
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ParamVAEModelSelect);
|
||||
export default memo(ParamVAEPrecision);
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasSendToToggle } from 'features/controlLayers/components/CanvasSendToToggle';
|
||||
import { ClearQueueIconButton } from 'features/queue/components/ClearQueueIconButton';
|
||||
import QueueFrontButton from 'features/queue/components/QueueFrontButton';
|
||||
import ProgressBar from 'features/system/components/ProgressBar';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { InvokeQueueBackButton } from './InvokeQueueBackButton';
|
||||
|
||||
const QueueControls = () => {
|
||||
const isPrependEnabled = useFeatureStatus('prependQueue');
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
|
||||
return (
|
||||
<Flex w="full" position="relative" borderRadius="base" gap={2} flexDir="column">
|
||||
<Flex gap={2}>
|
||||
{isPrependEnabled && <QueueFrontButton />}
|
||||
<InvokeQueueBackButton />
|
||||
<Spacer />
|
||||
{tab === 'generation' && <CanvasSendToToggle />}
|
||||
<ClearQueueIconButton />
|
||||
</Flex>
|
||||
<ProgressBar />
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { $isParametersPanelOpen, TABS_WITH_OPTIONS_PANEL } from 'features/ui/store/uiSlice';
|
||||
import { $isLeftPanelOpen, TABS_WITH_LEFT_PANEL } from 'features/ui/store/uiSlice';
|
||||
import type { RefObject } from 'react';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
|
||||
@@ -13,13 +13,13 @@ type Props = {
|
||||
};
|
||||
|
||||
const selectActiveTabShouldShowBadge = createSelector(selectActiveTab, (activeTab) =>
|
||||
TABS_WITH_OPTIONS_PANEL.includes(activeTab)
|
||||
TABS_WITH_LEFT_PANEL.includes(activeTab)
|
||||
);
|
||||
|
||||
export const QueueCountBadge = memo(({ targetRef }: Props) => {
|
||||
const [badgePos, setBadgePos] = useState<{ x: string; y: string } | null>(null);
|
||||
const activeTabShouldShowBadge = useAppSelector(selectActiveTabShouldShowBadge);
|
||||
const isParametersPanelOpen = useStore($isParametersPanelOpen);
|
||||
const isParametersPanelOpen = useStore($isLeftPanelOpen);
|
||||
const { queueSize } = useGetQueueStatusQuery(undefined, {
|
||||
selectFromResult: (res) => ({
|
||||
queueSize: res.data ? res.data.queue.pending + res.data.queue.in_progress : 0,
|
||||
@@ -39,7 +39,7 @@ export const QueueCountBadge = memo(({ targetRef }: Props) => {
|
||||
}
|
||||
|
||||
const cb = () => {
|
||||
if (!$isParametersPanelOpen.get()) {
|
||||
if (!$isLeftPanelOpen.get()) {
|
||||
return;
|
||||
}
|
||||
const { x, y } = target.getBoundingClientRect();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { Combobox, Flex, FormControl, FormLabel, IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||
import { useModelCombobox } from 'common/hooks/useModelCombobox';
|
||||
@@ -6,6 +6,7 @@ import { refinerModelChanged, selectRefinerModel } from 'features/controlLayers/
|
||||
import { zModelIdentifierField } from 'features/nodes/types/common';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiXBold } from 'react-icons/pi';
|
||||
import { useRefinerModels } from 'services/api/hooks/modelsByType';
|
||||
import type { MainModelConfig } from 'services/api/types';
|
||||
|
||||
@@ -33,21 +34,32 @@ const ParamSDXLRefinerModelSelect = () => {
|
||||
isLoading,
|
||||
optionsFilter,
|
||||
});
|
||||
const onReset = useCallback(() => {
|
||||
_onChange(null);
|
||||
}, [_onChange]);
|
||||
|
||||
return (
|
||||
<FormControl isDisabled={!options.length} isInvalid={!options.length} w="full">
|
||||
<InformationalPopover feature="refinerModel">
|
||||
<FormLabel>{t('sdxl.refinermodel')}</FormLabel>
|
||||
</InformationalPopover>
|
||||
<Box w="full" minW={0}>
|
||||
<Flex w="full" minW={0} gap={2} alignItems="center">
|
||||
<Combobox
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
noOptionsMessage={noOptionsMessage}
|
||||
isClearable
|
||||
/>
|
||||
</Box>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon={<PiXBold />}
|
||||
aria-label={t('common.reset')}
|
||||
onClick={onReset}
|
||||
isDisabled={!value}
|
||||
/>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import BboxScaledHeight from 'features/parameters/components/Bbox/BboxScaledHeig
|
||||
import BboxScaledWidth from 'features/parameters/components/Bbox/BboxScaledWidth';
|
||||
import BboxScaleMethod from 'features/parameters/components/Bbox/BboxScaleMethod';
|
||||
import { BboxSettings } from 'features/parameters/components/Bbox/BboxSettings';
|
||||
import { ParamDenoisingStrength } from 'features/parameters/components/Core/ParamDenoisingStrength';
|
||||
import { ParamSeedNumberInput } from 'features/parameters/components/Seed/ParamSeedNumberInput';
|
||||
import { ParamSeedRandomize } from 'features/parameters/components/Seed/ParamSeedRandomize';
|
||||
import { ParamSeedShuffle } from 'features/parameters/components/Seed/ParamSeedShuffle';
|
||||
@@ -67,11 +68,12 @@ export const ImageSettingsAccordion = memo(() => {
|
||||
>
|
||||
<Flex px={4} pt={4} w="full" h="full" flexDir="column" data-testid="image-settings-accordion">
|
||||
<BboxSettings />
|
||||
<Flex pt={4} gap={4} alignItems="center">
|
||||
<Flex py={3} gap={4} alignItems="center">
|
||||
<ParamSeedNumberInput />
|
||||
<ParamSeedShuffle />
|
||||
<ParamSeedRandomize />
|
||||
</Flex>
|
||||
<ParamDenoisingStrength />
|
||||
<Expander label={t('accordions.advanced.options')} isOpen={isOpenExpander} onToggle={onToggleExpander}>
|
||||
<Flex gap={4} pb={4} flexDir="column">
|
||||
<BboxScaleMethod />
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
|
||||
import { CanvasEditor } from 'features/controlLayers/components/CanvasEditor';
|
||||
import { CanvasRightPanelContent } from 'features/controlLayers/components/CanvasRightPanel';
|
||||
import { CanvasTabContent } from 'features/controlLayers/components/CanvasTabContent';
|
||||
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
|
||||
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
||||
import { useIsImageViewerOpen } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup';
|
||||
import QueueControls from 'features/queue/components/QueueControls';
|
||||
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
|
||||
@@ -13,19 +12,23 @@ import FloatingParametersPanelButtons from 'features/ui/components/FloatingParam
|
||||
import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage';
|
||||
import { TabMountGate } from 'features/ui/components/TabMountGate';
|
||||
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 { WorkflowsTabContent } from 'features/ui/components/tabs/WorkflowsTabContent';
|
||||
import { TabVisibilityGate } from 'features/ui/components/TabVisibilityGate';
|
||||
import { VerticalNavBar } from 'features/ui/components/VerticalNavBar';
|
||||
import type { UsePanelOptions } from 'features/ui/hooks/usePanel';
|
||||
import { usePanel } from 'features/ui/hooks/usePanel';
|
||||
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import {
|
||||
$isGalleryPanelOpen,
|
||||
$isParametersPanelOpen,
|
||||
selectUiSlice,
|
||||
TABS_WITH_GALLERY_PANEL,
|
||||
TABS_WITH_OPTIONS_PANEL,
|
||||
$isLeftPanelOpen,
|
||||
$isRightPanelOpen,
|
||||
LEFT_PANEL_MIN_SIZE_PCT,
|
||||
LEFT_PANEL_MIN_SIZE_PX,
|
||||
RIGHT_PANEL_MIN_SIZE_PCT,
|
||||
RIGHT_PANEL_MIN_SIZE_PX,
|
||||
selectWithLeftPanel,
|
||||
selectWithRightPanel,
|
||||
} from 'features/ui/store/uiSlice';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo, useMemo, useRef } from 'react';
|
||||
@@ -37,95 +40,75 @@ import ParametersPanelUpscale from './ParametersPanels/ParametersPanelUpscale';
|
||||
import ResizeHandle from './tabs/ResizeHandle';
|
||||
|
||||
const panelStyles: CSSProperties = { position: 'relative', height: '100%', width: '100%' };
|
||||
const GALLERY_MIN_SIZE_PX = 310;
|
||||
const GALLERY_MIN_SIZE_PCT = 20;
|
||||
const OPTIONS_PANEL_MIN_SIZE_PX = 430;
|
||||
const OPTIONS_PANEL_MIN_SIZE_PCT = 20;
|
||||
|
||||
const onGalleryPanelCollapse = (isCollapsed: boolean) => $isGalleryPanelOpen.set(!isCollapsed);
|
||||
const onParametersPanelCollapse = (isCollapsed: boolean) => $isParametersPanelOpen.set(!isCollapsed);
|
||||
|
||||
const selectShouldShowGalleryPanel = createSelector(selectUiSlice, (ui) =>
|
||||
TABS_WITH_GALLERY_PANEL.includes(ui.activeTab)
|
||||
);
|
||||
const selectShouldShowOptionsPanel = createSelector(selectUiSlice, (ui) =>
|
||||
TABS_WITH_OPTIONS_PANEL.includes(ui.activeTab)
|
||||
);
|
||||
const onLeftPanelCollapse = (isCollapsed: boolean) => $isLeftPanelOpen.set(!isCollapsed);
|
||||
const onRightPanelCollapse = (isCollapsed: boolean) => $isRightPanelOpen.set(!isCollapsed);
|
||||
|
||||
export const AppContent = memo(() => {
|
||||
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
||||
const isImageViewerOpen = useIsImageViewerOpen();
|
||||
const shouldShowGalleryPanel = useAppSelector(selectShouldShowGalleryPanel);
|
||||
const shouldShowOptionsPanel = useAppSelector(selectShouldShowOptionsPanel);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useScopeOnFocus('gallery', ref);
|
||||
|
||||
const optionsPanelUsePanelOptions = useMemo<UsePanelOptions>(
|
||||
() => ({
|
||||
id: 'options-panel',
|
||||
unit: 'pixels',
|
||||
minSize: OPTIONS_PANEL_MIN_SIZE_PX,
|
||||
defaultSize: OPTIONS_PANEL_MIN_SIZE_PCT,
|
||||
panelGroupRef,
|
||||
panelGroupDirection: 'horizontal',
|
||||
onCollapse: onParametersPanelCollapse,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const galleryPanelUsePanelOptions = useMemo<UsePanelOptions>(
|
||||
() => ({
|
||||
id: 'gallery-panel',
|
||||
unit: 'pixels',
|
||||
minSize: GALLERY_MIN_SIZE_PX,
|
||||
defaultSize: GALLERY_MIN_SIZE_PCT,
|
||||
panelGroupRef,
|
||||
panelGroupDirection: 'horizontal',
|
||||
onCollapse: onGalleryPanelCollapse,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
||||
const panelStorage = usePanelStorage();
|
||||
|
||||
const optionsPanel = usePanel(optionsPanelUsePanelOptions);
|
||||
const withLeftPanel = useAppSelector(selectWithLeftPanel);
|
||||
const leftPanelUsePanelOptions = useMemo<UsePanelOptions>(
|
||||
() => ({
|
||||
id: 'left-panel',
|
||||
unit: 'pixels',
|
||||
minSize: LEFT_PANEL_MIN_SIZE_PX,
|
||||
defaultSize: LEFT_PANEL_MIN_SIZE_PCT,
|
||||
panelGroupRef,
|
||||
panelGroupDirection: 'horizontal',
|
||||
onCollapse: onLeftPanelCollapse,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const leftPanel = usePanel(leftPanelUsePanelOptions);
|
||||
useHotkeys(['t', 'o'], leftPanel.toggle, { enabled: withLeftPanel }, [leftPanel.toggle, withLeftPanel]);
|
||||
|
||||
const galleryPanel = usePanel(galleryPanelUsePanelOptions);
|
||||
const withRightPanel = useAppSelector(selectWithRightPanel);
|
||||
const rightPanelUsePanelOptions = useMemo<UsePanelOptions>(
|
||||
() => ({
|
||||
id: 'right-panel',
|
||||
unit: 'pixels',
|
||||
minSize: RIGHT_PANEL_MIN_SIZE_PX,
|
||||
defaultSize: RIGHT_PANEL_MIN_SIZE_PCT,
|
||||
panelGroupRef,
|
||||
panelGroupDirection: 'horizontal',
|
||||
onCollapse: onRightPanelCollapse,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const rightPanel = usePanel(rightPanelUsePanelOptions);
|
||||
useHotkeys('g', rightPanel.toggle, { enabled: withRightPanel }, [rightPanel.toggle, withRightPanel]);
|
||||
|
||||
useHotkeys('g', galleryPanel.toggle, { enabled: shouldShowGalleryPanel }, [
|
||||
galleryPanel.toggle,
|
||||
shouldShowGalleryPanel,
|
||||
]);
|
||||
useHotkeys(['t', 'o'], optionsPanel.toggle, { enabled: shouldShowOptionsPanel }, [
|
||||
optionsPanel.toggle,
|
||||
shouldShowOptionsPanel,
|
||||
]);
|
||||
useHotkeys(
|
||||
'shift+r',
|
||||
() => {
|
||||
optionsPanel.reset();
|
||||
galleryPanel.reset();
|
||||
leftPanel.reset();
|
||||
rightPanel.reset();
|
||||
},
|
||||
[optionsPanel.reset, galleryPanel.reset]
|
||||
[leftPanel.reset, rightPanel.reset]
|
||||
);
|
||||
useHotkeys(
|
||||
'f',
|
||||
() => {
|
||||
if (optionsPanel.isCollapsed || galleryPanel.isCollapsed) {
|
||||
optionsPanel.expand();
|
||||
galleryPanel.expand();
|
||||
if (leftPanel.isCollapsed || rightPanel.isCollapsed) {
|
||||
leftPanel.expand();
|
||||
rightPanel.expand();
|
||||
} else {
|
||||
optionsPanel.collapse();
|
||||
galleryPanel.collapse();
|
||||
leftPanel.collapse();
|
||||
rightPanel.collapse();
|
||||
}
|
||||
},
|
||||
[
|
||||
optionsPanel.isCollapsed,
|
||||
galleryPanel.isCollapsed,
|
||||
optionsPanel.expand,
|
||||
galleryPanel.expand,
|
||||
optionsPanel.collapse,
|
||||
galleryPanel.collapse,
|
||||
leftPanel.isCollapsed,
|
||||
rightPanel.isCollapsed,
|
||||
leftPanel.expand,
|
||||
rightPanel.expand,
|
||||
leftPanel.collapse,
|
||||
rightPanel.collapse,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -141,63 +124,100 @@ export const AppContent = memo(() => {
|
||||
style={panelStyles}
|
||||
storage={panelStorage}
|
||||
>
|
||||
<Panel order={0} collapsible style={panelStyles} {...optionsPanel.panelProps}>
|
||||
<Flex flexDir="column" w="full" h="full" gap={2}>
|
||||
<QueueControls />
|
||||
<Box position="relative" w="full" h="full">
|
||||
{withLeftPanel && (
|
||||
<>
|
||||
<Panel order={0} collapsible style={panelStyles} {...leftPanel.panelProps}>
|
||||
<TabMountGate tab="generation">
|
||||
<TabVisibilityGate tab="generation">
|
||||
<ParametersPanelTextToImage />
|
||||
<Flex flexDir="column" w="full" h="full" gap={2}>
|
||||
<QueueControls />
|
||||
<Box position="relative" w="full" h="full">
|
||||
<ParametersPanelTextToImage />
|
||||
</Box>
|
||||
</Flex>
|
||||
</TabVisibilityGate>
|
||||
</TabMountGate>
|
||||
<TabMountGate tab="upscaling">
|
||||
<TabVisibilityGate tab="upscaling">
|
||||
<ParametersPanelUpscale />
|
||||
<Flex flexDir="column" w="full" h="full" gap={2}>
|
||||
<QueueControls />
|
||||
<Box position="relative" w="full" h="full">
|
||||
<ParametersPanelUpscale />
|
||||
</Box>
|
||||
</Flex>
|
||||
</TabVisibilityGate>
|
||||
</TabMountGate>
|
||||
<TabMountGate tab="workflows">
|
||||
<TabVisibilityGate tab="workflows">
|
||||
<NodeEditorPanelGroup />
|
||||
<Flex flexDir="column" w="full" h="full" gap={2}>
|
||||
<QueueControls />
|
||||
<Box position="relative" w="full" h="full">
|
||||
<NodeEditorPanelGroup />
|
||||
</Box>
|
||||
</Flex>
|
||||
</TabVisibilityGate>
|
||||
</TabMountGate>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Panel>
|
||||
<ResizeHandle id="options-main-handle" orientation="vertical" {...optionsPanel.resizeHandleProps} />
|
||||
</Panel>
|
||||
<ResizeHandle id="left-main-handle" orientation="vertical" {...leftPanel.resizeHandleProps} />
|
||||
</>
|
||||
)}
|
||||
<Panel id="main-panel" order={1} minSize={20} style={panelStyles}>
|
||||
<TabMountGate tab="generation">
|
||||
<TabVisibilityGate tab="generation">
|
||||
<CanvasEditor />
|
||||
<CanvasTabContent />
|
||||
</TabVisibilityGate>
|
||||
</TabMountGate>
|
||||
<TabMountGate tab="upscaling">
|
||||
<TabVisibilityGate tab="upscaling">
|
||||
<ImageViewer />
|
||||
</TabVisibilityGate>
|
||||
</TabMountGate>
|
||||
{/* upscaling tab has no content of its own - uses image viewer only */}
|
||||
<TabMountGate tab="workflows">
|
||||
<TabVisibilityGate tab="workflows">
|
||||
<NodesTab />
|
||||
<WorkflowsTabContent />
|
||||
</TabVisibilityGate>
|
||||
</TabMountGate>
|
||||
<TabMountGate tab="gallery">
|
||||
<TabVisibilityGate tab="gallery">
|
||||
<ImageViewer />
|
||||
</TabVisibilityGate>
|
||||
</TabMountGate>
|
||||
<TabMountGate tab="models">
|
||||
<TabVisibilityGate tab="models">
|
||||
<ModelManagerTab />
|
||||
</TabVisibilityGate>
|
||||
</TabMountGate>
|
||||
<TabMountGate tab="queue">
|
||||
<TabVisibilityGate tab="queue">
|
||||
<QueueTab />
|
||||
</TabVisibilityGate>
|
||||
</TabMountGate>
|
||||
{isImageViewerOpen && <ImageViewer />}
|
||||
</Panel>
|
||||
<ResizeHandle id="main-gallery-handle" orientation="vertical" {...galleryPanel.resizeHandleProps} />
|
||||
<Panel order={2} style={panelStyles} collapsible {...galleryPanel.panelProps}>
|
||||
<GalleryPanelContent />
|
||||
</Panel>
|
||||
{withRightPanel && (
|
||||
<>
|
||||
<ResizeHandle id="main-right-handle" orientation="vertical" {...rightPanel.resizeHandleProps} />
|
||||
<Panel order={2} style={panelStyles} collapsible {...rightPanel.panelProps}>
|
||||
<RightPanelContent />
|
||||
</Panel>
|
||||
</>
|
||||
)}
|
||||
</PanelGroup>
|
||||
{shouldShowOptionsPanel && <FloatingParametersPanelButtons panelApi={optionsPanel} />}
|
||||
{shouldShowGalleryPanel && <FloatingGalleryButton panelApi={galleryPanel} />}
|
||||
<TabMountGate tab="models">
|
||||
<TabVisibilityGate tab="models">
|
||||
<ModelManagerTab />
|
||||
</TabVisibilityGate>
|
||||
</TabMountGate>
|
||||
<TabMountGate tab="models">
|
||||
<TabVisibilityGate tab="queue">
|
||||
<QueueTab />
|
||||
</TabVisibilityGate>
|
||||
</TabMountGate>
|
||||
{withLeftPanel && <FloatingParametersPanelButtons panelApi={leftPanel} />}
|
||||
{withRightPanel && <FloatingGalleryButton panelApi={rightPanel} />}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
AppContent.displayName = 'AppContent';
|
||||
|
||||
const RightPanelContent = memo(() => {
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
|
||||
if (tab === 'generation') {
|
||||
return <CanvasRightPanelContent />;
|
||||
}
|
||||
|
||||
return <GalleryPanelContent />;
|
||||
});
|
||||
RightPanelContent.displayName = 'RightPanelContent';
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import type { ChakraProps } from '@invoke-ai/ui-library';
|
||||
import { Box, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
|
||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
||||
import { CanvasPanelContent } from 'features/controlLayers/components/CanvasPanelContent';
|
||||
import { selectIsSDXL } from 'features/controlLayers/store/paramsSlice';
|
||||
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
|
||||
import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion';
|
||||
import { CompositingSettingsAccordion } from 'features/settingsAccordions/components/CompositingSettingsAccordion/CompositingSettingsAccordion';
|
||||
@@ -17,47 +14,22 @@ import { StylePresetMenuTrigger } from 'features/stylePresets/components/StylePr
|
||||
import { $isMenuOpen } from 'features/stylePresets/store/isMenuOpen';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { memo } from 'react';
|
||||
|
||||
const overlayScrollbarsStyles: CSSProperties = {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
};
|
||||
|
||||
const baseStyles: ChakraProps['sx'] = {
|
||||
fontWeight: 'semibold',
|
||||
fontSize: 'sm',
|
||||
color: 'base.300',
|
||||
};
|
||||
|
||||
const selectedStyles: ChakraProps['sx'] = {
|
||||
borderColor: 'base.800',
|
||||
borderBottomColor: 'base.900',
|
||||
color: 'invokeBlue.300',
|
||||
};
|
||||
|
||||
const ParametersPanelTextToImage = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const isSDXL = useAppSelector(selectIsSDXL);
|
||||
const onChangeTabs = useCallback(
|
||||
(i: number) => {
|
||||
if (i === 1) {
|
||||
dispatch(isImageViewerOpenChanged(false));
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const isMenuOpen = useStore($isMenuOpen);
|
||||
|
||||
return (
|
||||
<Flex w="full" h="full" flexDir="column" gap={2}>
|
||||
<StylePresetMenuTrigger />
|
||||
<Flex w="full" h="full" position="relative">
|
||||
<Box position="absolute" top={0} left={0} right={0} bottom={0} ref={ref}>
|
||||
<Box position="absolute" top={0} left={0} right={0} bottom={0}>
|
||||
{isMenuOpen && (
|
||||
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
|
||||
<Flex gap={2} flexDirection="column" h="full" w="full">
|
||||
@@ -68,43 +40,11 @@ const ParametersPanelTextToImage = () => {
|
||||
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
|
||||
<Flex gap={2} flexDirection="column" h="full" w="full">
|
||||
<Prompts />
|
||||
<Tabs
|
||||
defaultIndex={0}
|
||||
variant="enclosed"
|
||||
display="flex"
|
||||
flexDir="column"
|
||||
w="full"
|
||||
h="full"
|
||||
gap={2}
|
||||
onChange={onChangeTabs}
|
||||
>
|
||||
<TabList gap={2} fontSize="sm" borderColor="base.800" alignItems="center" w="full" pe={1}>
|
||||
<Tab sx={baseStyles} _selected={selectedStyles} data-testid="generation-tab-settings-tab-button">
|
||||
{t('common.settingsLabel')}
|
||||
</Tab>
|
||||
<Tab
|
||||
sx={baseStyles}
|
||||
_selected={selectedStyles}
|
||||
data-testid="generation-tab-control-layers-tab-button"
|
||||
>
|
||||
{t('controlLayers.layer_other')}
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanels w="full" h="full">
|
||||
<TabPanel p={0} w="full" h="full">
|
||||
<Flex gap={2} flexDirection="column" h="full" w="full">
|
||||
<ImageSettingsAccordion />
|
||||
<GenerationSettingsAccordion />
|
||||
<CompositingSettingsAccordion />
|
||||
{isSDXL && <RefinerSettingsAccordion />}
|
||||
<AdvancedSettingsAccordion />
|
||||
</Flex>
|
||||
</TabPanel>
|
||||
<TabPanel p={0} w="full" h="full">
|
||||
<CanvasPanelContent />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
<ImageSettingsAccordion />
|
||||
<GenerationSettingsAccordion />
|
||||
<CompositingSettingsAccordion />
|
||||
{isSDXL && <RefinerSettingsAccordion />}
|
||||
<AdvancedSettingsAccordion />
|
||||
</Flex>
|
||||
</OverlayScrollbarsComponent>
|
||||
</Box>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { TabMountGate } from 'features/ui/components/TabMountGate';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MdZoomOutMap } from 'react-icons/md';
|
||||
import { PiFlowArrowBold } from 'react-icons/pi';
|
||||
import { PiFlowArrowBold, PiImageBold } from 'react-icons/pi';
|
||||
import { RiBox2Line, RiInputMethodLine, RiPlayList2Fill } from 'react-icons/ri';
|
||||
|
||||
import { TabButton } from './TabButton';
|
||||
@@ -36,6 +36,9 @@ export const VerticalNavBar = memo(() => {
|
||||
<TabMountGate tab="queue">
|
||||
<TabButton tab="queue" icon={<RiPlayList2Fill />} label={t('ui.tabs.queue')} />
|
||||
</TabMountGate>
|
||||
<TabMountGate tab="gallery">
|
||||
<TabButton tab="gallery" icon={<PiImageBold />} label={t('ui.tabs.gallery')} />
|
||||
</TabMountGate>
|
||||
</Flex>
|
||||
<Spacer />
|
||||
<StatusIndicator />
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Box } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
|
||||
import NodeEditor from 'features/nodes/components/NodeEditor';
|
||||
import { selectWorkflowMode } from 'features/nodes/store/workflowSlice';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo, useRef } from 'react';
|
||||
import { ReactFlowProvider } from 'reactflow';
|
||||
|
||||
const NodesTab = () => {
|
||||
const mode = useAppSelector(selectWorkflowMode);
|
||||
const activeTabName = useAppSelector(selectActiveTab);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useScopeOnFocus('workflows', ref);
|
||||
|
||||
return (
|
||||
<Box
|
||||
display={activeTabName === 'workflows' ? undefined : 'none'}
|
||||
hidden={activeTabName !== 'workflows'}
|
||||
ref={ref}
|
||||
layerStyle="first"
|
||||
position="relative"
|
||||
w="full"
|
||||
h="full"
|
||||
p={2}
|
||||
borderRadius="base"
|
||||
>
|
||||
{mode === 'edit' && (
|
||||
<ReactFlowProvider>
|
||||
<NodeEditor />
|
||||
</ReactFlowProvider>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(NodesTab);
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
||||
import NodeEditor from 'features/nodes/components/NodeEditor';
|
||||
import { selectWorkflowMode } from 'features/nodes/store/workflowSlice';
|
||||
import { memo } from 'react';
|
||||
import { ReactFlowProvider } from 'reactflow';
|
||||
|
||||
export const WorkflowsTabContent = memo(() => {
|
||||
const mode = useAppSelector(selectWorkflowMode);
|
||||
|
||||
if (mode === 'edit') {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<NodeEditor />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return <ImageViewer />;
|
||||
});
|
||||
|
||||
WorkflowsTabContent.displayName = 'WorkflowsTabContent';
|
||||
@@ -107,6 +107,12 @@ export const usePanel = (arg: UsePanelOptions): UsePanelReturn => {
|
||||
}
|
||||
|
||||
const minSizePct = getSizeAsPercentage(arg.minSize, arg.panelGroupRef, arg.panelGroupDirection);
|
||||
|
||||
if (minSizePct > 100) {
|
||||
// This can happen when the panel is hidden
|
||||
return;
|
||||
}
|
||||
|
||||
_setMinSize(minSizePct);
|
||||
|
||||
if (arg.defaultSize && arg.defaultSize > minSizePct) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { createSelector, createSlice } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { workflowLoadRequested } from 'features/nodes/store/actions';
|
||||
import { atom } from 'nanostores';
|
||||
@@ -78,7 +78,14 @@ export const uiPersistConfig: PersistConfig<UIState> = {
|
||||
persistDenylist: ['shouldShowImageDetails'],
|
||||
};
|
||||
|
||||
export const $isGalleryPanelOpen = atom(true);
|
||||
export const $isParametersPanelOpen = atom(true);
|
||||
export const TABS_WITH_GALLERY_PANEL: TabName[] = ['generation', 'upscaling', 'workflows'] as const;
|
||||
export const TABS_WITH_OPTIONS_PANEL: TabName[] = ['generation', 'upscaling', 'workflows'] as const;
|
||||
export const LEFT_PANEL_MIN_SIZE_PX = 390;
|
||||
export const LEFT_PANEL_MIN_SIZE_PCT = 20;
|
||||
export const TABS_WITH_LEFT_PANEL: TabName[] = ['generation', 'upscaling', 'workflows'] as const;
|
||||
export const $isLeftPanelOpen = atom(true);
|
||||
export const selectWithLeftPanel = createSelector(selectUiSlice, (ui) => TABS_WITH_LEFT_PANEL.includes(ui.activeTab));
|
||||
|
||||
export const TABS_WITH_RIGHT_PANEL: TabName[] = ['generation', 'upscaling', 'workflows', 'gallery'] as const;
|
||||
export const RIGHT_PANEL_MIN_SIZE_PX = 390;
|
||||
export const RIGHT_PANEL_MIN_SIZE_PCT = 20;
|
||||
export const $isRightPanelOpen = atom(true);
|
||||
export const selectWithRightPanel = createSelector(selectUiSlice, (ui) => TABS_WITH_RIGHT_PANEL.includes(ui.activeTab));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type TabName = 'generation' | 'upscaling' | 'workflows' | 'models' | 'queue';
|
||||
export type TabName = 'generation' | 'upscaling' | 'workflows' | 'models' | 'queue' | 'gallery';
|
||||
|
||||
export interface UIState {
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user