mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-05 03:35:04 -05:00
Merge branch 'release-candidate-2' into release-candidate-2
- This includes #949 "Bug fixes for new Threshold and Perlin Options"
This commit is contained in:
@@ -19,6 +19,8 @@ import { MdDelete, MdFace, MdHd, MdImage, MdInfo } from 'react-icons/md';
|
||||
import InvokePopover from './InvokePopover';
|
||||
import UpscaleOptions from '../options/AdvancedOptions/Upscale/UpscaleOptions';
|
||||
import FaceRestoreOptions from '../options/AdvancedOptions/FaceRestore/FaceRestoreOptions';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
@@ -54,6 +56,8 @@ const CurrentImageButtons = ({
|
||||
}: CurrentImageButtonsProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const intermediateImage = useAppSelector(
|
||||
(state: RootState) => state.gallery.intermediateImage
|
||||
);
|
||||
@@ -71,19 +75,163 @@ const CurrentImageButtons = ({
|
||||
|
||||
const handleClickUseAsInitialImage = () =>
|
||||
dispatch(setInitialImagePath(image.url));
|
||||
useHotkeys(
|
||||
'shift+i',
|
||||
() => {
|
||||
if (image) {
|
||||
handleClickUseAsInitialImage();
|
||||
toast({
|
||||
title: 'Sent To Image To Image',
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: 'No Image Loaded',
|
||||
description: 'No image found to send to image to image module.',
|
||||
status: 'error',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
[image]
|
||||
);
|
||||
|
||||
const handleClickUseAllParameters = () =>
|
||||
dispatch(setAllParameters(image.metadata));
|
||||
useHotkeys(
|
||||
'a',
|
||||
() => {
|
||||
if (['txt2img', 'img2img'].includes(image?.metadata?.image?.type)) {
|
||||
handleClickUseAllParameters();
|
||||
toast({
|
||||
title: 'Parameters Set',
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: 'Parameters Not Set',
|
||||
description: 'No metadata found for this image.',
|
||||
status: 'error',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
[image]
|
||||
);
|
||||
|
||||
// Non-null assertion: this button is disabled if there is no seed.
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const handleClickUseSeed = () => dispatch(setSeed(image.metadata.image.seed));
|
||||
useHotkeys(
|
||||
's',
|
||||
() => {
|
||||
if (image?.metadata?.image?.seed) {
|
||||
handleClickUseSeed();
|
||||
toast({
|
||||
title: 'Seed Set',
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: 'Seed Not Set',
|
||||
description: 'Could not find seed for this image.',
|
||||
status: 'error',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
[image]
|
||||
);
|
||||
|
||||
const handleClickUpscale = () => dispatch(runESRGAN(image));
|
||||
useHotkeys(
|
||||
'u',
|
||||
() => {
|
||||
if (
|
||||
isESRGANAvailable &&
|
||||
Boolean(!intermediateImage) &&
|
||||
isConnected &&
|
||||
!isProcessing &&
|
||||
upscalingLevel
|
||||
) {
|
||||
handleClickUpscale();
|
||||
} else {
|
||||
toast({
|
||||
title: 'Upscaling Failed',
|
||||
status: 'error',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
image,
|
||||
isESRGANAvailable,
|
||||
intermediateImage,
|
||||
isConnected,
|
||||
isProcessing,
|
||||
upscalingLevel,
|
||||
]
|
||||
);
|
||||
|
||||
const handleClickFixFaces = () => dispatch(runGFPGAN(image));
|
||||
useHotkeys(
|
||||
'r',
|
||||
() => {
|
||||
if (
|
||||
isGFPGANAvailable &&
|
||||
Boolean(!intermediateImage) &&
|
||||
isConnected &&
|
||||
!isProcessing &&
|
||||
gfpganStrength
|
||||
) {
|
||||
handleClickFixFaces();
|
||||
} else {
|
||||
toast({
|
||||
title: 'Face Restoration Failed',
|
||||
status: 'error',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
image,
|
||||
isGFPGANAvailable,
|
||||
intermediateImage,
|
||||
isConnected,
|
||||
isProcessing,
|
||||
gfpganStrength,
|
||||
]
|
||||
);
|
||||
|
||||
const handleClickShowImageDetails = () =>
|
||||
setShouldShowImageDetails(!shouldShowImageDetails);
|
||||
useHotkeys(
|
||||
'i',
|
||||
() => {
|
||||
if (image) {
|
||||
handleClickShowImageDetails();
|
||||
} else {
|
||||
toast({
|
||||
title: 'Failed to load metadata',
|
||||
status: 'error',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
[image, shouldShowImageDetails]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="current-image-options">
|
||||
|
||||
@@ -67,6 +67,44 @@
|
||||
}
|
||||
}
|
||||
|
||||
.current-image-next-prev-buttons {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: calc(100% - 2rem);
|
||||
padding: 0.5rem;
|
||||
margin-left: 1rem;
|
||||
z-index: 1;
|
||||
height: calc($app-metadata-height - 1rem);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.next-prev-button-trigger-area {
|
||||
width: 7rem;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
pointer-events: auto;
|
||||
|
||||
&.prev-button-trigger-area {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&.next-button-trigger-area {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.next-prev-button {
|
||||
font-size: 5rem;
|
||||
fill: var(--text-color-secondary);
|
||||
filter: drop-shadow(0 0 1rem var(--text-color-secondary));
|
||||
opacity: 70%;
|
||||
}
|
||||
|
||||
.current-image-metadata-viewer {
|
||||
border-radius: 0.5rem;
|
||||
position: absolute;
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { Image } from '@chakra-ui/react';
|
||||
import { useAppSelector } from '../../app/store';
|
||||
import { IconButton, Image } from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../app/store';
|
||||
import { useState } from 'react';
|
||||
import ImageMetadataViewer from './ImageMetadataViewer';
|
||||
import CurrentImageButtons from './CurrentImageButtons';
|
||||
import { MdPhoto } from 'react-icons/md';
|
||||
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
|
||||
import { selectNextImage, selectPrevImage } from './gallerySlice';
|
||||
|
||||
/**
|
||||
* Displays the current image if there is one, plus associated actions.
|
||||
*/
|
||||
const CurrentImageDisplay = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const { currentImage, intermediateImage } = useAppSelector(
|
||||
(state: RootState) => state.gallery
|
||||
);
|
||||
@@ -19,6 +25,22 @@ const CurrentImageDisplay = () => {
|
||||
|
||||
const imageToDisplay = intermediateImage || currentImage;
|
||||
|
||||
const handleCurrentImagePreviewMouseOver = () => {
|
||||
setShouldShowNextPrevButtons(true);
|
||||
};
|
||||
|
||||
const handleCurrentImagePreviewMouseOut = () => {
|
||||
setShouldShowNextPrevButtons(false);
|
||||
};
|
||||
|
||||
const handleClickPrevButton = () => {
|
||||
dispatch(selectPrevImage());
|
||||
};
|
||||
|
||||
const handleClickNextButton = () => {
|
||||
dispatch(selectNextImage());
|
||||
};
|
||||
|
||||
return imageToDisplay ? (
|
||||
<div className="current-image-display">
|
||||
<div className="current-image-tools">
|
||||
@@ -40,6 +62,38 @@ const CurrentImageDisplay = () => {
|
||||
<ImageMetadataViewer image={imageToDisplay} />
|
||||
</div>
|
||||
)}
|
||||
{!shouldShowImageDetails && (
|
||||
<div className="current-image-next-prev-buttons">
|
||||
<div
|
||||
className="next-prev-button-trigger-area prev-button-trigger-area"
|
||||
onMouseOver={handleCurrentImagePreviewMouseOver}
|
||||
onMouseOut={handleCurrentImagePreviewMouseOut}
|
||||
>
|
||||
{shouldShowNextPrevButtons && (
|
||||
<IconButton
|
||||
aria-label="Previous image"
|
||||
icon={<FaAngleLeft className="next-prev-button" />}
|
||||
variant="unstyled"
|
||||
onClick={handleClickPrevButton}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="next-prev-button-trigger-area next-button-trigger-area"
|
||||
onMouseOver={handleCurrentImagePreviewMouseOver}
|
||||
onMouseOut={handleCurrentImagePreviewMouseOut}
|
||||
>
|
||||
{shouldShowNextPrevButtons && (
|
||||
<IconButton
|
||||
aria-label="Next image"
|
||||
icon={<FaAngleRight className="next-prev-button" />}
|
||||
variant="unstyled"
|
||||
onClick={handleClickNextButton}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -27,6 +27,7 @@ import { deleteImage } from '../../app/socketio/actions';
|
||||
import { RootState } from '../../app/store';
|
||||
import { setShouldConfirmOnDelete, SystemState } from '../system/systemSlice';
|
||||
import * as InvokeAI from '../../app/invokeai';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
interface DeleteImageModalProps {
|
||||
/**
|
||||
@@ -67,6 +68,14 @@ const DeleteImageModal = forwardRef(
|
||||
onClose();
|
||||
};
|
||||
|
||||
useHotkeys(
|
||||
'del',
|
||||
() => {
|
||||
shouldConfirmOnDelete ? onOpen() : handleDelete();
|
||||
},
|
||||
[image, shouldConfirmOnDelete]
|
||||
);
|
||||
|
||||
const handleChangeShouldConfirmOnDelete = (
|
||||
e: ChangeEvent<HTMLInputElement>
|
||||
) => dispatch(setShouldConfirmOnDelete(!e.target.checked));
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Button } from '@chakra-ui/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';
|
||||
|
||||
/**
|
||||
@@ -25,6 +27,22 @@ const ImageGallery = () => {
|
||||
dispatch(requestImages());
|
||||
};
|
||||
|
||||
useHotkeys(
|
||||
'left',
|
||||
() => {
|
||||
dispatch(selectPrevImage());
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'right',
|
||||
() => {
|
||||
dispatch(selectNextImage());
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="image-gallery-container">
|
||||
{images.length ? (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { clamp } from 'lodash';
|
||||
import _, { clamp } from 'lodash';
|
||||
import * as InvokeAI from '../../app/invokeai';
|
||||
|
||||
export interface GalleryState {
|
||||
@@ -85,6 +85,32 @@ export const gallerySlice = createSlice({
|
||||
clearIntermediateImage: (state) => {
|
||||
state.intermediateImage = undefined;
|
||||
},
|
||||
selectNextImage: (state) => {
|
||||
const { images, currentImage } = state;
|
||||
if (currentImage) {
|
||||
const currentImageIndex = images.findIndex(
|
||||
(i) => i.uuid === currentImage.uuid
|
||||
);
|
||||
if (_.inRange(currentImageIndex, 0, images.length)) {
|
||||
const newCurrentImage = images[currentImageIndex + 1];
|
||||
state.currentImage = newCurrentImage;
|
||||
state.currentImageUuid = newCurrentImage.uuid;
|
||||
}
|
||||
}
|
||||
},
|
||||
selectPrevImage: (state) => {
|
||||
const { images, currentImage } = state;
|
||||
if (currentImage) {
|
||||
const currentImageIndex = images.findIndex(
|
||||
(i) => i.uuid === currentImage.uuid
|
||||
);
|
||||
if (_.inRange(currentImageIndex, 1, images.length + 1)) {
|
||||
const newCurrentImage = images[currentImageIndex - 1];
|
||||
state.currentImage = newCurrentImage;
|
||||
state.currentImageUuid = newCurrentImage.uuid;
|
||||
}
|
||||
}
|
||||
},
|
||||
addGalleryImages: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
@@ -122,6 +148,8 @@ export const {
|
||||
setCurrentImage,
|
||||
addGalleryImages,
|
||||
setIntermediateImage,
|
||||
selectNextImage,
|
||||
selectPrevImage,
|
||||
} = gallerySlice.actions;
|
||||
|
||||
export default gallerySlice.reducer;
|
||||
|
||||
@@ -4,12 +4,23 @@ import { cancelProcessing } from '../../../app/socketio/actions';
|
||||
import { useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import IAIIconButton from '../../../common/components/IAIIconButton';
|
||||
import { systemSelector } from '../../../common/hooks/useCheckParameters';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
export default function CancelButton() {
|
||||
const dispatch = useAppDispatch();
|
||||
const { isProcessing, isConnected } = useAppSelector(systemSelector);
|
||||
const handleClickCancel = () => dispatch(cancelProcessing());
|
||||
|
||||
useHotkeys(
|
||||
'shift+x',
|
||||
() => {
|
||||
if (isConnected || isProcessing) {
|
||||
handleClickCancel();
|
||||
}
|
||||
},
|
||||
[isConnected, isProcessing]
|
||||
);
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
icon={<MdCancel />}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FormControl, Textarea } from '@chakra-ui/react';
|
||||
import { ChangeEvent, KeyboardEvent } from 'react';
|
||||
import { ChangeEvent, KeyboardEvent, useRef } from 'react';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import { generateImage } from '../../../app/socketio/actions';
|
||||
|
||||
@@ -9,6 +9,7 @@ import { isEqual } from 'lodash';
|
||||
import useCheckParameters, {
|
||||
systemSelector,
|
||||
} from '../../../common/hooks/useCheckParameters';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
export const optionsSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
@@ -28,6 +29,7 @@ export const optionsSelector = createSelector(
|
||||
* Prompt input text area.
|
||||
*/
|
||||
const PromptInput = () => {
|
||||
const promptRef = useRef<HTMLTextAreaElement>(null);
|
||||
const { prompt } = useAppSelector(optionsSelector);
|
||||
const { isProcessing } = useAppSelector(systemSelector);
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -37,6 +39,24 @@ const PromptInput = () => {
|
||||
dispatch(setPrompt(e.target.value));
|
||||
};
|
||||
|
||||
useHotkeys(
|
||||
'ctrl+enter',
|
||||
() => {
|
||||
if (isReady) {
|
||||
dispatch(generateImage());
|
||||
}
|
||||
},
|
||||
[isReady]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'alt+a',
|
||||
() => {
|
||||
promptRef.current?.focus();
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && e.shiftKey === false && isReady) {
|
||||
e.preventDefault();
|
||||
@@ -60,6 +80,7 @@ const PromptInput = () => {
|
||||
onKeyDown={handleKeyDown}
|
||||
resize="vertical"
|
||||
height={30}
|
||||
ref={promptRef}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
53
frontend/src/features/system/HotkeysModal/HotkeysModal.scss
Normal file
53
frontend/src/features/system/HotkeysModal/HotkeysModal.scss
Normal file
@@ -0,0 +1,53 @@
|
||||
@use '../../../styles/Mixins/' as *;
|
||||
|
||||
.hotkeys-modal {
|
||||
display: grid;
|
||||
padding: 1rem;
|
||||
background-color: var(--settings-modal-bg) !important;
|
||||
row-gap: 1rem;
|
||||
font-family: Inter;
|
||||
|
||||
h1 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.hotkeys-modal-items {
|
||||
display: grid;
|
||||
row-gap: 0.5rem;
|
||||
max-height: 32rem;
|
||||
overflow-y: scroll;
|
||||
@include HideScrollbar;
|
||||
}
|
||||
|
||||
.hotkey-modal-item {
|
||||
display: grid;
|
||||
grid-template-columns: auto max-content;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--background-color);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.3rem;
|
||||
|
||||
.hotkey-info {
|
||||
display: grid;
|
||||
|
||||
.hotkey-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hotkey-description {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.hotkey-key {
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
border: 2px solid var(--settings-modal-bg);
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 0.3rem;
|
||||
}
|
||||
}
|
||||
98
frontend/src/features/system/HotkeysModal/HotkeysModal.tsx
Normal file
98
frontend/src/features/system/HotkeysModal/HotkeysModal.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import {
|
||||
Modal,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalOverlay,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import React, { cloneElement, ReactElement } from 'react';
|
||||
import HotkeysModalItem from './HotkeysModalItem';
|
||||
|
||||
type HotkeysModalProps = {
|
||||
/* The button to open the Settings Modal */
|
||||
children: ReactElement;
|
||||
};
|
||||
|
||||
export default function HotkeysModal({ children }: HotkeysModalProps) {
|
||||
const {
|
||||
isOpen: isHotkeyModalOpen,
|
||||
onOpen: onHotkeysModalOpen,
|
||||
onClose: onHotkeysModalClose,
|
||||
} = useDisclosure();
|
||||
|
||||
const hotkeys = [
|
||||
{ title: 'Invoke', desc: 'Generate an image', hotkey: 'Ctrl+Enter' },
|
||||
{ title: 'Cancel', desc: 'Cancel image generation', hotkey: 'Shift+X' },
|
||||
{
|
||||
title: 'Set Seed',
|
||||
desc: 'Use the seed of the current image',
|
||||
hotkey: 'S',
|
||||
},
|
||||
{
|
||||
title: 'Set Parameters',
|
||||
desc: 'Use all parameters of the current image',
|
||||
hotkey: 'A',
|
||||
},
|
||||
{ title: 'Restore Faces', desc: 'Restore the current image', hotkey: 'R' },
|
||||
{ title: 'Upscale', desc: 'Upscale the current image', hotkey: 'U' },
|
||||
{
|
||||
title: 'Show Info',
|
||||
desc: 'Show metadata info of the current image',
|
||||
hotkey: 'I',
|
||||
},
|
||||
{
|
||||
title: 'Send To Image To Image',
|
||||
desc: 'Send the current image to Image to Image module',
|
||||
hotkey: 'Shift+I',
|
||||
},
|
||||
{ title: 'Delete Image', desc: 'Delete the current image', hotkey: 'Del' },
|
||||
{
|
||||
title: 'Focus Prompt',
|
||||
desc: 'Focus the prompt input area',
|
||||
hotkey: 'Alt+A',
|
||||
},
|
||||
{
|
||||
title: 'Previous Image',
|
||||
desc: 'Display the previous image in the gallery',
|
||||
hotkey: 'Arrow left',
|
||||
},
|
||||
{
|
||||
title: 'Next Image',
|
||||
desc: 'Display the next image in the gallery',
|
||||
hotkey: 'Arrow right',
|
||||
},
|
||||
];
|
||||
|
||||
const renderHotkeyModalItems = () => {
|
||||
const hotkeyModalItemsToRender: ReactElement[] = [];
|
||||
|
||||
hotkeys.forEach((hotkey, i) => {
|
||||
hotkeyModalItemsToRender.push(
|
||||
<HotkeysModalItem
|
||||
key={i}
|
||||
title={hotkey.title}
|
||||
description={hotkey.desc}
|
||||
hotkey={hotkey.hotkey}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return hotkeyModalItemsToRender;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{cloneElement(children, {
|
||||
onClick: onHotkeysModalOpen,
|
||||
})}
|
||||
<Modal isOpen={isHotkeyModalOpen} onClose={onHotkeysModalClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent className="hotkeys-modal">
|
||||
<ModalCloseButton />
|
||||
<h1>Keyboard Shorcuts</h1>
|
||||
<div className="hotkeys-modal-items">{renderHotkeyModalItems()}</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
|
||||
interface HotkeysModalProps {
|
||||
hotkey: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export default function HotkeysModalItem(props: HotkeysModalProps) {
|
||||
const { title, hotkey, description } = props;
|
||||
return (
|
||||
<div className="hotkey-modal-item">
|
||||
<div className="hotkey-info">
|
||||
<p className="hotkey-title">{title}</p>
|
||||
{description && <p className="hotkey-description">{description}</p>}
|
||||
</div>
|
||||
<div className="hotkey-key">{hotkey}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
.settings-modal {
|
||||
background-color: var(--settings-modal-bg) !important;
|
||||
font-family: Inter;
|
||||
|
||||
.settings-modal-content {
|
||||
display: grid;
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
.site-header-right-side {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, max-content);
|
||||
grid-template-columns: repeat(6, max-content);
|
||||
align-items: center;
|
||||
column-gap: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { IconButton, Link, useColorMode } from '@chakra-ui/react';
|
||||
|
||||
import { FaSun, FaMoon, FaGithub } from 'react-icons/fa';
|
||||
import { MdHelp, MdSettings } from 'react-icons/md';
|
||||
import { MdHelp, MdKeyboard, MdSettings } from 'react-icons/md';
|
||||
|
||||
import InvokeAILogo from '../../assets/images/logo.png';
|
||||
import HotkeysModal from './HotkeysModal/HotkeysModal';
|
||||
|
||||
import SettingsModal from './SettingsModal/SettingsModal';
|
||||
import StatusIndicator from './StatusIndicator';
|
||||
|
||||
@@ -40,6 +42,16 @@ const SiteHeader = () => {
|
||||
/>
|
||||
</SettingsModal>
|
||||
|
||||
<HotkeysModal>
|
||||
<IconButton
|
||||
aria-label="Hotkeys"
|
||||
variant="link"
|
||||
fontSize={24}
|
||||
size={'sm'}
|
||||
icon={<MdKeyboard />}
|
||||
/>
|
||||
</HotkeysModal>
|
||||
|
||||
<IconButton
|
||||
aria-label="Link to Github Issues"
|
||||
variant="link"
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
@use '../features/system/SiteHeader.scss';
|
||||
@use '../features/system/StatusIndicator.scss';
|
||||
@use '../features/system/SettingsModal/SettingsModal.scss';
|
||||
@use '../features/system/HotkeysModal/HotkeysModal.scss';
|
||||
@use '../features/system/Console.scss';
|
||||
|
||||
// options
|
||||
|
||||
Reference in New Issue
Block a user