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:
psychedelicious 2023-08-23 14:17:18 +10:00
parent 6d10e40c9b
commit 73318c2847
39 changed files with 682 additions and 1507 deletions

View File

@ -90,7 +90,6 @@
"overlayscrollbars-react": "^0.5.0",
"patch-package": "^8.0.0",
"query-string": "^8.1.0",
"re-resizable": "^6.9.11",
"react": "^18.2.0",
"react-colorful": "^5.6.1",
"react-dom": "^18.2.0",

View File

@ -19,7 +19,7 @@
"toggleAutoscroll": "Toggle autoscroll",
"toggleLogViewer": "Toggle Log Viewer",
"showGallery": "Show Gallery",
"showOptionsPanel": "Show Options Panel",
"showOptionsPanel": "Show Side Panel",
"menu": "Menu"
},
"common": {
@ -577,7 +577,7 @@
"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.",
"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",
"shouldLogToConsole": "Console Logging",
"developer": "Developer",

View File

@ -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 { appStarted } from 'app/store/middleware/listenerMiddleware/listeners/appStarted';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
@ -6,21 +6,17 @@ import { PartialAppConfig } from 'app/types/invokeai';
import ImageUploader from 'common/components/ImageUploader';
import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal';
import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal';
import GalleryDrawer from 'features/gallery/components/GalleryPanel';
import SiteHeader from 'features/system/components/SiteHeader';
import { configChanged } from 'features/system/store/configSlice';
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 ParametersDrawer from 'features/ui/components/ParametersDrawer';
import i18n from 'i18n';
import { size } from 'lodash-es';
import { ReactNode, memo, useCallback, useEffect } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import AppErrorBoundaryFallback from './AppErrorBoundaryFallback';
import GlobalHotkeys from './GlobalHotkeys';
import Toaster from './Toaster';
import AppErrorBoundaryFallback from './AppErrorBoundaryFallback';
const DEFAULT_CONFIG = {};
@ -83,15 +79,6 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
</Flex>
</Grid>
</ImageUploader>
<GalleryDrawer />
<ParametersDrawer />
<Portal>
<FloatingParametersPanelButtons />
</Portal>
<Portal>
<FloatingGalleryButton />
</Portal>
</Grid>
<DeleteImageModal />
<ChangeBoardModal />

View File

@ -1,30 +1,21 @@
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
import {
ctrlKeyPressed,
metaKeyPressed,
shiftKeyPressed,
} from 'features/ui/store/hotkeysSlice';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import {
setActiveTab,
toggleGalleryPanel,
toggleParametersPanel,
togglePinGalleryPanel,
togglePinParametersPanel,
} from 'features/ui/store/uiSlice';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { isEqual } from 'lodash-es';
import React, { memo } from 'react';
import { isHotkeyPressed, useHotkeys } from 'react-hotkeys-hook';
const globalHotkeysSelector = createSelector(
[stateSelector],
({ hotkeys, ui }) => {
({ hotkeys }) => {
const { shift, ctrl, meta } = hotkeys;
const { shouldPinParametersPanel, shouldPinGallery } = ui;
return { shift, ctrl, meta, shouldPinGallery, shouldPinParametersPanel };
return { shift, ctrl, meta };
},
{
memoizeOptions: {
@ -41,9 +32,7 @@ const globalHotkeysSelector = createSelector(
*/
const GlobalHotkeys: React.FC = () => {
const dispatch = useAppDispatch();
const { shift, ctrl, meta, shouldPinParametersPanel, shouldPinGallery } =
useAppSelector(globalHotkeysSelector);
const activeTabName = useAppSelector(activeTabNameSelector);
const { shift, ctrl, meta } = useAppSelector(globalHotkeysSelector);
useHotkeys(
'*',
@ -68,34 +57,6 @@ const GlobalHotkeys: React.FC = () => {
[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', () => {
dispatch(setActiveTab('txt2img'));
});
@ -112,6 +73,10 @@ const GlobalHotkeys: React.FC = () => {
dispatch(setActiveTab('nodes'));
});
useHotkeys('5', () => {
dispatch(setActiveTab('modelManager'));
});
return null;
};

View File

@ -83,10 +83,6 @@ const ControlNet = (props: ControlNetProps) => {
p: 3,
borderRadius: 'base',
position: 'relative',
bg: 'base.200',
_dark: {
bg: 'base.850',
},
}}
>
<Flex sx={{ gap: 2, alignItems: 'center' }}>

View File

@ -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);

View File

@ -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);

View File

@ -18,7 +18,6 @@ import { FaImages, FaServer } from 'react-icons/fa';
import { galleryViewChanged } from '../store/gallerySlice';
import BoardsList from './Boards/BoardsList/BoardsList';
import GalleryBoardName from './GalleryBoardName';
import GalleryPinButton from './GalleryPinButton';
import GallerySettingsPopover from './GallerySettingsPopover';
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
@ -75,7 +74,6 @@ const ImageGalleryContent = () => {
onToggle={onToggleBoardList}
/>
<GallerySettingsPopover />
<GalleryPinButton />
</Flex>
<Box>
<BoardsList isOpen={isBoardListOpen} />

View File

@ -1,109 +1,85 @@
import { Flex } from '@chakra-ui/react';
import { useAppSelector } from 'app/store/storeHooks';
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 { memo } from 'react';
import { MdDeviceHub } from 'react-icons/md';
import 'reactflow/dist/style.css';
import AddNodePopover from './flow/AddNodePopover/AddNodePopover';
import { Flow } from './flow/Flow';
const NodeEditor = () => {
const [isPanelCollapsed, setIsPanelCollapsed] = useState(false);
const isReady = useAppSelector((state) => state.nodes.isReady);
return (
<PanelGroup
id="node-editor"
autoSaveId="node-editor"
direction="horizontal"
style={{ height: '100%', width: '100%' }}
<Flex
layerStyle="first"
sx={{
position: 'relative',
width: 'full',
height: 'full',
borderRadius: 'base',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Panel
id="node-editor-panel-group"
collapsible
onCollapse={setIsPanelCollapsed}
minSize={25}
>
<NodeEditorPanelGroup />
</Panel>
<ResizeHandle
collapsedDirection={isPanelCollapsed ? 'left' : undefined}
/>
<Panel id="node-editor-content">
<Flex
layerStyle="first"
sx={{
position: 'relative',
width: 'full',
height: 'full',
borderRadius: 'base',
alignItems: 'center',
justifyContent: 'center',
}}
>
<AnimatePresence>
{isReady && (
<motion.div
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
transition: { duration: 0.2 },
}}
exit={{
opacity: 0,
transition: { duration: 0.2 },
}}
style={{ position: 'relative', width: '100%', height: '100%' }}
>
<Flow />
<AddNodePopover />
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{!isReady && (
<motion.div
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
transition: { duration: 0.2 },
}}
exit={{
opacity: 0,
transition: { duration: 0.2 },
}}
style={{ position: 'absolute', width: '100%', height: '100%' }}
>
<Flex
layerStyle="first"
sx={{
position: 'relative',
width: 'full',
height: 'full',
borderRadius: 'base',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'none',
}}
>
<IAINoContentFallback
label="Loading Nodes..."
icon={MdDeviceHub}
/>
</Flex>
</motion.div>
)}
</AnimatePresence>
</Flex>
</Panel>
</PanelGroup>
<AnimatePresence>
{isReady && (
<motion.div
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
transition: { duration: 0.2 },
}}
exit={{
opacity: 0,
transition: { duration: 0.2 },
}}
style={{ position: 'relative', width: '100%', height: '100%' }}
>
<Flow />
<AddNodePopover />
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{!isReady && (
<motion.div
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
transition: { duration: 0.2 },
}}
exit={{
opacity: 0,
transition: { duration: 0.2 },
}}
style={{ position: 'absolute', width: '100%', height: '100%' }}
>
<Flex
layerStyle="first"
sx={{
position: 'relative',
width: 'full',
height: 'full',
borderRadius: 'base',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'none',
}}
>
<IAINoContentFallback
label="Loading Nodes..."
icon={MdDeviceHub}
/>
</Flex>
</motion.div>
)}
</AnimatePresence>
</Flex>
);
};

View File

@ -1,23 +1,36 @@
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
import { memo, useState } from 'react';
import { Panel, PanelGroup } from 'react-resizable-panels';
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
import { memo, useCallback, useRef, useState } from 'react';
import {
ImperativePanelGroupHandle,
Panel,
PanelGroup,
} from 'react-resizable-panels';
import 'reactflow/dist/style.css';
import WorkflowPanel from './workflow/WorkflowPanel';
import InspectorPanel from './inspector/InspectorPanel';
import WorkflowPanel from './workflow/WorkflowPanel';
const NodeEditorPanelGroup = () => {
const [isTopPanelCollapsed, setIsTopPanelCollapsed] = 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 (
<PanelGroup
id="node-editor-panel_group"
autoSaveId="node-editor-panel_group"
ref={panelGroupRef}
id="workflow-panel-group"
direction="vertical"
style={{ height: '100%', width: '100%' }}
storage={panelStorage}
>
<Panel
id="node-editor-panel_group_workflow"
id="workflow"
collapsible
onCollapse={setIsTopPanelCollapsed}
minSize={25}
@ -26,6 +39,7 @@ const NodeEditorPanelGroup = () => {
</Panel>
<ResizeHandle
direction="vertical"
onDoubleClick={handleDoubleClickHandle}
collapsedDirection={
isTopPanelCollapsed
? 'top'
@ -35,7 +49,7 @@ const NodeEditorPanelGroup = () => {
}
/>
<Panel
id="node-editor-panel_group_inspector"
id="inspector"
collapsible
onCollapse={setIsBottomPanelCollapsed}
minSize={25}

View File

@ -23,9 +23,8 @@ import { useFeatureStatus } from '../../../../system/hooks/useFeatureStatus';
const promptInputSelector = createSelector(
[stateSelector, activeTabNameSelector],
({ generation, ui }, activeTabName) => {
({ generation }, activeTabName) => {
return {
shouldPinParametersPanel: ui.shouldPinParametersPanel,
prompt: generation.positivePrompt,
activeTabName,
};
@ -42,8 +41,7 @@ const promptInputSelector = createSelector(
*/
const ParamPositiveConditioning = () => {
const dispatch = useAppDispatch();
const { prompt, shouldPinParametersPanel, activeTabName } =
useAppSelector(promptInputSelector);
const { prompt, activeTabName } = useAppSelector(promptInputSelector);
const isReady = useIsReadyToInvoke();
const promptRef = useRef<HTMLTextAreaElement>(null);
const { isOpen, onClose, onOpen } = useDisclosure();
@ -148,7 +146,7 @@ const ParamPositiveConditioning = () => {
<Box
sx={{
position: 'absolute',
top: shouldPinParametersPanel ? 5 : 0,
top: 0,
insetInlineEnd: 0,
}}
>

View File

@ -55,11 +55,11 @@ const selector = createSelector(
interface InvokeButton
extends Omit<IAIButtonProps | IAIIconButtonProps, 'aria-label'> {
iconButton?: boolean;
asIconButton?: boolean;
}
export default function InvokeButton(props: InvokeButton) {
const { iconButton = false, ...rest } = props;
const { asIconButton = false, sx, ...rest } = props;
const dispatch = useAppDispatch();
const { isReady, isProcessing } = useIsReadyToInvoke();
const { activeTabName } = useAppSelector(selector);
@ -87,21 +87,22 @@ export default function InvokeButton(props: InvokeButton) {
<Box style={{ position: 'relative' }}>
{!isReady && (
<Box
borderRadius="base"
style={{
sx={{
position: 'absolute',
bottom: '0',
left: '0',
right: '0',
height: '100%',
overflow: 'clip',
borderRadius: 'base',
...sx,
}}
{...rest}
>
<ProgressBar />
</Box>
)}
{iconButton ? (
{asIconButton ? (
<IAIIconButton
aria-label={t('parameters.invoke')}
type="submit"
@ -112,12 +113,13 @@ export default function InvokeButton(props: InvokeButton) {
colorScheme="accent"
isLoading={isProcessing}
id="invoke-button"
{...rest}
sx={{
w: 'full',
flexGrow: 1,
...(isProcessing ? IN_PROGRESS_STYLES : {}),
...sx,
}}
{...rest}
/>
) : (
<IAIButton
@ -131,13 +133,14 @@ export default function InvokeButton(props: InvokeButton) {
leftIcon={isProcessing ? undefined : <FaPlay />}
isLoading={isProcessing}
loadingText={t('parameters.invoke')}
{...rest}
sx={{
w: 'full',
flexGrow: 1,
fontWeight: 700,
...(isProcessing ? IN_PROGRESS_STYLES : {}),
...sx,
}}
{...rest}
>
Invoke
</IAIButton>
@ -166,7 +169,11 @@ export const InvokeButtonTooltipContent = memo(() => {
))}
</UnorderedList>
)}
<Divider opacity={0.2} />
<Divider
opacity={0.2}
borderColor="base.50"
_dark={{ borderColor: 'base.900' }}
/>
<Text fontWeight={400} fontStyle="oblique 10deg">
Adding images to{' '}
<Text as="span" fontWeight={600}>

View File

@ -9,10 +9,6 @@ export default function ParamSDXLConcatButton() {
(state: RootState) => state.sdxl.shouldConcatSDXLStylePrompt
);
const shouldPinParametersPanel = useAppSelector(
(state: RootState) => state.ui.shouldPinParametersPanel
);
const dispatch = useAppDispatch();
const handleShouldConcatPromptChange = () => {
@ -31,7 +27,7 @@ export default function ParamSDXLConcatButton() {
sx={{
position: 'absolute',
insetInlineEnd: 1,
top: shouldPinParametersPanel ? 12 : 20,
top: 6,
border: 'none',
color: shouldConcatSDXLStylePrompt ? 'accent.500' : 'base.500',
_hover: {

View File

@ -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);

View File

@ -42,6 +42,7 @@ import {
memo,
useCallback,
useEffect,
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { LogLevelName } from 'roarr';
@ -113,6 +114,7 @@ type SettingsModalProps = {
const SettingsModal = ({ children, config }: SettingsModalProps) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const [countdown, setCountdown] = useState(3);
const shouldShowBetaLayout = config?.shouldShowBetaLayout ?? true;
const shouldShowDeveloperSettings =
@ -179,8 +181,15 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
});
onSettingsModalClose();
onRefreshModalOpen();
setInterval(() => setCountdown((prev) => prev - 1), 1000);
}, [onSettingsModalClose, onRefreshModalOpen]);
useEffect(() => {
if (countdown <= 0) {
window.location.reload();
}
}, [countdown]);
const handleLogLevelChanged = useCallback(
(v: string) => {
dispatch(consoleLogLevelChanged(v as LogLevelName));
@ -381,6 +390,7 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
isOpen={isRefreshModalOpen}
onClose={onRefreshModalClose}
isCentered
closeOnEsc={false}
>
<ModalOverlay backdropFilter="blur(40px)" />
<ModalContent>
@ -388,7 +398,9 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
<ModalBody>
<Flex justifyContent="center">
<Text fontSize="lg">
<Text>{t('settings.resetComplete')}</Text>
<Text>
{t('settings.resetComplete')} Reloading in {countdown}...
</Text>
</Text>
</Flex>
</ModalBody>

View File

@ -1,65 +1,61 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { Flex } from '@chakra-ui/layout';
import { Portal } from '@chakra-ui/portal';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
import { setShouldShowGallery } from 'features/ui/store/uiSlice';
import { isEqual } from 'lodash-es';
import { memo } from 'react';
import { RefObject, memo } from 'react';
import { useTranslation } from 'react-i18next';
import { MdPhotoLibrary } from 'react-icons/md';
import { activeTabNameSelector, uiSelector } from '../store/uiSelectors';
import { NO_GALLERY_TABS } from './InvokeTabs';
import { ImperativePanelHandle } from 'react-resizable-panels';
const floatingGalleryButtonSelector = createSelector(
[activeTabNameSelector, uiSelector],
(activeTabName, ui) => {
const { shouldPinGallery, shouldShowGallery } = ui;
type Props = {
isGalleryCollapsed: boolean;
galleryPanelRef: RefObject<ImperativePanelHandle>;
};
return {
shouldPinGallery,
shouldShowGalleryButton: NO_GALLERY_TABS.includes(activeTabName)
? false
: !shouldShowGallery,
};
},
{ memoizeOptions: { resultEqualityCheck: isEqual } }
);
const FloatingGalleryButton = () => {
const FloatingGalleryButton = ({
isGalleryCollapsed,
galleryPanelRef,
}: Props) => {
const { t } = useTranslation();
const { shouldPinGallery, shouldShowGalleryButton } = useAppSelector(
floatingGalleryButtonSelector
);
const dispatch = useAppDispatch();
const handleShowGallery = () => {
dispatch(setShouldShowGallery(true));
shouldPinGallery && dispatch(requestCanvasRescale());
galleryPanelRef.current?.expand();
dispatch(requestCanvasRescale());
};
return shouldShowGalleryButton ? (
<IAIIconButton
tooltip="Show Gallery (G)"
tooltipProps={{ placement: 'top' }}
aria-label={t('accessibility.showGallery')}
onClick={handleShowGallery}
sx={{
pos: 'absolute',
top: '50%',
transform: 'translate(0, -50%)',
p: 0,
insetInlineEnd: 0,
px: 3,
h: 48,
w: 8,
borderStartEndRadius: 0,
borderEndEndRadius: 0,
shadow: '2xl',
}}
>
<MdPhotoLibrary />
</IAIIconButton>
) : null;
if (!isGalleryCollapsed) {
return null;
}
return (
<Portal>
<Flex
pos="absolute"
transform="translate(0, -50%)"
minW={8}
top="50%"
insetInlineEnd={0}
>
<IAIIconButton
tooltip="Show Gallery (G)"
tooltipProps={{ placement: 'top' }}
aria-label={t('common.showGalleryPanel')}
onClick={handleShowGallery}
icon={<MdPhotoLibrary />}
sx={{
p: 0,
px: 3,
h: 48,
borderStartEndRadius: 0,
borderEndEndRadius: 0,
shadow: '2xl',
}}
/>
</Flex>
</Portal>
);
};
export default memo(FloatingGalleryButton);

View File

@ -1,20 +1,14 @@
import { ChakraProps, Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { ChakraProps, Flex, Portal } from '@chakra-ui/react';
import { useAppDispatch } 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 {
activeTabNameSelector,
uiSelector,
} from 'features/ui/store/uiSelectors';
import { setShouldShowParametersPanel } from 'features/ui/store/uiSlice';
import { isEqual } from 'lodash-es';
import { memo } from 'react';
import { RefObject, memo } from 'react';
import { useTranslation } from 'react-i18next';
import { FaSlidersH } from 'react-icons/fa';
import { ImperativePanelHandle } from 'react-resizable-panels';
const floatingButtonStyles: ChakraProps['sx'] = {
borderStartStartRadius: 0,
@ -22,81 +16,50 @@ const floatingButtonStyles: ChakraProps['sx'] = {
shadow: '2xl',
};
export const floatingParametersPanelButtonSelector = createSelector(
[uiSelector, activeTabNameSelector],
(ui, activeTabName) => {
const {
shouldPinParametersPanel,
shouldUseCanvasBetaLayout,
shouldShowParametersPanel,
} = ui;
type Props = {
isSidePanelCollapsed: boolean;
sidePanelRef: RefObject<ImperativePanelHandle>;
};
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 dispatch = useAppDispatch();
const FloatingSidePanelButtons = ({
isSidePanelCollapsed,
sidePanelRef,
}: Props) => {
const { t } = useTranslation();
const {
shouldShowProcessButtons,
shouldShowParametersPanelButton,
shouldPinParametersPanel,
} = useAppSelector(floatingParametersPanelButtonSelector);
const dispatch = useAppDispatch();
const handleShowOptionsPanel = () => {
dispatch(setShouldShowParametersPanel(true));
shouldPinParametersPanel && dispatch(requestCanvasRescale());
const handleShowSidePanel = () => {
sidePanelRef.current?.expand();
dispatch(requestCanvasRescale());
};
if (!shouldShowParametersPanelButton) {
if (!isSidePanelCollapsed) {
return null;
}
return (
<Flex
pos="absolute"
transform="translate(0, -50%)"
minW={8}
top="50%"
insetInlineStart="4.5rem"
direction="column"
gap={2}
>
<IAIIconButton
tooltip="Show Options Panel (O)"
tooltipProps={{ placement: 'top' }}
aria-label={t('accessibility.showOptionsPanel')}
onClick={handleShowOptionsPanel}
sx={floatingButtonStyles}
<Portal>
<Flex
pos="absolute"
transform="translate(0, -50%)"
minW={8}
top="50%"
insetInlineStart="4.5rem"
direction="column"
gap={2}
>
<FaSlidersH />
</IAIIconButton>
{shouldShowProcessButtons && (
<>
<InvokeButton iconButton sx={floatingButtonStyles} />
<CancelButton sx={floatingButtonStyles} />
</>
)}
</Flex>
<IAIIconButton
tooltip="Show Side Panel (O, T)"
aria-label={t('common.showOptionsPanel')}
onClick={handleShowSidePanel}
sx={floatingButtonStyles}
icon={<FaSlidersH />}
/>
<InvokeButton asIconButton sx={floatingButtonStyles} />
<CancelButton sx={floatingButtonStyles} />
</Flex>
</Portal>
);
};
export default memo(FloatingParametersPanelButtons);
export default memo(FloatingSidePanelButtons);

View File

@ -11,12 +11,13 @@ import {
} from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
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 { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent';
import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup';
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 { isEqual } from 'lodash-es';
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 { MdDeviceHub, MdGridOn } from 'react-icons/md';
import { Panel, PanelGroup } from 'react-resizable-panels';
import { useMinimumPanelSize } from '../hooks/useMinimumPanelSize';
import { usePanel } from '../hooks/usePanel';
import { usePanelStorage } from '../hooks/usePanelStorage';
import {
activeTabIndexSelector,
activeTabNameSelector,
} from '../store/uiSelectors';
import FloatingGalleryButton from './FloatingGalleryButton';
import FloatingSidePanelButtons from './FloatingParametersPanelButtons';
import ParametersPanel from './ParametersPanel';
import ImageTab from './tabs/ImageToImage/ImageToImageTab';
import ModelManagerTab from './tabs/ModelManager/ModelManagerTab';
import NodesTab from './tabs/Nodes/NodesTab';
@ -89,32 +94,20 @@ const enabledTabsSelector = createSelector(
}
);
const MIN_GALLERY_WIDTH = 350;
const DEFAULT_GALLERY_PCT = 20;
const SIDE_PANEL_MIN_SIZE_PX = 448;
const MAIN_PANEL_MIN_SIZE_PX = 448;
const GALLERY_PANEL_MIN_SIZE_PX = 360;
export const NO_GALLERY_TABS: InvokeTabName[] = ['modelManager'];
export const NO_SIDE_PANEL_TABS: InvokeTabName[] = ['modelManager'];
const InvokeTabs = () => {
const activeTab = useAppSelector(activeTabIndexSelector);
const activeTabName = useAppSelector(activeTabNameSelector);
const enabledTabs = useAppSelector(enabledTabsSelector);
const { shouldPinGallery, shouldPinParametersPanel, shouldShowGallery } =
useAppSelector((state: RootState) => state.ui);
const { t } = useTranslation();
const dispatch = useAppDispatch();
useHotkeys(
'f',
() => {
dispatch(togglePanels());
(shouldPinGallery || shouldPinParametersPanel) &&
dispatch(requestCanvasRescale());
},
[shouldPinGallery, shouldPinParametersPanel]
);
const handleResizeGallery = useCallback(() => {
if (activeTabName === 'unifiedCanvas') {
dispatch(requestCanvasRescale());
@ -153,9 +146,6 @@ const InvokeTabs = () => {
[enabledTabs]
);
const { ref: galleryPanelRef, minSizePct: galleryMinSizePct } =
useMinimumPanelSize(MIN_GALLERY_WIDTH, DEFAULT_GALLERY_PCT, 'app');
const handleTabChange = useCallback(
(index: number) => {
const activeTabName = tabMap[index];
@ -167,6 +157,63 @@ const InvokeTabs = () => {
[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 (
<Tabs
variant="appTabs"
@ -195,35 +242,68 @@ const InvokeTabs = () => {
autoSaveId="app"
direction="horizontal"
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}
</TabPanels>
</Panel>
{shouldPinGallery &&
shouldShowGallery &&
!NO_GALLERY_TABS.includes(activeTabName) && (
<>
<ResizeHandle />
<Panel
ref={galleryPanelRef}
onResize={handleResizeGallery}
id="gallery"
order={3}
defaultSize={
galleryMinSizePct > DEFAULT_GALLERY_PCT &&
galleryMinSizePct < 100 // prevent this error https://github.com/bvaughn/react-resizable-panels/blob/main/packages/react-resizable-panels/src/Panel.ts#L96
? galleryMinSizePct
: DEFAULT_GALLERY_PCT
}
minSize={galleryMinSizePct}
maxSize={50}
>
<ImageGalleryContent />
</Panel>
</>
)}
{!NO_GALLERY_TABS.includes(activeTabName) && (
<>
<ResizeHandle
isCollapsed={isGalleryPanelCollapsed}
onDoubleClick={resetGalleryPanel}
collapsedDirection={isGalleryPanelCollapsed ? 'right' : undefined}
/>
<Panel
id="gallery"
ref={galleryPanelRef}
order={2}
onResize={handleResizeGallery}
defaultSize={galleryPanelMinSize}
minSize={galleryPanelMinSize}
onCollapse={setIsGalleryPanelCollapsed}
collapsible
>
<ImageGalleryContent />
</Panel>
<FloatingGalleryButton
isGalleryCollapsed={isGalleryPanelCollapsed}
galleryPanelRef={galleryPanelRef}
/>
</>
)}
</PanelGroup>
</Tabs>
);

View File

@ -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);

View File

@ -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';

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -1,2 +0,0 @@
export type Placement = 'top' | 'right' | 'bottom' | 'left';
export type LangDirection = 'ltr' | 'rtl' | undefined;

View File

@ -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';
};

View File

@ -1,73 +1,63 @@
import { Box, Flex } from '@chakra-ui/react';
import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { Box } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
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 {
ImperativePanelGroupHandle,
Panel,
PanelGroup,
} from 'react-resizable-panels';
import ParametersPinnedWrapper from '../../ParametersPinnedWrapper';
import ResizeHandle from '../ResizeHandle';
import TextToImageTabMain from '../TextToImage/TextToImageTabMain';
import ImageToImageTabParameters from './ImageToImageTabParameters';
const ImageToImageTab = () => {
const dispatch = useAppDispatch();
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
const model = useAppSelector((state: RootState) => state.generation.model);
const handleDoubleClickHandle = useCallback(() => {
if (!panelGroupRef.current) {
return;
}
panelGroupRef.current.setLayout([50, 50]);
}, []);
const panelStorage = usePanelStorage();
return (
<Flex sx={{ gap: 4, w: 'full', h: 'full' }}>
<ParametersPinnedWrapper>
{model && model.base_model === 'sdxl' ? (
<SDXLImageToImageTabParameters />
) : (
<ImageToImageTabParameters />
)}
</ParametersPinnedWrapper>
<Box sx={{ w: 'full', h: 'full' }}>
<PanelGroup
ref={panelGroupRef}
autoSaveId="imageTab.content"
direction="horizontal"
style={{ height: '100%', width: '100%' }}
<Box sx={{ w: 'full', h: 'full' }}>
<PanelGroup
ref={panelGroupRef}
autoSaveId="imageTab.content"
direction="horizontal"
style={{ height: '100%', width: '100%' }}
storage={panelStorage}
units="percentages"
>
<Panel
id="imageTab.content.initImage"
order={0}
defaultSize={50}
minSize={25}
style={{ position: 'relative' }}
>
<Panel
id="imageTab.content.initImage"
order={0}
defaultSize={50}
minSize={25}
style={{ position: 'relative' }}
>
<InitialImageDisplay />
</Panel>
<ResizeHandle onDoubleClick={handleDoubleClickHandle} />
<Panel
id="imageTab.content.selectedImage"
order={1}
defaultSize={50}
minSize={25}
onResize={() => {
dispatch(requestCanvasRescale());
}}
>
<TextToImageTabMain />
</Panel>
</PanelGroup>
</Box>
</Flex>
<InitialImageDisplay />
</Panel>
<ResizeHandle onDoubleClick={handleDoubleClickHandle} />
<Panel
id="imageTab.content.selectedImage"
order={1}
defaultSize={50}
minSize={25}
onResize={() => {
dispatch(requestCanvasRescale());
}}
>
<TextToImageTabMain />
</Panel>
</PanelGroup>
</Box>
);
};

View File

@ -5,16 +5,27 @@ import { PanelResizeHandle } from 'react-resizable-panels';
type ResizeHandleProps = Omit<FlexProps, 'direction'> & {
direction?: 'horizontal' | 'vertical';
collapsedDirection?: 'top' | 'bottom' | 'left' | 'right';
isCollapsed?: boolean;
};
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 hoverBg = useColorModeValue('base.300', 'base.700');
if (direction === 'horizontal') {
return (
<PanelResizeHandle>
<PanelResizeHandle
style={{
visibility: isCollapsed ? 'hidden' : 'visible',
width: isCollapsed ? 0 : 'auto',
}}
>
<Flex
className="resize-handle-horizontal"
sx={{
@ -50,7 +61,12 @@ const ResizeHandle = (props: ResizeHandleProps) => {
}
return (
<PanelResizeHandle>
<PanelResizeHandle
style={{
visibility: isCollapsed ? 'hidden' : 'visible',
width: isCollapsed ? 0 : 'auto',
}}
>
<Flex
className="resize-handle-vertical"
sx={{

View File

@ -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 ParametersPinnedWrapper from '../../ParametersPinnedWrapper';
import TextToImageTabMain from './TextToImageTabMain';
import TextToImageTabParameters from './TextToImageTabParameters';
const TextToImageTab = () => {
const model = useAppSelector((state: RootState) => state.generation.model);
return (
<Flex sx={{ gap: 4, w: 'full', h: 'full' }}>
<ParametersPinnedWrapper>
{model && model.base_model === 'sdxl' ? (
<SDXLTextToImageTabParameters />
) : (
<TextToImageTabParameters />
)}
</ParametersPinnedWrapper>
<TextToImageTabMain />
</Flex>
);
return <TextToImageTabMain />;
};
export default memo(TextToImageTab);

View File

@ -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;
}

View File

@ -2,6 +2,7 @@ import { Flex } from '@chakra-ui/react';
import IAICanvasRedoButton from 'features/canvas/components/IAICanvasToolbar/IAICanvasRedoButton';
import IAICanvasUndoButton from 'features/canvas/components/IAICanvasToolbar/IAICanvasUndoButton';
import { memo } from 'react';
import UnifiedCanvasSettings from './UnifiedCanvasToolSettings/UnifiedCanvasSettings';
import UnifiedCanvasCopyToClipboard from './UnifiedCanvasToolbar/UnifiedCanvasCopyToClipboard';
import UnifiedCanvasDownloadImage from './UnifiedCanvasToolbar/UnifiedCanvasDownloadImage';
@ -9,12 +10,10 @@ import UnifiedCanvasFileUploader from './UnifiedCanvasToolbar/UnifiedCanvasFileU
import UnifiedCanvasLayerSelect from './UnifiedCanvasToolbar/UnifiedCanvasLayerSelect';
import UnifiedCanvasMergeVisible from './UnifiedCanvasToolbar/UnifiedCanvasMergeVisible';
import UnifiedCanvasMoveTool from './UnifiedCanvasToolbar/UnifiedCanvasMoveTool';
import UnifiedCanvasProcessingButtons from './UnifiedCanvasToolbar/UnifiedCanvasProcessingButtons';
import UnifiedCanvasResetCanvas from './UnifiedCanvasToolbar/UnifiedCanvasResetCanvas';
import UnifiedCanvasResetView from './UnifiedCanvasToolbar/UnifiedCanvasResetView';
import UnifiedCanvasSaveToGallery from './UnifiedCanvasToolbar/UnifiedCanvasSaveToGallery';
import UnifiedCanvasToolSelect from './UnifiedCanvasToolbar/UnifiedCanvasToolSelect';
import { memo } from 'react';
const UnifiedCanvasToolbarBeta = () => {
return (
@ -47,7 +46,6 @@ const UnifiedCanvasToolbarBeta = () => {
</Flex>
<UnifiedCanvasSettings />
<UnifiedCanvasProcessingButtons />
</Flex>
);
};

View File

@ -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 ParametersPinnedWrapper from '../../ParametersPinnedWrapper';
import UnifiedCanvasContent from './UnifiedCanvasContent';
import UnifiedCanvasParameters from './UnifiedCanvasParameters';
const UnifiedCanvasTab = () => {
const model = useAppSelector((state: RootState) => state.generation.model);
return (
<Flex sx={{ gap: 4, w: 'full', h: 'full' }}>
<ParametersPinnedWrapper>
{model && model.base_model === 'sdxl' ? (
<SDXLUnifiedCanvasTabParameters />
) : (
<UnifiedCanvasParameters />
)}
</ParametersPinnedWrapper>
<UnifiedCanvasContent />
</Flex>
);
return <UnifiedCanvasContent />;
};
export default memo(UnifiedCanvasTab);

View File

@ -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 };
};

View 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,
};
};

View File

@ -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 };
};

View File

@ -6,4 +6,5 @@ import { UIState } from './uiTypes';
export const uiPersistDenylist: (keyof UIState)[] = [
'shouldShowImageDetails',
'globalContextMenuCloseTrigger',
'panels',
];

View File

@ -8,19 +8,16 @@ import { UIState } from './uiTypes';
export const initialUIState: UIState = {
activeTab: 0,
shouldPinParametersPanel: true,
shouldShowParametersPanel: true,
shouldShowImageDetails: false,
shouldUseCanvasBetaLayout: false,
shouldShowExistingModelsInSearch: false,
shouldUseSliders: false,
shouldPinGallery: true,
shouldShowGallery: true,
shouldHidePreview: false,
shouldShowProgressInViewer: true,
shouldShowEmbeddingPicker: false,
favoriteSchedulers: [],
globalContextMenuCloseTrigger: 0,
panels: {},
};
export const uiSlice = createSlice({
@ -30,13 +27,6 @@ export const uiSlice = createSlice({
setActiveTab: (state, action: PayloadAction<InvokeTabName>) => {
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>) => {
state.shouldShowImageDetails = action.payload;
},
@ -55,36 +45,6 @@ export const uiSlice = createSlice({
setShouldUseSliders: (state, action: PayloadAction<boolean>) => {
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>) => {
state.shouldShowProgressInViewer = action.payload;
},
@ -100,6 +60,12 @@ export const uiSlice = createSlice({
contextMenusClosed: (state) => {
state.globalContextMenuCloseTrigger += 1;
},
panelsChanged: (
state,
action: PayloadAction<{ name: string; value: string }>
) => {
state.panels[action.payload.name] = action.payload.value;
},
},
extraReducers(builder) {
builder.addCase(initialImageChanged, (state) => {
@ -110,23 +76,16 @@ export const uiSlice = createSlice({
export const {
setActiveTab,
setShouldPinParametersPanel,
setShouldShowParametersPanel,
setShouldShowImageDetails,
setShouldUseCanvasBetaLayout,
setShouldShowExistingModelsInSearch,
setShouldUseSliders,
setShouldHidePreview,
setShouldShowGallery,
togglePanels,
togglePinGalleryPanel,
togglePinParametersPanel,
toggleParametersPanel,
toggleGalleryPanel,
setShouldShowProgressInViewer,
favoriteSchedulersChanged,
toggleEmbeddingPicker,
contextMenusClosed,
panelsChanged,
} = uiSlice.actions;
export default uiSlice.reducer;

View File

@ -14,17 +14,14 @@ export type Rect = Coordinates & Dimensions;
export interface UIState {
activeTab: number;
shouldPinParametersPanel: boolean;
shouldShowParametersPanel: boolean;
shouldShowImageDetails: boolean;
shouldUseCanvasBetaLayout: boolean;
shouldShowExistingModelsInSearch: boolean;
shouldUseSliders: boolean;
shouldHidePreview: boolean;
shouldPinGallery: boolean;
shouldShowGallery: boolean;
shouldShowProgressInViewer: boolean;
shouldShowEmbeddingPicker: boolean;
favoriteSchedulers: SchedulerParam[];
globalContextMenuCloseTrigger: number;
panels: Record<string, string>;
}

View File

@ -45,6 +45,11 @@ export const theme: ThemeOverride = {
color: 'base.900',
'.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: {
bg: 'base.100',
color: 'base.900',

View File

@ -5633,11 +5633,6 @@ rc@^1.2.7:
minimist "^1.2.0"
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:
version "1.2.6"
resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz#29f9b14e944a376b03fb650eed2a754dd128ea3a"