mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Adds pin feature to options panel
This commit is contained in:
parent
dc556cb1a7
commit
e58b7a7ef9
517
frontend/dist/assets/index.2d90b6f0.js
vendored
Normal file
517
frontend/dist/assets/index.2d90b6f0.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/dist/assets/index.352e4760.css
vendored
1
frontend/dist/assets/index.352e4760.css
vendored
File diff suppressed because one or more lines are too long
517
frontend/dist/assets/index.64b87783.js
vendored
517
frontend/dist/assets/index.64b87783.js
vendored
File diff suppressed because one or more lines are too long
1
frontend/dist/assets/index.8c67e6b9.css
vendored
Normal file
1
frontend/dist/assets/index.8c67e6b9.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
frontend/dist/index.html
vendored
4
frontend/dist/index.html
vendored
@ -6,8 +6,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>InvokeAI - A Stable Diffusion Toolkit</title>
|
||||
<link rel="shortcut icon" type="icon" href="./assets/favicon.0d253ced.ico" />
|
||||
<script type="module" crossorigin src="./assets/index.64b87783.js"></script>
|
||||
<link rel="stylesheet" href="./assets/index.352e4760.css">
|
||||
<script type="module" crossorigin src="./assets/index.2d90b6f0.js"></script>
|
||||
<link rel="stylesheet" href="./assets/index.8c67e6b9.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -11,11 +11,7 @@
|
||||
display: grid;
|
||||
row-gap: 0.5rem;
|
||||
padding: $app-padding;
|
||||
grid-auto-rows: max-content;
|
||||
grid-auto-rows: min-content auto;
|
||||
width: $app-width;
|
||||
height: $app-height;
|
||||
}
|
||||
|
||||
.app-console {
|
||||
z-index: 20;
|
||||
}
|
||||
|
@ -6,16 +6,52 @@ import Loading from '../Loading';
|
||||
import { useAppDispatch } from './store';
|
||||
import { requestSystemConfig } from './socketio/actions';
|
||||
import { keepGUIAlive } from './utils';
|
||||
import InvokeTabs from '../features/tabs/InvokeTabs';
|
||||
import InvokeTabs, { tabMap } from '../features/tabs/InvokeTabs';
|
||||
import ImageUploader from '../common/components/ImageUploader';
|
||||
import { RootState, useAppSelector } from '../app/store';
|
||||
|
||||
import ShowHideGalleryButton from '../features/tabs/ShowHideGalleryButton';
|
||||
import ShowHideOptionsPanelButton from '../features/tabs/ShowHideOptionsPanelButton';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { GalleryState } from '../features/gallery/gallerySlice';
|
||||
import { OptionsState } from '../features/options/optionsSlice';
|
||||
|
||||
keepGUIAlive();
|
||||
|
||||
const appSelector = createSelector(
|
||||
[(state: RootState) => state.gallery, (state: RootState) => state.options],
|
||||
(gallery: GalleryState, options: OptionsState) => {
|
||||
const { shouldShowGallery, shouldHoldGalleryOpen, shouldPinGallery } =
|
||||
gallery;
|
||||
const {
|
||||
shouldShowOptionsPanel,
|
||||
shouldHoldOptionsPanelOpen,
|
||||
shouldPinOptionsPanel,
|
||||
activeTab,
|
||||
} = options;
|
||||
|
||||
return {
|
||||
shouldShowGalleryButton: !(
|
||||
shouldShowGallery ||
|
||||
(shouldHoldGalleryOpen && !shouldPinGallery)
|
||||
),
|
||||
shouldShowOptionsPanelButton:
|
||||
!(
|
||||
shouldShowOptionsPanel ||
|
||||
(shouldHoldOptionsPanelOpen && !shouldPinOptionsPanel)
|
||||
) && ['txt2img', 'img2img', 'inpainting'].includes(tabMap[activeTab]),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const App = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [isReady, setIsReady] = useState<boolean>(false);
|
||||
|
||||
const { shouldShowGalleryButton, shouldShowOptionsPanelButton } =
|
||||
useAppSelector(appSelector);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(requestSystemConfig());
|
||||
setIsReady(true);
|
||||
@ -32,6 +68,8 @@ const App = () => {
|
||||
<div className="app-console">
|
||||
<Console />
|
||||
</div>
|
||||
{shouldShowGalleryButton && <ShowHideGalleryButton />}
|
||||
{shouldShowOptionsPanelButton && <ShowHideOptionsPanelButton />}
|
||||
</ImageUploader>
|
||||
</div>
|
||||
) : (
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { FormControl, FormLabel, Select, SelectProps } from '@chakra-ui/react';
|
||||
import { MouseEvent } from 'react';
|
||||
|
||||
interface Props extends SelectProps {
|
||||
label: string;
|
||||
@ -21,7 +22,16 @@ const IAISelect = (props: Props) => {
|
||||
...rest
|
||||
} = props;
|
||||
return (
|
||||
<FormControl isDisabled={isDisabled} className={`invokeai__select ${styleClass}`}>
|
||||
<FormControl
|
||||
isDisabled={isDisabled}
|
||||
className={`invokeai__select ${styleClass}`}
|
||||
onClick={(e: MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
e.nativeEvent.stopImmediatePropagation();
|
||||
e.nativeEvent.stopPropagation();
|
||||
e.nativeEvent.cancelBubble = true;
|
||||
}}
|
||||
>
|
||||
<FormLabel
|
||||
fontSize={fontSize}
|
||||
marginBottom={1}
|
||||
|
@ -142,10 +142,10 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
|
||||
return (
|
||||
<ContextMenu.Root
|
||||
onOpenChange={(open: boolean) => {
|
||||
dispatch(setShouldHoldGalleryOpen(open));
|
||||
dispatch(setShouldShowGallery(true));
|
||||
}}
|
||||
// onOpenChange={(open: boolean) => {
|
||||
// dispatch(setShouldHoldGalleryOpen(open));
|
||||
// dispatch(setShouldShowGallery(true));
|
||||
// }}
|
||||
>
|
||||
<ContextMenu.Trigger>
|
||||
<Box
|
||||
|
@ -1,24 +1,24 @@
|
||||
@use '../../styles/Mixins/' as *;
|
||||
|
||||
.image-gallery-area-enter {
|
||||
.image-gallery-wrapper-enter {
|
||||
transform: translateX(150%);
|
||||
}
|
||||
|
||||
.image-gallery-area-enter-active {
|
||||
.image-gallery-wrapper-enter-active {
|
||||
transform: translateX(0);
|
||||
transition: all 120ms ease-out;
|
||||
}
|
||||
|
||||
.image-gallery-area-exit {
|
||||
.image-gallery-wrapper-exit {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.image-gallery-area-exit-active {
|
||||
.image-gallery-wrapper-exit-active {
|
||||
transform: translateX(150%);
|
||||
transition: all 120ms ease-out;
|
||||
}
|
||||
|
||||
.image-gallery-area {
|
||||
.image-gallery-wrapper {
|
||||
z-index: 10;
|
||||
|
||||
&[data-pinned='false'] {
|
||||
@ -29,6 +29,7 @@
|
||||
|
||||
.image-gallery-popup {
|
||||
border-radius: 0;
|
||||
box-shadow: 0 0 1rem var(--text-color-a3);
|
||||
.image-gallery-container {
|
||||
max-height: calc($app-height + 5rem);
|
||||
}
|
||||
|
@ -96,8 +96,8 @@ export default function ImageGallery() {
|
||||
const timeoutIdRef = useRef<number | null>(null);
|
||||
|
||||
const handleSetShouldPinGallery = () => {
|
||||
dispatch(setNeedsCache(true));
|
||||
dispatch(setShouldPinGallery(!shouldPinGallery));
|
||||
dispatch(setNeedsCache(true));
|
||||
};
|
||||
|
||||
const handleToggleGallery = () => {
|
||||
@ -106,7 +106,7 @@ export default function ImageGallery() {
|
||||
|
||||
const handleOpenGallery = () => {
|
||||
dispatch(setShouldShowGallery(true));
|
||||
dispatch(setNeedsCache(true));
|
||||
shouldPinGallery && dispatch(setNeedsCache(true));
|
||||
};
|
||||
|
||||
const handleCloseGallery = () => {
|
||||
@ -117,7 +117,7 @@ export default function ImageGallery() {
|
||||
);
|
||||
dispatch(setShouldShowGallery(false));
|
||||
dispatch(setShouldHoldGalleryOpen(false));
|
||||
dispatch(setNeedsCache(true));
|
||||
shouldPinGallery && dispatch(setNeedsCache(true));
|
||||
};
|
||||
|
||||
const handleClickLoadMore = () => {
|
||||
@ -251,16 +251,20 @@ export default function ImageGallery() {
|
||||
galleryContainerRef.current.scrollTop = galleryScrollPosition;
|
||||
}, [galleryScrollPosition, shouldShowGallery]);
|
||||
|
||||
useEffect(() => {
|
||||
setShouldShowButtons(galleryWidth >= 280);
|
||||
}, [galleryWidth]);
|
||||
|
||||
return (
|
||||
<CSSTransition
|
||||
nodeRef={galleryRef}
|
||||
in={shouldShowGallery || (shouldHoldGalleryOpen && !shouldPinGallery)}
|
||||
unmountOnExit
|
||||
timeout={200}
|
||||
classNames="image-gallery-area"
|
||||
classNames="image-gallery-wrapper"
|
||||
>
|
||||
<div
|
||||
className="image-gallery-area"
|
||||
className="image-gallery-wrapper"
|
||||
data-pinned={shouldPinGallery}
|
||||
ref={galleryRef}
|
||||
onMouseLeave={!shouldPinGallery ? setCloseGalleryTimer : undefined}
|
||||
@ -270,7 +274,6 @@ export default function ImageGallery() {
|
||||
<Resizable
|
||||
minWidth={galleryMinWidth}
|
||||
maxWidth={galleryMaxWidth}
|
||||
// maxHeight={'100%'}
|
||||
className={'image-gallery-popup'}
|
||||
handleStyles={{ left: { width: '15px' } }}
|
||||
enable={{
|
||||
@ -316,9 +319,9 @@ export default function ImageGallery() {
|
||||
Number(galleryMaxWidth)
|
||||
);
|
||||
|
||||
if (newWidth >= 320 && !shouldShowButtons) {
|
||||
if (newWidth >= 280 && !shouldShowButtons) {
|
||||
setShouldShowButtons(true);
|
||||
} else if (newWidth < 320 && shouldShowButtons) {
|
||||
} else if (newWidth < 280 && shouldShowButtons) {
|
||||
setShouldShowButtons(false);
|
||||
}
|
||||
|
||||
@ -448,15 +451,6 @@ export default function ImageGallery() {
|
||||
icon={<BsPinAngleFill />}
|
||||
data-selected={shouldPinGallery}
|
||||
/>
|
||||
|
||||
<IAIIconButton
|
||||
size={'sm'}
|
||||
aria-label={'Close Gallery'}
|
||||
tooltip={'Close Gallery (G)'}
|
||||
onClick={handleCloseGallery}
|
||||
className="image-gallery-icon-btn"
|
||||
icon={<MdClear />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="image-gallery-container" ref={galleryContainerRef}>
|
||||
|
@ -1,20 +0,0 @@
|
||||
@use '../../styles/Mixins/' as *;
|
||||
|
||||
.show-hide-gallery-button {
|
||||
position: absolute !important;
|
||||
top: 50%;
|
||||
right: -1rem;
|
||||
transform: translate(0, -50%);
|
||||
z-index: 10;
|
||||
|
||||
border-radius: 0.5rem 0 0 0.5rem !important;
|
||||
padding: 0 0.5rem;
|
||||
|
||||
@include Button(
|
||||
$btn-width: 1rem,
|
||||
$btn-height: 12rem,
|
||||
$icon-size: 20px,
|
||||
$btn-color: var(--btn-grey),
|
||||
$btn-color-hover: var(--btn-grey-hover)
|
||||
);
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { MdPhotoLibrary } from 'react-icons/md';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import IAIIconButton from '../../common/components/IAIIconButton';
|
||||
import { setShouldShowGallery } from '../gallery/gallerySlice';
|
||||
import { selectNextImage, selectPrevImage } from './gallerySlice';
|
||||
|
||||
const ShowHideGalleryButton = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { shouldPinGallery, shouldShowGallery } = useAppSelector(
|
||||
(state: RootState) => state.gallery
|
||||
);
|
||||
|
||||
const handleShowGalleryToggle = () => {
|
||||
dispatch(setShouldShowGallery(!shouldShowGallery));
|
||||
};
|
||||
|
||||
// useHotkeys(
|
||||
// 'g',
|
||||
// () => {
|
||||
// handleShowGalleryToggle();
|
||||
// },
|
||||
// [shouldShowGallery]
|
||||
// );
|
||||
|
||||
// useHotkeys(
|
||||
// 'left',
|
||||
// () => {
|
||||
// dispatch(selectPrevImage());
|
||||
// },
|
||||
// []
|
||||
// );
|
||||
|
||||
// useHotkeys(
|
||||
// 'right',
|
||||
// () => {
|
||||
// dispatch(selectNextImage());
|
||||
// },
|
||||
// []
|
||||
// );
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
tooltip="Show Gallery (G)"
|
||||
tooltipPlacement="top"
|
||||
aria-label="Show Gallery"
|
||||
onClick={handleShowGalleryToggle}
|
||||
styleClass="show-hide-gallery-button"
|
||||
onMouseOver={!shouldPinGallery ? handleShowGalleryToggle : undefined}
|
||||
>
|
||||
<MdPhotoLibrary />
|
||||
</IAIIconButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShowHideGalleryButton;
|
@ -42,6 +42,10 @@ export interface OptionsState {
|
||||
activeTab: number;
|
||||
shouldShowImageDetails: boolean;
|
||||
showDualDisplay: boolean;
|
||||
shouldShowOptionsPanel: boolean;
|
||||
shouldPinOptionsPanel: boolean;
|
||||
optionsPanelScrollPosition: number;
|
||||
shouldHoldOptionsPanelOpen: boolean;
|
||||
}
|
||||
|
||||
const initialOptionsState: OptionsState = {
|
||||
@ -75,6 +79,10 @@ const initialOptionsState: OptionsState = {
|
||||
activeTab: 0,
|
||||
shouldShowImageDetails: false,
|
||||
showDualDisplay: true,
|
||||
shouldShowOptionsPanel: true,
|
||||
shouldPinOptionsPanel: true,
|
||||
optionsPanelScrollPosition: 0,
|
||||
shouldHoldOptionsPanelOpen: false,
|
||||
};
|
||||
|
||||
const initialState: OptionsState = initialOptionsState;
|
||||
@ -324,6 +332,18 @@ export const optionsSlice = createSlice({
|
||||
clearInitialImage: (state) => {
|
||||
state.initialImage = undefined;
|
||||
},
|
||||
setShouldPinOptionsPanel: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldPinOptionsPanel = action.payload;
|
||||
},
|
||||
setShouldShowOptionsPanel: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldShowOptionsPanel = action.payload;
|
||||
},
|
||||
setOptionsPanelScrollPosition: (state, action: PayloadAction<number>) => {
|
||||
state.optionsPanelScrollPosition = action.payload;
|
||||
},
|
||||
setShouldHoldOptionsPanelOpen: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldHoldOptionsPanelOpen = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -366,6 +386,10 @@ export const {
|
||||
setShowDualDisplay,
|
||||
setInitialImage,
|
||||
clearInitialImage,
|
||||
setShouldShowOptionsPanel,
|
||||
setShouldPinOptionsPanel,
|
||||
setOptionsPanelScrollPosition,
|
||||
setShouldHoldOptionsPanelOpen,
|
||||
} = optionsSlice.actions;
|
||||
|
||||
export default optionsSlice.reducer;
|
||||
|
@ -1,3 +1,10 @@
|
||||
.console-resizable {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.console {
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
@ -41,12 +48,13 @@
|
||||
position: fixed !important;
|
||||
left: 0.5rem;
|
||||
bottom: 0.5rem;
|
||||
z-index: 21;
|
||||
|
||||
&:hover {
|
||||
background: var(--console-icon-button-bg-color-hover) !important;
|
||||
}
|
||||
|
||||
&.error-seen {
|
||||
&[data-error-seen='true'] {
|
||||
background: var(--status-bad-color) !important;
|
||||
&:hover {
|
||||
background: var(--status-bad-color) !important;
|
||||
@ -59,12 +67,13 @@
|
||||
position: fixed !important;
|
||||
left: 0.5rem;
|
||||
bottom: 3rem;
|
||||
z-index: 21;
|
||||
|
||||
&:hover {
|
||||
background: var(--console-icon-button-bg-color-hover) !important;
|
||||
}
|
||||
|
||||
&.autoscroll-enabled {
|
||||
&[data-autoscroll-enabled='true'] {
|
||||
background: var(--accent-color) !important;
|
||||
&:hover {
|
||||
background: var(--accent-color-hover) !important;
|
||||
|
@ -2,7 +2,7 @@ import { IconButton, Tooltip } from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../app/store';
|
||||
import { errorSeen, setShouldShowLogViewer, SystemState } from './systemSlice';
|
||||
import { useLayoutEffect, useRef, useState } from 'react';
|
||||
import { UIEvent, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { FaAngleDoubleDown, FaCode, FaMinus } from 'react-icons/fa';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
@ -75,6 +75,17 @@ const Console = () => {
|
||||
[shouldShowLogViewer]
|
||||
);
|
||||
|
||||
const handleOnScroll = () => {
|
||||
if (!viewerRef.current) return;
|
||||
if (
|
||||
shouldAutoscroll &&
|
||||
viewerRef.current.scrollTop <
|
||||
viewerRef.current.scrollHeight - viewerRef.current.clientHeight
|
||||
) {
|
||||
setShouldAutoscroll(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{shouldShowLogViewer && (
|
||||
@ -83,10 +94,16 @@ const Console = () => {
|
||||
width: '100%',
|
||||
height: 200,
|
||||
}}
|
||||
style={{ display: 'flex', position: 'fixed', left: 0, bottom: 0 }}
|
||||
style={{
|
||||
display: 'flex',
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
zIndex: 20,
|
||||
}}
|
||||
maxHeight={'90vh'}
|
||||
>
|
||||
<div className="console" ref={viewerRef}>
|
||||
<div className="console" ref={viewerRef} onScroll={handleOnScroll}>
|
||||
{log.map((entry, i) => {
|
||||
const { timestamp, message, level } = entry;
|
||||
return (
|
||||
@ -100,11 +117,13 @@ const Console = () => {
|
||||
</Resizable>
|
||||
)}
|
||||
{shouldShowLogViewer && (
|
||||
<Tooltip hasArrow label={shouldAutoscroll ? 'Autoscroll On' : 'Autoscroll Off'}>
|
||||
<Tooltip
|
||||
hasArrow
|
||||
label={shouldAutoscroll ? 'Autoscroll On' : 'Autoscroll Off'}
|
||||
>
|
||||
<IconButton
|
||||
className={`console-autoscroll-icon-button ${
|
||||
shouldAutoscroll && 'autoscroll-enabled'
|
||||
}`}
|
||||
className={'console-autoscroll-icon-button'}
|
||||
data-autoscroll-enabled={shouldAutoscroll}
|
||||
size="sm"
|
||||
aria-label="Toggle autoscroll"
|
||||
variant={'solid'}
|
||||
@ -113,16 +132,17 @@ const Console = () => {
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip hasArrow label={shouldShowLogViewer ? 'Hide Console' : 'Show Console'}>
|
||||
<Tooltip
|
||||
hasArrow
|
||||
label={shouldShowLogViewer ? 'Hide Console' : 'Show Console'}
|
||||
>
|
||||
<IconButton
|
||||
className={`console-toggle-icon-button ${
|
||||
(hasError || !wasErrorSeen) && 'error-seen'
|
||||
}`}
|
||||
className={'console-toggle-icon-button'}
|
||||
data-error-seen={hasError || !wasErrorSeen}
|
||||
size="sm"
|
||||
position={'fixed'}
|
||||
variant={'solid'}
|
||||
aria-label="Toggle Log Viewer"
|
||||
// colorScheme={hasError || !wasErrorSeen ? 'red' : 'gray'}
|
||||
icon={shouldShowLogViewer ? <FaMinus /> : <FaCode />}
|
||||
onClick={handleClickLogViewerToggle}
|
||||
/>
|
||||
|
30
frontend/src/features/tabs/FloatingShowHideButton.scss
Normal file
30
frontend/src/features/tabs/FloatingShowHideButton.scss
Normal file
@ -0,0 +1,30 @@
|
||||
@use '../../styles/Mixins/' as *;
|
||||
|
||||
.floating-show-hide-button {
|
||||
position: absolute !important;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
z-index: 20;
|
||||
padding: 0;
|
||||
min-width: 2rem !important;
|
||||
|
||||
filter: drop-shadow(0 0 1rem var(--text-color-a3));
|
||||
|
||||
&.left {
|
||||
left: 0;
|
||||
border-radius: 0 0.5rem 0.5rem 0 !important;
|
||||
}
|
||||
|
||||
&.right {
|
||||
right: 0;
|
||||
border-radius: 0.5rem 0 0 0.5rem !important;
|
||||
}
|
||||
|
||||
@include Button(
|
||||
$btn-width: 1rem,
|
||||
$btn-height: 12rem,
|
||||
$icon-size: 20px,
|
||||
$btn-color: var(--btn-grey),
|
||||
$btn-color-hover: var(--btn-grey-hover)
|
||||
);
|
||||
}
|
@ -8,15 +8,6 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.image-to-image-panel {
|
||||
display: grid;
|
||||
row-gap: 1rem;
|
||||
grid-auto-rows: max-content;
|
||||
height: $app-content-height;
|
||||
overflow-y: scroll;
|
||||
@include HideScrollbar;
|
||||
}
|
||||
|
||||
.image-to-image-strength-main-option {
|
||||
display: grid;
|
||||
grid-template-columns: none !important;
|
||||
|
@ -17,6 +17,7 @@ import MainOptions from '../../options/MainOptions/MainOptions';
|
||||
import OptionsAccordion from '../../options/OptionsAccordion';
|
||||
import ProcessButtons from '../../options/ProcessButtons/ProcessButtons';
|
||||
import PromptInput from '../../options/PromptInput/PromptInput';
|
||||
import InvokeOptionsPanel from '../InvokeOptionsPanel';
|
||||
|
||||
export default function ImageToImagePanel() {
|
||||
const showAdvancedOptions = useAppSelector(
|
||||
@ -45,14 +46,14 @@ export default function ImageToImagePanel() {
|
||||
options: <UpscaleOptions />,
|
||||
},
|
||||
other: {
|
||||
header: <OutputHeader /> ,
|
||||
header: <OutputHeader />,
|
||||
feature: Feature.OTHER,
|
||||
options: <OutputOptions />,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="image-to-image-panel">
|
||||
<InvokeOptionsPanel>
|
||||
<PromptInput />
|
||||
<ProcessButtons />
|
||||
<MainOptions />
|
||||
@ -65,6 +66,6 @@ export default function ImageToImagePanel() {
|
||||
{showAdvancedOptions ? (
|
||||
<OptionsAccordion accordionInfo={imageToImageAccordions} />
|
||||
) : null}
|
||||
</div>
|
||||
</InvokeOptionsPanel>
|
||||
);
|
||||
}
|
||||
|
@ -85,7 +85,7 @@
|
||||
|
||||
// Overrides
|
||||
.inpainting-workarea-overrides {
|
||||
.image-gallery-area {
|
||||
.image-gallery-wrapper {
|
||||
.chakra-popover__popper {
|
||||
inset: 0 auto auto -75px !important;
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import MainOptions from '../../options/MainOptions/MainOptions';
|
||||
import OptionsAccordion from '../../options/OptionsAccordion';
|
||||
import ProcessButtons from '../../options/ProcessButtons/ProcessButtons';
|
||||
import PromptInput from '../../options/PromptInput/PromptInput';
|
||||
import InvokeOptionsPanel from '../InvokeOptionsPanel';
|
||||
|
||||
export default function InpaintingPanel() {
|
||||
const showAdvancedOptions = useAppSelector(
|
||||
@ -45,7 +46,7 @@ export default function InpaintingPanel() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="image-to-image-panel">
|
||||
<InvokeOptionsPanel>
|
||||
<PromptInput />
|
||||
<ProcessButtons />
|
||||
<MainOptions />
|
||||
@ -58,6 +59,6 @@ export default function InpaintingPanel() {
|
||||
{showAdvancedOptions ? (
|
||||
<OptionsAccordion accordionInfo={imageToImageAccordions} />
|
||||
) : null}
|
||||
</div>
|
||||
</InvokeOptionsPanel>
|
||||
);
|
||||
}
|
||||
|
51
frontend/src/features/tabs/InvokeOptionsPanel.scss
Normal file
51
frontend/src/features/tabs/InvokeOptionsPanel.scss
Normal file
@ -0,0 +1,51 @@
|
||||
@use '../../styles/Mixins/' as *;
|
||||
|
||||
.options-panel-wrapper-enter {
|
||||
transform: translateX(-150%);
|
||||
}
|
||||
|
||||
.options-panel-wrapper-enter-active {
|
||||
transform: translateX(0);
|
||||
transition: all 120ms ease-out;
|
||||
}
|
||||
|
||||
.options-panel-wrapper-exit {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.options-panel-wrapper-exit-active {
|
||||
transform: translateX(-150%);
|
||||
transition: all 120ms ease-out;
|
||||
}
|
||||
|
||||
.options-panel-wrapper {
|
||||
z-index: 20;
|
||||
background-color: var(--background-color);
|
||||
grid-auto-rows: max-content;
|
||||
height: $app-content-height;
|
||||
width: $options-bar-max-width;
|
||||
max-width: $options-bar-max-width;
|
||||
flex-shrink: 0;
|
||||
|
||||
.options-panel {
|
||||
display: grid;
|
||||
row-gap: 1rem;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
@include HideScrollbar;
|
||||
}
|
||||
|
||||
&[data-pinned='false'] {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
box-shadow: 0 0 1rem var(--text-color-a3);
|
||||
width: calc($options-bar-max-width + 2rem);
|
||||
max-width: calc($options-bar-max-width + 2rem);
|
||||
height: 100%;
|
||||
|
||||
.options-panel {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
137
frontend/src/features/tabs/InvokeOptionsPanel.tsx
Normal file
137
frontend/src/features/tabs/InvokeOptionsPanel.tsx
Normal file
@ -0,0 +1,137 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { MouseEvent, ReactNode, useEffect, useRef } from 'react';
|
||||
import { BsPinAngleFill } from 'react-icons/bs';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import IAIIconButton from '../../common/components/IAIIconButton';
|
||||
import {
|
||||
OptionsState,
|
||||
setOptionsPanelScrollPosition,
|
||||
setShouldHoldOptionsPanelOpen,
|
||||
setShouldPinOptionsPanel,
|
||||
setShouldShowOptionsPanel,
|
||||
} from '../options/optionsSlice';
|
||||
import { setNeedsCache } from './Inpainting/inpaintingSlice';
|
||||
|
||||
type Props = { children: ReactNode };
|
||||
|
||||
const optionsPanelSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => {
|
||||
const {
|
||||
shouldShowOptionsPanel,
|
||||
shouldHoldOptionsPanelOpen,
|
||||
shouldPinOptionsPanel,
|
||||
optionsPanelScrollPosition,
|
||||
} = options;
|
||||
|
||||
return {
|
||||
shouldShowOptionsPanel,
|
||||
shouldHoldOptionsPanelOpen,
|
||||
shouldPinOptionsPanel,
|
||||
optionsPanelScrollPosition,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const InvokeOptionsPanel = (props: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const {
|
||||
shouldShowOptionsPanel,
|
||||
shouldHoldOptionsPanelOpen,
|
||||
shouldPinOptionsPanel,
|
||||
optionsPanelScrollPosition,
|
||||
} = useAppSelector(optionsPanelSelector);
|
||||
|
||||
const optionsPanelRef = useRef<HTMLDivElement>(null);
|
||||
const optionsPanelContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const timeoutIdRef = useRef<number | null>(null);
|
||||
|
||||
const { children } = props;
|
||||
|
||||
const handleCloseOptionsPanel = () => {
|
||||
dispatch(
|
||||
setOptionsPanelScrollPosition(
|
||||
optionsPanelContainerRef.current
|
||||
? optionsPanelContainerRef.current.scrollTop
|
||||
: 0
|
||||
)
|
||||
);
|
||||
dispatch(setShouldShowOptionsPanel(false));
|
||||
dispatch(setShouldHoldOptionsPanelOpen(false));
|
||||
shouldPinOptionsPanel && dispatch(setNeedsCache(true));
|
||||
};
|
||||
|
||||
const setCloseOptionsPanelTimer = () => {
|
||||
timeoutIdRef.current = window.setTimeout(
|
||||
() => handleCloseOptionsPanel(),
|
||||
500
|
||||
);
|
||||
};
|
||||
|
||||
const cancelCloseOptionsPanelTimer = () => {
|
||||
timeoutIdRef.current && window.clearTimeout(timeoutIdRef.current);
|
||||
};
|
||||
|
||||
const handleClickPinOptionsPanel = () => {
|
||||
dispatch(setShouldPinOptionsPanel(!shouldPinOptionsPanel));
|
||||
dispatch(setNeedsCache(true));
|
||||
};
|
||||
|
||||
// // set gallery scroll position
|
||||
// useEffect(() => {
|
||||
// if (!optionsPanelContainerRef.current) return;
|
||||
// optionsPanelContainerRef.current.scrollTop = optionsPanelScrollPosition;
|
||||
// }, [optionsPanelScrollPosition, shouldShowOptionsPanel]);
|
||||
|
||||
return (
|
||||
<CSSTransition
|
||||
nodeRef={optionsPanelRef}
|
||||
in={
|
||||
shouldShowOptionsPanel ||
|
||||
(shouldHoldOptionsPanelOpen && !shouldPinOptionsPanel)
|
||||
}
|
||||
unmountOnExit
|
||||
timeout={200}
|
||||
classNames="options-panel-wrapper"
|
||||
>
|
||||
<div
|
||||
className="options-panel-wrapper"
|
||||
data-pinned={shouldPinOptionsPanel}
|
||||
tabIndex={1}
|
||||
ref={optionsPanelRef}
|
||||
onMouseEnter={
|
||||
!shouldPinOptionsPanel ? cancelCloseOptionsPanelTimer : undefined
|
||||
}
|
||||
onMouseOver={
|
||||
!shouldPinOptionsPanel ? cancelCloseOptionsPanelTimer : undefined
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="options-panel"
|
||||
ref={optionsPanelContainerRef}
|
||||
onMouseLeave={(e: MouseEvent<HTMLDivElement>) => {
|
||||
if (e.target !== optionsPanelContainerRef.current) {
|
||||
cancelCloseOptionsPanelTimer();
|
||||
} else {
|
||||
!shouldPinOptionsPanel && setCloseOptionsPanelTimer();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<IAIIconButton
|
||||
size={'sm'}
|
||||
aria-label={'Pin Options Panel'}
|
||||
tooltip={'Pin Options Panel (Shift+P)'}
|
||||
onClick={handleClickPinOptionsPanel}
|
||||
icon={<BsPinAngleFill />}
|
||||
data-selected={shouldPinOptionsPanel}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
);
|
||||
};
|
||||
|
||||
export default InvokeOptionsPanel;
|
@ -4,13 +4,15 @@
|
||||
display: grid !important;
|
||||
grid-template-columns: min-content auto;
|
||||
column-gap: 0.5rem;
|
||||
// height: 100%;
|
||||
}
|
||||
|
||||
.app-tabs-list {
|
||||
display: grid;
|
||||
row-gap: 0.3rem;
|
||||
grid-auto-rows: max-content;
|
||||
grid-auto-rows: min-content;
|
||||
color: var(--tab-list-text-inactive);
|
||||
z-index: 15;
|
||||
|
||||
button {
|
||||
font-size: 0.85rem;
|
||||
@ -39,5 +41,6 @@
|
||||
.app-tabs-panels {
|
||||
.app-tabs-panel {
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
@ -10,12 +10,6 @@
|
||||
column-gap: 0.5rem;
|
||||
height: 100%;
|
||||
|
||||
.workarea-options-panel {
|
||||
width: $options-bar-max-width;
|
||||
max-width: $options-bar-max-width;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.workarea-split-view {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { RootState, useAppSelector } from '../../app/store';
|
||||
import ImageGallery from '../gallery/ImageGallery';
|
||||
import ShowHideGalleryButton from '../gallery/ShowHideGalleryButton';
|
||||
|
||||
type InvokeWorkareaProps = {
|
||||
optionsPanel: ReactNode;
|
||||
@ -12,9 +10,6 @@ type InvokeWorkareaProps = {
|
||||
const InvokeWorkarea = (props: InvokeWorkareaProps) => {
|
||||
const { optionsPanel, children, styleClass } = props;
|
||||
|
||||
const { shouldShowGallery, shouldHoldGalleryOpen, shouldPinGallery } =
|
||||
useAppSelector((state: RootState) => state.gallery);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
@ -22,13 +17,10 @@ const InvokeWorkarea = (props: InvokeWorkareaProps) => {
|
||||
}
|
||||
>
|
||||
<div className="workarea-main">
|
||||
<div className="workarea-options-panel">{optionsPanel}</div>
|
||||
{optionsPanel}
|
||||
{children}
|
||||
<ImageGallery />
|
||||
</div>
|
||||
{!(shouldShowGallery || (shouldHoldGalleryOpen && !shouldPinGallery)) && (
|
||||
<ShowHideGalleryButton />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
26
frontend/src/features/tabs/ShowHideGalleryButton.tsx
Normal file
26
frontend/src/features/tabs/ShowHideGalleryButton.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { MdPhotoLibrary } from 'react-icons/md';
|
||||
import { useAppDispatch } from '../../app/store';
|
||||
import IAIIconButton from '../../common/components/IAIIconButton';
|
||||
import { setShouldShowGallery } from '../gallery/gallerySlice';
|
||||
|
||||
const ShowHideGalleryButton = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleShowGallery = () => {
|
||||
dispatch(setShouldShowGallery(true));
|
||||
};
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
tooltip="Show Gallery (G)"
|
||||
tooltipPlacement="top"
|
||||
aria-label="Show Gallery"
|
||||
styleClass="floating-show-hide-button right"
|
||||
onMouseOver={handleShowGallery}
|
||||
>
|
||||
<MdPhotoLibrary />
|
||||
</IAIIconButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShowHideGalleryButton;
|
26
frontend/src/features/tabs/ShowHideOptionsPanelButton.tsx
Normal file
26
frontend/src/features/tabs/ShowHideOptionsPanelButton.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { IoMdOptions } from 'react-icons/io';
|
||||
import { useAppDispatch } from '../../app/store';
|
||||
import IAIIconButton from '../../common/components/IAIIconButton';
|
||||
import { setShouldShowOptionsPanel } from '../options/optionsSlice';
|
||||
|
||||
const ShowHideOptionsPanelButton = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleShowOptionsPanel = () => {
|
||||
dispatch(setShouldShowOptionsPanel(true));
|
||||
};
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
tooltip="Show Options Panel (G)"
|
||||
tooltipPlacement="top"
|
||||
aria-label="Show Options Panel"
|
||||
styleClass="floating-show-hide-button left"
|
||||
onMouseOver={handleShowOptionsPanel}
|
||||
>
|
||||
<IoMdOptions />
|
||||
</IAIIconButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShowHideOptionsPanelButton;
|
@ -4,12 +4,3 @@
|
||||
padding: 1rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.text-to-image-panel {
|
||||
display: grid;
|
||||
row-gap: 1rem;
|
||||
grid-auto-rows: max-content;
|
||||
height: $app-content-height;
|
||||
overflow-y: scroll;
|
||||
@include HideScrollbar;
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import MainOptions from '../../options/MainOptions/MainOptions';
|
||||
import OptionsAccordion from '../../options/OptionsAccordion';
|
||||
import ProcessButtons from '../../options/ProcessButtons/ProcessButtons';
|
||||
import PromptInput from '../../options/PromptInput/PromptInput';
|
||||
import InvokeOptionsPanel from '../InvokeOptionsPanel';
|
||||
|
||||
export default function TextToImagePanel() {
|
||||
const showAdvancedOptions = useAppSelector(
|
||||
@ -50,7 +51,7 @@ export default function TextToImagePanel() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-to-image-panel">
|
||||
<InvokeOptionsPanel>
|
||||
<PromptInput />
|
||||
<ProcessButtons />
|
||||
<MainOptions />
|
||||
@ -58,6 +59,6 @@ export default function TextToImagePanel() {
|
||||
{showAdvancedOptions ? (
|
||||
<OptionsAccordion accordionInfo={textToImageAccordions} />
|
||||
) : null}
|
||||
</div>
|
||||
</InvokeOptionsPanel>
|
||||
);
|
||||
}
|
||||
|
@ -25,6 +25,8 @@
|
||||
--destructive-color: rgb(185, 55, 55);
|
||||
--destructive-color-hover: rgb(255, 75, 75);
|
||||
|
||||
--text-color-a3: rgba(255, 255, 255, 0.3);
|
||||
|
||||
// Error status colors
|
||||
--border-color-invalid: rgb(255, 80, 50);
|
||||
--box-shadow-color-invalid: rgb(210, 30, 10);
|
||||
|
@ -25,6 +25,8 @@
|
||||
--destructive-color: rgb(237, 51, 51);
|
||||
--destructive-color-hover: rgb(255, 55, 55);
|
||||
|
||||
--text-color-a3: rgba(0, 0, 0, 0.3);
|
||||
|
||||
// Error status colors
|
||||
--border-color-invalid: rgb(255, 80, 50);
|
||||
--box-shadow-color-invalid: none;
|
||||
|
@ -31,15 +31,16 @@
|
||||
@use '../features/gallery/ImageGallery.scss';
|
||||
@use '../features/gallery/HoverableImage.scss';
|
||||
@use '../features/gallery/InvokePopover.scss';
|
||||
@use '../features/gallery/ShowHideGalleryButton.scss';
|
||||
@use '../features/gallery/ImageMetaDataViewer/ImageMetadataViewer.scss';
|
||||
|
||||
// Tabs
|
||||
@use '../features/tabs/InvokeTabs.scss';
|
||||
@use '../features/tabs/InvokeWorkarea.scss';
|
||||
@use '../features/tabs/InvokeOptionsPanel.scss';
|
||||
@use '../features/tabs/TextToImage/TextToImage.scss';
|
||||
@use '../features/tabs/ImageToImage/ImageToImage.scss';
|
||||
@use '../features/tabs/Inpainting/Inpainting.scss';
|
||||
@use '../features/tabs/FloatingShowHideButton.scss';
|
||||
|
||||
// Component Shared
|
||||
@use '../common/components/IAINumberInput.scss';
|
||||
|
Loading…
Reference in New Issue
Block a user