diff --git a/frontend/src/app/App.scss b/frontend/src/app/App.scss index f9b1c9f54d..67aee81cdb 100644 --- a/frontend/src/app/App.scss +++ b/frontend/src/app/App.scss @@ -15,3 +15,7 @@ width: $app-width; height: $app-height; } + +.app-console { + z-index: 9999; +} diff --git a/frontend/src/app/App.tsx b/frontend/src/app/App.tsx index 18975e2ca9..f91bc24c51 100644 --- a/frontend/src/app/App.tsx +++ b/frontend/src/app/App.tsx @@ -26,7 +26,9 @@ const App = () => { - +
+ +
) : ( diff --git a/frontend/src/features/gallery/CurrentImageDisplay.scss b/frontend/src/features/gallery/CurrentImageDisplay.scss index 35739fa9e0..5f1e8c9df5 100644 --- a/frontend/src/features/gallery/CurrentImageDisplay.scss +++ b/frontend/src/features/gallery/CurrentImageDisplay.scss @@ -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); + } +} diff --git a/frontend/src/features/gallery/ImageGallery.scss b/frontend/src/features/gallery/ImageGallery.scss index 135caf3e31..984b2a6e4c 100644 --- a/frontend/src/features/gallery/ImageGallery.scss +++ b/frontend/src/features/gallery/ImageGallery.scss @@ -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); + } +} diff --git a/frontend/src/features/gallery/ImageGallery.tsx b/frontend/src/features/gallery/ImageGallery.tsx index 70ccdf54a9..2bb13318b3 100644 --- a/frontend/src/features/gallery/ImageGallery.tsx +++ b/frontend/src/features/gallery/ImageGallery.tsx @@ -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 ( -
- {images.length ? ( - <> -

- Your Invocations -

-
- {images.map((image) => { - const { uuid } = image; - const isSelected = currentImageUuid === uuid; - return ( - - ); - })} -
- - ) : ( -
+
+ {!shouldShowGallery && ( +
+ + )} + + {shouldShowGallery && ( + +
+

Your Invocations

+ } + /> +
+
+ {images.length ? ( +
+ {images.map((image) => { + const { uuid } = image; + const isSelected = currentImageUuid === uuid; + return ( + + ); + })} +
+ ) : ( +
+ +

No Images In Gallery

+
+ )} + +
+
)} -
); -}; - -export default ImageGallery; +} diff --git a/frontend/src/features/gallery/ImageGalleryOld.tsx b/frontend/src/features/gallery/ImageGalleryOld.tsx new file mode 100644 index 0000000000..e1ff8e03bb --- /dev/null +++ b/frontend/src/features/gallery/ImageGalleryOld.tsx @@ -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 ( +
+ + + +
+ Your Invocations + +
+ +
+ {images.length ? ( +
+ {images.map((image) => { + const { uuid } = image; + const isSelected = currentImageUuid === uuid; + return ( + + ); + })} +
+ ) : ( +
+ +

No Images In Gallery

+
+ )} + +
+
+
+
+
+ ); +}; + +export default ImageGallery; diff --git a/frontend/src/features/gallery/ImageMetaDataViewer/ImageMetadataViewer.scss b/frontend/src/features/gallery/ImageMetaDataViewer/ImageMetadataViewer.scss index 4eb3dc5fce..e5c33672ae 100644 --- a/frontend/src/features/gallery/ImageMetaDataViewer/ImageMetadataViewer.scss +++ b/frontend/src/features/gallery/ImageMetaDataViewer/ImageMetadataViewer.scss @@ -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 { diff --git a/frontend/src/features/gallery/gallerySlice.ts b/frontend/src/features/gallery/gallerySlice.ts index 415b80d326..02ec67bb20 100644 --- a/frontend/src/features/gallery/gallerySlice.ts +++ b/frontend/src/features/gallery/gallerySlice.ts @@ -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) => { + state.shouldShowGallery = action.payload; + }, }, }); @@ -150,6 +155,7 @@ export const { setIntermediateImage, selectNextImage, selectPrevImage, + setShouldShowGallery, } = gallerySlice.actions; export default gallerySlice.reducer; diff --git a/frontend/src/features/system/Console.tsx b/frontend/src/features/system/Console.tsx index 799488f486..22a6632936 100644 --- a/frontend/src/features/system/Console.tsx +++ b/frontend/src/features/system/Console.tsx @@ -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 && ( diff --git a/frontend/src/features/system/HotkeysModal/HotkeysModal.tsx b/frontend/src/features/system/HotkeysModal/HotkeysModal.tsx index 16fb7d6bf6..12004640ef 100644 --- a/frontend/src/features/system/HotkeysModal/HotkeysModal.tsx +++ b/frontend/src/features/system/HotkeysModal/HotkeysModal.tsx @@ -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 = () => { diff --git a/frontend/src/features/tabs/ImageToImage/ImageToImage.scss b/frontend/src/features/tabs/ImageToImage/ImageToImage.scss index d1c444d748..18fd97aeb2 100644 --- a/frontend/src/features/tabs/ImageToImage/ImageToImage.scss +++ b/frontend/src/features/tabs/ImageToImage/ImageToImage.scss @@ -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; diff --git a/frontend/src/features/tabs/ImageToImage/ImageToImage.tsx b/frontend/src/features/tabs/ImageToImage/ImageToImage.tsx index 03a4d35d9a..585dc8f323 100644 --- a/frontend/src/features/tabs/ImageToImage/ImageToImage.tsx +++ b/frontend/src/features/tabs/ImageToImage/ImageToImage.tsx @@ -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 (
- - +
+ + +
); } diff --git a/frontend/src/features/tabs/TextToImage/TextToImage.scss b/frontend/src/features/tabs/TextToImage/TextToImage.scss index af67928b5b..843209d71b 100644 --- a/frontend/src/features/tabs/TextToImage/TextToImage.scss +++ b/frontend/src/features/tabs/TextToImage/TextToImage.scss @@ -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; + } +} diff --git a/frontend/src/features/tabs/TextToImage/TextToImage.tsx b/frontend/src/features/tabs/TextToImage/TextToImage.tsx index 476bed6e63..9b2b8f1988 100644 --- a/frontend/src/features/tabs/TextToImage/TextToImage.tsx +++ b/frontend/src/features/tabs/TextToImage/TextToImage.tsx @@ -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 (
- - +
+ + +
); } diff --git a/frontend/src/styles/Mixins/_Variables.scss b/frontend/src/styles/Mixins/_Variables.scss index cf7691669a..1f8b013e27 100644 --- a/frontend/src/styles/Mixins/_Variables.scss +++ b/frontend/src/styles/Mixins/_Variables.scss @@ -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 diff --git a/frontend/src/styles/_Animations.scss b/frontend/src/styles/_Animations.scss new file mode 100644 index 0000000000..0794183a05 --- /dev/null +++ b/frontend/src/styles/_Animations.scss @@ -0,0 +1,8 @@ +@keyframes slideOut { + from { + transform: translateX(10rem); + } + to { + transform: translateX(0); + } +} diff --git a/frontend/src/styles/_Colors_Dark.scss b/frontend/src/styles/_Colors_Dark.scss index 83e5f41678..b87fbb3fdf 100644 --- a/frontend/src/styles/_Colors_Dark.scss +++ b/frontend/src/styles/_Colors_Dark.scss @@ -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); } diff --git a/frontend/src/styles/_Colors_Light.scss b/frontend/src/styles/_Colors_Light.scss index 4d394cf601..370fabaa93 100644 --- a/frontend/src/styles/_Colors_Light.scss +++ b/frontend/src/styles/_Colors_Light.scss @@ -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); } diff --git a/frontend/src/styles/index.scss b/frontend/src/styles/index.scss index b18cc3101a..b16b8d60f4 100644 --- a/frontend/src/styles/index.scss +++ b/frontend/src/styles/index.scss @@ -2,6 +2,7 @@ @use 'Colors_Dark'; @use 'Colors_Light'; @use 'Fonts'; +@use 'Animations'; // Component Styles //app