mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): remove floating panels, move all to resizable panels
There is a console error we can ignore when toggling gallery panel on canvas - this will be resolved in the next release of the resizable library
This commit is contained in:
@ -90,7 +90,6 @@
|
|||||||
"overlayscrollbars-react": "^0.5.0",
|
"overlayscrollbars-react": "^0.5.0",
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"query-string": "^8.1.0",
|
"query-string": "^8.1.0",
|
||||||
"re-resizable": "^6.9.11",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
"toggleAutoscroll": "Toggle autoscroll",
|
"toggleAutoscroll": "Toggle autoscroll",
|
||||||
"toggleLogViewer": "Toggle Log Viewer",
|
"toggleLogViewer": "Toggle Log Viewer",
|
||||||
"showGallery": "Show Gallery",
|
"showGallery": "Show Gallery",
|
||||||
"showOptionsPanel": "Show Options Panel",
|
"showOptionsPanel": "Show Side Panel",
|
||||||
"menu": "Menu"
|
"menu": "Menu"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@ -577,7 +577,7 @@
|
|||||||
"resetWebUI": "Reset Web UI",
|
"resetWebUI": "Reset Web UI",
|
||||||
"resetWebUIDesc1": "Resetting the web UI only resets the browser's local cache of your images and remembered settings. It does not delete any images from disk.",
|
"resetWebUIDesc1": "Resetting the web UI only resets the browser's local cache of your images and remembered settings. It does not delete any images from disk.",
|
||||||
"resetWebUIDesc2": "If images aren't showing up in the gallery or something else isn't working, please try resetting before submitting an issue on GitHub.",
|
"resetWebUIDesc2": "If images aren't showing up in the gallery or something else isn't working, please try resetting before submitting an issue on GitHub.",
|
||||||
"resetComplete": "Web UI has been reset. Refresh the page to reload.",
|
"resetComplete": "Web UI has been reset.",
|
||||||
"consoleLogLevel": "Log Level",
|
"consoleLogLevel": "Log Level",
|
||||||
"shouldLogToConsole": "Console Logging",
|
"shouldLogToConsole": "Console Logging",
|
||||||
"developer": "Developer",
|
"developer": "Developer",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Flex, Grid, Portal } from '@chakra-ui/react';
|
import { Flex, Grid } from '@chakra-ui/react';
|
||||||
import { useLogger } from 'app/logging/useLogger';
|
import { useLogger } from 'app/logging/useLogger';
|
||||||
import { appStarted } from 'app/store/middleware/listenerMiddleware/listeners/appStarted';
|
import { appStarted } from 'app/store/middleware/listenerMiddleware/listeners/appStarted';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
@ -6,21 +6,17 @@ import { PartialAppConfig } from 'app/types/invokeai';
|
|||||||
import ImageUploader from 'common/components/ImageUploader';
|
import ImageUploader from 'common/components/ImageUploader';
|
||||||
import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal';
|
import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal';
|
||||||
import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal';
|
import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal';
|
||||||
import GalleryDrawer from 'features/gallery/components/GalleryPanel';
|
|
||||||
import SiteHeader from 'features/system/components/SiteHeader';
|
import SiteHeader from 'features/system/components/SiteHeader';
|
||||||
import { configChanged } from 'features/system/store/configSlice';
|
import { configChanged } from 'features/system/store/configSlice';
|
||||||
import { languageSelector } from 'features/system/store/systemSelectors';
|
import { languageSelector } from 'features/system/store/systemSelectors';
|
||||||
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
|
|
||||||
import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons';
|
|
||||||
import InvokeTabs from 'features/ui/components/InvokeTabs';
|
import InvokeTabs from 'features/ui/components/InvokeTabs';
|
||||||
import ParametersDrawer from 'features/ui/components/ParametersDrawer';
|
|
||||||
import i18n from 'i18n';
|
import i18n from 'i18n';
|
||||||
import { size } from 'lodash-es';
|
import { size } from 'lodash-es';
|
||||||
import { ReactNode, memo, useCallback, useEffect } from 'react';
|
import { ReactNode, memo, useCallback, useEffect } from 'react';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
|
import AppErrorBoundaryFallback from './AppErrorBoundaryFallback';
|
||||||
import GlobalHotkeys from './GlobalHotkeys';
|
import GlobalHotkeys from './GlobalHotkeys';
|
||||||
import Toaster from './Toaster';
|
import Toaster from './Toaster';
|
||||||
import AppErrorBoundaryFallback from './AppErrorBoundaryFallback';
|
|
||||||
|
|
||||||
const DEFAULT_CONFIG = {};
|
const DEFAULT_CONFIG = {};
|
||||||
|
|
||||||
@ -83,15 +79,6 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Grid>
|
</Grid>
|
||||||
</ImageUploader>
|
</ImageUploader>
|
||||||
|
|
||||||
<GalleryDrawer />
|
|
||||||
<ParametersDrawer />
|
|
||||||
<Portal>
|
|
||||||
<FloatingParametersPanelButtons />
|
|
||||||
</Portal>
|
|
||||||
<Portal>
|
|
||||||
<FloatingGalleryButton />
|
|
||||||
</Portal>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<DeleteImageModal />
|
<DeleteImageModal />
|
||||||
<ChangeBoardModal />
|
<ChangeBoardModal />
|
||||||
|
@ -1,30 +1,21 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
|
||||||
import {
|
import {
|
||||||
ctrlKeyPressed,
|
ctrlKeyPressed,
|
||||||
metaKeyPressed,
|
metaKeyPressed,
|
||||||
shiftKeyPressed,
|
shiftKeyPressed,
|
||||||
} from 'features/ui/store/hotkeysSlice';
|
} from 'features/ui/store/hotkeysSlice';
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||||
import {
|
|
||||||
setActiveTab,
|
|
||||||
toggleGalleryPanel,
|
|
||||||
toggleParametersPanel,
|
|
||||||
togglePinGalleryPanel,
|
|
||||||
togglePinParametersPanel,
|
|
||||||
} from 'features/ui/store/uiSlice';
|
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
import { isHotkeyPressed, useHotkeys } from 'react-hotkeys-hook';
|
import { isHotkeyPressed, useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
const globalHotkeysSelector = createSelector(
|
const globalHotkeysSelector = createSelector(
|
||||||
[stateSelector],
|
[stateSelector],
|
||||||
({ hotkeys, ui }) => {
|
({ hotkeys }) => {
|
||||||
const { shift, ctrl, meta } = hotkeys;
|
const { shift, ctrl, meta } = hotkeys;
|
||||||
const { shouldPinParametersPanel, shouldPinGallery } = ui;
|
return { shift, ctrl, meta };
|
||||||
return { shift, ctrl, meta, shouldPinGallery, shouldPinParametersPanel };
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
memoizeOptions: {
|
memoizeOptions: {
|
||||||
@ -41,9 +32,7 @@ const globalHotkeysSelector = createSelector(
|
|||||||
*/
|
*/
|
||||||
const GlobalHotkeys: React.FC = () => {
|
const GlobalHotkeys: React.FC = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { shift, ctrl, meta, shouldPinParametersPanel, shouldPinGallery } =
|
const { shift, ctrl, meta } = useAppSelector(globalHotkeysSelector);
|
||||||
useAppSelector(globalHotkeysSelector);
|
|
||||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'*',
|
'*',
|
||||||
@ -68,34 +57,6 @@ const GlobalHotkeys: React.FC = () => {
|
|||||||
[shift, ctrl, meta]
|
[shift, ctrl, meta]
|
||||||
);
|
);
|
||||||
|
|
||||||
useHotkeys('o', () => {
|
|
||||||
dispatch(toggleParametersPanel());
|
|
||||||
if (activeTabName === 'unifiedCanvas' && shouldPinParametersPanel) {
|
|
||||||
dispatch(requestCanvasRescale());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
useHotkeys(['shift+o'], () => {
|
|
||||||
dispatch(togglePinParametersPanel());
|
|
||||||
if (activeTabName === 'unifiedCanvas') {
|
|
||||||
dispatch(requestCanvasRescale());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
useHotkeys('g', () => {
|
|
||||||
dispatch(toggleGalleryPanel());
|
|
||||||
if (activeTabName === 'unifiedCanvas' && shouldPinGallery) {
|
|
||||||
dispatch(requestCanvasRescale());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
useHotkeys(['shift+g'], () => {
|
|
||||||
dispatch(togglePinGalleryPanel());
|
|
||||||
if (activeTabName === 'unifiedCanvas') {
|
|
||||||
dispatch(requestCanvasRescale());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
useHotkeys('1', () => {
|
useHotkeys('1', () => {
|
||||||
dispatch(setActiveTab('txt2img'));
|
dispatch(setActiveTab('txt2img'));
|
||||||
});
|
});
|
||||||
@ -112,6 +73,10 @@ const GlobalHotkeys: React.FC = () => {
|
|||||||
dispatch(setActiveTab('nodes'));
|
dispatch(setActiveTab('nodes'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useHotkeys('5', () => {
|
||||||
|
dispatch(setActiveTab('modelManager'));
|
||||||
|
});
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -83,10 +83,6 @@ const ControlNet = (props: ControlNetProps) => {
|
|||||||
p: 3,
|
p: 3,
|
||||||
borderRadius: 'base',
|
borderRadius: 'base',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
bg: 'base.200',
|
|
||||||
_dark: {
|
|
||||||
bg: 'base.850',
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex sx={{ gap: 2, alignItems: 'center' }}>
|
<Flex sx={{ gap: 2, alignItems: 'center' }}>
|
||||||
|
@ -1,119 +0,0 @@
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { gallerySelector } from 'features/gallery/store/gallerySelectors';
|
|
||||||
import { setGalleryImageMinimumWidth } from 'features/gallery/store/gallerySlice';
|
|
||||||
|
|
||||||
import { clamp, isEqual } from 'lodash-es';
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
|
||||||
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
|
||||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
|
||||||
import ResizableDrawer from 'features/ui/components/common/ResizableDrawer/ResizableDrawer';
|
|
||||||
import {
|
|
||||||
activeTabNameSelector,
|
|
||||||
uiSelector,
|
|
||||||
} from 'features/ui/store/uiSelectors';
|
|
||||||
import { setShouldShowGallery } from 'features/ui/store/uiSlice';
|
|
||||||
import { memo } from 'react';
|
|
||||||
import ImageGalleryContent from './ImageGalleryContent';
|
|
||||||
|
|
||||||
const selector = createSelector(
|
|
||||||
[activeTabNameSelector, uiSelector, gallerySelector, isStagingSelector],
|
|
||||||
(activeTabName, ui, gallery, isStaging) => {
|
|
||||||
const { shouldPinGallery, shouldShowGallery } = ui;
|
|
||||||
const { galleryImageMinimumWidth } = gallery;
|
|
||||||
|
|
||||||
return {
|
|
||||||
activeTabName,
|
|
||||||
isStaging,
|
|
||||||
shouldPinGallery,
|
|
||||||
shouldShowGallery,
|
|
||||||
galleryImageMinimumWidth,
|
|
||||||
isResizable: activeTabName !== 'unifiedCanvas',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memoizeOptions: {
|
|
||||||
resultEqualityCheck: isEqual,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const GalleryDrawer = () => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const {
|
|
||||||
shouldPinGallery,
|
|
||||||
shouldShowGallery,
|
|
||||||
galleryImageMinimumWidth,
|
|
||||||
// activeTabName,
|
|
||||||
// isStaging,
|
|
||||||
// isResizable,
|
|
||||||
} = useAppSelector(selector);
|
|
||||||
|
|
||||||
const handleCloseGallery = () => {
|
|
||||||
dispatch(setShouldShowGallery(false));
|
|
||||||
shouldPinGallery && dispatch(requestCanvasRescale());
|
|
||||||
};
|
|
||||||
|
|
||||||
useHotkeys(
|
|
||||||
'esc',
|
|
||||||
() => {
|
|
||||||
dispatch(setShouldShowGallery(false));
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: () => !shouldPinGallery,
|
|
||||||
preventDefault: true,
|
|
||||||
},
|
|
||||||
[shouldPinGallery]
|
|
||||||
);
|
|
||||||
|
|
||||||
const IMAGE_SIZE_STEP = 32;
|
|
||||||
|
|
||||||
useHotkeys(
|
|
||||||
'shift+up',
|
|
||||||
() => {
|
|
||||||
if (galleryImageMinimumWidth < 256) {
|
|
||||||
const newMinWidth = clamp(
|
|
||||||
galleryImageMinimumWidth + IMAGE_SIZE_STEP,
|
|
||||||
32,
|
|
||||||
256
|
|
||||||
);
|
|
||||||
dispatch(setGalleryImageMinimumWidth(newMinWidth));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[galleryImageMinimumWidth]
|
|
||||||
);
|
|
||||||
|
|
||||||
useHotkeys(
|
|
||||||
'shift+down',
|
|
||||||
() => {
|
|
||||||
if (galleryImageMinimumWidth > 32) {
|
|
||||||
const newMinWidth = clamp(
|
|
||||||
galleryImageMinimumWidth - IMAGE_SIZE_STEP,
|
|
||||||
32,
|
|
||||||
256
|
|
||||||
);
|
|
||||||
dispatch(setGalleryImageMinimumWidth(newMinWidth));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[galleryImageMinimumWidth]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (shouldPinGallery) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ResizableDrawer
|
|
||||||
direction="right"
|
|
||||||
isResizable={true}
|
|
||||||
isOpen={shouldShowGallery}
|
|
||||||
onClose={handleCloseGallery}
|
|
||||||
minWidth={400}
|
|
||||||
>
|
|
||||||
<ImageGalleryContent />
|
|
||||||
</ResizableDrawer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(GalleryDrawer);
|
|
@ -1,45 +0,0 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { stateSelector } from 'app/store/store';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
|
||||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
|
||||||
import { togglePinGalleryPanel } from 'features/ui/store/uiSlice';
|
|
||||||
import { memo } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
|
|
||||||
|
|
||||||
const selector = createSelector(
|
|
||||||
[stateSelector],
|
|
||||||
(state) => {
|
|
||||||
const { shouldPinGallery } = state.ui;
|
|
||||||
|
|
||||||
return {
|
|
||||||
shouldPinGallery,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
defaultSelectorOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
const GalleryPinButton = () => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const { shouldPinGallery } = useAppSelector(selector);
|
|
||||||
|
|
||||||
const handleSetShouldPinGallery = () => {
|
|
||||||
dispatch(togglePinGalleryPanel());
|
|
||||||
dispatch(requestCanvasRescale());
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<IAIIconButton
|
|
||||||
size="sm"
|
|
||||||
aria-label={t('gallery.pinGallery')}
|
|
||||||
tooltip={`${t('gallery.pinGallery')} (Shift+G)`}
|
|
||||||
onClick={handleSetShouldPinGallery}
|
|
||||||
icon={shouldPinGallery ? <BsPinAngleFill /> : <BsPinAngle />}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(GalleryPinButton);
|
|
@ -18,7 +18,6 @@ import { FaImages, FaServer } from 'react-icons/fa';
|
|||||||
import { galleryViewChanged } from '../store/gallerySlice';
|
import { galleryViewChanged } from '../store/gallerySlice';
|
||||||
import BoardsList from './Boards/BoardsList/BoardsList';
|
import BoardsList from './Boards/BoardsList/BoardsList';
|
||||||
import GalleryBoardName from './GalleryBoardName';
|
import GalleryBoardName from './GalleryBoardName';
|
||||||
import GalleryPinButton from './GalleryPinButton';
|
|
||||||
import GallerySettingsPopover from './GallerySettingsPopover';
|
import GallerySettingsPopover from './GallerySettingsPopover';
|
||||||
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
|
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
|
||||||
|
|
||||||
@ -75,7 +74,6 @@ const ImageGalleryContent = () => {
|
|||||||
onToggle={onToggleBoardList}
|
onToggle={onToggleBoardList}
|
||||||
/>
|
/>
|
||||||
<GallerySettingsPopover />
|
<GallerySettingsPopover />
|
||||||
<GalleryPinButton />
|
|
||||||
</Flex>
|
</Flex>
|
||||||
<Box>
|
<Box>
|
||||||
<BoardsList isOpen={isBoardListOpen} />
|
<BoardsList isOpen={isBoardListOpen} />
|
||||||
|
@ -1,38 +1,16 @@
|
|||||||
import { Flex } from '@chakra-ui/react';
|
import { Flex } from '@chakra-ui/react';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
|
|
||||||
import { memo, useState } from 'react';
|
|
||||||
import { MdDeviceHub } from 'react-icons/md';
|
|
||||||
import { Panel, PanelGroup } from 'react-resizable-panels';
|
|
||||||
import 'reactflow/dist/style.css';
|
|
||||||
import NodeEditorPanelGroup from './sidePanel/NodeEditorPanelGroup';
|
|
||||||
import { Flow } from './flow/Flow';
|
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { MdDeviceHub } from 'react-icons/md';
|
||||||
|
import 'reactflow/dist/style.css';
|
||||||
import AddNodePopover from './flow/AddNodePopover/AddNodePopover';
|
import AddNodePopover from './flow/AddNodePopover/AddNodePopover';
|
||||||
|
import { Flow } from './flow/Flow';
|
||||||
|
|
||||||
const NodeEditor = () => {
|
const NodeEditor = () => {
|
||||||
const [isPanelCollapsed, setIsPanelCollapsed] = useState(false);
|
|
||||||
const isReady = useAppSelector((state) => state.nodes.isReady);
|
const isReady = useAppSelector((state) => state.nodes.isReady);
|
||||||
return (
|
return (
|
||||||
<PanelGroup
|
|
||||||
id="node-editor"
|
|
||||||
autoSaveId="node-editor"
|
|
||||||
direction="horizontal"
|
|
||||||
style={{ height: '100%', width: '100%' }}
|
|
||||||
>
|
|
||||||
<Panel
|
|
||||||
id="node-editor-panel-group"
|
|
||||||
collapsible
|
|
||||||
onCollapse={setIsPanelCollapsed}
|
|
||||||
minSize={25}
|
|
||||||
>
|
|
||||||
<NodeEditorPanelGroup />
|
|
||||||
</Panel>
|
|
||||||
<ResizeHandle
|
|
||||||
collapsedDirection={isPanelCollapsed ? 'left' : undefined}
|
|
||||||
/>
|
|
||||||
<Panel id="node-editor-content">
|
|
||||||
<Flex
|
<Flex
|
||||||
layerStyle="first"
|
layerStyle="first"
|
||||||
sx={{
|
sx={{
|
||||||
@ -102,8 +80,6 @@ const NodeEditor = () => {
|
|||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Panel>
|
|
||||||
</PanelGroup>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,23 +1,36 @@
|
|||||||
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
|
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
|
||||||
import { memo, useState } from 'react';
|
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
|
||||||
import { Panel, PanelGroup } from 'react-resizable-panels';
|
import { memo, useCallback, useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
ImperativePanelGroupHandle,
|
||||||
|
Panel,
|
||||||
|
PanelGroup,
|
||||||
|
} from 'react-resizable-panels';
|
||||||
import 'reactflow/dist/style.css';
|
import 'reactflow/dist/style.css';
|
||||||
import WorkflowPanel from './workflow/WorkflowPanel';
|
|
||||||
import InspectorPanel from './inspector/InspectorPanel';
|
import InspectorPanel from './inspector/InspectorPanel';
|
||||||
|
import WorkflowPanel from './workflow/WorkflowPanel';
|
||||||
|
|
||||||
const NodeEditorPanelGroup = () => {
|
const NodeEditorPanelGroup = () => {
|
||||||
const [isTopPanelCollapsed, setIsTopPanelCollapsed] = useState(false);
|
const [isTopPanelCollapsed, setIsTopPanelCollapsed] = useState(false);
|
||||||
const [isBottomPanelCollapsed, setIsBottomPanelCollapsed] = useState(false);
|
const [isBottomPanelCollapsed, setIsBottomPanelCollapsed] = useState(false);
|
||||||
|
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
||||||
|
const panelStorage = usePanelStorage();
|
||||||
|
const handleDoubleClickHandle = useCallback(() => {
|
||||||
|
if (!panelGroupRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
panelGroupRef.current.setLayout([50, 50]);
|
||||||
|
}, []);
|
||||||
return (
|
return (
|
||||||
<PanelGroup
|
<PanelGroup
|
||||||
id="node-editor-panel_group"
|
ref={panelGroupRef}
|
||||||
autoSaveId="node-editor-panel_group"
|
id="workflow-panel-group"
|
||||||
direction="vertical"
|
direction="vertical"
|
||||||
style={{ height: '100%', width: '100%' }}
|
style={{ height: '100%', width: '100%' }}
|
||||||
|
storage={panelStorage}
|
||||||
>
|
>
|
||||||
<Panel
|
<Panel
|
||||||
id="node-editor-panel_group_workflow"
|
id="workflow"
|
||||||
collapsible
|
collapsible
|
||||||
onCollapse={setIsTopPanelCollapsed}
|
onCollapse={setIsTopPanelCollapsed}
|
||||||
minSize={25}
|
minSize={25}
|
||||||
@ -26,6 +39,7 @@ const NodeEditorPanelGroup = () => {
|
|||||||
</Panel>
|
</Panel>
|
||||||
<ResizeHandle
|
<ResizeHandle
|
||||||
direction="vertical"
|
direction="vertical"
|
||||||
|
onDoubleClick={handleDoubleClickHandle}
|
||||||
collapsedDirection={
|
collapsedDirection={
|
||||||
isTopPanelCollapsed
|
isTopPanelCollapsed
|
||||||
? 'top'
|
? 'top'
|
||||||
@ -35,7 +49,7 @@ const NodeEditorPanelGroup = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Panel
|
<Panel
|
||||||
id="node-editor-panel_group_inspector"
|
id="inspector"
|
||||||
collapsible
|
collapsible
|
||||||
onCollapse={setIsBottomPanelCollapsed}
|
onCollapse={setIsBottomPanelCollapsed}
|
||||||
minSize={25}
|
minSize={25}
|
||||||
|
@ -23,9 +23,8 @@ import { useFeatureStatus } from '../../../../system/hooks/useFeatureStatus';
|
|||||||
|
|
||||||
const promptInputSelector = createSelector(
|
const promptInputSelector = createSelector(
|
||||||
[stateSelector, activeTabNameSelector],
|
[stateSelector, activeTabNameSelector],
|
||||||
({ generation, ui }, activeTabName) => {
|
({ generation }, activeTabName) => {
|
||||||
return {
|
return {
|
||||||
shouldPinParametersPanel: ui.shouldPinParametersPanel,
|
|
||||||
prompt: generation.positivePrompt,
|
prompt: generation.positivePrompt,
|
||||||
activeTabName,
|
activeTabName,
|
||||||
};
|
};
|
||||||
@ -42,8 +41,7 @@ const promptInputSelector = createSelector(
|
|||||||
*/
|
*/
|
||||||
const ParamPositiveConditioning = () => {
|
const ParamPositiveConditioning = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { prompt, shouldPinParametersPanel, activeTabName } =
|
const { prompt, activeTabName } = useAppSelector(promptInputSelector);
|
||||||
useAppSelector(promptInputSelector);
|
|
||||||
const isReady = useIsReadyToInvoke();
|
const isReady = useIsReadyToInvoke();
|
||||||
const promptRef = useRef<HTMLTextAreaElement>(null);
|
const promptRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const { isOpen, onClose, onOpen } = useDisclosure();
|
const { isOpen, onClose, onOpen } = useDisclosure();
|
||||||
@ -148,7 +146,7 @@ const ParamPositiveConditioning = () => {
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: shouldPinParametersPanel ? 5 : 0,
|
top: 0,
|
||||||
insetInlineEnd: 0,
|
insetInlineEnd: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -55,11 +55,11 @@ const selector = createSelector(
|
|||||||
|
|
||||||
interface InvokeButton
|
interface InvokeButton
|
||||||
extends Omit<IAIButtonProps | IAIIconButtonProps, 'aria-label'> {
|
extends Omit<IAIButtonProps | IAIIconButtonProps, 'aria-label'> {
|
||||||
iconButton?: boolean;
|
asIconButton?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function InvokeButton(props: InvokeButton) {
|
export default function InvokeButton(props: InvokeButton) {
|
||||||
const { iconButton = false, ...rest } = props;
|
const { asIconButton = false, sx, ...rest } = props;
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { isReady, isProcessing } = useIsReadyToInvoke();
|
const { isReady, isProcessing } = useIsReadyToInvoke();
|
||||||
const { activeTabName } = useAppSelector(selector);
|
const { activeTabName } = useAppSelector(selector);
|
||||||
@ -87,21 +87,22 @@ export default function InvokeButton(props: InvokeButton) {
|
|||||||
<Box style={{ position: 'relative' }}>
|
<Box style={{ position: 'relative' }}>
|
||||||
{!isReady && (
|
{!isReady && (
|
||||||
<Box
|
<Box
|
||||||
borderRadius="base"
|
sx={{
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: '0',
|
bottom: '0',
|
||||||
left: '0',
|
left: '0',
|
||||||
right: '0',
|
right: '0',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
overflow: 'clip',
|
overflow: 'clip',
|
||||||
|
borderRadius: 'base',
|
||||||
|
...sx,
|
||||||
}}
|
}}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<ProgressBar />
|
<ProgressBar />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{iconButton ? (
|
{asIconButton ? (
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label={t('parameters.invoke')}
|
aria-label={t('parameters.invoke')}
|
||||||
type="submit"
|
type="submit"
|
||||||
@ -112,12 +113,13 @@ export default function InvokeButton(props: InvokeButton) {
|
|||||||
colorScheme="accent"
|
colorScheme="accent"
|
||||||
isLoading={isProcessing}
|
isLoading={isProcessing}
|
||||||
id="invoke-button"
|
id="invoke-button"
|
||||||
{...rest}
|
|
||||||
sx={{
|
sx={{
|
||||||
w: 'full',
|
w: 'full',
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
...(isProcessing ? IN_PROGRESS_STYLES : {}),
|
...(isProcessing ? IN_PROGRESS_STYLES : {}),
|
||||||
|
...sx,
|
||||||
}}
|
}}
|
||||||
|
{...rest}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<IAIButton
|
<IAIButton
|
||||||
@ -131,13 +133,14 @@ export default function InvokeButton(props: InvokeButton) {
|
|||||||
leftIcon={isProcessing ? undefined : <FaPlay />}
|
leftIcon={isProcessing ? undefined : <FaPlay />}
|
||||||
isLoading={isProcessing}
|
isLoading={isProcessing}
|
||||||
loadingText={t('parameters.invoke')}
|
loadingText={t('parameters.invoke')}
|
||||||
{...rest}
|
|
||||||
sx={{
|
sx={{
|
||||||
w: 'full',
|
w: 'full',
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
...(isProcessing ? IN_PROGRESS_STYLES : {}),
|
...(isProcessing ? IN_PROGRESS_STYLES : {}),
|
||||||
|
...sx,
|
||||||
}}
|
}}
|
||||||
|
{...rest}
|
||||||
>
|
>
|
||||||
Invoke
|
Invoke
|
||||||
</IAIButton>
|
</IAIButton>
|
||||||
@ -166,7 +169,11 @@ export const InvokeButtonTooltipContent = memo(() => {
|
|||||||
))}
|
))}
|
||||||
</UnorderedList>
|
</UnorderedList>
|
||||||
)}
|
)}
|
||||||
<Divider opacity={0.2} />
|
<Divider
|
||||||
|
opacity={0.2}
|
||||||
|
borderColor="base.50"
|
||||||
|
_dark={{ borderColor: 'base.900' }}
|
||||||
|
/>
|
||||||
<Text fontWeight={400} fontStyle="oblique 10deg">
|
<Text fontWeight={400} fontStyle="oblique 10deg">
|
||||||
Adding images to{' '}
|
Adding images to{' '}
|
||||||
<Text as="span" fontWeight={600}>
|
<Text as="span" fontWeight={600}>
|
||||||
|
@ -9,10 +9,6 @@ export default function ParamSDXLConcatButton() {
|
|||||||
(state: RootState) => state.sdxl.shouldConcatSDXLStylePrompt
|
(state: RootState) => state.sdxl.shouldConcatSDXLStylePrompt
|
||||||
);
|
);
|
||||||
|
|
||||||
const shouldPinParametersPanel = useAppSelector(
|
|
||||||
(state: RootState) => state.ui.shouldPinParametersPanel
|
|
||||||
);
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const handleShouldConcatPromptChange = () => {
|
const handleShouldConcatPromptChange = () => {
|
||||||
@ -31,7 +27,7 @@ export default function ParamSDXLConcatButton() {
|
|||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
insetInlineEnd: 1,
|
insetInlineEnd: 1,
|
||||||
top: shouldPinParametersPanel ? 12 : 20,
|
top: 6,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
color: shouldConcatSDXLStylePrompt ? 'accent.500' : 'base.500',
|
color: shouldConcatSDXLStylePrompt ? 'accent.500' : 'base.500',
|
||||||
_hover: {
|
_hover: {
|
||||||
|
@ -0,0 +1,82 @@
|
|||||||
|
import {
|
||||||
|
Flex,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
Text,
|
||||||
|
useDisclosure,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { LOCALSTORAGE_KEYS, LOCALSTORAGE_PREFIX } from 'app/store/constants';
|
||||||
|
import IAIButton from 'common/components/IAIButton';
|
||||||
|
import { memo, useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onSettingsModalClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ResetWebUIButton = ({ onSettingsModalClose }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [countdown, setCountdown] = useState(5);
|
||||||
|
|
||||||
|
const {
|
||||||
|
isOpen: isRefreshModalOpen,
|
||||||
|
onOpen: onRefreshModalOpen,
|
||||||
|
onClose: onRefreshModalClose,
|
||||||
|
} = useDisclosure();
|
||||||
|
|
||||||
|
const handleClickResetWebUI = useCallback(() => {
|
||||||
|
// Only remove our keys
|
||||||
|
Object.keys(window.localStorage).forEach((key) => {
|
||||||
|
if (
|
||||||
|
LOCALSTORAGE_KEYS.includes(key) ||
|
||||||
|
key.startsWith(LOCALSTORAGE_PREFIX)
|
||||||
|
) {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onSettingsModalClose();
|
||||||
|
onRefreshModalOpen();
|
||||||
|
setInterval(() => setCountdown((prev) => prev - 1), 1000);
|
||||||
|
}, [onSettingsModalClose, onRefreshModalOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (countdown <= 0) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}, [countdown]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IAIButton colorScheme="error" onClick={handleClickResetWebUI}>
|
||||||
|
{t('settings.resetWebUI')}
|
||||||
|
</IAIButton>
|
||||||
|
<Modal
|
||||||
|
closeOnOverlayClick={false}
|
||||||
|
isOpen={isRefreshModalOpen}
|
||||||
|
onClose={onRefreshModalClose}
|
||||||
|
isCentered
|
||||||
|
closeOnEsc={false}
|
||||||
|
>
|
||||||
|
<ModalOverlay backdropFilter="blur(40px)" />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader />
|
||||||
|
<ModalBody>
|
||||||
|
<Flex justifyContent="center">
|
||||||
|
<Text fontSize="lg">
|
||||||
|
<Text>{t('settings.resetComplete')}</Text>
|
||||||
|
<Text>Reloading in {countdown}...</Text>
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ResetWebUIButton);
|
@ -42,6 +42,7 @@ import {
|
|||||||
memo,
|
memo,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { LogLevelName } from 'roarr';
|
import { LogLevelName } from 'roarr';
|
||||||
@ -113,6 +114,7 @@ type SettingsModalProps = {
|
|||||||
const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [countdown, setCountdown] = useState(3);
|
||||||
|
|
||||||
const shouldShowBetaLayout = config?.shouldShowBetaLayout ?? true;
|
const shouldShowBetaLayout = config?.shouldShowBetaLayout ?? true;
|
||||||
const shouldShowDeveloperSettings =
|
const shouldShowDeveloperSettings =
|
||||||
@ -179,8 +181,15 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
|||||||
});
|
});
|
||||||
onSettingsModalClose();
|
onSettingsModalClose();
|
||||||
onRefreshModalOpen();
|
onRefreshModalOpen();
|
||||||
|
setInterval(() => setCountdown((prev) => prev - 1), 1000);
|
||||||
}, [onSettingsModalClose, onRefreshModalOpen]);
|
}, [onSettingsModalClose, onRefreshModalOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (countdown <= 0) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}, [countdown]);
|
||||||
|
|
||||||
const handleLogLevelChanged = useCallback(
|
const handleLogLevelChanged = useCallback(
|
||||||
(v: string) => {
|
(v: string) => {
|
||||||
dispatch(consoleLogLevelChanged(v as LogLevelName));
|
dispatch(consoleLogLevelChanged(v as LogLevelName));
|
||||||
@ -381,6 +390,7 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
|||||||
isOpen={isRefreshModalOpen}
|
isOpen={isRefreshModalOpen}
|
||||||
onClose={onRefreshModalClose}
|
onClose={onRefreshModalClose}
|
||||||
isCentered
|
isCentered
|
||||||
|
closeOnEsc={false}
|
||||||
>
|
>
|
||||||
<ModalOverlay backdropFilter="blur(40px)" />
|
<ModalOverlay backdropFilter="blur(40px)" />
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
@ -388,7 +398,9 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
|||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Flex justifyContent="center">
|
<Flex justifyContent="center">
|
||||||
<Text fontSize="lg">
|
<Text fontSize="lg">
|
||||||
<Text>{t('settings.resetComplete')}</Text>
|
<Text>
|
||||||
|
{t('settings.resetComplete')} Reloading in {countdown}...
|
||||||
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
@ -1,65 +1,61 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { Flex } from '@chakra-ui/layout';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { Portal } from '@chakra-ui/portal';
|
||||||
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||||
import { setShouldShowGallery } from 'features/ui/store/uiSlice';
|
import { RefObject, memo } from 'react';
|
||||||
import { isEqual } from 'lodash-es';
|
|
||||||
import { memo } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { MdPhotoLibrary } from 'react-icons/md';
|
import { MdPhotoLibrary } from 'react-icons/md';
|
||||||
import { activeTabNameSelector, uiSelector } from '../store/uiSelectors';
|
import { ImperativePanelHandle } from 'react-resizable-panels';
|
||||||
import { NO_GALLERY_TABS } from './InvokeTabs';
|
|
||||||
|
|
||||||
const floatingGalleryButtonSelector = createSelector(
|
type Props = {
|
||||||
[activeTabNameSelector, uiSelector],
|
isGalleryCollapsed: boolean;
|
||||||
(activeTabName, ui) => {
|
galleryPanelRef: RefObject<ImperativePanelHandle>;
|
||||||
const { shouldPinGallery, shouldShowGallery } = ui;
|
|
||||||
|
|
||||||
return {
|
|
||||||
shouldPinGallery,
|
|
||||||
shouldShowGalleryButton: NO_GALLERY_TABS.includes(activeTabName)
|
|
||||||
? false
|
|
||||||
: !shouldShowGallery,
|
|
||||||
};
|
};
|
||||||
},
|
|
||||||
{ memoizeOptions: { resultEqualityCheck: isEqual } }
|
|
||||||
);
|
|
||||||
|
|
||||||
const FloatingGalleryButton = () => {
|
const FloatingGalleryButton = ({
|
||||||
|
isGalleryCollapsed,
|
||||||
|
galleryPanelRef,
|
||||||
|
}: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { shouldPinGallery, shouldShowGalleryButton } = useAppSelector(
|
|
||||||
floatingGalleryButtonSelector
|
|
||||||
);
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const handleShowGallery = () => {
|
const handleShowGallery = () => {
|
||||||
dispatch(setShouldShowGallery(true));
|
galleryPanelRef.current?.expand();
|
||||||
shouldPinGallery && dispatch(requestCanvasRescale());
|
dispatch(requestCanvasRescale());
|
||||||
};
|
};
|
||||||
|
|
||||||
return shouldShowGalleryButton ? (
|
if (!isGalleryCollapsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<Flex
|
||||||
|
pos="absolute"
|
||||||
|
transform="translate(0, -50%)"
|
||||||
|
minW={8}
|
||||||
|
top="50%"
|
||||||
|
insetInlineEnd={0}
|
||||||
|
>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
tooltip="Show Gallery (G)"
|
tooltip="Show Gallery (G)"
|
||||||
tooltipProps={{ placement: 'top' }}
|
tooltipProps={{ placement: 'top' }}
|
||||||
aria-label={t('accessibility.showGallery')}
|
aria-label={t('common.showGalleryPanel')}
|
||||||
onClick={handleShowGallery}
|
onClick={handleShowGallery}
|
||||||
|
icon={<MdPhotoLibrary />}
|
||||||
sx={{
|
sx={{
|
||||||
pos: 'absolute',
|
|
||||||
top: '50%',
|
|
||||||
transform: 'translate(0, -50%)',
|
|
||||||
p: 0,
|
p: 0,
|
||||||
insetInlineEnd: 0,
|
|
||||||
px: 3,
|
px: 3,
|
||||||
h: 48,
|
h: 48,
|
||||||
w: 8,
|
|
||||||
borderStartEndRadius: 0,
|
borderStartEndRadius: 0,
|
||||||
borderEndEndRadius: 0,
|
borderEndEndRadius: 0,
|
||||||
shadow: '2xl',
|
shadow: '2xl',
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<MdPhotoLibrary />
|
</Flex>
|
||||||
</IAIIconButton>
|
</Portal>
|
||||||
) : null;
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(FloatingGalleryButton);
|
export default memo(FloatingGalleryButton);
|
||||||
|
@ -1,20 +1,14 @@
|
|||||||
import { ChakraProps, Flex } from '@chakra-ui/react';
|
import { ChakraProps, Flex, Portal } from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||||
import CancelButton from 'features/parameters/components/ProcessButtons/CancelButton';
|
import CancelButton from 'features/parameters/components/ProcessButtons/CancelButton';
|
||||||
import InvokeButton from 'features/parameters/components/ProcessButtons/InvokeButton';
|
import InvokeButton from 'features/parameters/components/ProcessButtons/InvokeButton';
|
||||||
import {
|
import { RefObject, memo } from 'react';
|
||||||
activeTabNameSelector,
|
|
||||||
uiSelector,
|
|
||||||
} from 'features/ui/store/uiSelectors';
|
|
||||||
import { setShouldShowParametersPanel } from 'features/ui/store/uiSlice';
|
|
||||||
import { isEqual } from 'lodash-es';
|
|
||||||
import { memo } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { FaSlidersH } from 'react-icons/fa';
|
import { FaSlidersH } from 'react-icons/fa';
|
||||||
|
import { ImperativePanelHandle } from 'react-resizable-panels';
|
||||||
|
|
||||||
const floatingButtonStyles: ChakraProps['sx'] = {
|
const floatingButtonStyles: ChakraProps['sx'] = {
|
||||||
borderStartStartRadius: 0,
|
borderStartStartRadius: 0,
|
||||||
@ -22,55 +16,29 @@ const floatingButtonStyles: ChakraProps['sx'] = {
|
|||||||
shadow: '2xl',
|
shadow: '2xl',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const floatingParametersPanelButtonSelector = createSelector(
|
type Props = {
|
||||||
[uiSelector, activeTabNameSelector],
|
isSidePanelCollapsed: boolean;
|
||||||
(ui, activeTabName) => {
|
sidePanelRef: RefObject<ImperativePanelHandle>;
|
||||||
const {
|
|
||||||
shouldPinParametersPanel,
|
|
||||||
shouldUseCanvasBetaLayout,
|
|
||||||
shouldShowParametersPanel,
|
|
||||||
} = ui;
|
|
||||||
|
|
||||||
const canvasBetaLayoutCheck =
|
|
||||||
shouldUseCanvasBetaLayout && activeTabName === 'unifiedCanvas';
|
|
||||||
|
|
||||||
const shouldShowProcessButtons =
|
|
||||||
!canvasBetaLayoutCheck &&
|
|
||||||
(!shouldPinParametersPanel || !shouldShowParametersPanel);
|
|
||||||
|
|
||||||
const shouldShowParametersPanelButton =
|
|
||||||
!canvasBetaLayoutCheck &&
|
|
||||||
!shouldShowParametersPanel &&
|
|
||||||
['txt2img', 'img2img', 'unifiedCanvas'].includes(activeTabName);
|
|
||||||
|
|
||||||
return {
|
|
||||||
shouldPinParametersPanel,
|
|
||||||
shouldShowParametersPanelButton,
|
|
||||||
shouldShowProcessButtons,
|
|
||||||
};
|
};
|
||||||
},
|
|
||||||
{ memoizeOptions: { resultEqualityCheck: isEqual } }
|
|
||||||
);
|
|
||||||
|
|
||||||
const FloatingParametersPanelButtons = () => {
|
const FloatingSidePanelButtons = ({
|
||||||
const dispatch = useAppDispatch();
|
isSidePanelCollapsed,
|
||||||
|
sidePanelRef,
|
||||||
|
}: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {
|
const dispatch = useAppDispatch();
|
||||||
shouldShowProcessButtons,
|
|
||||||
shouldShowParametersPanelButton,
|
|
||||||
shouldPinParametersPanel,
|
|
||||||
} = useAppSelector(floatingParametersPanelButtonSelector);
|
|
||||||
|
|
||||||
const handleShowOptionsPanel = () => {
|
const handleShowSidePanel = () => {
|
||||||
dispatch(setShouldShowParametersPanel(true));
|
sidePanelRef.current?.expand();
|
||||||
shouldPinParametersPanel && dispatch(requestCanvasRescale());
|
dispatch(requestCanvasRescale());
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!shouldShowParametersPanelButton) {
|
if (!isSidePanelCollapsed) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Portal>
|
||||||
<Flex
|
<Flex
|
||||||
pos="absolute"
|
pos="absolute"
|
||||||
transform="translate(0, -50%)"
|
transform="translate(0, -50%)"
|
||||||
@ -81,22 +49,17 @@ const FloatingParametersPanelButtons = () => {
|
|||||||
gap={2}
|
gap={2}
|
||||||
>
|
>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
tooltip="Show Options Panel (O)"
|
tooltip="Show Side Panel (O, T)"
|
||||||
tooltipProps={{ placement: 'top' }}
|
aria-label={t('common.showOptionsPanel')}
|
||||||
aria-label={t('accessibility.showOptionsPanel')}
|
onClick={handleShowSidePanel}
|
||||||
onClick={handleShowOptionsPanel}
|
|
||||||
sx={floatingButtonStyles}
|
sx={floatingButtonStyles}
|
||||||
>
|
icon={<FaSlidersH />}
|
||||||
<FaSlidersH />
|
/>
|
||||||
</IAIIconButton>
|
<InvokeButton asIconButton sx={floatingButtonStyles} />
|
||||||
{shouldShowProcessButtons && (
|
|
||||||
<>
|
|
||||||
<InvokeButton iconButton sx={floatingButtonStyles} />
|
|
||||||
<CancelButton sx={floatingButtonStyles} />
|
<CancelButton sx={floatingButtonStyles} />
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</Portal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(FloatingParametersPanelButtons);
|
export default memo(FloatingSidePanelButtons);
|
||||||
|
@ -11,12 +11,13 @@ import {
|
|||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import AuxiliaryProgressIndicator from 'app/components/AuxiliaryProgressIndicator';
|
import AuxiliaryProgressIndicator from 'app/components/AuxiliaryProgressIndicator';
|
||||||
import { RootState, stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||||
import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent';
|
import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent';
|
||||||
|
import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup';
|
||||||
import { InvokeTabName, tabMap } from 'features/ui/store/tabMap';
|
import { InvokeTabName, tabMap } from 'features/ui/store/tabMap';
|
||||||
import { setActiveTab, togglePanels } from 'features/ui/store/uiSlice';
|
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||||
import { ResourceKey } from 'i18next';
|
import { ResourceKey } from 'i18next';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
import { MouseEvent, ReactNode, memo, useCallback, useMemo } from 'react';
|
import { MouseEvent, ReactNode, memo, useCallback, useMemo } from 'react';
|
||||||
@ -25,11 +26,15 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { FaCube, FaFont, FaImage } from 'react-icons/fa';
|
import { FaCube, FaFont, FaImage } from 'react-icons/fa';
|
||||||
import { MdDeviceHub, MdGridOn } from 'react-icons/md';
|
import { MdDeviceHub, MdGridOn } from 'react-icons/md';
|
||||||
import { Panel, PanelGroup } from 'react-resizable-panels';
|
import { Panel, PanelGroup } from 'react-resizable-panels';
|
||||||
import { useMinimumPanelSize } from '../hooks/useMinimumPanelSize';
|
import { usePanel } from '../hooks/usePanel';
|
||||||
|
import { usePanelStorage } from '../hooks/usePanelStorage';
|
||||||
import {
|
import {
|
||||||
activeTabIndexSelector,
|
activeTabIndexSelector,
|
||||||
activeTabNameSelector,
|
activeTabNameSelector,
|
||||||
} from '../store/uiSelectors';
|
} from '../store/uiSelectors';
|
||||||
|
import FloatingGalleryButton from './FloatingGalleryButton';
|
||||||
|
import FloatingSidePanelButtons from './FloatingParametersPanelButtons';
|
||||||
|
import ParametersPanel from './ParametersPanel';
|
||||||
import ImageTab from './tabs/ImageToImage/ImageToImageTab';
|
import ImageTab from './tabs/ImageToImage/ImageToImageTab';
|
||||||
import ModelManagerTab from './tabs/ModelManager/ModelManagerTab';
|
import ModelManagerTab from './tabs/ModelManager/ModelManagerTab';
|
||||||
import NodesTab from './tabs/Nodes/NodesTab';
|
import NodesTab from './tabs/Nodes/NodesTab';
|
||||||
@ -89,32 +94,20 @@ const enabledTabsSelector = createSelector(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const MIN_GALLERY_WIDTH = 350;
|
const SIDE_PANEL_MIN_SIZE_PX = 448;
|
||||||
const DEFAULT_GALLERY_PCT = 20;
|
const MAIN_PANEL_MIN_SIZE_PX = 448;
|
||||||
|
const GALLERY_PANEL_MIN_SIZE_PX = 360;
|
||||||
|
|
||||||
export const NO_GALLERY_TABS: InvokeTabName[] = ['modelManager'];
|
export const NO_GALLERY_TABS: InvokeTabName[] = ['modelManager'];
|
||||||
|
export const NO_SIDE_PANEL_TABS: InvokeTabName[] = ['modelManager'];
|
||||||
|
|
||||||
const InvokeTabs = () => {
|
const InvokeTabs = () => {
|
||||||
const activeTab = useAppSelector(activeTabIndexSelector);
|
const activeTab = useAppSelector(activeTabIndexSelector);
|
||||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
const enabledTabs = useAppSelector(enabledTabsSelector);
|
const enabledTabs = useAppSelector(enabledTabsSelector);
|
||||||
|
|
||||||
const { shouldPinGallery, shouldPinParametersPanel, shouldShowGallery } =
|
|
||||||
useAppSelector((state: RootState) => state.ui);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
useHotkeys(
|
|
||||||
'f',
|
|
||||||
() => {
|
|
||||||
dispatch(togglePanels());
|
|
||||||
(shouldPinGallery || shouldPinParametersPanel) &&
|
|
||||||
dispatch(requestCanvasRescale());
|
|
||||||
},
|
|
||||||
[shouldPinGallery, shouldPinParametersPanel]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleResizeGallery = useCallback(() => {
|
const handleResizeGallery = useCallback(() => {
|
||||||
if (activeTabName === 'unifiedCanvas') {
|
if (activeTabName === 'unifiedCanvas') {
|
||||||
dispatch(requestCanvasRescale());
|
dispatch(requestCanvasRescale());
|
||||||
@ -153,9 +146,6 @@ const InvokeTabs = () => {
|
|||||||
[enabledTabs]
|
[enabledTabs]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { ref: galleryPanelRef, minSizePct: galleryMinSizePct } =
|
|
||||||
useMinimumPanelSize(MIN_GALLERY_WIDTH, DEFAULT_GALLERY_PCT, 'app');
|
|
||||||
|
|
||||||
const handleTabChange = useCallback(
|
const handleTabChange = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
const activeTabName = tabMap[index];
|
const activeTabName = tabMap[index];
|
||||||
@ -167,6 +157,63 @@ const InvokeTabs = () => {
|
|||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
minSize: sidePanelMinSize,
|
||||||
|
isCollapsed: isSidePanelCollapsed,
|
||||||
|
setIsCollapsed: setIsSidePanelCollapsed,
|
||||||
|
ref: sidePanelRef,
|
||||||
|
reset: resetSidePanel,
|
||||||
|
expand: expandSidePanel,
|
||||||
|
collapse: collapseSidePanel,
|
||||||
|
toggle: toggleSidePanel,
|
||||||
|
} = usePanel(SIDE_PANEL_MIN_SIZE_PX, 'pixels');
|
||||||
|
|
||||||
|
const {
|
||||||
|
ref: galleryPanelRef,
|
||||||
|
minSize: galleryPanelMinSize,
|
||||||
|
isCollapsed: isGalleryPanelCollapsed,
|
||||||
|
setIsCollapsed: setIsGalleryPanelCollapsed,
|
||||||
|
reset: resetGalleryPanel,
|
||||||
|
expand: expandGalleryPanel,
|
||||||
|
collapse: collapseGalleryPanel,
|
||||||
|
toggle: toggleGalleryPanel,
|
||||||
|
} = usePanel(GALLERY_PANEL_MIN_SIZE_PX, 'pixels');
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'f',
|
||||||
|
() => {
|
||||||
|
if (isGalleryPanelCollapsed || isSidePanelCollapsed) {
|
||||||
|
expandGalleryPanel();
|
||||||
|
expandSidePanel();
|
||||||
|
} else {
|
||||||
|
collapseSidePanel();
|
||||||
|
collapseGalleryPanel();
|
||||||
|
}
|
||||||
|
dispatch(requestCanvasRescale());
|
||||||
|
},
|
||||||
|
[dispatch, isGalleryPanelCollapsed, isSidePanelCollapsed]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['t', 'o'],
|
||||||
|
() => {
|
||||||
|
toggleSidePanel();
|
||||||
|
dispatch(requestCanvasRescale());
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'g',
|
||||||
|
() => {
|
||||||
|
toggleGalleryPanel();
|
||||||
|
dispatch(requestCanvasRescale());
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const panelStorage = usePanelStorage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
variant="appTabs"
|
variant="appTabs"
|
||||||
@ -195,33 +242,66 @@ const InvokeTabs = () => {
|
|||||||
autoSaveId="app"
|
autoSaveId="app"
|
||||||
direction="horizontal"
|
direction="horizontal"
|
||||||
style={{ height: '100%', width: '100%' }}
|
style={{ height: '100%', width: '100%' }}
|
||||||
|
storage={panelStorage}
|
||||||
|
units="pixels"
|
||||||
>
|
>
|
||||||
<Panel id="main">
|
{!NO_SIDE_PANEL_TABS.includes(activeTabName) && (
|
||||||
|
<>
|
||||||
|
<Panel
|
||||||
|
order={0}
|
||||||
|
id="side"
|
||||||
|
ref={sidePanelRef}
|
||||||
|
defaultSize={sidePanelMinSize}
|
||||||
|
minSize={sidePanelMinSize}
|
||||||
|
onResize={handleResizeGallery}
|
||||||
|
onCollapse={setIsSidePanelCollapsed}
|
||||||
|
collapsible
|
||||||
|
>
|
||||||
|
{activeTabName === 'nodes' ? (
|
||||||
|
<NodeEditorPanelGroup />
|
||||||
|
) : (
|
||||||
|
<ParametersPanel />
|
||||||
|
)}
|
||||||
|
</Panel>
|
||||||
|
<ResizeHandle
|
||||||
|
onDoubleClick={resetSidePanel}
|
||||||
|
isCollapsed={isSidePanelCollapsed}
|
||||||
|
collapsedDirection={isSidePanelCollapsed ? 'left' : undefined}
|
||||||
|
/>
|
||||||
|
<FloatingSidePanelButtons
|
||||||
|
isSidePanelCollapsed={isSidePanelCollapsed}
|
||||||
|
sidePanelRef={sidePanelRef}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Panel id="main" order={1} minSize={MAIN_PANEL_MIN_SIZE_PX}>
|
||||||
<TabPanels style={{ height: '100%', width: '100%' }}>
|
<TabPanels style={{ height: '100%', width: '100%' }}>
|
||||||
{tabPanels}
|
{tabPanels}
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
</Panel>
|
</Panel>
|
||||||
{shouldPinGallery &&
|
{!NO_GALLERY_TABS.includes(activeTabName) && (
|
||||||
shouldShowGallery &&
|
|
||||||
!NO_GALLERY_TABS.includes(activeTabName) && (
|
|
||||||
<>
|
<>
|
||||||
<ResizeHandle />
|
<ResizeHandle
|
||||||
|
isCollapsed={isGalleryPanelCollapsed}
|
||||||
|
onDoubleClick={resetGalleryPanel}
|
||||||
|
collapsedDirection={isGalleryPanelCollapsed ? 'right' : undefined}
|
||||||
|
/>
|
||||||
<Panel
|
<Panel
|
||||||
ref={galleryPanelRef}
|
|
||||||
onResize={handleResizeGallery}
|
|
||||||
id="gallery"
|
id="gallery"
|
||||||
order={3}
|
ref={galleryPanelRef}
|
||||||
defaultSize={
|
order={2}
|
||||||
galleryMinSizePct > DEFAULT_GALLERY_PCT &&
|
onResize={handleResizeGallery}
|
||||||
galleryMinSizePct < 100 // prevent this error https://github.com/bvaughn/react-resizable-panels/blob/main/packages/react-resizable-panels/src/Panel.ts#L96
|
defaultSize={galleryPanelMinSize}
|
||||||
? galleryMinSizePct
|
minSize={galleryPanelMinSize}
|
||||||
: DEFAULT_GALLERY_PCT
|
onCollapse={setIsGalleryPanelCollapsed}
|
||||||
}
|
collapsible
|
||||||
minSize={galleryMinSizePct}
|
|
||||||
maxSize={50}
|
|
||||||
>
|
>
|
||||||
<ImageGalleryContent />
|
<ImageGalleryContent />
|
||||||
</Panel>
|
</Panel>
|
||||||
|
<FloatingGalleryButton
|
||||||
|
isGalleryCollapsed={isGalleryPanelCollapsed}
|
||||||
|
galleryPanelRef={galleryPanelRef}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</PanelGroup>
|
</PanelGroup>
|
||||||
|
@ -1,116 +0,0 @@
|
|||||||
import { Flex } from '@chakra-ui/react';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { RootState } from 'app/store/store';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
|
||||||
import SDXLImageToImageTabParameters from 'features/sdxl/components/SDXLImageToImageTabParameters';
|
|
||||||
import SDXLTextToImageTabParameters from 'features/sdxl/components/SDXLTextToImageTabParameters';
|
|
||||||
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
|
|
||||||
import {
|
|
||||||
activeTabNameSelector,
|
|
||||||
uiSelector,
|
|
||||||
} from 'features/ui/store/uiSelectors';
|
|
||||||
import { setShouldShowParametersPanel } from 'features/ui/store/uiSlice';
|
|
||||||
import { memo, useMemo } from 'react';
|
|
||||||
import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants';
|
|
||||||
import PinParametersPanelButton from './PinParametersPanelButton';
|
|
||||||
import ResizableDrawer from './common/ResizableDrawer/ResizableDrawer';
|
|
||||||
import ImageToImageTabParameters from './tabs/ImageToImage/ImageToImageTabParameters';
|
|
||||||
import TextToImageTabParameters from './tabs/TextToImage/TextToImageTabParameters';
|
|
||||||
import UnifiedCanvasParameters from './tabs/UnifiedCanvas/UnifiedCanvasParameters';
|
|
||||||
|
|
||||||
const selector = createSelector(
|
|
||||||
[uiSelector, activeTabNameSelector],
|
|
||||||
(ui, activeTabName) => {
|
|
||||||
const { shouldPinParametersPanel, shouldShowParametersPanel } = ui;
|
|
||||||
|
|
||||||
return {
|
|
||||||
activeTabName,
|
|
||||||
shouldPinParametersPanel,
|
|
||||||
shouldShowParametersPanel,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
defaultSelectorOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
const ParametersDrawer = () => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { shouldPinParametersPanel, shouldShowParametersPanel, activeTabName } =
|
|
||||||
useAppSelector(selector);
|
|
||||||
|
|
||||||
const handleClosePanel = () => {
|
|
||||||
dispatch(setShouldShowParametersPanel(false));
|
|
||||||
};
|
|
||||||
|
|
||||||
const model = useAppSelector((state: RootState) => state.generation.model);
|
|
||||||
|
|
||||||
const drawerContent = useMemo(() => {
|
|
||||||
if (activeTabName === 'txt2img') {
|
|
||||||
return model && model.base_model === 'sdxl' ? (
|
|
||||||
<SDXLTextToImageTabParameters />
|
|
||||||
) : (
|
|
||||||
<TextToImageTabParameters />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeTabName === 'img2img') {
|
|
||||||
return model && model.base_model === 'sdxl' ? (
|
|
||||||
<SDXLImageToImageTabParameters />
|
|
||||||
) : (
|
|
||||||
<ImageToImageTabParameters />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeTabName === 'unifiedCanvas') {
|
|
||||||
return <UnifiedCanvasParameters />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}, [activeTabName, model]);
|
|
||||||
|
|
||||||
if (shouldPinParametersPanel) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ResizableDrawer
|
|
||||||
direction="left"
|
|
||||||
isResizable={false}
|
|
||||||
isOpen={shouldShowParametersPanel}
|
|
||||||
onClose={handleClosePanel}
|
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
sx={{
|
|
||||||
flexDir: 'column',
|
|
||||||
h: 'full',
|
|
||||||
w: PARAMETERS_PANEL_WIDTH,
|
|
||||||
gap: 2,
|
|
||||||
position: 'relative',
|
|
||||||
flexShrink: 0,
|
|
||||||
overflowY: 'auto',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
paddingBottom={4}
|
|
||||||
justifyContent="space-between"
|
|
||||||
alignItems="center"
|
|
||||||
>
|
|
||||||
<InvokeAILogoComponent />
|
|
||||||
<PinParametersPanelButton />
|
|
||||||
</Flex>
|
|
||||||
<Flex
|
|
||||||
sx={{
|
|
||||||
gap: 2,
|
|
||||||
flexDirection: 'column',
|
|
||||||
h: 'full',
|
|
||||||
w: 'full',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{drawerContent}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</ResizableDrawer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(ParametersDrawer);
|
|
@ -0,0 +1,109 @@
|
|||||||
|
import { Box, Flex } from '@chakra-ui/react';
|
||||||
|
import { RootState } from 'app/store/store';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import SDXLImageToImageTabParameters from 'features/sdxl/components/SDXLImageToImageTabParameters';
|
||||||
|
import SDXLTextToImageTabParameters from 'features/sdxl/components/SDXLTextToImageTabParameters';
|
||||||
|
import SDXLUnifiedCanvasTabParameters from 'features/sdxl/components/SDXLUnifiedCanvasTabParameters';
|
||||||
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||||
|
import { PropsWithChildren, memo } from 'react';
|
||||||
|
import { activeTabNameSelector } from '../store/uiSelectors';
|
||||||
|
import ImageToImageTabParameters from './tabs/ImageToImage/ImageToImageTabParameters';
|
||||||
|
import TextToImageTabParameters from './tabs/TextToImage/TextToImageTabParameters';
|
||||||
|
import UnifiedCanvasParameters from './tabs/UnifiedCanvas/UnifiedCanvasParameters';
|
||||||
|
|
||||||
|
const ParametersPanel = () => {
|
||||||
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
|
const model = useAppSelector((state: RootState) => state.generation.model);
|
||||||
|
|
||||||
|
if (activeTabName === 'txt2img') {
|
||||||
|
return (
|
||||||
|
<ParametersPanelWrapper>
|
||||||
|
{model && model.base_model === 'sdxl' ? (
|
||||||
|
<SDXLTextToImageTabParameters />
|
||||||
|
) : (
|
||||||
|
<TextToImageTabParameters />
|
||||||
|
)}
|
||||||
|
</ParametersPanelWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeTabName === 'img2img') {
|
||||||
|
return (
|
||||||
|
<ParametersPanelWrapper>
|
||||||
|
{model && model.base_model === 'sdxl' ? (
|
||||||
|
<SDXLImageToImageTabParameters />
|
||||||
|
) : (
|
||||||
|
<ImageToImageTabParameters />
|
||||||
|
)}
|
||||||
|
</ParametersPanelWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeTabName === 'unifiedCanvas') {
|
||||||
|
return (
|
||||||
|
<ParametersPanelWrapper>
|
||||||
|
{model && model.base_model === 'sdxl' ? (
|
||||||
|
<SDXLUnifiedCanvasTabParameters />
|
||||||
|
) : (
|
||||||
|
<UnifiedCanvasParameters />
|
||||||
|
)}
|
||||||
|
</ParametersPanelWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ParametersPanel);
|
||||||
|
|
||||||
|
const ParametersPanelWrapper = memo((props: PropsWithChildren) => {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
position: 'relative',
|
||||||
|
borderRadius: 'base',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<OverlayScrollbarsComponent
|
||||||
|
defer
|
||||||
|
style={{ height: '100%', width: '100%' }}
|
||||||
|
options={{
|
||||||
|
scrollbars: {
|
||||||
|
visibility: 'auto',
|
||||||
|
autoHide: 'scroll',
|
||||||
|
autoHideDelay: 800,
|
||||||
|
theme: 'os-theme-dark',
|
||||||
|
},
|
||||||
|
overflow: {
|
||||||
|
x: 'hidden',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
gap: 2,
|
||||||
|
flexDirection: 'column',
|
||||||
|
h: 'full',
|
||||||
|
w: 'full',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Flex>
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ParametersPanelWrapper.displayName = 'ParametersPanelWrapper';
|
@ -1,57 +0,0 @@
|
|||||||
import { Box, Flex } from '@chakra-ui/react';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { PropsWithChildren, memo } from 'react';
|
|
||||||
import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants';
|
|
||||||
import { uiSelector } from '../store/uiSelectors';
|
|
||||||
import PinParametersPanelButton from './PinParametersPanelButton';
|
|
||||||
|
|
||||||
const selector = createSelector(uiSelector, (ui) => {
|
|
||||||
const { shouldPinParametersPanel, shouldShowParametersPanel } = ui;
|
|
||||||
|
|
||||||
return {
|
|
||||||
shouldPinParametersPanel,
|
|
||||||
shouldShowParametersPanel,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
type ParametersPinnedWrapperProps = PropsWithChildren;
|
|
||||||
|
|
||||||
const ParametersPinnedWrapper = (props: ParametersPinnedWrapperProps) => {
|
|
||||||
const { shouldPinParametersPanel, shouldShowParametersPanel } =
|
|
||||||
useAppSelector(selector);
|
|
||||||
|
|
||||||
if (!(shouldPinParametersPanel && shouldShowParametersPanel)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: 'relative',
|
|
||||||
h: 'full',
|
|
||||||
w: PARAMETERS_PANEL_WIDTH,
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
sx={{
|
|
||||||
gap: 2,
|
|
||||||
flexDirection: 'column',
|
|
||||||
h: 'full',
|
|
||||||
w: 'full',
|
|
||||||
position: 'absolute',
|
|
||||||
overflowY: 'auto',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<PinParametersPanelButton
|
|
||||||
sx={{ position: 'absolute', top: 0, insetInlineEnd: 0 }}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(ParametersPinnedWrapper);
|
|
@ -1,59 +0,0 @@
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import IAIIconButton, {
|
|
||||||
IAIIconButtonProps,
|
|
||||||
} from 'common/components/IAIIconButton';
|
|
||||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
|
|
||||||
import { setShouldPinParametersPanel } from '../store/uiSlice';
|
|
||||||
import { memo } from 'react';
|
|
||||||
|
|
||||||
type PinParametersPanelButtonProps = Omit<IAIIconButtonProps, 'aria-label'>;
|
|
||||||
|
|
||||||
const PinParametersPanelButton = (props: PinParametersPanelButtonProps) => {
|
|
||||||
const { sx } = props;
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const shouldPinParametersPanel = useAppSelector(
|
|
||||||
(state) => state.ui.shouldPinParametersPanel
|
|
||||||
);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const handleClickPinOptionsPanel = () => {
|
|
||||||
dispatch(setShouldPinParametersPanel(!shouldPinParametersPanel));
|
|
||||||
dispatch(requestCanvasRescale());
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IAIIconButton
|
|
||||||
{...props}
|
|
||||||
tooltip={t('common.pinOptionsPanel')}
|
|
||||||
aria-label={t('common.pinOptionsPanel')}
|
|
||||||
onClick={handleClickPinOptionsPanel}
|
|
||||||
icon={shouldPinParametersPanel ? <BsPinAngleFill /> : <BsPinAngle />}
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
sx={{
|
|
||||||
color: 'base.500',
|
|
||||||
_hover: {
|
|
||||||
color: 'base.600',
|
|
||||||
},
|
|
||||||
_active: {
|
|
||||||
color: 'base.700',
|
|
||||||
},
|
|
||||||
_dark: {
|
|
||||||
color: 'base.500',
|
|
||||||
_hover: {
|
|
||||||
color: 'base.400',
|
|
||||||
},
|
|
||||||
_active: {
|
|
||||||
color: 'base.300',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...sx,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(PinParametersPanelButton);
|
|
@ -1,196 +0,0 @@
|
|||||||
import {
|
|
||||||
Box,
|
|
||||||
chakra,
|
|
||||||
ChakraProps,
|
|
||||||
Slide,
|
|
||||||
useOutsideClick,
|
|
||||||
useTheme,
|
|
||||||
SlideDirection,
|
|
||||||
useColorMode,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import {
|
|
||||||
Resizable,
|
|
||||||
ResizableProps,
|
|
||||||
ResizeCallback,
|
|
||||||
ResizeStartCallback,
|
|
||||||
} from 're-resizable';
|
|
||||||
import { ReactNode, memo, useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import { LangDirection } from './types';
|
|
||||||
import {
|
|
||||||
getHandleEnables,
|
|
||||||
getMinMaxDimensions,
|
|
||||||
getSlideDirection,
|
|
||||||
getStyles,
|
|
||||||
} from './util';
|
|
||||||
import { mode } from 'theme/util/mode';
|
|
||||||
|
|
||||||
type ResizableDrawerProps = ResizableProps & {
|
|
||||||
children: ReactNode;
|
|
||||||
isResizable: boolean;
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
direction?: SlideDirection;
|
|
||||||
initialWidth?: number;
|
|
||||||
minWidth?: number;
|
|
||||||
maxWidth?: number;
|
|
||||||
initialHeight?: number;
|
|
||||||
minHeight?: number;
|
|
||||||
maxHeight?: number;
|
|
||||||
onResizeStart?: ResizeStartCallback;
|
|
||||||
onResizeStop?: ResizeCallback;
|
|
||||||
onResize?: ResizeCallback;
|
|
||||||
handleWidth?: string | number;
|
|
||||||
handleInteractWidth?: string | number;
|
|
||||||
sx?: ChakraProps['sx'];
|
|
||||||
};
|
|
||||||
|
|
||||||
const ChakraResizeable = chakra(Resizable, {
|
|
||||||
shouldForwardProp: (prop) => !['sx'].includes(prop),
|
|
||||||
});
|
|
||||||
|
|
||||||
const ResizableDrawer = ({
|
|
||||||
direction = 'left',
|
|
||||||
isResizable,
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
children,
|
|
||||||
initialWidth,
|
|
||||||
minWidth,
|
|
||||||
maxWidth,
|
|
||||||
initialHeight,
|
|
||||||
minHeight,
|
|
||||||
maxHeight,
|
|
||||||
onResizeStart,
|
|
||||||
onResizeStop,
|
|
||||||
onResize,
|
|
||||||
sx = {},
|
|
||||||
}: ResizableDrawerProps) => {
|
|
||||||
const langDirection = useTheme().direction as LangDirection;
|
|
||||||
const { colorMode } = useColorMode();
|
|
||||||
const outsideClickRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const defaultWidth = useMemo(
|
|
||||||
() =>
|
|
||||||
initialWidth ??
|
|
||||||
minWidth ??
|
|
||||||
(['left', 'right'].includes(direction) ? 'auto' : '100%'),
|
|
||||||
[initialWidth, minWidth, direction]
|
|
||||||
);
|
|
||||||
|
|
||||||
const defaultHeight = useMemo(
|
|
||||||
() =>
|
|
||||||
initialHeight ??
|
|
||||||
minHeight ??
|
|
||||||
(['top', 'bottom'].includes(direction) ? 'auto' : '100%'),
|
|
||||||
[initialHeight, minHeight, direction]
|
|
||||||
);
|
|
||||||
|
|
||||||
const [width, setWidth] = useState<number | string>(defaultWidth);
|
|
||||||
|
|
||||||
const [height, setHeight] = useState<number | string>(defaultHeight);
|
|
||||||
|
|
||||||
useOutsideClick({
|
|
||||||
ref: outsideClickRef,
|
|
||||||
handler: () => {
|
|
||||||
onClose();
|
|
||||||
},
|
|
||||||
enabled: isOpen,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleEnables = useMemo(
|
|
||||||
() => (isResizable ? getHandleEnables({ direction, langDirection }) : {}),
|
|
||||||
[isResizable, langDirection, direction]
|
|
||||||
);
|
|
||||||
|
|
||||||
const minMaxDimensions = useMemo(
|
|
||||||
() =>
|
|
||||||
getMinMaxDimensions({
|
|
||||||
direction,
|
|
||||||
minWidth,
|
|
||||||
maxWidth,
|
|
||||||
minHeight,
|
|
||||||
maxHeight,
|
|
||||||
}),
|
|
||||||
[minWidth, maxWidth, minHeight, maxHeight, direction]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { containerStyles, handleStyles } = useMemo(
|
|
||||||
() =>
|
|
||||||
getStyles({
|
|
||||||
isResizable,
|
|
||||||
direction,
|
|
||||||
}),
|
|
||||||
[isResizable, direction]
|
|
||||||
);
|
|
||||||
|
|
||||||
const slideDirection = useMemo(
|
|
||||||
() => getSlideDirection(direction, langDirection),
|
|
||||||
[direction, langDirection]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (['left', 'right'].includes(direction)) {
|
|
||||||
setHeight('100vh');
|
|
||||||
// setHeight(isPinned ? '100%' : '100vh');
|
|
||||||
}
|
|
||||||
if (['top', 'bottom'].includes(direction)) {
|
|
||||||
setWidth('100vw');
|
|
||||||
// setWidth(isPinned ? '100%' : '100vw');
|
|
||||||
}
|
|
||||||
}, [direction]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Slide
|
|
||||||
direction={slideDirection}
|
|
||||||
in={isOpen}
|
|
||||||
motionProps={{ initial: false }}
|
|
||||||
style={{ width: 'full' }}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
ref={outsideClickRef}
|
|
||||||
sx={{
|
|
||||||
width: 'full',
|
|
||||||
height: 'full',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ChakraResizeable
|
|
||||||
size={{
|
|
||||||
width: isResizable ? width : defaultWidth,
|
|
||||||
height: isResizable ? height : defaultHeight,
|
|
||||||
}}
|
|
||||||
enable={handleEnables}
|
|
||||||
handleStyles={handleStyles}
|
|
||||||
{...minMaxDimensions}
|
|
||||||
sx={{
|
|
||||||
borderColor: mode('base.200', 'base.800')(colorMode),
|
|
||||||
p: 4,
|
|
||||||
bg: mode('base.50', 'base.900')(colorMode),
|
|
||||||
height: 'full',
|
|
||||||
shadow: isOpen ? 'dark-lg' : undefined,
|
|
||||||
...containerStyles,
|
|
||||||
...sx,
|
|
||||||
}}
|
|
||||||
onResizeStart={(event, direction, elementRef) => {
|
|
||||||
onResizeStart && onResizeStart(event, direction, elementRef);
|
|
||||||
}}
|
|
||||||
onResize={(event, direction, elementRef, delta) => {
|
|
||||||
onResize && onResize(event, direction, elementRef, delta);
|
|
||||||
}}
|
|
||||||
onResizeStop={(event, direction, elementRef, delta) => {
|
|
||||||
if (['left', 'right'].includes(direction)) {
|
|
||||||
setWidth(Number(width) + delta.width);
|
|
||||||
}
|
|
||||||
if (['top', 'bottom'].includes(direction)) {
|
|
||||||
setHeight(Number(height) + delta.height);
|
|
||||||
}
|
|
||||||
onResizeStop && onResizeStop(event, direction, elementRef, delta);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ChakraResizeable>
|
|
||||||
</Box>
|
|
||||||
</Slide>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(ResizableDrawer);
|
|
@ -1,2 +0,0 @@
|
|||||||
export type Placement = 'top' | 'right' | 'bottom' | 'left';
|
|
||||||
export type LangDirection = 'ltr' | 'rtl' | undefined;
|
|
@ -1,283 +0,0 @@
|
|||||||
import { SlideDirection } from '@chakra-ui/react';
|
|
||||||
import { AnimationProps } from 'framer-motion';
|
|
||||||
import { HandleStyles } from 're-resizable';
|
|
||||||
import { CSSProperties } from 'react';
|
|
||||||
import { LangDirection } from './types';
|
|
||||||
|
|
||||||
export type GetHandleEnablesOptions = {
|
|
||||||
direction: SlideDirection;
|
|
||||||
langDirection: LangDirection;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine handles to enable. `re-resizable` doesn't handle RTL, so we have to do that here.
|
|
||||||
*/
|
|
||||||
export const getHandleEnables = ({
|
|
||||||
direction,
|
|
||||||
langDirection,
|
|
||||||
}: GetHandleEnablesOptions) => {
|
|
||||||
const top = direction === 'bottom';
|
|
||||||
|
|
||||||
const right =
|
|
||||||
(langDirection !== 'rtl' && direction === 'left') ||
|
|
||||||
(langDirection === 'rtl' && direction === 'right');
|
|
||||||
|
|
||||||
const bottom = direction === 'top';
|
|
||||||
|
|
||||||
const left =
|
|
||||||
(langDirection !== 'rtl' && direction === 'right') ||
|
|
||||||
(langDirection === 'rtl' && direction === 'left');
|
|
||||||
|
|
||||||
return {
|
|
||||||
top,
|
|
||||||
right,
|
|
||||||
bottom,
|
|
||||||
left,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetDefaultSizeOptions = {
|
|
||||||
initialWidth?: string | number;
|
|
||||||
initialHeight?: string | number;
|
|
||||||
direction: SlideDirection;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get default sizes based on direction and initial values
|
|
||||||
export const getDefaultSize = ({
|
|
||||||
initialWidth,
|
|
||||||
initialHeight,
|
|
||||||
direction,
|
|
||||||
}: GetDefaultSizeOptions) => {
|
|
||||||
const width =
|
|
||||||
initialWidth ?? (['left', 'right'].includes(direction) ? 500 : '100vw');
|
|
||||||
|
|
||||||
const height =
|
|
||||||
initialHeight ?? (['top', 'bottom'].includes(direction) ? 500 : '100vh');
|
|
||||||
|
|
||||||
return { width, height };
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetMinMaxDimensionsOptions = {
|
|
||||||
direction: SlideDirection;
|
|
||||||
minWidth?: number;
|
|
||||||
maxWidth?: number;
|
|
||||||
minHeight?: number;
|
|
||||||
maxHeight?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get the min/max width/height based on direction and provided values
|
|
||||||
export const getMinMaxDimensions = ({
|
|
||||||
direction,
|
|
||||||
minWidth,
|
|
||||||
maxWidth,
|
|
||||||
minHeight,
|
|
||||||
maxHeight,
|
|
||||||
}: GetMinMaxDimensionsOptions) => {
|
|
||||||
const minW =
|
|
||||||
minWidth ?? (['left', 'right'].includes(direction) ? 10 : undefined);
|
|
||||||
|
|
||||||
const maxW =
|
|
||||||
maxWidth ?? (['left', 'right'].includes(direction) ? '95vw' : undefined);
|
|
||||||
|
|
||||||
const minH =
|
|
||||||
minHeight ?? (['top', 'bottom'].includes(direction) ? 10 : undefined);
|
|
||||||
|
|
||||||
const maxH =
|
|
||||||
maxHeight ?? (['top', 'bottom'].includes(direction) ? '95vh' : undefined);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...(minW ? { minWidth: minW } : {}),
|
|
||||||
...(maxW ? { maxWidth: maxW } : {}),
|
|
||||||
...(minH ? { minHeight: minH } : {}),
|
|
||||||
...(maxH ? { maxHeight: maxH } : {}),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetAnimationsOptions = {
|
|
||||||
direction: SlideDirection;
|
|
||||||
langDirection: LangDirection;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get the framer-motion animation props, taking into account language direction
|
|
||||||
export const getAnimations = ({
|
|
||||||
direction,
|
|
||||||
langDirection,
|
|
||||||
}: GetAnimationsOptions): AnimationProps => {
|
|
||||||
const baseAnimation = {
|
|
||||||
initial: { opacity: 0 },
|
|
||||||
animate: { opacity: 1 },
|
|
||||||
exit: { opacity: 0 },
|
|
||||||
// chakra consumes the transition prop, which, for it, is a string.
|
|
||||||
// however we know the transition prop will make it to framer motion,
|
|
||||||
// which wants it as an object. cast as string to satisfy TS.
|
|
||||||
transition: { duration: 0.2, ease: 'easeInOut' },
|
|
||||||
};
|
|
||||||
|
|
||||||
const langDirectionFactor = langDirection === 'rtl' ? -1 : 1;
|
|
||||||
|
|
||||||
if (direction === 'top') {
|
|
||||||
return {
|
|
||||||
...baseAnimation,
|
|
||||||
initial: { y: -999 },
|
|
||||||
animate: { y: 0 },
|
|
||||||
exit: { y: -999 },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (direction === 'right') {
|
|
||||||
return {
|
|
||||||
...baseAnimation,
|
|
||||||
initial: { x: 999 * langDirectionFactor },
|
|
||||||
animate: { x: 0 },
|
|
||||||
exit: { x: 999 * langDirectionFactor },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (direction === 'bottom') {
|
|
||||||
return {
|
|
||||||
...baseAnimation,
|
|
||||||
initial: { y: 999 },
|
|
||||||
animate: { y: 0 },
|
|
||||||
exit: { y: 999 },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (direction === 'left') {
|
|
||||||
return {
|
|
||||||
...baseAnimation,
|
|
||||||
initial: { x: -999 * langDirectionFactor },
|
|
||||||
animate: { x: 0 },
|
|
||||||
exit: { x: -999 * langDirectionFactor },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetResizableStylesProps = {
|
|
||||||
isResizable: boolean;
|
|
||||||
direction: SlideDirection;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Expand the handle hitbox
|
|
||||||
const HANDLE_INTERACT_PADDING = '0.75rem';
|
|
||||||
|
|
||||||
// Visible padding around handle
|
|
||||||
const HANDLE_PADDING = '1rem';
|
|
||||||
const HANDLE_WIDTH = '5px';
|
|
||||||
|
|
||||||
// Get the styles for the container and handle. Do not need to handle langDirection here bc we use direction-agnostic CSS
|
|
||||||
export const getStyles = ({
|
|
||||||
isResizable,
|
|
||||||
direction,
|
|
||||||
}: GetResizableStylesProps): {
|
|
||||||
containerStyles: CSSProperties; // technically this could be ChakraProps['sx'], but we cannot use this for HandleStyles so leave it as CSSProperties to be consistent
|
|
||||||
handleStyles: HandleStyles;
|
|
||||||
} => {
|
|
||||||
// if (!isResizable) {
|
|
||||||
// return { containerStyles: {}, handleStyles: {} };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Calculate the positioning offset of the handle hitbox so it is centered over the handle
|
|
||||||
const handleOffset = `calc((2 * ${HANDLE_INTERACT_PADDING} + ${HANDLE_WIDTH}) / -2)`;
|
|
||||||
|
|
||||||
if (direction === 'top') {
|
|
||||||
return {
|
|
||||||
containerStyles: {
|
|
||||||
borderBottomWidth: HANDLE_WIDTH,
|
|
||||||
paddingBottom: HANDLE_PADDING,
|
|
||||||
},
|
|
||||||
handleStyles: isResizable
|
|
||||||
? {
|
|
||||||
top: {
|
|
||||||
paddingTop: HANDLE_INTERACT_PADDING,
|
|
||||||
paddingBottom: HANDLE_INTERACT_PADDING,
|
|
||||||
bottom: handleOffset,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (direction === 'left') {
|
|
||||||
return {
|
|
||||||
containerStyles: {
|
|
||||||
borderInlineEndWidth: HANDLE_WIDTH,
|
|
||||||
paddingInlineEnd: HANDLE_PADDING,
|
|
||||||
},
|
|
||||||
handleStyles: isResizable
|
|
||||||
? {
|
|
||||||
right: {
|
|
||||||
paddingInlineStart: HANDLE_INTERACT_PADDING,
|
|
||||||
paddingInlineEnd: HANDLE_INTERACT_PADDING,
|
|
||||||
insetInlineEnd: handleOffset,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (direction === 'bottom') {
|
|
||||||
return {
|
|
||||||
containerStyles: {
|
|
||||||
borderTopWidth: HANDLE_WIDTH,
|
|
||||||
paddingTop: HANDLE_PADDING,
|
|
||||||
},
|
|
||||||
handleStyles: isResizable
|
|
||||||
? {
|
|
||||||
bottom: {
|
|
||||||
paddingTop: HANDLE_INTERACT_PADDING,
|
|
||||||
paddingBottom: HANDLE_INTERACT_PADDING,
|
|
||||||
top: handleOffset,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (direction === 'right') {
|
|
||||||
return {
|
|
||||||
containerStyles: {
|
|
||||||
borderInlineStartWidth: HANDLE_WIDTH,
|
|
||||||
paddingInlineStart: HANDLE_PADDING,
|
|
||||||
},
|
|
||||||
handleStyles: isResizable
|
|
||||||
? {
|
|
||||||
left: {
|
|
||||||
paddingInlineStart: HANDLE_INTERACT_PADDING,
|
|
||||||
paddingInlineEnd: HANDLE_INTERACT_PADDING,
|
|
||||||
insetInlineStart: handleOffset,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { containerStyles: {}, handleStyles: {} };
|
|
||||||
};
|
|
||||||
|
|
||||||
// Chakra's Slide does not handle langDirection, so we need to do it here
|
|
||||||
export const getSlideDirection = (
|
|
||||||
direction: SlideDirection,
|
|
||||||
langDirection: LangDirection
|
|
||||||
) => {
|
|
||||||
if (['top', 'bottom'].includes(direction)) {
|
|
||||||
return direction;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (direction === 'left') {
|
|
||||||
if (langDirection === 'rtl') {
|
|
||||||
return 'right';
|
|
||||||
}
|
|
||||||
return 'left';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (direction === 'right') {
|
|
||||||
if (langDirection === 'rtl') {
|
|
||||||
return 'left';
|
|
||||||
}
|
|
||||||
return 'right';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'left';
|
|
||||||
};
|
|
@ -1,48 +1,39 @@
|
|||||||
import { Box, Flex } from '@chakra-ui/react';
|
import { Box } from '@chakra-ui/react';
|
||||||
import { RootState } from 'app/store/store';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||||
import InitialImageDisplay from 'features/parameters/components/Parameters/ImageToImage/InitialImageDisplay';
|
import InitialImageDisplay from 'features/parameters/components/Parameters/ImageToImage/InitialImageDisplay';
|
||||||
import SDXLImageToImageTabParameters from 'features/sdxl/components/SDXLImageToImageTabParameters';
|
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
|
||||||
import { memo, useCallback, useRef } from 'react';
|
import { memo, useCallback, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
ImperativePanelGroupHandle,
|
ImperativePanelGroupHandle,
|
||||||
Panel,
|
Panel,
|
||||||
PanelGroup,
|
PanelGroup,
|
||||||
} from 'react-resizable-panels';
|
} from 'react-resizable-panels';
|
||||||
import ParametersPinnedWrapper from '../../ParametersPinnedWrapper';
|
|
||||||
import ResizeHandle from '../ResizeHandle';
|
import ResizeHandle from '../ResizeHandle';
|
||||||
import TextToImageTabMain from '../TextToImage/TextToImageTabMain';
|
import TextToImageTabMain from '../TextToImage/TextToImageTabMain';
|
||||||
import ImageToImageTabParameters from './ImageToImageTabParameters';
|
|
||||||
|
|
||||||
const ImageToImageTab = () => {
|
const ImageToImageTab = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
||||||
const model = useAppSelector((state: RootState) => state.generation.model);
|
|
||||||
|
|
||||||
const handleDoubleClickHandle = useCallback(() => {
|
const handleDoubleClickHandle = useCallback(() => {
|
||||||
if (!panelGroupRef.current) {
|
if (!panelGroupRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
panelGroupRef.current.setLayout([50, 50]);
|
panelGroupRef.current.setLayout([50, 50]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const panelStorage = usePanelStorage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex sx={{ gap: 4, w: 'full', h: 'full' }}>
|
|
||||||
<ParametersPinnedWrapper>
|
|
||||||
{model && model.base_model === 'sdxl' ? (
|
|
||||||
<SDXLImageToImageTabParameters />
|
|
||||||
) : (
|
|
||||||
<ImageToImageTabParameters />
|
|
||||||
)}
|
|
||||||
</ParametersPinnedWrapper>
|
|
||||||
<Box sx={{ w: 'full', h: 'full' }}>
|
<Box sx={{ w: 'full', h: 'full' }}>
|
||||||
<PanelGroup
|
<PanelGroup
|
||||||
ref={panelGroupRef}
|
ref={panelGroupRef}
|
||||||
autoSaveId="imageTab.content"
|
autoSaveId="imageTab.content"
|
||||||
direction="horizontal"
|
direction="horizontal"
|
||||||
style={{ height: '100%', width: '100%' }}
|
style={{ height: '100%', width: '100%' }}
|
||||||
|
storage={panelStorage}
|
||||||
|
units="percentages"
|
||||||
>
|
>
|
||||||
<Panel
|
<Panel
|
||||||
id="imageTab.content.initImage"
|
id="imageTab.content.initImage"
|
||||||
@ -67,7 +58,6 @@ const ImageToImageTab = () => {
|
|||||||
</Panel>
|
</Panel>
|
||||||
</PanelGroup>
|
</PanelGroup>
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -5,16 +5,27 @@ import { PanelResizeHandle } from 'react-resizable-panels';
|
|||||||
type ResizeHandleProps = Omit<FlexProps, 'direction'> & {
|
type ResizeHandleProps = Omit<FlexProps, 'direction'> & {
|
||||||
direction?: 'horizontal' | 'vertical';
|
direction?: 'horizontal' | 'vertical';
|
||||||
collapsedDirection?: 'top' | 'bottom' | 'left' | 'right';
|
collapsedDirection?: 'top' | 'bottom' | 'left' | 'right';
|
||||||
|
isCollapsed?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ResizeHandle = (props: ResizeHandleProps) => {
|
const ResizeHandle = (props: ResizeHandleProps) => {
|
||||||
const { direction = 'horizontal', collapsedDirection, ...rest } = props;
|
const {
|
||||||
|
direction = 'horizontal',
|
||||||
|
collapsedDirection,
|
||||||
|
isCollapsed = false,
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
const bg = useColorModeValue('base.100', 'base.850');
|
const bg = useColorModeValue('base.100', 'base.850');
|
||||||
const hoverBg = useColorModeValue('base.300', 'base.700');
|
const hoverBg = useColorModeValue('base.300', 'base.700');
|
||||||
|
|
||||||
if (direction === 'horizontal') {
|
if (direction === 'horizontal') {
|
||||||
return (
|
return (
|
||||||
<PanelResizeHandle>
|
<PanelResizeHandle
|
||||||
|
style={{
|
||||||
|
visibility: isCollapsed ? 'hidden' : 'visible',
|
||||||
|
width: isCollapsed ? 0 : 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Flex
|
<Flex
|
||||||
className="resize-handle-horizontal"
|
className="resize-handle-horizontal"
|
||||||
sx={{
|
sx={{
|
||||||
@ -50,7 +61,12 @@ const ResizeHandle = (props: ResizeHandleProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelResizeHandle>
|
<PanelResizeHandle
|
||||||
|
style={{
|
||||||
|
visibility: isCollapsed ? 'hidden' : 'visible',
|
||||||
|
width: isCollapsed ? 0 : 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Flex
|
<Flex
|
||||||
className="resize-handle-vertical"
|
className="resize-handle-vertical"
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -1,26 +1,8 @@
|
|||||||
import { Flex } from '@chakra-ui/react';
|
|
||||||
import { RootState } from 'app/store/store';
|
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import SDXLTextToImageTabParameters from 'features/sdxl/components/SDXLTextToImageTabParameters';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import ParametersPinnedWrapper from '../../ParametersPinnedWrapper';
|
|
||||||
import TextToImageTabMain from './TextToImageTabMain';
|
import TextToImageTabMain from './TextToImageTabMain';
|
||||||
import TextToImageTabParameters from './TextToImageTabParameters';
|
|
||||||
|
|
||||||
const TextToImageTab = () => {
|
const TextToImageTab = () => {
|
||||||
const model = useAppSelector((state: RootState) => state.generation.model);
|
return <TextToImageTabMain />;
|
||||||
return (
|
|
||||||
<Flex sx={{ gap: 4, w: 'full', h: 'full' }}>
|
|
||||||
<ParametersPinnedWrapper>
|
|
||||||
{model && model.base_model === 'sdxl' ? (
|
|
||||||
<SDXLTextToImageTabParameters />
|
|
||||||
) : (
|
|
||||||
<TextToImageTabParameters />
|
|
||||||
)}
|
|
||||||
</ParametersPinnedWrapper>
|
|
||||||
<TextToImageTabMain />
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(TextToImageTab);
|
export default memo(TextToImageTab);
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
import { Flex } from '@chakra-ui/layout';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
|
||||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
|
||||||
import CancelButton from 'features/parameters/components/ProcessButtons/CancelButton';
|
|
||||||
import InvokeButton from 'features/parameters/components/ProcessButtons/InvokeButton';
|
|
||||||
import { setShouldShowParametersPanel } from 'features/ui/store/uiSlice';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { FaSlidersH } from 'react-icons/fa';
|
|
||||||
|
|
||||||
export default function UnifiedCanvasProcessingButtons() {
|
|
||||||
const shouldPinParametersPanel = useAppSelector(
|
|
||||||
(state) => state.ui.shouldPinParametersPanel
|
|
||||||
);
|
|
||||||
const shouldShowParametersPanel = useAppSelector(
|
|
||||||
(state) => state.ui.shouldShowParametersPanel
|
|
||||||
);
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const handleShowOptionsPanel = () => {
|
|
||||||
dispatch(setShouldShowParametersPanel(true));
|
|
||||||
shouldPinParametersPanel && dispatch(requestCanvasRescale());
|
|
||||||
};
|
|
||||||
|
|
||||||
return !shouldPinParametersPanel || !shouldShowParametersPanel ? (
|
|
||||||
<Flex flexDirection="column" gap={2}>
|
|
||||||
<IAIIconButton
|
|
||||||
tooltip={`${t('parameters.showOptionsPanel')} (O)`}
|
|
||||||
tooltipProps={{ placement: 'top' }}
|
|
||||||
aria-label={t('parameters.showOptionsPanel')}
|
|
||||||
onClick={handleShowOptionsPanel}
|
|
||||||
>
|
|
||||||
<FaSlidersH />
|
|
||||||
</IAIIconButton>
|
|
||||||
<Flex>
|
|
||||||
<InvokeButton iconButton />
|
|
||||||
</Flex>
|
|
||||||
<Flex>
|
|
||||||
<CancelButton width="100%" height="40px" btnGroupWidth="100%" />
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
) : null;
|
|
||||||
}
|
|
@ -2,6 +2,7 @@ import { Flex } from '@chakra-ui/react';
|
|||||||
|
|
||||||
import IAICanvasRedoButton from 'features/canvas/components/IAICanvasToolbar/IAICanvasRedoButton';
|
import IAICanvasRedoButton from 'features/canvas/components/IAICanvasToolbar/IAICanvasRedoButton';
|
||||||
import IAICanvasUndoButton from 'features/canvas/components/IAICanvasToolbar/IAICanvasUndoButton';
|
import IAICanvasUndoButton from 'features/canvas/components/IAICanvasToolbar/IAICanvasUndoButton';
|
||||||
|
import { memo } from 'react';
|
||||||
import UnifiedCanvasSettings from './UnifiedCanvasToolSettings/UnifiedCanvasSettings';
|
import UnifiedCanvasSettings from './UnifiedCanvasToolSettings/UnifiedCanvasSettings';
|
||||||
import UnifiedCanvasCopyToClipboard from './UnifiedCanvasToolbar/UnifiedCanvasCopyToClipboard';
|
import UnifiedCanvasCopyToClipboard from './UnifiedCanvasToolbar/UnifiedCanvasCopyToClipboard';
|
||||||
import UnifiedCanvasDownloadImage from './UnifiedCanvasToolbar/UnifiedCanvasDownloadImage';
|
import UnifiedCanvasDownloadImage from './UnifiedCanvasToolbar/UnifiedCanvasDownloadImage';
|
||||||
@ -9,12 +10,10 @@ import UnifiedCanvasFileUploader from './UnifiedCanvasToolbar/UnifiedCanvasFileU
|
|||||||
import UnifiedCanvasLayerSelect from './UnifiedCanvasToolbar/UnifiedCanvasLayerSelect';
|
import UnifiedCanvasLayerSelect from './UnifiedCanvasToolbar/UnifiedCanvasLayerSelect';
|
||||||
import UnifiedCanvasMergeVisible from './UnifiedCanvasToolbar/UnifiedCanvasMergeVisible';
|
import UnifiedCanvasMergeVisible from './UnifiedCanvasToolbar/UnifiedCanvasMergeVisible';
|
||||||
import UnifiedCanvasMoveTool from './UnifiedCanvasToolbar/UnifiedCanvasMoveTool';
|
import UnifiedCanvasMoveTool from './UnifiedCanvasToolbar/UnifiedCanvasMoveTool';
|
||||||
import UnifiedCanvasProcessingButtons from './UnifiedCanvasToolbar/UnifiedCanvasProcessingButtons';
|
|
||||||
import UnifiedCanvasResetCanvas from './UnifiedCanvasToolbar/UnifiedCanvasResetCanvas';
|
import UnifiedCanvasResetCanvas from './UnifiedCanvasToolbar/UnifiedCanvasResetCanvas';
|
||||||
import UnifiedCanvasResetView from './UnifiedCanvasToolbar/UnifiedCanvasResetView';
|
import UnifiedCanvasResetView from './UnifiedCanvasToolbar/UnifiedCanvasResetView';
|
||||||
import UnifiedCanvasSaveToGallery from './UnifiedCanvasToolbar/UnifiedCanvasSaveToGallery';
|
import UnifiedCanvasSaveToGallery from './UnifiedCanvasToolbar/UnifiedCanvasSaveToGallery';
|
||||||
import UnifiedCanvasToolSelect from './UnifiedCanvasToolbar/UnifiedCanvasToolSelect';
|
import UnifiedCanvasToolSelect from './UnifiedCanvasToolbar/UnifiedCanvasToolSelect';
|
||||||
import { memo } from 'react';
|
|
||||||
|
|
||||||
const UnifiedCanvasToolbarBeta = () => {
|
const UnifiedCanvasToolbarBeta = () => {
|
||||||
return (
|
return (
|
||||||
@ -47,7 +46,6 @@ const UnifiedCanvasToolbarBeta = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<UnifiedCanvasSettings />
|
<UnifiedCanvasSettings />
|
||||||
<UnifiedCanvasProcessingButtons />
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,26 +1,8 @@
|
|||||||
import { Flex } from '@chakra-ui/react';
|
|
||||||
import { RootState } from 'app/store/store';
|
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import SDXLUnifiedCanvasTabParameters from 'features/sdxl/components/SDXLUnifiedCanvasTabParameters';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import ParametersPinnedWrapper from '../../ParametersPinnedWrapper';
|
|
||||||
import UnifiedCanvasContent from './UnifiedCanvasContent';
|
import UnifiedCanvasContent from './UnifiedCanvasContent';
|
||||||
import UnifiedCanvasParameters from './UnifiedCanvasParameters';
|
|
||||||
|
|
||||||
const UnifiedCanvasTab = () => {
|
const UnifiedCanvasTab = () => {
|
||||||
const model = useAppSelector((state: RootState) => state.generation.model);
|
return <UnifiedCanvasContent />;
|
||||||
return (
|
|
||||||
<Flex sx={{ gap: 4, w: 'full', h: 'full' }}>
|
|
||||||
<ParametersPinnedWrapper>
|
|
||||||
{model && model.base_model === 'sdxl' ? (
|
|
||||||
<SDXLUnifiedCanvasTabParameters />
|
|
||||||
) : (
|
|
||||||
<UnifiedCanvasParameters />
|
|
||||||
)}
|
|
||||||
</ParametersPinnedWrapper>
|
|
||||||
<UnifiedCanvasContent />
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(UnifiedCanvasTab);
|
export default memo(UnifiedCanvasTab);
|
||||||
|
@ -1,75 +0,0 @@
|
|||||||
// adapted from https://github.com/bvaughn/react-resizable-panels/issues/141#issuecomment-1540048714
|
|
||||||
|
|
||||||
import {
|
|
||||||
RefObject,
|
|
||||||
useCallback,
|
|
||||||
useLayoutEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { ImperativePanelHandle } from 'react-resizable-panels';
|
|
||||||
|
|
||||||
export const useMinimumPanelSize = (
|
|
||||||
minSizePx: number,
|
|
||||||
defaultSizePct: number,
|
|
||||||
groupId: string,
|
|
||||||
orientation: 'horizontal' | 'vertical' = 'horizontal'
|
|
||||||
): {
|
|
||||||
ref: RefObject<ImperativePanelHandle>;
|
|
||||||
minSizePct: number;
|
|
||||||
} => {
|
|
||||||
const ref = useRef<ImperativePanelHandle>(null);
|
|
||||||
const [minSizePct, setMinSizePct] = useState(defaultSizePct);
|
|
||||||
|
|
||||||
const handleWindowResize = useCallback(() => {
|
|
||||||
const size = ref.current?.getSize();
|
|
||||||
|
|
||||||
if (size !== undefined && size < minSizePct) {
|
|
||||||
ref.current?.resize(minSizePct);
|
|
||||||
}
|
|
||||||
}, [minSizePct]);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
const panelGroup = document.querySelector(
|
|
||||||
`[data-panel-group-id="${groupId}"]`
|
|
||||||
);
|
|
||||||
const resizeHandles = document.querySelectorAll(
|
|
||||||
orientation === 'horizontal'
|
|
||||||
? '.resize-handle-horizontal'
|
|
||||||
: '.resize-handle-vertical'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!panelGroup) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const observer = new ResizeObserver(() => {
|
|
||||||
let dim =
|
|
||||||
orientation === 'horizontal'
|
|
||||||
? panelGroup.getBoundingClientRect().width
|
|
||||||
: panelGroup.getBoundingClientRect().height;
|
|
||||||
|
|
||||||
resizeHandles.forEach((resizeHandle) => {
|
|
||||||
dim -=
|
|
||||||
orientation === 'horizontal'
|
|
||||||
? resizeHandle.getBoundingClientRect().width
|
|
||||||
: resizeHandle.getBoundingClientRect().height;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Minimum size in pixels is a percentage of the PanelGroup's width/height
|
|
||||||
setMinSizePct((minSizePx / dim) * 100);
|
|
||||||
});
|
|
||||||
observer.observe(panelGroup);
|
|
||||||
resizeHandles.forEach((resizeHandle) => {
|
|
||||||
observer.observe(resizeHandle);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('resize', handleWindowResize);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
observer.disconnect();
|
|
||||||
window.removeEventListener('resize', handleWindowResize);
|
|
||||||
};
|
|
||||||
}, [groupId, handleWindowResize, minSizePct, minSizePx, orientation]);
|
|
||||||
|
|
||||||
return { ref, minSizePct };
|
|
||||||
};
|
|
52
invokeai/frontend/web/src/features/ui/hooks/usePanel.ts
Normal file
52
invokeai/frontend/web/src/features/ui/hooks/usePanel.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { useCallback, useRef, useState } from 'react';
|
||||||
|
import { flushSync } from 'react-dom';
|
||||||
|
import { ImperativePanelHandle, Units } from 'react-resizable-panels';
|
||||||
|
|
||||||
|
export const usePanel = (minSize: number, units: Units) => {
|
||||||
|
const ref = useRef<ImperativePanelHandle>(null);
|
||||||
|
|
||||||
|
const [isCollapsed, setIsCollapsed] = useState(() =>
|
||||||
|
Boolean(ref.current?.getCollapsed())
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggle = useCallback(() => {
|
||||||
|
if (ref.current?.getCollapsed()) {
|
||||||
|
flushSync(() => {
|
||||||
|
ref.current?.expand();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
flushSync(() => {
|
||||||
|
ref.current?.collapse();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const expand = useCallback(() => {
|
||||||
|
flushSync(() => {
|
||||||
|
ref.current?.expand();
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const collapse = useCallback(() => {
|
||||||
|
flushSync(() => {
|
||||||
|
ref.current?.collapse();
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const reset = useCallback(() => {
|
||||||
|
flushSync(() => {
|
||||||
|
ref.current?.resize(minSize, units);
|
||||||
|
});
|
||||||
|
}, [minSize, units]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ref,
|
||||||
|
minSize,
|
||||||
|
isCollapsed,
|
||||||
|
setIsCollapsed,
|
||||||
|
reset,
|
||||||
|
toggle,
|
||||||
|
expand,
|
||||||
|
collapse,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,17 @@
|
|||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { panelsChanged } from '../store/uiSlice';
|
||||||
|
|
||||||
|
export const usePanelStorage = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const panels = useAppSelector((state) => state.ui.panels);
|
||||||
|
const getItem = useCallback((name: string) => panels[name] ?? '', [panels]);
|
||||||
|
const setItem = useCallback(
|
||||||
|
(name: string, value: string) => {
|
||||||
|
dispatch(panelsChanged({ name, value }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { getItem, setItem };
|
||||||
|
};
|
@ -6,4 +6,5 @@ import { UIState } from './uiTypes';
|
|||||||
export const uiPersistDenylist: (keyof UIState)[] = [
|
export const uiPersistDenylist: (keyof UIState)[] = [
|
||||||
'shouldShowImageDetails',
|
'shouldShowImageDetails',
|
||||||
'globalContextMenuCloseTrigger',
|
'globalContextMenuCloseTrigger',
|
||||||
|
'panels',
|
||||||
];
|
];
|
||||||
|
@ -8,19 +8,16 @@ import { UIState } from './uiTypes';
|
|||||||
|
|
||||||
export const initialUIState: UIState = {
|
export const initialUIState: UIState = {
|
||||||
activeTab: 0,
|
activeTab: 0,
|
||||||
shouldPinParametersPanel: true,
|
|
||||||
shouldShowParametersPanel: true,
|
|
||||||
shouldShowImageDetails: false,
|
shouldShowImageDetails: false,
|
||||||
shouldUseCanvasBetaLayout: false,
|
shouldUseCanvasBetaLayout: false,
|
||||||
shouldShowExistingModelsInSearch: false,
|
shouldShowExistingModelsInSearch: false,
|
||||||
shouldUseSliders: false,
|
shouldUseSliders: false,
|
||||||
shouldPinGallery: true,
|
|
||||||
shouldShowGallery: true,
|
|
||||||
shouldHidePreview: false,
|
shouldHidePreview: false,
|
||||||
shouldShowProgressInViewer: true,
|
shouldShowProgressInViewer: true,
|
||||||
shouldShowEmbeddingPicker: false,
|
shouldShowEmbeddingPicker: false,
|
||||||
favoriteSchedulers: [],
|
favoriteSchedulers: [],
|
||||||
globalContextMenuCloseTrigger: 0,
|
globalContextMenuCloseTrigger: 0,
|
||||||
|
panels: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const uiSlice = createSlice({
|
export const uiSlice = createSlice({
|
||||||
@ -30,13 +27,6 @@ export const uiSlice = createSlice({
|
|||||||
setActiveTab: (state, action: PayloadAction<InvokeTabName>) => {
|
setActiveTab: (state, action: PayloadAction<InvokeTabName>) => {
|
||||||
setActiveTabReducer(state, action.payload);
|
setActiveTabReducer(state, action.payload);
|
||||||
},
|
},
|
||||||
setShouldPinParametersPanel: (state, action: PayloadAction<boolean>) => {
|
|
||||||
state.shouldPinParametersPanel = action.payload;
|
|
||||||
state.shouldShowParametersPanel = true;
|
|
||||||
},
|
|
||||||
setShouldShowParametersPanel: (state, action: PayloadAction<boolean>) => {
|
|
||||||
state.shouldShowParametersPanel = action.payload;
|
|
||||||
},
|
|
||||||
setShouldShowImageDetails: (state, action: PayloadAction<boolean>) => {
|
setShouldShowImageDetails: (state, action: PayloadAction<boolean>) => {
|
||||||
state.shouldShowImageDetails = action.payload;
|
state.shouldShowImageDetails = action.payload;
|
||||||
},
|
},
|
||||||
@ -55,36 +45,6 @@ export const uiSlice = createSlice({
|
|||||||
setShouldUseSliders: (state, action: PayloadAction<boolean>) => {
|
setShouldUseSliders: (state, action: PayloadAction<boolean>) => {
|
||||||
state.shouldUseSliders = action.payload;
|
state.shouldUseSliders = action.payload;
|
||||||
},
|
},
|
||||||
setShouldShowGallery: (state, action: PayloadAction<boolean>) => {
|
|
||||||
state.shouldShowGallery = action.payload;
|
|
||||||
},
|
|
||||||
togglePinGalleryPanel: (state) => {
|
|
||||||
state.shouldPinGallery = !state.shouldPinGallery;
|
|
||||||
if (!state.shouldPinGallery) {
|
|
||||||
state.shouldShowGallery = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
togglePinParametersPanel: (state) => {
|
|
||||||
state.shouldPinParametersPanel = !state.shouldPinParametersPanel;
|
|
||||||
if (!state.shouldPinParametersPanel) {
|
|
||||||
state.shouldShowParametersPanel = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
toggleParametersPanel: (state) => {
|
|
||||||
state.shouldShowParametersPanel = !state.shouldShowParametersPanel;
|
|
||||||
},
|
|
||||||
toggleGalleryPanel: (state) => {
|
|
||||||
state.shouldShowGallery = !state.shouldShowGallery;
|
|
||||||
},
|
|
||||||
togglePanels: (state) => {
|
|
||||||
if (state.shouldShowGallery || state.shouldShowParametersPanel) {
|
|
||||||
state.shouldShowGallery = false;
|
|
||||||
state.shouldShowParametersPanel = false;
|
|
||||||
} else {
|
|
||||||
state.shouldShowGallery = true;
|
|
||||||
state.shouldShowParametersPanel = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setShouldShowProgressInViewer: (state, action: PayloadAction<boolean>) => {
|
setShouldShowProgressInViewer: (state, action: PayloadAction<boolean>) => {
|
||||||
state.shouldShowProgressInViewer = action.payload;
|
state.shouldShowProgressInViewer = action.payload;
|
||||||
},
|
},
|
||||||
@ -100,6 +60,12 @@ export const uiSlice = createSlice({
|
|||||||
contextMenusClosed: (state) => {
|
contextMenusClosed: (state) => {
|
||||||
state.globalContextMenuCloseTrigger += 1;
|
state.globalContextMenuCloseTrigger += 1;
|
||||||
},
|
},
|
||||||
|
panelsChanged: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ name: string; value: string }>
|
||||||
|
) => {
|
||||||
|
state.panels[action.payload.name] = action.payload.value;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers(builder) {
|
extraReducers(builder) {
|
||||||
builder.addCase(initialImageChanged, (state) => {
|
builder.addCase(initialImageChanged, (state) => {
|
||||||
@ -110,23 +76,16 @@ export const uiSlice = createSlice({
|
|||||||
|
|
||||||
export const {
|
export const {
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
setShouldPinParametersPanel,
|
|
||||||
setShouldShowParametersPanel,
|
|
||||||
setShouldShowImageDetails,
|
setShouldShowImageDetails,
|
||||||
setShouldUseCanvasBetaLayout,
|
setShouldUseCanvasBetaLayout,
|
||||||
setShouldShowExistingModelsInSearch,
|
setShouldShowExistingModelsInSearch,
|
||||||
setShouldUseSliders,
|
setShouldUseSliders,
|
||||||
setShouldHidePreview,
|
setShouldHidePreview,
|
||||||
setShouldShowGallery,
|
|
||||||
togglePanels,
|
|
||||||
togglePinGalleryPanel,
|
|
||||||
togglePinParametersPanel,
|
|
||||||
toggleParametersPanel,
|
|
||||||
toggleGalleryPanel,
|
|
||||||
setShouldShowProgressInViewer,
|
setShouldShowProgressInViewer,
|
||||||
favoriteSchedulersChanged,
|
favoriteSchedulersChanged,
|
||||||
toggleEmbeddingPicker,
|
toggleEmbeddingPicker,
|
||||||
contextMenusClosed,
|
contextMenusClosed,
|
||||||
|
panelsChanged,
|
||||||
} = uiSlice.actions;
|
} = uiSlice.actions;
|
||||||
|
|
||||||
export default uiSlice.reducer;
|
export default uiSlice.reducer;
|
||||||
|
@ -14,17 +14,14 @@ export type Rect = Coordinates & Dimensions;
|
|||||||
|
|
||||||
export interface UIState {
|
export interface UIState {
|
||||||
activeTab: number;
|
activeTab: number;
|
||||||
shouldPinParametersPanel: boolean;
|
|
||||||
shouldShowParametersPanel: boolean;
|
|
||||||
shouldShowImageDetails: boolean;
|
shouldShowImageDetails: boolean;
|
||||||
shouldUseCanvasBetaLayout: boolean;
|
shouldUseCanvasBetaLayout: boolean;
|
||||||
shouldShowExistingModelsInSearch: boolean;
|
shouldShowExistingModelsInSearch: boolean;
|
||||||
shouldUseSliders: boolean;
|
shouldUseSliders: boolean;
|
||||||
shouldHidePreview: boolean;
|
shouldHidePreview: boolean;
|
||||||
shouldPinGallery: boolean;
|
|
||||||
shouldShowGallery: boolean;
|
|
||||||
shouldShowProgressInViewer: boolean;
|
shouldShowProgressInViewer: boolean;
|
||||||
shouldShowEmbeddingPicker: boolean;
|
shouldShowEmbeddingPicker: boolean;
|
||||||
favoriteSchedulers: SchedulerParam[];
|
favoriteSchedulers: SchedulerParam[];
|
||||||
globalContextMenuCloseTrigger: number;
|
globalContextMenuCloseTrigger: number;
|
||||||
|
panels: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,11 @@ export const theme: ThemeOverride = {
|
|||||||
color: 'base.900',
|
color: 'base.900',
|
||||||
'.chakra-ui-dark &': { bg: 'base.800', color: 'base.100' },
|
'.chakra-ui-dark &': { bg: 'base.800', color: 'base.100' },
|
||||||
},
|
},
|
||||||
|
third: {
|
||||||
|
bg: 'base.300',
|
||||||
|
color: 'base.900',
|
||||||
|
'.chakra-ui-dark &': { bg: 'base.750', color: 'base.100' },
|
||||||
|
},
|
||||||
nodeBody: {
|
nodeBody: {
|
||||||
bg: 'base.100',
|
bg: 'base.100',
|
||||||
color: 'base.900',
|
color: 'base.900',
|
||||||
|
@ -5633,11 +5633,6 @@ rc@^1.2.7:
|
|||||||
minimist "^1.2.0"
|
minimist "^1.2.0"
|
||||||
strip-json-comments "~2.0.1"
|
strip-json-comments "~2.0.1"
|
||||||
|
|
||||||
re-resizable@^6.9.11:
|
|
||||||
version "6.9.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.9.11.tgz#f356e27877f12d926d076ab9ad9ff0b95912b475"
|
|
||||||
integrity sha512-a3hiLWck/NkmyLvGWUuvkAmN1VhwAz4yOhS6FdMTaxCUVN9joIWkT11wsO68coG/iEYuwn+p/7qAmfQzRhiPLQ==
|
|
||||||
|
|
||||||
react-clientside-effect@^1.2.6:
|
react-clientside-effect@^1.2.6:
|
||||||
version "1.2.6"
|
version "1.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz#29f9b14e944a376b03fb650eed2a754dd128ea3a"
|
resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz#29f9b14e944a376b03fb650eed2a754dd128ea3a"
|
||||||
|
Reference in New Issue
Block a user