Fixes edge cases, adds invoke button to header when options floating

This commit is contained in:
psychedelicious 2022-10-30 15:55:34 +11:00
parent e58b7a7ef9
commit 62c97dd7e6
26 changed files with 212 additions and 137 deletions

View File

@ -6,7 +6,7 @@ import Loading from '../Loading';
import { useAppDispatch } from './store'; import { useAppDispatch } from './store';
import { requestSystemConfig } from './socketio/actions'; import { requestSystemConfig } from './socketio/actions';
import { keepGUIAlive } from './utils'; import { keepGUIAlive } from './utils';
import InvokeTabs, { tabMap } from '../features/tabs/InvokeTabs'; import InvokeTabs from '../features/tabs/InvokeTabs';
import ImageUploader from '../common/components/ImageUploader'; import ImageUploader from '../common/components/ImageUploader';
import { RootState, useAppSelector } from '../app/store'; import { RootState, useAppSelector } from '../app/store';
@ -15,19 +15,23 @@ import ShowHideOptionsPanelButton from '../features/tabs/ShowHideOptionsPanelBut
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { GalleryState } from '../features/gallery/gallerySlice'; import { GalleryState } from '../features/gallery/gallerySlice';
import { OptionsState } from '../features/options/optionsSlice'; import { OptionsState } from '../features/options/optionsSlice';
import { activeTabNameSelector } from '../features/options/optionsSelectors';
keepGUIAlive(); keepGUIAlive();
const appSelector = createSelector( const appSelector = createSelector(
[(state: RootState) => state.gallery, (state: RootState) => state.options], [
(gallery: GalleryState, options: OptionsState) => { (state: RootState) => state.gallery,
(state: RootState) => state.options,
activeTabNameSelector,
],
(gallery: GalleryState, options: OptionsState, activeTabName) => {
const { shouldShowGallery, shouldHoldGalleryOpen, shouldPinGallery } = const { shouldShowGallery, shouldHoldGalleryOpen, shouldPinGallery } =
gallery; gallery;
const { const {
shouldShowOptionsPanel, shouldShowOptionsPanel,
shouldHoldOptionsPanelOpen, shouldHoldOptionsPanelOpen,
shouldPinOptionsPanel, shouldPinOptionsPanel,
activeTab,
} = options; } = options;
return { return {
@ -39,7 +43,7 @@ const appSelector = createSelector(
!( !(
shouldShowOptionsPanel || shouldShowOptionsPanel ||
(shouldHoldOptionsPanelOpen && !shouldPinOptionsPanel) (shouldHoldOptionsPanelOpen && !shouldPinOptionsPanel)
) && ['txt2img', 'img2img', 'inpainting'].includes(tabMap[activeTab]), ) && ['txt2img', 'img2img', 'inpainting'].includes(activeTabName),
}; };
} }
); );

View File

@ -1,22 +1,18 @@
import { Button, ButtonProps, Tooltip } from '@chakra-ui/react'; import { Button, ButtonProps, Tooltip } from '@chakra-ui/react';
interface Props extends ButtonProps { export interface IAIButtonProps extends ButtonProps {
label: string; label: string;
tooltip?: string; tooltip?: string;
} }
/** /**
* Reusable customized button component. Originally was more customized - now probably unecessary. * Reusable customized button component.
*
* TODO: Get rid of this.
*/ */
const IAIButton = (props: Props) => { const IAIButton = (props: IAIButtonProps) => {
const { label, tooltip = '', size = 'sm', ...rest } = props; const { label, tooltip = '', ...rest } = props;
return ( return (
<Tooltip label={tooltip}> <Tooltip label={tooltip}>
<Button size={size} {...rest}> <Button {...rest}>{label}</Button>
{label}
</Button>
</Tooltip> </Tooltip>
); );
}; };

View File

@ -1,23 +1,11 @@
import { useCallback, ReactNode, useState, useEffect } from 'react'; import { useCallback, ReactNode, useState, useEffect } from 'react';
import { RootState, useAppDispatch, useAppSelector } from '../../app/store'; import { useAppDispatch, useAppSelector } from '../../app/store';
import { tabMap } from '../../features/tabs/InvokeTabs';
import { FileRejection, useDropzone } from 'react-dropzone'; import { FileRejection, useDropzone } from 'react-dropzone';
import { Heading, Spinner, useToast } from '@chakra-ui/react'; import { Heading, Spinner, useToast } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { OptionsState } from '../../features/options/optionsSlice';
import { uploadImage } from '../../app/socketio/actions'; import { uploadImage } from '../../app/socketio/actions';
import { ImageUploadDestination, UploadImagePayload } from '../../app/invokeai'; import { ImageUploadDestination, UploadImagePayload } from '../../app/invokeai';
import { ImageUploaderTriggerContext } from '../../app/contexts/ImageUploaderTriggerContext'; import { ImageUploaderTriggerContext } from '../../app/contexts/ImageUploaderTriggerContext';
import { activeTabNameSelector } from '../../features/options/optionsSelectors';
const appSelector = createSelector(
(state: RootState) => state.options,
(options: OptionsState) => {
const { activeTab } = options;
return {
activeTabName: tabMap[activeTab],
};
}
);
type ImageUploaderProps = { type ImageUploaderProps = {
children: ReactNode; children: ReactNode;
@ -26,7 +14,7 @@ type ImageUploaderProps = {
const ImageUploader = (props: ImageUploaderProps) => { const ImageUploader = (props: ImageUploaderProps) => {
const { children } = props; const { children } = props;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { activeTabName } = useAppSelector(appSelector); const activeTabName = useAppSelector(activeTabNameSelector);
const toast = useToast({}); const toast = useToast({});
const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false); const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false);

View File

@ -3,11 +3,11 @@ import _ from 'lodash';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useAppSelector } from '../../app/store'; import { useAppSelector } from '../../app/store';
import { RootState } from '../../app/store'; import { RootState } from '../../app/store';
import { activeTabNameSelector } from '../../features/options/optionsSelectors';
import { OptionsState } from '../../features/options/optionsSlice'; import { OptionsState } from '../../features/options/optionsSlice';
import { SystemState } from '../../features/system/systemSlice'; import { SystemState } from '../../features/system/systemSlice';
import { InpaintingState } from '../../features/tabs/Inpainting/inpaintingSlice'; import { InpaintingState } from '../../features/tabs/Inpainting/inpaintingSlice';
import { tabMap } from '../../features/tabs/InvokeTabs';
import { validateSeedWeights } from '../util/seedWeightPairs'; import { validateSeedWeights } from '../util/seedWeightPairs';
export const useCheckParametersSelector = createSelector( export const useCheckParametersSelector = createSelector(
@ -15,8 +15,9 @@ export const useCheckParametersSelector = createSelector(
(state: RootState) => state.options, (state: RootState) => state.options,
(state: RootState) => state.system, (state: RootState) => state.system,
(state: RootState) => state.inpainting, (state: RootState) => state.inpainting,
activeTabNameSelector
], ],
(options: OptionsState, system: SystemState, inpainting: InpaintingState) => { (options: OptionsState, system: SystemState, inpainting: InpaintingState, activeTabName) => {
return { return {
// options // options
prompt: options.prompt, prompt: options.prompt,
@ -25,7 +26,7 @@ export const useCheckParametersSelector = createSelector(
maskPath: options.maskPath, maskPath: options.maskPath,
initialImage: options.initialImage, initialImage: options.initialImage,
seed: options.seed, seed: options.seed,
activeTabName: tabMap[options.activeTab], activeTabName,
// system // system
isProcessing: system.isProcessing, isProcessing: system.isProcessing,
isConnected: system.isConnected, isConnected: system.isConnected,

View File

@ -0,0 +1,20 @@
import { RefObject, useEffect } from 'react';
const useClickOutsideWatcher = (
ref: RefObject<HTMLElement>,
callback: () => void
) => {
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) {
callback();
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [ref, callback]);
};
export default useClickOutsideWatcher;

View File

@ -2,22 +2,26 @@ import { RootState, useAppSelector } from '../../app/store';
import CurrentImageButtons from './CurrentImageButtons'; import CurrentImageButtons from './CurrentImageButtons';
import { MdPhoto } from 'react-icons/md'; import { MdPhoto } from 'react-icons/md';
import CurrentImagePreview from './CurrentImagePreview'; import CurrentImagePreview from './CurrentImagePreview';
import { tabMap } from '../tabs/InvokeTabs';
import { GalleryState } from './gallerySlice'; import { GalleryState } from './gallerySlice';
import { OptionsState } from '../options/optionsSlice'; import { OptionsState } from '../options/optionsSlice';
import _ from 'lodash'; import _ from 'lodash';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { activeTabNameSelector } from '../options/optionsSelectors';
export const currentImageDisplaySelector = createSelector( export const currentImageDisplaySelector = createSelector(
[(state: RootState) => state.gallery, (state: RootState) => state.options], [
(gallery: GalleryState, options: OptionsState) => { (state: RootState) => state.gallery,
(state: RootState) => state.options,
activeTabNameSelector,
],
(gallery: GalleryState, options: OptionsState, activeTabName) => {
const { currentImage, intermediateImage } = gallery; const { currentImage, intermediateImage } = gallery;
const { activeTab, shouldShowImageDetails } = options; const { shouldShowImageDetails } = options;
return { return {
currentImage, currentImage,
intermediateImage, intermediateImage,
activeTabName: tabMap[activeTab], activeTabName,
shouldShowImageDetails, shouldShowImageDetails,
}; };
}, },
@ -32,11 +36,9 @@ export const currentImageDisplaySelector = createSelector(
* Displays the current image if there is one, plus associated actions. * Displays the current image if there is one, plus associated actions.
*/ */
const CurrentImageDisplay = () => { const CurrentImageDisplay = () => {
const { const { currentImage, intermediateImage, activeTabName } = useAppSelector(
currentImage, currentImageDisplaySelector
intermediateImage, );
activeTabName,
} = useAppSelector(currentImageDisplaySelector);
const imageToDisplay = intermediateImage || currentImage; const imageToDisplay = intermediateImage || currentImage;

View File

@ -7,11 +7,7 @@ import {
useToast, useToast,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from '../../app/store'; import { useAppDispatch, useAppSelector } from '../../app/store';
import { import { setCurrentImage } from './gallerySlice';
setCurrentImage,
setShouldHoldGalleryOpen,
setShouldShowGallery,
} from './gallerySlice';
import { FaCheck, FaTrashAlt } from 'react-icons/fa'; import { FaCheck, FaTrashAlt } from 'react-icons/fa';
import DeleteImageModal from './DeleteImageModal'; import DeleteImageModal from './DeleteImageModal';
import { memo, useState } from 'react'; import { memo, useState } from 'react';
@ -25,7 +21,6 @@ import {
} from '../options/optionsSlice'; } from '../options/optionsSlice';
import * as InvokeAI from '../../app/invokeai'; import * as InvokeAI from '../../app/invokeai';
import * as ContextMenu from '@radix-ui/react-context-menu'; import * as ContextMenu from '@radix-ui/react-context-menu';
import { tabMap } from '../tabs/InvokeTabs';
import { setImageToInpaint } from '../tabs/Inpainting/inpaintingSlice'; import { setImageToInpaint } from '../tabs/Inpainting/inpaintingSlice';
import { hoverableImageSelector } from './gallerySliceSelectors'; import { hoverableImageSelector } from './gallerySliceSelectors';
@ -118,7 +113,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
if (metadata?.image?.init_image_path) { if (metadata?.image?.init_image_path) {
const response = await fetch(metadata.image.init_image_path); const response = await fetch(metadata.image.init_image_path);
if (response.ok) { if (response.ok) {
dispatch(setActiveTab(tabMap.indexOf('img2img'))); dispatch(setActiveTab('img2img'));
dispatch(setAllImageToImageParameters(metadata)); dispatch(setAllImageToImageParameters(metadata));
toast({ toast({
title: 'Initial Image Set', title: 'Initial Image Set',

View File

@ -33,6 +33,7 @@ import { BiReset } from 'react-icons/bi';
import IAICheckbox from '../../common/components/IAICheckbox'; import IAICheckbox from '../../common/components/IAICheckbox';
import { setNeedsCache } from '../tabs/Inpainting/inpaintingSlice'; import { setNeedsCache } from '../tabs/Inpainting/inpaintingSlice';
import _ from 'lodash'; import _ from 'lodash';
import useClickOutsideWatcher from '../../common/hooks/useClickOutsideWatcher';
const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 320; const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 320;
@ -110,6 +111,7 @@ export default function ImageGallery() {
}; };
const handleCloseGallery = () => { const handleCloseGallery = () => {
if (shouldPinGallery) return;
dispatch( dispatch(
setGalleryScrollPosition( setGalleryScrollPosition(
galleryContainerRef.current ? galleryContainerRef.current.scrollTop : 0 galleryContainerRef.current ? galleryContainerRef.current.scrollTop : 0
@ -117,7 +119,7 @@ export default function ImageGallery() {
); );
dispatch(setShouldShowGallery(false)); dispatch(setShouldShowGallery(false));
dispatch(setShouldHoldGalleryOpen(false)); dispatch(setShouldHoldGalleryOpen(false));
shouldPinGallery && dispatch(setNeedsCache(true)); // dispatch(setNeedsCache(true));
}; };
const handleClickLoadMore = () => { const handleClickLoadMore = () => {
@ -255,6 +257,8 @@ export default function ImageGallery() {
setShouldShowButtons(galleryWidth >= 280); setShouldShowButtons(galleryWidth >= 280);
}, [galleryWidth]); }, [galleryWidth]);
useClickOutsideWatcher(galleryRef, handleCloseGallery);
return ( return (
<CSSTransition <CSSTransition
nodeRef={galleryRef} nodeRef={galleryRef}

View File

@ -143,9 +143,7 @@ export const gallerySlice = createSlice({
if (state.shouldAutoSwitchToNewImages) { if (state.shouldAutoSwitchToNewImages) {
state.currentImageUuid = uuid; state.currentImageUuid = uuid;
state.currentImage = newImage; state.currentImage = newImage;
if (category === 'result') { state.currentCategory = category;
state.currentCategory = 'result';
}
} }
state.intermediateImage = undefined; state.intermediateImage = undefined;
tempCategory.latest_mtime = mtime; tempCategory.latest_mtime = mtime;

View File

@ -1,12 +1,16 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { RootState } from '../../app/store'; import { RootState } from '../../app/store';
import { activeTabNameSelector } from '../options/optionsSelectors';
import { OptionsState } from '../options/optionsSlice'; import { OptionsState } from '../options/optionsSlice';
import { tabMap } from '../tabs/InvokeTabs';
import { GalleryState } from './gallerySlice'; import { GalleryState } from './gallerySlice';
export const imageGallerySelector = createSelector( export const imageGallerySelector = createSelector(
[(state: RootState) => state.gallery, (state: RootState) => state.options], [
(gallery: GalleryState, options: OptionsState) => { (state: RootState) => state.gallery,
(state: RootState) => state.options,
activeTabNameSelector,
],
(gallery: GalleryState, options: OptionsState, activeTabName) => {
const { const {
categories, categories,
currentCategory, currentCategory,
@ -21,8 +25,6 @@ export const imageGallerySelector = createSelector(
galleryWidth, galleryWidth,
} = gallery; } = gallery;
const { activeTab } = options;
return { return {
currentImageUuid, currentImageUuid,
shouldPinGallery, shouldPinGallery,
@ -31,7 +33,7 @@ export const imageGallerySelector = createSelector(
galleryImageMinimumWidth, galleryImageMinimumWidth,
galleryImageObjectFit, galleryImageObjectFit,
galleryGridTemplateColumns: `repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, auto))`, galleryGridTemplateColumns: `repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, auto))`,
activeTabName: tabMap[activeTab], activeTabName,
shouldHoldGalleryOpen, shouldHoldGalleryOpen,
shouldAutoSwitchToNewImages, shouldAutoSwitchToNewImages,
images: categories[currentCategory].images, images: categories[currentCategory].images,
@ -44,12 +46,12 @@ export const imageGallerySelector = createSelector(
); );
export const hoverableImageSelector = createSelector( export const hoverableImageSelector = createSelector(
[(state: RootState) => state.options, (state: RootState) => state.gallery], [(state: RootState) => state.options, (state: RootState) => state.gallery, activeTabNameSelector],
(options: OptionsState, gallery: GalleryState) => { (options: OptionsState, gallery: GalleryState, activeTabName) => {
return { return {
galleryImageObjectFit: gallery.galleryImageObjectFit, galleryImageObjectFit: gallery.galleryImageObjectFit,
galleryImageMinimumWidth: gallery.galleryImageMinimumWidth, galleryImageMinimumWidth: gallery.galleryImageMinimumWidth,
activeTabName: tabMap[options.activeTab], activeTabName,
}; };
} }
); );

View File

@ -2,14 +2,13 @@ import React, { ChangeEvent } from 'react';
import { HEIGHTS } from '../../../app/constants'; import { HEIGHTS } from '../../../app/constants';
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store'; import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
import IAISelect from '../../../common/components/IAISelect'; import IAISelect from '../../../common/components/IAISelect';
import { tabMap } from '../../tabs/InvokeTabs'; import { activeTabNameSelector } from '../optionsSelectors';
import { setHeight } from '../optionsSlice'; import { setHeight } from '../optionsSlice';
import { fontSize } from './MainOptions'; import { fontSize } from './MainOptions';
export default function MainHeight() { export default function MainHeight() {
const { activeTab, height } = useAppSelector( const { height } = useAppSelector((state: RootState) => state.options);
(state: RootState) => state.options const activeTabName = useAppSelector(activeTabNameSelector);
);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const handleChangeHeight = (e: ChangeEvent<HTMLSelectElement>) => const handleChangeHeight = (e: ChangeEvent<HTMLSelectElement>) =>
@ -17,7 +16,7 @@ export default function MainHeight() {
return ( return (
<IAISelect <IAISelect
isDisabled={tabMap[activeTab] === 'inpainting'} isDisabled={activeTabName === 'inpainting'}
label="Height" label="Height"
value={height} value={height}
flexGrow={1} flexGrow={1}

View File

@ -6,6 +6,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { SystemState } from '../../system/systemSlice'; import { SystemState } from '../../system/systemSlice';
import _ from 'lodash'; import _ from 'lodash';
import { IAIButtonProps } from '../../../common/components/IAIButton';
const cancelButtonSelector = createSelector( const cancelButtonSelector = createSelector(
(state: RootState) => state.system, (state: RootState) => state.system,
@ -23,7 +24,8 @@ const cancelButtonSelector = createSelector(
} }
); );
export default function CancelButton() { export default function CancelButton(props: Omit<IAIButtonProps, 'label'>) {
const { ...rest } = props;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { isProcessing, isConnected, isCancelable } = const { isProcessing, isConnected, isCancelable } =
useAppSelector(cancelButtonSelector); useAppSelector(cancelButtonSelector);
@ -47,6 +49,7 @@ export default function CancelButton() {
isDisabled={!isConnected || !isProcessing || !isCancelable} isDisabled={!isConnected || !isProcessing || !isCancelable}
onClick={handleClickCancel} onClick={handleClickCancel}
styleClass="cancel-btn" styleClass="cancel-btn"
{...rest}
/> />
); );
} }

View File

@ -1,21 +1,33 @@
import { useHotkeys } from 'react-hotkeys-hook';
import { generateImage } from '../../../app/socketio/actions'; import { generateImage } from '../../../app/socketio/actions';
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store'; import { useAppDispatch, useAppSelector } from '../../../app/store';
import IAIButton from '../../../common/components/IAIButton'; import IAIButton, {
IAIButtonProps,
} from '../../../common/components/IAIButton';
import useCheckParameters from '../../../common/hooks/useCheckParameters'; import useCheckParameters from '../../../common/hooks/useCheckParameters';
import { tabMap } from '../../tabs/InvokeTabs'; import { activeTabNameSelector } from '../optionsSelectors';
export default function InvokeButton() { export default function InvokeButton(props: Omit<IAIButtonProps, 'label'>) {
const { ...rest } = props;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isReady = useCheckParameters(); const isReady = useCheckParameters();
const activeTab = useAppSelector( const activeTabName = useAppSelector(activeTabNameSelector);
(state: RootState) => state.options.activeTab
);
const handleClickGenerate = () => { const handleClickGenerate = () => {
dispatch(generateImage(tabMap[activeTab])); dispatch(generateImage(activeTabName));
}; };
useHotkeys(
'ctrl+enter, cmd+enter',
() => {
if (isReady) {
dispatch(generateImage(activeTabName));
}
},
[isReady, activeTabName]
);
return ( return (
<IAIButton <IAIButton
label="Invoke" label="Invoke"
@ -24,6 +36,7 @@ export default function InvokeButton() {
isDisabled={!isReady} isDisabled={!isReady}
onClick={handleClickGenerate} onClick={handleClickGenerate}
className="invoke-btn" className="invoke-btn"
{...rest}
/> />
); );
} }

View File

@ -4,12 +4,13 @@
display: grid; display: grid;
grid-template-columns: auto max-content; grid-template-columns: auto max-content;
column-gap: 0.5rem; column-gap: 0.5rem;
}
.invoke-btn { .invoke-btn {
@include Button( @include Button(
$btn-color: var(--accent-color), $btn-color: var(--accent-color),
$btn-color-hover: var(--accent-color-hover), $btn-color-hover: var(--accent-color-hover),
$btn-width: 5rem // $btn-width: 5rem
); );
} }
@ -17,7 +18,6 @@
@include Button( @include Button(
$btn-color: var(--destructive-color), $btn-color: var(--destructive-color),
$btn-color-hover: var(--destructive-color-hover), $btn-color-hover: var(--destructive-color-hover),
$btn-width: 3rem // $btn-width: 3rem
); );
} }
}

View File

@ -8,14 +8,14 @@ import { createSelector } from '@reduxjs/toolkit';
import _ from 'lodash'; import _ from 'lodash';
import useCheckParameters from '../../../common/hooks/useCheckParameters'; import useCheckParameters from '../../../common/hooks/useCheckParameters';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { tabMap } from '../../tabs/InvokeTabs'; import { activeTabNameSelector } from '../optionsSelectors';
const promptInputSelector = createSelector( const promptInputSelector = createSelector(
(state: RootState) => state.options, [(state: RootState) => state.options, activeTabNameSelector],
(options: OptionsState) => { (options: OptionsState, activeTabName) => {
return { return {
prompt: options.prompt, prompt: options.prompt,
activeTabName: tabMap[options.activeTab], activeTabName,
}; };
}, },
{ {
@ -38,16 +38,6 @@ const PromptInput = () => {
dispatch(setPrompt(e.target.value)); dispatch(setPrompt(e.target.value));
}; };
useHotkeys(
'ctrl+enter, cmd+enter',
() => {
if (isReady) {
dispatch(generateImage(activeTabName));
}
},
[isReady, activeTabName]
);
useHotkeys( useHotkeys(
'alt+a', 'alt+a',
() => { () => {

View File

@ -0,0 +1,9 @@
import { createSelector } from '@reduxjs/toolkit';
import { RootState } from '../../app/store';
import { tabMap } from '../tabs/InvokeTabs';
import { OptionsState } from './optionsSlice';
export const activeTabNameSelector = createSelector(
(state: RootState) => state.options,
(options: OptionsState) => tabMap[options.activeTab]
);

View File

@ -134,7 +134,7 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
{ {
title: 'Quick Toggle Brush/Eraser', title: 'Quick Toggle Brush/Eraser',
desc: 'Quick toggle between brush and eraser', desc: 'Quick toggle between brush and eraser',
hotkey: 'Z', hotkey: 'X',
}, },
{ {
title: 'Decrease Brush Size', title: 'Decrease Brush Size',

View File

@ -24,3 +24,11 @@
align-items: center; align-items: center;
column-gap: 0.5rem; column-gap: 0.5rem;
} }
// Overrides
.process-buttons {
padding-left: 0.5rem;
button {
}
}

View File

@ -1,19 +1,37 @@
import { IconButton, Link, Tooltip, useColorMode } from '@chakra-ui/react'; import { IconButton, Link, Tooltip, useColorMode } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import _ from 'lodash';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { FaSun, FaMoon, FaGithub, FaDiscord } from 'react-icons/fa'; import { FaSun, FaMoon, FaGithub, FaDiscord } from 'react-icons/fa';
import { MdHelp, MdKeyboard, MdSettings } from 'react-icons/md'; import { MdHelp, MdKeyboard, MdSettings } from 'react-icons/md';
import { RootState, useAppSelector } from '../../app/store';
import InvokeAILogo from '../../assets/images/logo.png'; import InvokeAILogo from '../../assets/images/logo.png';
import { OptionsState } from '../options/optionsSlice';
import CancelButton from '../options/ProcessButtons/CancelButton';
import InvokeButton from '../options/ProcessButtons/InvokeButton';
import ProcessButtons from '../options/ProcessButtons/ProcessButtons';
import HotkeysModal from './HotkeysModal/HotkeysModal'; import HotkeysModal from './HotkeysModal/HotkeysModal';
import SettingsModal from './SettingsModal/SettingsModal'; import SettingsModal from './SettingsModal/SettingsModal';
import StatusIndicator from './StatusIndicator'; import StatusIndicator from './StatusIndicator';
const siteHeaderSelector = createSelector(
(state: RootState) => state.options,
(options: OptionsState) => {
const { shouldPinOptionsPanel } = options;
return { shouldShowProcessButtons: !shouldPinOptionsPanel };
},
{ memoizeOptions: { resultEqualityCheck: _.isEqual } }
);
/** /**
* Header, includes color mode toggle, settings button, status message. * Header, includes color mode toggle, settings button, status message.
*/ */
const SiteHeader = () => { const SiteHeader = () => {
const { shouldShowProcessButtons } = useAppSelector(siteHeaderSelector);
const { colorMode, toggleColorMode } = useColorMode(); const { colorMode, toggleColorMode } = useColorMode();
useHotkeys( useHotkeys(
@ -36,6 +54,12 @@ const SiteHeader = () => {
<h1> <h1>
invoke <strong>ai</strong> invoke <strong>ai</strong>
</h1> </h1>
{shouldShowProcessButtons && (
<div className="process-buttons process-buttons">
<InvokeButton size={'sm'} />
<CancelButton size={'sm'} />
</div>
)}
</div> </div>
<div className="site-header-right-side"> <div className="site-header-right-side">

View File

@ -8,7 +8,7 @@
padding: 0; padding: 0;
min-width: 2rem !important; min-width: 2rem !important;
filter: drop-shadow(0 0 1rem var(--text-color-a3)); filter: var(--floating-button-drop-shadow);
&.left { &.left {
left: 0; left: 0;

View File

@ -57,6 +57,7 @@ const InpaintingCanvas = () => {
isDrawing, isDrawing,
shouldLockBoundingBox, shouldLockBoundingBox,
shouldShowBoundingBox, shouldShowBoundingBox,
boundingBoxDimensions,
} = useAppSelector(inpaintingCanvasSelector); } = useAppSelector(inpaintingCanvasSelector);
const toast = useToast(); const toast = useToast();
@ -95,7 +96,7 @@ const InpaintingCanvas = () => {
}; };
image.src = imageToInpaint.url; image.src = imageToInpaint.url;
} else { } else {
setCanvasBgImage(null) setCanvasBgImage(null);
} }
}, [imageToInpaint, dispatch, stageScale, toast]); }, [imageToInpaint, dispatch, stageScale, toast]);
@ -243,7 +244,7 @@ const InpaintingCanvas = () => {
)} )}
{!shouldLockBoundingBox && ( {!shouldLockBoundingBox && (
<div style={{ pointerEvents: 'none' }}> <div style={{ pointerEvents: 'none' }}>
Transforming Bounding Box (M) {`Transforming Bounding Box ${boundingBoxDimensions.width}x${boundingBoxDimensions.height} (M)`}
</div> </div>
)} )}
</div> </div>

View File

@ -6,8 +6,8 @@ import {
useAppDispatch, useAppDispatch,
useAppSelector, useAppSelector,
} from '../../../../app/store'; } from '../../../../app/store';
import { activeTabNameSelector } from '../../../options/optionsSelectors';
import { OptionsState } from '../../../options/optionsSlice'; import { OptionsState } from '../../../options/optionsSlice';
import { tabMap } from '../../InvokeTabs';
import { import {
InpaintingState, InpaintingState,
setIsDrawing, setIsDrawing,
@ -16,12 +16,16 @@ import {
} from '../inpaintingSlice'; } from '../inpaintingSlice';
const keyboardEventManagerSelector = createSelector( const keyboardEventManagerSelector = createSelector(
[(state: RootState) => state.options, (state: RootState) => state.inpainting], [
(options: OptionsState, inpainting: InpaintingState) => { (state: RootState) => state.options,
(state: RootState) => state.inpainting,
activeTabNameSelector,
],
(options: OptionsState, inpainting: InpaintingState, activeTabName) => {
const { shouldShowMask, cursorPosition, shouldLockBoundingBox } = const { shouldShowMask, cursorPosition, shouldLockBoundingBox } =
inpainting; inpainting;
return { return {
activeTabName: tabMap[options.activeTab], activeTabName,
shouldShowMask, shouldShowMask,
isCursorOnCanvas: Boolean(cursorPosition), isCursorOnCanvas: Boolean(cursorPosition),
shouldLockBoundingBox, shouldLockBoundingBox,
@ -49,7 +53,7 @@ const KeyboardEventManager = () => {
useEffect(() => { useEffect(() => {
const listener = (e: KeyboardEvent) => { const listener = (e: KeyboardEvent) => {
if ( if (
!['z', ' '].includes(e.key) || !['x', ' '].includes(e.key) ||
activeTabName !== 'inpainting' || activeTabName !== 'inpainting' ||
!shouldShowMask !shouldShowMask
) { ) {
@ -83,7 +87,7 @@ const KeyboardEventManager = () => {
} }
switch (e.key) { switch (e.key) {
case 'z': { case 'x': {
dispatch(toggleTool()); dispatch(toggleTool());
break; break;
} }

View File

@ -1,6 +1,7 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import _ from 'lodash'; import _ from 'lodash';
import { RootState } from '../../../app/store'; import { RootState } from '../../../app/store';
import { activeTabNameSelector } from '../../options/optionsSelectors';
import { OptionsState } from '../../options/optionsSlice'; import { OptionsState } from '../../options/optionsSlice';
import { tabMap } from '../InvokeTabs'; import { tabMap } from '../InvokeTabs';
import { InpaintingState } from './inpaintingSlice'; import { InpaintingState } from './inpaintingSlice';
@ -18,8 +19,12 @@ export const inpaintingCanvasLinesSelector = createSelector(
); );
export const inpaintingControlsSelector = createSelector( export const inpaintingControlsSelector = createSelector(
[(state: RootState) => state.inpainting, (state: RootState) => state.options], [
(inpainting: InpaintingState, options: OptionsState) => { (state: RootState) => state.inpainting,
(state: RootState) => state.options,
activeTabNameSelector,
],
(inpainting: InpaintingState, options: OptionsState, activeTabName) => {
const { const {
tool, tool,
brushSize, brushSize,
@ -34,7 +39,7 @@ export const inpaintingControlsSelector = createSelector(
shouldShowBoundingBox, shouldShowBoundingBox,
} = inpainting; } = inpainting;
const { activeTab, showDualDisplay } = options; const { showDualDisplay } = options;
return { return {
tool, tool,
@ -46,7 +51,7 @@ export const inpaintingControlsSelector = createSelector(
canUndo: pastLines.length > 0, canUndo: pastLines.length > 0,
canRedo: futureLines.length > 0, canRedo: futureLines.length > 0,
isMaskEmpty: lines.length === 0, isMaskEmpty: lines.length === 0,
activeTabName: tabMap[activeTab], activeTabName,
showDualDisplay, showDualDisplay,
shouldShowBoundingBoxFill, shouldShowBoundingBoxFill,
shouldShowBoundingBox, shouldShowBoundingBox,
@ -75,6 +80,7 @@ export const inpaintingCanvasSelector = createSelector(
isDrawing, isDrawing,
shouldLockBoundingBox, shouldLockBoundingBox,
shouldShowBoundingBox, shouldShowBoundingBox,
boundingBoxDimensions,
} = inpainting; } = inpainting;
return { return {
tool, tool,
@ -89,6 +95,7 @@ export const inpaintingCanvasSelector = createSelector(
isDrawing, isDrawing,
shouldLockBoundingBox, shouldLockBoundingBox,
shouldShowBoundingBox, shouldShowBoundingBox,
boundingBoxDimensions,
}; };
}, },
{ {

View File

@ -1,9 +1,17 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { MouseEvent, ReactNode, useEffect, useRef } from 'react'; import {
FocusEvent,
MouseEvent,
ReactNode,
useCallback,
useEffect,
useRef,
} from 'react';
import { BsPinAngleFill } from 'react-icons/bs'; import { BsPinAngleFill } from 'react-icons/bs';
import { CSSTransition } from 'react-transition-group'; import { CSSTransition } from 'react-transition-group';
import { RootState, useAppDispatch, useAppSelector } from '../../app/store'; import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
import IAIIconButton from '../../common/components/IAIIconButton'; import IAIIconButton from '../../common/components/IAIIconButton';
import useClickOutsideWatcher from '../../common/hooks/useClickOutsideWatcher';
import { import {
OptionsState, OptionsState,
setOptionsPanelScrollPosition, setOptionsPanelScrollPosition,
@ -50,7 +58,8 @@ const InvokeOptionsPanel = (props: Props) => {
const { children } = props; const { children } = props;
const handleCloseOptionsPanel = () => { const handleCloseOptionsPanel = useCallback(() => {
if (shouldPinOptionsPanel) return;
dispatch( dispatch(
setOptionsPanelScrollPosition( setOptionsPanelScrollPosition(
optionsPanelContainerRef.current optionsPanelContainerRef.current
@ -60,8 +69,10 @@ const InvokeOptionsPanel = (props: Props) => {
); );
dispatch(setShouldShowOptionsPanel(false)); dispatch(setShouldShowOptionsPanel(false));
dispatch(setShouldHoldOptionsPanelOpen(false)); dispatch(setShouldHoldOptionsPanelOpen(false));
shouldPinOptionsPanel && dispatch(setNeedsCache(true)); // dispatch(setNeedsCache(true));
}; }, [dispatch, shouldPinOptionsPanel]);
useClickOutsideWatcher(optionsPanelRef, handleCloseOptionsPanel);
const setCloseOptionsPanelTimer = () => { const setCloseOptionsPanelTimer = () => {
timeoutIdRef.current = window.setTimeout( timeoutIdRef.current = window.setTimeout(

View File

@ -25,8 +25,6 @@
--destructive-color: rgb(185, 55, 55); --destructive-color: rgb(185, 55, 55);
--destructive-color-hover: rgb(255, 75, 75); --destructive-color-hover: rgb(255, 75, 75);
--text-color-a3: rgba(255, 255, 255, 0.3);
// Error status colors // Error status colors
--border-color-invalid: rgb(255, 80, 50); --border-color-invalid: rgb(255, 80, 50);
--box-shadow-color-invalid: rgb(210, 30, 10); --box-shadow-color-invalid: rgb(210, 30, 10);
@ -103,6 +101,6 @@
--context-menu-box-shadow: none; --context-menu-box-shadow: none;
--context-menu-bg-color-hover: rgb(30, 32, 42); --context-menu-bg-color-hover: rgb(30, 32, 42);
// Inpainting // Shadows
--inpaint-bg-color: rgb(30, 32, 42); --floating-button-drop-shadow: drop-shadow(0 0 1rem rgba(140, 101, 255, 0.5));
} }

View File

@ -25,8 +25,6 @@
--destructive-color: rgb(237, 51, 51); --destructive-color: rgb(237, 51, 51);
--destructive-color-hover: rgb(255, 55, 55); --destructive-color-hover: rgb(255, 55, 55);
--text-color-a3: rgba(0, 0, 0, 0.3);
// Error status colors // Error status colors
--border-color-invalid: rgb(255, 80, 50); --border-color-invalid: rgb(255, 80, 50);
--box-shadow-color-invalid: none; --box-shadow-color-invalid: none;
@ -104,6 +102,6 @@
0px 10px 20px -15px rgba(22, 23, 24, 0.2); 0px 10px 20px -15px rgba(22, 23, 24, 0.2);
--context-menu-bg-color-hover: var(--background-color-secondary); --context-menu-bg-color-hover: var(--background-color-secondary);
// Inpainting // Shadows
--inpaint-bg-color: rgb(180, 182, 184); --floating-button-drop-shadow: drop-shadow(0 0 1rem rgba(0, 0, 0, 0.3));
} }