mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
487 lines
15 KiB
TypeScript
487 lines
15 KiB
TypeScript
import { Button } from '@chakra-ui/button';
|
|
import { NumberSize, Resizable, Size } from 're-resizable';
|
|
|
|
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
|
import { useHotkeys } from 'react-hotkeys-hook';
|
|
import { MdClear, MdPhotoLibrary } from 'react-icons/md';
|
|
import { BsPinAngleFill } from 'react-icons/bs';
|
|
import { requestImages } from '../../app/socketio/actions';
|
|
import { useAppDispatch, useAppSelector } from '../../app/store';
|
|
import IAIIconButton from '../../common/components/IAIIconButton';
|
|
import {
|
|
selectNextImage,
|
|
selectPrevImage,
|
|
setCurrentCategory,
|
|
setGalleryImageMinimumWidth,
|
|
setGalleryImageObjectFit,
|
|
setGalleryScrollPosition,
|
|
setShouldAutoSwitchToNewImages,
|
|
setShouldHoldGalleryOpen,
|
|
setShouldPinGallery,
|
|
} from './gallerySlice';
|
|
import HoverableImage from './HoverableImage';
|
|
import { setShouldShowGallery } from '../gallery/gallerySlice';
|
|
import { ButtonGroup, Spacer, useToast } from '@chakra-ui/react';
|
|
import { CSSTransition } from 'react-transition-group';
|
|
import { Direction } from 're-resizable/lib/resizer';
|
|
import { imageGallerySelector } from './gallerySliceSelectors';
|
|
import { FaImage, FaUser, FaWrench } from 'react-icons/fa';
|
|
import IAIPopover from '../../common/components/IAIPopover';
|
|
import IAISlider from '../../common/components/IAISlider';
|
|
import { BiReset } from 'react-icons/bi';
|
|
import IAICheckbox from '../../common/components/IAICheckbox';
|
|
|
|
export default function ImageGallery() {
|
|
const dispatch = useAppDispatch();
|
|
const toast = useToast();
|
|
|
|
const {
|
|
images,
|
|
currentCategory,
|
|
currentImageUuid,
|
|
shouldPinGallery,
|
|
shouldShowGallery,
|
|
galleryScrollPosition,
|
|
galleryImageMinimumWidth,
|
|
galleryGridTemplateColumns,
|
|
activeTabName,
|
|
galleryImageObjectFit,
|
|
shouldHoldGalleryOpen,
|
|
shouldAutoSwitchToNewImages,
|
|
areMoreImagesAvailable,
|
|
} = useAppSelector(imageGallerySelector);
|
|
|
|
const [gallerySize, setGallerySize] = useState<Size>({
|
|
width: '300',
|
|
height: '100%',
|
|
});
|
|
|
|
const [galleryMaxSize, setGalleryMaxSize] = useState<Size>({
|
|
width: '590', // keep max at 590 for any tab
|
|
height: '100%',
|
|
});
|
|
|
|
const [galleryMinSize, setGalleryMinSize] = useState<Size>({
|
|
width: '300', // keep max at 590 for any tab
|
|
height: '100%',
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (activeTabName === 'inpainting' && shouldPinGallery) {
|
|
setGalleryMinSize((prevSize) => {
|
|
return { ...prevSize, width: '200' };
|
|
});
|
|
setGalleryMaxSize((prevSize) => {
|
|
return { ...prevSize, width: '200' };
|
|
});
|
|
setGallerySize((prevSize) => {
|
|
return { ...prevSize, width: Math.min(Number(prevSize.width), 200) };
|
|
});
|
|
} else {
|
|
setGalleryMaxSize((prevSize) => {
|
|
return { ...prevSize, width: '590', height: '100%' };
|
|
});
|
|
setGallerySize((prevSize) => {
|
|
return { ...prevSize, width: Math.min(Number(prevSize.width), 590) };
|
|
});
|
|
}
|
|
}, [activeTabName, shouldPinGallery, setGalleryMaxSize]);
|
|
|
|
useEffect(() => {
|
|
if (!shouldPinGallery) {
|
|
setGalleryMaxSize((prevSize) => {
|
|
// calculate vh in px
|
|
return {
|
|
...prevSize,
|
|
width: window.innerWidth,
|
|
};
|
|
});
|
|
}
|
|
}, [shouldPinGallery]);
|
|
|
|
const galleryRef = useRef<HTMLDivElement>(null);
|
|
const galleryContainerRef = useRef<HTMLDivElement>(null);
|
|
const timeoutIdRef = useRef<number | null>(null);
|
|
|
|
const handleSetShouldPinGallery = () => {
|
|
dispatch(setShouldPinGallery(!shouldPinGallery));
|
|
setGallerySize({
|
|
...gallerySize,
|
|
height: shouldPinGallery ? '100vh' : '100%',
|
|
});
|
|
};
|
|
|
|
const handleToggleGallery = () => {
|
|
shouldShowGallery ? handleCloseGallery() : handleOpenGallery();
|
|
};
|
|
|
|
const handleOpenGallery = () => {
|
|
dispatch(setShouldShowGallery(true));
|
|
};
|
|
|
|
const handleCloseGallery = () => {
|
|
dispatch(
|
|
setGalleryScrollPosition(
|
|
galleryContainerRef.current ? galleryContainerRef.current.scrollTop : 0
|
|
)
|
|
);
|
|
dispatch(setShouldShowGallery(false));
|
|
dispatch(setShouldHoldGalleryOpen(false));
|
|
};
|
|
|
|
const handleClickLoadMore = () => {
|
|
dispatch(requestImages(currentCategory));
|
|
};
|
|
|
|
const handleChangeGalleryImageMinimumWidth = (v: number) => {
|
|
dispatch(setGalleryImageMinimumWidth(v));
|
|
};
|
|
|
|
const setCloseGalleryTimer = () => {
|
|
timeoutIdRef.current = window.setTimeout(() => handleCloseGallery(), 500);
|
|
};
|
|
|
|
const cancelCloseGalleryTimer = () => {
|
|
timeoutIdRef.current && window.clearTimeout(timeoutIdRef.current);
|
|
};
|
|
|
|
useHotkeys(
|
|
'g',
|
|
() => {
|
|
handleToggleGallery();
|
|
},
|
|
[shouldShowGallery]
|
|
);
|
|
|
|
useHotkeys(
|
|
'left',
|
|
() => {
|
|
dispatch(selectPrevImage(currentCategory));
|
|
},
|
|
[currentCategory]
|
|
);
|
|
|
|
useHotkeys(
|
|
'right',
|
|
() => {
|
|
dispatch(selectNextImage(currentCategory));
|
|
},
|
|
[currentCategory]
|
|
);
|
|
|
|
useHotkeys(
|
|
'shift+p',
|
|
() => {
|
|
handleSetShouldPinGallery();
|
|
},
|
|
[shouldPinGallery]
|
|
);
|
|
|
|
const IMAGE_SIZE_STEP = 32;
|
|
|
|
useHotkeys(
|
|
'shift+up',
|
|
() => {
|
|
if (galleryImageMinimumWidth >= 256) {
|
|
return;
|
|
}
|
|
if (galleryImageMinimumWidth < 256) {
|
|
const newMinWidth = galleryImageMinimumWidth + IMAGE_SIZE_STEP;
|
|
if (newMinWidth <= 256) {
|
|
dispatch(setGalleryImageMinimumWidth(newMinWidth));
|
|
toast({
|
|
title: `Gallery Thumbnail Size set to ${newMinWidth}`,
|
|
status: 'success',
|
|
duration: 1000,
|
|
isClosable: true,
|
|
});
|
|
} else {
|
|
dispatch(setGalleryImageMinimumWidth(256));
|
|
toast({
|
|
title: `Gallery Thumbnail Size set to 256`,
|
|
status: 'success',
|
|
duration: 1000,
|
|
isClosable: true,
|
|
});
|
|
}
|
|
}
|
|
},
|
|
[galleryImageMinimumWidth]
|
|
);
|
|
|
|
useHotkeys(
|
|
'shift+down',
|
|
() => {
|
|
if (galleryImageMinimumWidth <= 32) {
|
|
return;
|
|
}
|
|
if (galleryImageMinimumWidth > 32) {
|
|
const newMinWidth = galleryImageMinimumWidth - IMAGE_SIZE_STEP;
|
|
if (newMinWidth > 32) {
|
|
dispatch(setGalleryImageMinimumWidth(newMinWidth));
|
|
toast({
|
|
title: `Gallery Thumbnail Size set to ${newMinWidth}`,
|
|
status: 'success',
|
|
duration: 1000,
|
|
isClosable: true,
|
|
});
|
|
} else {
|
|
dispatch(setGalleryImageMinimumWidth(32));
|
|
toast({
|
|
title: `Gallery Thumbnail Size set to 32`,
|
|
status: 'success',
|
|
duration: 1000,
|
|
isClosable: true,
|
|
});
|
|
}
|
|
}
|
|
},
|
|
[galleryImageMinimumWidth]
|
|
);
|
|
|
|
useHotkeys(
|
|
'shift+r',
|
|
() => {
|
|
dispatch(setGalleryImageMinimumWidth(64));
|
|
toast({
|
|
title: `Reset Gallery Image Size`,
|
|
status: 'success',
|
|
duration: 2500,
|
|
isClosable: true,
|
|
});
|
|
},
|
|
[galleryImageMinimumWidth]
|
|
);
|
|
|
|
// set gallery scroll position
|
|
useEffect(() => {
|
|
if (!galleryContainerRef.current) return;
|
|
galleryContainerRef.current.scrollTop = galleryScrollPosition;
|
|
}, [galleryScrollPosition, shouldShowGallery]);
|
|
|
|
return (
|
|
<CSSTransition
|
|
nodeRef={galleryRef}
|
|
in={shouldShowGallery || (shouldHoldGalleryOpen && !shouldPinGallery)}
|
|
unmountOnExit
|
|
timeout={200}
|
|
classNames="image-gallery-area"
|
|
>
|
|
<div
|
|
className="image-gallery-area"
|
|
data-pinned={shouldPinGallery}
|
|
ref={galleryRef}
|
|
onMouseLeave={!shouldPinGallery ? setCloseGalleryTimer : undefined}
|
|
onMouseEnter={!shouldPinGallery ? cancelCloseGalleryTimer : undefined}
|
|
onMouseOver={!shouldPinGallery ? cancelCloseGalleryTimer : undefined}
|
|
>
|
|
<Resizable
|
|
minWidth={galleryMinSize.width}
|
|
maxWidth={galleryMaxSize.width}
|
|
maxHeight={'100%'}
|
|
className={'image-gallery-popup'}
|
|
handleStyles={{ left: { width: '20px' } }}
|
|
enable={{
|
|
top: false,
|
|
right: false,
|
|
bottom: false,
|
|
left: true,
|
|
topRight: false,
|
|
bottomRight: false,
|
|
bottomLeft: false,
|
|
topLeft: false,
|
|
}}
|
|
size={gallerySize}
|
|
onResizeStop={(
|
|
_event: MouseEvent | TouchEvent,
|
|
_direction: Direction,
|
|
elementRef: HTMLElement,
|
|
delta: NumberSize
|
|
) => {
|
|
setGallerySize({
|
|
width: Number(gallerySize.width) + delta.width,
|
|
height: '100%',
|
|
});
|
|
elementRef.removeAttribute('data-resize-alert');
|
|
}}
|
|
onResize={(
|
|
_event: MouseEvent | TouchEvent,
|
|
_direction: Direction,
|
|
elementRef: HTMLElement,
|
|
delta: NumberSize
|
|
) => {
|
|
const newWidth = Number(gallerySize.width) + delta.width;
|
|
if (newWidth >= galleryMaxSize.width) {
|
|
elementRef.setAttribute('data-resize-alert', 'true');
|
|
} else {
|
|
elementRef.removeAttribute('data-resize-alert');
|
|
}
|
|
}}
|
|
>
|
|
<div className="image-gallery-header">
|
|
{/*{activeTabName !== 'inpainting' ? (
|
|
<>
|
|
<h1>Your Invocations</h1>
|
|
<Spacer />
|
|
</>
|
|
) : null}*/}
|
|
<div>
|
|
<ButtonGroup
|
|
size="sm"
|
|
isAttached
|
|
variant="solid"
|
|
className="image-gallery-category-btn-group"
|
|
>
|
|
<IAIIconButton
|
|
aria-label="Show Invocations"
|
|
tooltip="Show Invocations"
|
|
data-selected={currentCategory === 'result'}
|
|
icon={<FaImage />}
|
|
onClick={() => dispatch(setCurrentCategory('result'))}
|
|
/>
|
|
<IAIIconButton
|
|
aria-label="Show Uploads"
|
|
tooltip="Show Uploads"
|
|
data-selected={currentCategory === 'user'}
|
|
icon={<FaUser />}
|
|
onClick={() => dispatch(setCurrentCategory('user'))}
|
|
/>
|
|
</ButtonGroup>
|
|
</div>
|
|
<IAIPopover
|
|
trigger="hover"
|
|
hasArrow={activeTabName === 'inpainting' ? false : true}
|
|
triggerComponent={
|
|
<IAIIconButton
|
|
size={'sm'}
|
|
aria-label={'Gallery Settings'}
|
|
icon={<FaWrench />}
|
|
className="image-gallery-icon-btn"
|
|
cursor={'pointer'}
|
|
/>
|
|
}
|
|
>
|
|
<div className="image-gallery-settings-popover">
|
|
<div>
|
|
<IAISlider
|
|
value={galleryImageMinimumWidth}
|
|
onChange={handleChangeGalleryImageMinimumWidth}
|
|
min={32}
|
|
max={256}
|
|
width={100}
|
|
label={'Image Size'}
|
|
formLabelProps={{ style: { fontSize: '0.9rem' } }}
|
|
sliderThumbTooltipProps={{
|
|
label: `${galleryImageMinimumWidth}px`,
|
|
}}
|
|
/>
|
|
<IAIIconButton
|
|
size={'sm'}
|
|
aria-label={'Reset'}
|
|
tooltip={'Reset Size'}
|
|
onClick={() => dispatch(setGalleryImageMinimumWidth(64))}
|
|
icon={<BiReset />}
|
|
data-selected={shouldPinGallery}
|
|
styleClass="image-gallery-icon-btn"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<IAICheckbox
|
|
label="Maintain Aspect Ratio"
|
|
isChecked={galleryImageObjectFit === 'contain'}
|
|
onChange={() =>
|
|
dispatch(
|
|
setGalleryImageObjectFit(
|
|
galleryImageObjectFit === 'contain'
|
|
? 'cover'
|
|
: 'contain'
|
|
)
|
|
)
|
|
}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<IAICheckbox
|
|
label="Auto-Switch to New Images"
|
|
isChecked={shouldAutoSwitchToNewImages}
|
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
dispatch(setShouldAutoSwitchToNewImages(e.target.checked))
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</IAIPopover>
|
|
|
|
<IAIIconButton
|
|
size={'sm'}
|
|
aria-label={'Pin Gallery'}
|
|
tooltip={'Pin Gallery (Shift+P)'}
|
|
onClick={handleSetShouldPinGallery}
|
|
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 className="image-gallery-container" ref={galleryContainerRef}>
|
|
{images.length || areMoreImagesAvailable ? (
|
|
<>
|
|
<div
|
|
className="image-gallery"
|
|
style={{ gridTemplateColumns: galleryGridTemplateColumns }}
|
|
>
|
|
{images.map((image) => {
|
|
const { uuid } = image;
|
|
const isSelected = currentImageUuid === uuid;
|
|
return (
|
|
<HoverableImage
|
|
key={uuid}
|
|
image={image}
|
|
isSelected={isSelected}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
<Button
|
|
onClick={handleClickLoadMore}
|
|
isDisabled={!areMoreImagesAvailable}
|
|
className="image-gallery-load-more-btn"
|
|
>
|
|
{areMoreImagesAvailable ? 'Load More' : 'All Images Loaded'}
|
|
</Button>
|
|
</>
|
|
) : (
|
|
<div className="image-gallery-container-placeholder">
|
|
<MdPhotoLibrary />
|
|
<p>No Images In Gallery</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Resizable>
|
|
</div>
|
|
</CSSTransition>
|
|
);
|
|
}
|
|
|
|
// <IAIIconButton
|
|
// aria-label="Show Invocations"
|
|
// tooltip="Show Invocations"
|
|
// data-selected={currentCategory === 'result'}
|
|
// icon={<FaImage />}
|
|
// onClick={() => dispatch(setCurrentCategory('result'))}
|
|
// />
|
|
// <IAIIconButton
|
|
// aria-label="Show Uploads"
|
|
// tooltip="Show Uploads"
|
|
// data-selected={currentCategory === 'user'}
|
|
// icon={<FaUser />}
|
|
// onClick={() => dispatch(setCurrentCategory('user'))}
|
|
// />
|