Add Image Gallery Drawer

This commit is contained in:
blessedcoolant 2022-10-08 13:15:30 +13:00
parent 3b0c4b74b6
commit 2567f5faa5
19 changed files with 406 additions and 109 deletions

View File

@ -15,3 +15,7 @@
width: $app-width;
height: $app-height;
}
.app-console {
z-index: 9999;
}

View File

@ -26,7 +26,9 @@ const App = () => {
<SiteHeader />
<InvokeTabs />
</div>
<Console />
<div className="app-console">
<Console />
</div>
</div>
) : (
<Loading />

View File

@ -11,21 +11,6 @@
border-radius: 0.5rem;
}
.current-image-display-placeholder {
background-color: var(--background-color-secondary);
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
svg {
width: 10rem;
height: 10rem;
color: var(--svg-color);
}
}
.current-image-tools {
width: 100%;
height: 100%;
@ -106,3 +91,20 @@
filter: drop-shadow(0 0 1rem var(--text-color-secondary));
opacity: 70%;
}
.current-image-display-placeholder {
background-color: var(--background-color-secondary);
display: grid;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
border-radius: 0.5rem;
svg {
width: 10rem;
height: 10rem;
color: var(--svg-color);
}
}

View File

@ -1,43 +1,67 @@
@use '../../styles/Mixins/' as *;
.image-gallery-area {
.image-gallery-popup-btn {
@include Button(
$btn-width: 3rem,
$btn-height: 3rem,
$icon-size: 22px,
$btn-color: var(--btn-grey),
$btn-color-hover: var(--btn-grey-hover)
);
}
}
.image-gallery-popup {
background-color: var(--tab-color);
position: fixed !important;
top: 0;
right: 0;
padding: 1rem;
animation: slideOut 0.3s ease-out;
display: grid;
grid-auto-rows: max-content;
row-gap: 1rem;
border-left-width: 0.2rem;
border-color: var(--gallery-resizeable-color);
}
.image-gallery-header {
display: grid;
grid-template-columns: auto max-content;
align-items: center;
h1 {
font-weight: bold;
}
}
.image-gallery-close-btn {
background-color: var(--btn-load-more) !important;
&:hover {
background-color: var(--btn-load-more-hover) !important;
}
}
.image-gallery-container {
display: grid;
row-gap: 1rem;
grid-auto-rows: max-content;
min-width: 16rem;
}
.image-gallery-container-placeholder {
display: grid;
background-color: var(--background-color-secondary);
border-radius: 0.5rem;
place-items: center;
padding: 2rem 0;
p {
color: var(--subtext-color-bright);
}
svg {
width: 5rem;
height: 5rem;
color: var(--svg-color);
}
gap: 1rem;
max-height: $app-gallery-popover-height;
overflow-y: scroll;
@include HideScrollbar;
}
.image-gallery {
display: grid;
grid-template-columns: repeat(2, max-content);
grid-template-columns: repeat(auto-fill, minmax(120px, auto));
gap: 0.6rem;
justify-items: center;
max-height: $app-gallery-height;
overflow-y: scroll;
@include HideScrollbar;
}
.image-gallery-load-more-btn {
background-color: var(--btn-load-more) !important;
font-size: 0.85rem !important;
font-family: Inter;
&:disabled {
&:hover {
@ -49,3 +73,22 @@
background-color: var(--btn-load-more-hover) !important;
}
}
.image-gallery-container-placeholder {
display: grid;
background-color: var(--background-color-secondary);
border-radius: 0.5rem;
place-items: center;
padding: 2rem 0;
p {
color: var(--subtext-color-bright);
font-family: Inter;
}
svg {
width: 5rem;
height: 5rem;
color: var(--svg-color);
}
}

View File

@ -1,32 +1,47 @@
import { Button } from '@chakra-ui/react';
import { Button, IconButton } from '@chakra-ui/button';
import { Resizable } from 're-resizable';
import React from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { MdPhotoLibrary } from 'react-icons/md';
import { MdClear, MdPhotoLibrary } from 'react-icons/md';
import { requestImages } from '../../app/socketio/actions';
import { RootState, useAppDispatch } from '../../app/store';
import { useAppSelector } from '../../app/store';
import { selectNextImage, selectPrevImage } from './gallerySlice';
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
import {
selectNextImage,
selectPrevImage,
setShouldShowGallery,
} from './gallerySlice';
import HoverableImage from './HoverableImage';
/**
* Simple image gallery.
*/
const ImageGallery = () => {
const { images, currentImageUuid, areMoreImagesAvailable } = useAppSelector(
(state: RootState) => state.gallery
);
export default function ImageGallery() {
const {
images,
currentImageUuid,
areMoreImagesAvailable,
shouldShowGallery,
} = useAppSelector((state: RootState) => state.gallery);
const dispatch = useAppDispatch();
/**
* I don't like that this needs to rerender whenever the current image is changed.
* What if we have a large number of images? I suppose pagination (planned) will
* mitigate this issue.
*
* TODO: Refactor if performance complaints, or after migrating to new API which supports pagination.
*/
const handleShowGalleryToggle = () => {
dispatch(setShouldShowGallery(!shouldShowGallery));
};
const handleGalleryClose = () => {
dispatch(setShouldShowGallery(false));
};
const handleClickLoadMore = () => {
dispatch(requestImages());
};
useHotkeys(
'g',
() => {
handleShowGalleryToggle();
},
[shouldShowGallery]
);
useHotkeys(
'left',
() => {
@ -44,41 +59,64 @@ const ImageGallery = () => {
);
return (
<div className="image-gallery-container">
{images.length ? (
<>
<p>
<strong>Your Invocations</strong>
</p>
<div className="image-gallery">
{images.map((image) => {
const { uuid } = image;
const isSelected = currentImageUuid === uuid;
return (
<HoverableImage
key={uuid}
image={image}
isSelected={isSelected}
/>
);
})}
</div>
</>
) : (
<div className="image-gallery-container-placeholder">
<div className="image-gallery-area">
{!shouldShowGallery && (
<Button
colorScheme="teal"
onClick={handleShowGalleryToggle}
className="image-gallery-popup-btn"
>
<MdPhotoLibrary />
<p>No Images In Gallery</p>
</div>
</Button>
)}
{shouldShowGallery && (
<Resizable
defaultSize={{ width: 'auto', height: '100%' }}
minWidth={'18%'}
className="image-gallery-popup"
>
<div className="image-gallery-header">
<h1>Your Invocations</h1>
<IconButton
size={'sm'}
aria-label={'Close Gallery'}
onClick={handleGalleryClose}
className="image-gallery-close-btn"
icon={<MdClear />}
/>
</div>
<div className="image-gallery-container">
{images.length ? (
<div className="image-gallery">
{images.map((image) => {
const { uuid } = image;
const isSelected = currentImageUuid === uuid;
return (
<HoverableImage
key={uuid}
image={image}
isSelected={isSelected}
/>
);
})}
</div>
) : (
<div className="image-gallery-container-placeholder">
<MdPhotoLibrary />
<p>No Images In Gallery</p>
</div>
)}
<Button
onClick={handleClickLoadMore}
isDisabled={!areMoreImagesAvailable}
className="image-gallery-load-more-btn"
>
{areMoreImagesAvailable ? 'Load More' : 'All Images Loaded'}
</Button>
</div>
</Resizable>
)}
<Button
onClick={handleClickLoadMore}
isDisabled={!areMoreImagesAvailable}
className="image-gallery-load-more-btn"
>
{areMoreImagesAvailable ? 'Load More' : 'All Images Loaded'}
</Button>
</div>
);
};
export default ImageGallery;
}

View File

@ -0,0 +1,129 @@
import {
Button,
Drawer,
DrawerBody,
DrawerCloseButton,
DrawerContent,
DrawerHeader,
useDisclosure,
} from '@chakra-ui/react';
import React from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { MdPhotoLibrary } from 'react-icons/md';
import { requestImages } from '../../app/socketio/actions';
import { RootState, useAppDispatch } from '../../app/store';
import { useAppSelector } from '../../app/store';
import { selectNextImage, selectPrevImage } from './gallerySlice';
import HoverableImage from './HoverableImage';
/**
* Simple image gallery.
*/
const ImageGalleryOld = () => {
const { images, currentImageUuid, areMoreImagesAvailable } = useAppSelector(
(state: RootState) => state.gallery
);
const dispatch = useAppDispatch();
const { isOpen, onOpen, onClose } = useDisclosure();
/**
* I don't like that this needs to rerender whenever the current image is changed.
* What if we have a large number of images? I suppose pagination (planned) will
* mitigate this issue.
*
* TODO: Refactor if performance complaints, or after migrating to new API which supports pagination.
*/
const handleClickLoadMore = () => {
dispatch(requestImages());
};
useHotkeys(
'g',
() => {
if (isOpen) {
onClose();
} else {
onOpen();
}
},
[isOpen]
);
useHotkeys(
'left',
() => {
dispatch(selectPrevImage());
},
[]
);
useHotkeys(
'right',
() => {
dispatch(selectNextImage());
},
[]
);
return (
<div className="image-gallery-area">
<Button
colorScheme="teal"
onClick={onOpen}
className="image-gallery-popup-btn"
>
<MdPhotoLibrary />
</Button>
<Drawer
isOpen={isOpen}
placement="right"
onClose={onClose}
autoFocus={false}
trapFocus={false}
closeOnOverlayClick={false}
>
<DrawerContent className="image-gallery-popup">
<div className="image-gallery-header">
<DrawerHeader>Your Invocations</DrawerHeader>
<DrawerCloseButton />
</div>
<DrawerBody className="image-gallery-body">
<div className="image-gallery-container">
{images.length ? (
<div className="image-gallery">
{images.map((image) => {
const { uuid } = image;
const isSelected = currentImageUuid === uuid;
return (
<HoverableImage
key={uuid}
image={image}
isSelected={isSelected}
/>
);
})}
</div>
) : (
<div className="image-gallery-container-placeholder">
<MdPhotoLibrary />
<p>No Images In Gallery</p>
</div>
)}
<Button
onClick={handleClickLoadMore}
isDisabled={!areMoreImagesAvailable}
className="image-gallery-load-more-btn"
>
{areMoreImagesAvailable ? 'Load More' : 'All Images Loaded'}
</Button>
</div>
</DrawerBody>
</DrawerContent>
</Drawer>
</div>
);
};
export default ImageGallery;

View File

@ -6,8 +6,8 @@
padding: 1rem;
background-color: var(--metadata-bg-color);
overflow: scroll;
max-height: calc($app-content-height - 4rem);
z-index: 1;
max-height: $app-metadata-height;
z-index: 10;
}
.image-json-viewer {

View File

@ -11,12 +11,14 @@ export interface GalleryState {
areMoreImagesAvailable: boolean;
latest_mtime?: number;
earliest_mtime?: number;
shouldShowGallery: boolean;
}
const initialState: GalleryState = {
currentImageUuid: '',
images: [],
areMoreImagesAvailable: true,
shouldShowGallery: true,
};
export const gallerySlice = createSlice({
@ -138,6 +140,9 @@ export const gallerySlice = createSlice({
state.areMoreImagesAvailable = areMoreImagesAvailable;
}
},
setShouldShowGallery: (state, action: PayloadAction<boolean>) => {
state.shouldShowGallery = action.payload;
},
},
});
@ -150,6 +155,7 @@ export const {
setIntermediateImage,
selectNextImage,
selectPrevImage,
setShouldShowGallery,
} = gallerySlice.actions;
export default gallerySlice.reducer;

View File

@ -7,6 +7,7 @@ import { FaAngleDoubleDown, FaCode, FaMinus } from 'react-icons/fa';
import { createSelector } from '@reduxjs/toolkit';
import { isEqual } from 'lodash';
import { Resizable } from 're-resizable';
import { useHotkeys } from 'react-hotkeys-hook';
const logSelector = createSelector(
(state: RootState) => state.system,
@ -66,6 +67,14 @@ const Console = () => {
dispatch(setShouldShowLogViewer(!shouldShowLogViewer));
};
useHotkeys(
'`',
() => {
dispatch(setShouldShowLogViewer(!shouldShowLogViewer));
},
[shouldShowLogViewer]
);
return (
<>
{shouldShowLogViewer && (

View File

@ -23,6 +23,11 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
const hotkeys = [
{ title: 'Invoke', desc: 'Generate an image', hotkey: 'Ctrl+Enter' },
{ title: 'Cancel', desc: 'Cancel image generation', hotkey: 'Shift+X' },
{
title: 'Toggle Gallery',
desc: 'Open and close the gallery drawer',
hotkey: 'G',
},
{
title: 'Set Seed',
desc: 'Use the seed of the current image',
@ -71,6 +76,11 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
desc: 'Switch between dark and light modes',
hotkey: 'Shift+D',
},
{
title: 'Console Toggle',
desc: 'Open and close console',
hotkey: '`',
},
];
const renderHotkeyModalItems = () => {

View File

@ -2,7 +2,7 @@
.image-to-image-workarea {
display: grid;
grid-template-columns: max-content auto max-content;
grid-template-columns: max-content auto;
column-gap: 1rem;
}
@ -16,6 +16,22 @@
@include HideScrollbar;
}
.image-to-image-display-area {
display: grid;
grid-template-areas: 'image-to-image-display-area';
.image-to-image-display {
grid-area: image-to-image-display-area;
}
.image-gallery-area {
grid-area: image-to-image-display-area;
z-index: 2;
place-self: end;
margin: 1rem;
}
}
.image-to-image-strength-main-option {
display: grid;
grid-template-columns: none !important;
@ -52,7 +68,7 @@
.image-to-image-dual-preview {
grid-area: img2img-preview;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-columns: max-content max-content;
column-gap: 0.5rem;
padding: 0 1rem;
place-content: center;

View File

@ -1,15 +1,16 @@
import React from 'react';
import ImageGallery from '../../gallery/ImageGallery';
import ImageToImageDisplay from './ImageToImageDisplay';
import ImageToImagePanel from './ImageToImagePanel';
import ImageToImageDisplay from './ImageToImageDisplay';
import ImageGallery from '../../gallery/ImageGallery';
export default function ImageToImage() {
return (
<div className="image-to-image-workarea">
<ImageToImagePanel />
<ImageToImageDisplay />
<ImageGallery />
<div className="image-to-image-display-area">
<ImageToImageDisplay />
<ImageGallery />
</div>
</div>
);
}

View File

@ -2,7 +2,7 @@
.text-to-image-workarea {
display: grid;
grid-template-columns: max-content auto max-content;
grid-template-columns: max-content auto;
column-gap: 1rem;
}
@ -14,3 +14,20 @@
overflow-y: scroll;
@include HideScrollbar;
}
.text-to-image-display {
display: grid;
grid-template-areas: 'text-to-image-display';
.current-image-display,
.current-image-display-placeholder {
grid-area: text-to-image-display;
}
.image-gallery-area {
grid-area: text-to-image-display;
z-index: 2;
place-self: end;
margin: 1rem;
}
}

View File

@ -1,14 +1,16 @@
import React from 'react';
import TextToImagePanel from './TextToImagePanel';
import CurrentImageDisplay from '../../gallery/CurrentImageDisplay';
import ImageGallery from '../../gallery/ImageGallery';
import TextToImagePanel from './TextToImagePanel';
export default function TextToImage() {
return (
<div className="text-to-image-workarea">
<TextToImagePanel />
<CurrentImageDisplay />
<ImageGallery />
<div className="text-to-image-display">
<CurrentImageDisplay />
<ImageGallery />
</div>
</div>
);
}

View File

@ -8,6 +8,9 @@ $app-width: calc(100vw - $app-cutoff);
$app-height: calc(100vh - $app-cutoff);
$app-content-height: calc(100vh - $app-content-height-cutoff);
$app-gallery-height: calc(100vh - ($app-content-height-cutoff + 6rem));
$app-gallery-popover-height: calc(
100vh - ($app-content-height-cutoff - 2.5rem)
);
$app-metadata-height: calc(100vh - ($app-content-height-cutoff + 4.4rem));
// option bar

View File

@ -0,0 +1,8 @@
@keyframes slideOut {
from {
transform: translateX(10rem);
}
to {
transform: translateX(0);
}
}

View File

@ -46,7 +46,7 @@
--btn-red-hover: rgb(255, 75, 75);
--btn-load-more: rgb(30, 32, 42);
--btn-load-more-hover: rgb(36, 38, 48);
--btn-load-more-hover: rgb(54, 56, 66);
// Switch
--switch-bg-color: rgb(100, 102, 110);
@ -92,4 +92,7 @@
// Img2Img
--img2img-img-bg-color: rgb(30, 32, 42);
// Gallery
--gallery-resizeable-color: rgb(36, 38, 48);
}

View File

@ -46,7 +46,7 @@
--btn-red-hover: rgb(255, 55, 55);
--btn-load-more: rgb(202, 204, 206);
--btn-load-more-hover: rgb(206, 208, 210);
--btn-load-more-hover: rgb(178, 180, 182);
// Switch
--switch-bg-color: rgb(178, 180, 182);
@ -91,4 +91,7 @@
// Img2Img
--img2img-img-bg-color: rgb(180, 182, 184);
// Gallery
--gallery-resizeable-color: rgb(192, 194, 196);
}

View File

@ -2,6 +2,7 @@
@use 'Colors_Dark';
@use 'Colors_Light';
@use 'Fonts';
@use 'Animations';
// Component Styles
//app