mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Final WebUI build for Release 2.1
- squashed commit of 52 commits from PR #1327 don't log base64 progress images Fresh Build For WebUI [WebUI] Loopback Default False Fixes bugs/styling - Fixes missing web app state on new version: Adds stateReconciler to redux-persist. When we add more values to the state and then release the update app, they will be automatically merged in. Reseting web UI will be needed far less. 7159ec - Fixes console z-index - Moves reset web UI button to visible area Decreases gallery width on inpainting Increases workarea split padding to 1rem Adds missing tooltips to site header Changes inpainting controls settings to hover Fixes hotkeys and settings buttons not working Improves bounding box interactions - Bounding box can now be moved by dragging any of its edges - Bounding box does not affect drawing if already drawing a stroke - Can lock bounding box to draw directly on the bounding box edges - Removes spacebar-hold behaviour due to technical issues Fixes silent crash when init image too large To send the mask to the server, the UI rendered the mask onto the init image and sent the whole image. The mask was then cropped by the server. If the image was too large, the app silently failed. Maybe it exceeds the websocket size limit. Fixed by cropping the mask in the UI layer, sending only bounding-box-sized mask image data. Disabled bounding box settings when locked Styles image uploader Builds fresh bundle Improves bounding box interaction Added spacebar-hold-to-transform back. Address bounding box feedback - Adds back toggle to hide bounding box - Box quick toggle = q, normal toggle = shift + q - Styles canvas alert icons Adds hints when unable to invoke - Popover on Invoke button indicates why exactly it is disabled, e.g. prompt is empty, something else is processing, etc. - There may be more than one reason; all are displayed. Fix Inpainting Alerts Styling Preventing unnecessary re-renders across the app Code Split Inpaint Options Isolate features to their own components so they dont re-render the other stuff each time. [TESTING] Remove global isReady checking I dont believe this is need at all because the isready state is constantly updated when needed and tracked real time in the Redux store. This causes massive re-renders. @psychedelicious If this is absolutely essential for a reason that I do not see, please hit me up on Discord. Fresh Bundle Fix Bounding Box Settings re-rendering on brush stroke [Code Splitting] Bounding Box Options Isolated all bounding box components to trigger unnecessary re-renders. Still need to fix bounding box triggering re-renders on the control panel inside the canvas itself. But the options panel should be a good to go with this change. Inpainting Controls Code Spitting and Performance Codesplit the entirety of the inpainting controls. Created new selectors for each and every component to ensure there are no unnecessary re-renders. App feels a lot smoother. Fixes rerenders on ClearBrushHistory Fixes crash when requesting post-generation upscale/face restoration - Moves the inpainting paste to before the postprocessing. Removes unused isReady state Changes Report Bug icon to a bug Restores shift+q bounding box shortcut Adds alert for bounding box size to status icons Adds asCheckbox to IAIIconButton Rough draft of this. Not happy with the styling but it's clearer than having them look just like buttons. Fixes crash related to old value of progress_latents in state Styling changes and settings modal minor refactor Fixes: uploaded JPG images not loading Reworks CurrentImageButtons.tsx - Change all icons to FA iconset for consistency - Refactors IAIIconButton, IAIButton, IAIPopover to handle ref forwarding - Redesigns buttons into group Only generate 1 iteration when seed fixed & variations disabled Fixes progress images select Fixes edge case: upload over gets stuck while alt tabbing - Press esc to close it now Fixes display progress images select typing Fixes current image button rerenders Adds min width to ImageUploader Makes fast-latents in progress default Update Icon Button Checkbox Style Styling Fixes next/prev image buttons Refactor canvas buttons + more Add Save Intermediates Step Count For accurate mode only. Co-Authored-By: Richard Macarthy <richardmacarthy@protonmail.com> Restores "initial image" text Address feedback - moves mask clear button - fixes intermediates - shrinks inpainting icons by 10% Fix Loopback Styling Adds escape hotkey to close floating panels Readd Hotkey for Dual Display Updated Current Image Button Styling
This commit is contained in:
parent
5e87062cf8
commit
96b34c0f85
@ -187,7 +187,10 @@ class InvokeAIWebServer:
|
||||
base_path = (
|
||||
self.result_path if category == "result" else self.init_image_path
|
||||
)
|
||||
paths = glob.glob(os.path.join(base_path, "*.png"))
|
||||
|
||||
paths = []
|
||||
for ext in ("*.png", "*.jpg", "*.jpeg"):
|
||||
paths.extend(glob.glob(os.path.join(base_path, ext)))
|
||||
|
||||
image_paths = sorted(
|
||||
paths, key=lambda x: os.path.getmtime(x), reverse=True
|
||||
@ -203,13 +206,19 @@ class InvokeAIWebServer:
|
||||
image_array = []
|
||||
|
||||
for path in image_paths:
|
||||
metadata = retrieve_metadata(path)
|
||||
if os.path.splitext(path)[1] == ".png":
|
||||
metadata = retrieve_metadata(path)
|
||||
sd_metadata = metadata["sd-metadata"]
|
||||
else:
|
||||
sd_metadata = {}
|
||||
|
||||
(width, height) = Image.open(path).size
|
||||
|
||||
image_array.append(
|
||||
{
|
||||
"url": self.get_url_from_image_path(path),
|
||||
"mtime": os.path.getmtime(path),
|
||||
"metadata": metadata["sd-metadata"],
|
||||
"metadata": sd_metadata,
|
||||
"width": width,
|
||||
"height": height,
|
||||
"category": category,
|
||||
@ -236,7 +245,9 @@ class InvokeAIWebServer:
|
||||
self.result_path if category == "result" else self.init_image_path
|
||||
)
|
||||
|
||||
paths = glob.glob(os.path.join(base_path, "*.png"))
|
||||
paths = []
|
||||
for ext in ("*.png", "*.jpg", "*.jpeg"):
|
||||
paths.extend(glob.glob(os.path.join(base_path, ext)))
|
||||
|
||||
image_paths = sorted(
|
||||
paths, key=lambda x: os.path.getmtime(x), reverse=True
|
||||
@ -254,9 +265,12 @@ class InvokeAIWebServer:
|
||||
image_paths = image_paths[slice(0, page_size)]
|
||||
|
||||
image_array = []
|
||||
|
||||
for path in image_paths:
|
||||
metadata = retrieve_metadata(path)
|
||||
if os.path.splitext(path)[1] == ".png":
|
||||
metadata = retrieve_metadata(path)
|
||||
sd_metadata = metadata["sd-metadata"]
|
||||
else:
|
||||
sd_metadata = {}
|
||||
|
||||
(width, height) = Image.open(path).size
|
||||
|
||||
@ -264,7 +278,7 @@ class InvokeAIWebServer:
|
||||
{
|
||||
"url": self.get_url_from_image_path(path),
|
||||
"mtime": os.path.getmtime(path),
|
||||
"metadata": metadata["sd-metadata"],
|
||||
"metadata": sd_metadata,
|
||||
"width": width,
|
||||
"height": height,
|
||||
"category": category,
|
||||
@ -573,11 +587,7 @@ class InvokeAIWebServer:
|
||||
)
|
||||
)
|
||||
)
|
||||
# crop the mask image
|
||||
cropped_mask_image = copy_image_from_bounding_box(
|
||||
mask_image, **generation_parameters["bounding_box"]
|
||||
)
|
||||
generation_parameters["init_mask"] = cropped_mask_image
|
||||
generation_parameters["init_mask"] = mask_image
|
||||
|
||||
totalSteps = self.calculate_real_steps(
|
||||
steps=generation_parameters["steps"],
|
||||
@ -605,8 +615,9 @@ class InvokeAIWebServer:
|
||||
progress.set_current_status_has_steps(True)
|
||||
|
||||
if (
|
||||
generation_parameters['progress_images'] and step % 5 == 0 \
|
||||
and step < generation_parameters['steps'] - 1
|
||||
generation_parameters["progress_images"]
|
||||
and step % generation_parameters['save_intermediates'] == 0
|
||||
and step < generation_parameters["steps"] - 1
|
||||
):
|
||||
image = self.generate.sample_to_image(sample)
|
||||
metadata = self.parameters_to_generated_image_metadata(
|
||||
@ -637,14 +648,16 @@ class InvokeAIWebServer:
|
||||
},
|
||||
)
|
||||
|
||||
if generation_parameters['progress_latents']:
|
||||
if generation_parameters["progress_latents"]:
|
||||
image = self.generate.sample_to_lowres_estimated_image(sample)
|
||||
(width, height) = image.size
|
||||
width *= 8
|
||||
height *= 8
|
||||
buffered = io.BytesIO()
|
||||
image.save(buffered, format="PNG")
|
||||
img_base64 = "data:image/png;base64," + base64.b64encode(buffered.getvalue()).decode('UTF-8')
|
||||
img_base64 = "data:image/png;base64," + base64.b64encode(
|
||||
buffered.getvalue()
|
||||
).decode("UTF-8")
|
||||
self.socketio.emit(
|
||||
"intermediateResult",
|
||||
{
|
||||
@ -654,7 +667,7 @@ class InvokeAIWebServer:
|
||||
"metadata": {},
|
||||
"width": width,
|
||||
"height": height,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
self.socketio.emit("progressUpdate", progress.to_formatted_dict())
|
||||
@ -672,6 +685,14 @@ class InvokeAIWebServer:
|
||||
step_index = 1
|
||||
nonlocal prior_variations
|
||||
|
||||
# paste the inpainting image back onto the original
|
||||
if "init_mask" in generation_parameters:
|
||||
image = paste_image_into_bounding_box(
|
||||
Image.open(init_img_path),
|
||||
image,
|
||||
**generation_parameters["bounding_box"],
|
||||
)
|
||||
|
||||
progress.set_current_status("Generation Complete")
|
||||
|
||||
self.socketio.emit("progressUpdate", progress.to_formatted_dict())
|
||||
@ -760,14 +781,6 @@ class InvokeAIWebServer:
|
||||
self.socketio.emit("progressUpdate", progress.to_formatted_dict())
|
||||
eventlet.sleep(0)
|
||||
|
||||
# paste the inpainting image back onto the original
|
||||
if "init_mask" in generation_parameters:
|
||||
image = paste_image_into_bounding_box(
|
||||
Image.open(init_img_path),
|
||||
image,
|
||||
**generation_parameters["bounding_box"],
|
||||
)
|
||||
|
||||
# restore the stashed URLS and discard the paths, we are about to send the result to client
|
||||
if "init_img" in all_parameters:
|
||||
all_parameters["init_img"] = init_img_url
|
||||
|
1
frontend/dist/assets/index.52c8231e.css
vendored
1
frontend/dist/assets/index.52c8231e.css
vendored
File diff suppressed because one or more lines are too long
501
frontend/dist/assets/index.8eb7dfe4.js
vendored
Normal file
501
frontend/dist/assets/index.8eb7dfe4.js
vendored
Normal file
File diff suppressed because one or more lines are too long
517
frontend/dist/assets/index.bf9dd1fc.js
vendored
Normal file
517
frontend/dist/assets/index.bf9dd1fc.js
vendored
Normal file
File diff suppressed because one or more lines are too long
517
frontend/dist/assets/index.cc049b93.js
vendored
517
frontend/dist/assets/index.cc049b93.js
vendored
File diff suppressed because one or more lines are too long
1
frontend/dist/assets/index.f9f4c989.css
vendored
Normal file
1
frontend/dist/assets/index.f9f4c989.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
frontend/dist/index.html
vendored
4
frontend/dist/index.html
vendored
@ -6,8 +6,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>InvokeAI - A Stable Diffusion Toolkit</title>
|
||||
<link rel="shortcut icon" type="icon" href="./assets/favicon.0d253ced.ico" />
|
||||
<script type="module" crossorigin src="./assets/index.cc049b93.js"></script>
|
||||
<link rel="stylesheet" href="./assets/index.52c8231e.css">
|
||||
<script type="module" crossorigin src="./assets/index.bf9dd1fc.js"></script>
|
||||
<link rel="stylesheet" href="./assets/index.f9f4c989.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import ProgressBar from '../features/system/ProgressBar';
|
||||
import SiteHeader from '../features/system/SiteHeader';
|
||||
import Console from '../features/system/Console';
|
||||
import Loading from '../Loading';
|
||||
import { useAppDispatch } from './store';
|
||||
import { requestSystemConfig } from './socketio/actions';
|
||||
import { keepGUIAlive } from './utils';
|
||||
@ -79,17 +78,14 @@ const appSelector = createSelector(
|
||||
const App = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [isReady, setIsReady] = useState<boolean>(false);
|
||||
|
||||
const { shouldShowGalleryButton, shouldShowOptionsPanelButton } =
|
||||
useAppSelector(appSelector);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(requestSystemConfig());
|
||||
setIsReady(true);
|
||||
}, [dispatch]);
|
||||
|
||||
return isReady ? (
|
||||
return (
|
||||
<div className="App">
|
||||
<ImageUploader>
|
||||
<ProgressBar />
|
||||
@ -104,8 +100,6 @@ const App = () => {
|
||||
{shouldShowOptionsPanelButton && <FloatingOptionsPanelButtons />}
|
||||
</ImageUploader>
|
||||
</div>
|
||||
) : (
|
||||
<Loading />
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
// TODO: use Enums?
|
||||
|
||||
import { InProgressImageType } from '../features/system/systemSlice';
|
||||
|
||||
// Valid samplers
|
||||
export const SAMPLERS: Array<string> = [
|
||||
'ddim',
|
||||
@ -38,8 +40,11 @@ export const NUMPY_RAND_MAX = 4294967295;
|
||||
|
||||
export const FACETOOL_TYPES = ['gfpgan', 'codeformer'] as const;
|
||||
|
||||
export const IN_PROGRESS_IMAGE_TYPES: Array<{ key: string; value: string }> = [
|
||||
{ key: "None", value: 'none'},
|
||||
{ key: "Fast", value: 'latents' },
|
||||
{ key: "Accurate", value: 'full-res' }
|
||||
export const IN_PROGRESS_IMAGE_TYPES: Array<{
|
||||
key: string;
|
||||
value: InProgressImageType;
|
||||
}> = [
|
||||
{ key: 'None', value: 'none' },
|
||||
{ key: 'Fast', value: 'latents' },
|
||||
{ key: 'Accurate', value: 'full-res' },
|
||||
];
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import _ from 'lodash';
|
||||
import { RootState } from '../../app/store';
|
||||
import { RootState } from '../store';
|
||||
import { activeTabNameSelector } from '../../features/options/optionsSelectors';
|
||||
import { OptionsState } from '../../features/options/optionsSlice';
|
||||
|
||||
@ -25,7 +25,7 @@ export const readinessSelector = createSelector(
|
||||
prompt,
|
||||
shouldGenerateVariations,
|
||||
seedWeights,
|
||||
maskPath,
|
||||
// maskPath,
|
||||
initialImage,
|
||||
seed,
|
||||
} = options;
|
||||
@ -34,33 +34,45 @@ export const readinessSelector = createSelector(
|
||||
|
||||
const { imageToInpaint } = inpainting;
|
||||
|
||||
let isReady = true;
|
||||
const reasonsWhyNotReady: string[] = [];
|
||||
|
||||
// Cannot generate without a prompt
|
||||
if (!prompt || Boolean(prompt.match(/^[\s\r\n]+$/))) {
|
||||
return false;
|
||||
isReady = false;
|
||||
reasonsWhyNotReady.push('Missing prompt');
|
||||
}
|
||||
|
||||
if (activeTabName === 'img2img' && !initialImage) {
|
||||
return false;
|
||||
isReady = false;
|
||||
reasonsWhyNotReady.push('No initial image selected');
|
||||
}
|
||||
|
||||
if (activeTabName === 'inpainting' && !imageToInpaint) {
|
||||
return false;
|
||||
isReady = false;
|
||||
reasonsWhyNotReady.push('No inpainting image selected');
|
||||
}
|
||||
|
||||
// Cannot generate with a mask without img2img
|
||||
if (maskPath && !initialImage) {
|
||||
return false;
|
||||
}
|
||||
// // We don't use mask paths now.
|
||||
// // Cannot generate with a mask without img2img
|
||||
// if (maskPath && !initialImage) {
|
||||
// isReady = false;
|
||||
// reasonsWhyNotReady.push(
|
||||
// 'On ImageToImage tab, but no mask is provided.'
|
||||
// );
|
||||
// }
|
||||
|
||||
// TODO: job queue
|
||||
// Cannot generate if already processing an image
|
||||
if (isProcessing) {
|
||||
return false;
|
||||
isReady = false;
|
||||
reasonsWhyNotReady.push('System Busy');
|
||||
}
|
||||
|
||||
// Cannot generate if not connected
|
||||
if (!isConnected) {
|
||||
return false;
|
||||
isReady = false;
|
||||
reasonsWhyNotReady.push('System Disconnected');
|
||||
}
|
||||
|
||||
// Cannot generate variations without valid seed weights
|
||||
@ -68,11 +80,12 @@ export const readinessSelector = createSelector(
|
||||
shouldGenerateVariations &&
|
||||
(!(validateSeedWeights(seedWeights) || seedWeights === '') || seed === -1)
|
||||
) {
|
||||
return false;
|
||||
isReady = false;
|
||||
reasonsWhyNotReady.push('Seed-Weights badly formatted.');
|
||||
}
|
||||
|
||||
// All good
|
||||
return true;
|
||||
return { isReady, reasonsWhyNotReady };
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
|
@ -146,12 +146,14 @@ const makeSocketIOListeners = (
|
||||
...data,
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
message: `Intermediate image generated: ${data.url}`,
|
||||
})
|
||||
);
|
||||
if (!data.isBase64) {
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
message: `Intermediate image generated: ${data.url}`,
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
@ -5,12 +5,16 @@ import type { TypedUseSelectorHook } from 'react-redux';
|
||||
import { persistReducer } from 'redux-persist';
|
||||
import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web
|
||||
|
||||
import optionsReducer from '../features/options/optionsSlice';
|
||||
import galleryReducer from '../features/gallery/gallerySlice';
|
||||
import inpaintingReducer from '../features/tabs/Inpainting/inpaintingSlice';
|
||||
import optionsReducer, { OptionsState } from '../features/options/optionsSlice';
|
||||
import galleryReducer, { GalleryState } from '../features/gallery/gallerySlice';
|
||||
import inpaintingReducer, {
|
||||
InpaintingState,
|
||||
} from '../features/tabs/Inpainting/inpaintingSlice';
|
||||
|
||||
import systemReducer from '../features/system/systemSlice';
|
||||
import systemReducer, { SystemState } from '../features/system/systemSlice';
|
||||
import { socketioMiddleware } from './socketio/middleware';
|
||||
import autoMergeLevel2 from 'redux-persist/es/stateReconciler/autoMergeLevel2';
|
||||
import { PersistPartial } from 'redux-persist/es/persistReducer';
|
||||
|
||||
/**
|
||||
* redux-persist provides an easy and reliable way to persist state across reloads.
|
||||
@ -33,12 +37,14 @@ import { socketioMiddleware } from './socketio/middleware';
|
||||
const rootPersistConfig = {
|
||||
key: 'root',
|
||||
storage,
|
||||
stateReconciler: autoMergeLevel2,
|
||||
blacklist: ['gallery', 'system', 'inpainting'],
|
||||
};
|
||||
|
||||
const systemPersistConfig = {
|
||||
key: 'system',
|
||||
storage,
|
||||
stateReconciler: autoMergeLevel2,
|
||||
blacklist: [
|
||||
'isCancelable',
|
||||
'isConnected',
|
||||
@ -58,6 +64,7 @@ const systemPersistConfig = {
|
||||
const galleryPersistConfig = {
|
||||
key: 'gallery',
|
||||
storage,
|
||||
stateReconciler: autoMergeLevel2,
|
||||
whitelist: [
|
||||
'galleryWidth',
|
||||
'shouldPinGallery',
|
||||
@ -71,17 +78,26 @@ const galleryPersistConfig = {
|
||||
const inpaintingPersistConfig = {
|
||||
key: 'inpainting',
|
||||
storage,
|
||||
stateReconciler: autoMergeLevel2,
|
||||
blacklist: ['pastLines', 'futuresLines', 'cursorPosition'],
|
||||
};
|
||||
|
||||
const reducers = combineReducers({
|
||||
options: optionsReducer,
|
||||
gallery: persistReducer(galleryPersistConfig, galleryReducer),
|
||||
system: persistReducer(systemPersistConfig, systemReducer),
|
||||
inpainting: persistReducer(inpaintingPersistConfig, inpaintingReducer),
|
||||
gallery: persistReducer<GalleryState>(galleryPersistConfig, galleryReducer),
|
||||
system: persistReducer<SystemState>(systemPersistConfig, systemReducer),
|
||||
inpainting: persistReducer<InpaintingState>(
|
||||
inpaintingPersistConfig,
|
||||
inpaintingReducer
|
||||
),
|
||||
});
|
||||
|
||||
const persistedReducer = persistReducer(rootPersistConfig, reducers);
|
||||
const persistedReducer = persistReducer<{
|
||||
options: OptionsState;
|
||||
gallery: GalleryState & PersistPartial;
|
||||
system: SystemState & PersistPartial;
|
||||
inpainting: InpaintingState & PersistPartial;
|
||||
}>(rootPersistConfig, reducers);
|
||||
|
||||
// Continue with store setup
|
||||
export const store = configureStore({
|
||||
|
3
frontend/src/common/components/IAIButton.scss
Normal file
3
frontend/src/common/components/IAIButton.scss
Normal file
@ -0,0 +1,3 @@
|
||||
.invokeai__button {
|
||||
justify-content: space-between;
|
||||
}
|
@ -1,23 +1,32 @@
|
||||
import { Button, ButtonProps, Tooltip } from '@chakra-ui/react';
|
||||
import {
|
||||
Button,
|
||||
ButtonProps,
|
||||
forwardRef,
|
||||
Tooltip,
|
||||
TooltipProps,
|
||||
} from '@chakra-ui/react';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export interface IAIButtonProps extends ButtonProps {
|
||||
label: string;
|
||||
tooltip?: string;
|
||||
tooltipProps?: Omit<TooltipProps, 'children'>;
|
||||
styleClass?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable customized button component.
|
||||
*/
|
||||
const IAIButton = (props: IAIButtonProps) => {
|
||||
const { label, tooltip = '', styleClass, ...rest } = props;
|
||||
const IAIButton = forwardRef((props: IAIButtonProps, forwardedRef) => {
|
||||
const { children, tooltip = '', tooltipProps, styleClass, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label={tooltip}>
|
||||
<Button className={styleClass ? styleClass : ''} {...rest}>
|
||||
{label}
|
||||
<Tooltip label={tooltip} {...tooltipProps}>
|
||||
<Button
|
||||
ref={forwardedRef}
|
||||
className={['invokeai__button', styleClass].join(' ')}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default IAIButton;
|
||||
|
@ -1,6 +1,6 @@
|
||||
@use '../../styles/Mixins/' as *;
|
||||
|
||||
.icon-button {
|
||||
.invokeai__icon-button {
|
||||
background-color: var(--btn-grey);
|
||||
cursor: pointer;
|
||||
|
||||
@ -8,13 +8,68 @@
|
||||
background-color: var(--btn-grey-hover);
|
||||
}
|
||||
|
||||
&[data-selected=true] {
|
||||
&[data-selected='true'] {
|
||||
background-color: var(--accent-color);
|
||||
&:hover {
|
||||
background-color: var(--accent-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&[data-variant='link'] {
|
||||
background: none !important;
|
||||
&:hover {
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-selected='true'] {
|
||||
border-color: var(--accent-color);
|
||||
&:hover {
|
||||
border-color: var(--accent-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-alert='true'] {
|
||||
animation-name: pulseColor;
|
||||
animation-duration: 1s;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-iteration-count: infinite;
|
||||
&:hover {
|
||||
animation: none;
|
||||
background-color: var(--accent-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-as-checkbox='true'] {
|
||||
background-color: var(--btn-grey);
|
||||
border: 3px solid var(--btn-grey);
|
||||
|
||||
svg {
|
||||
fill: var(--text-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--btn-grey);
|
||||
border-color: var(--btn-checkbox-border-hover);
|
||||
svg {
|
||||
fill: var(--text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulseColor {
|
||||
0% {
|
||||
background-color: var(--accent-color);
|
||||
}
|
||||
50% {
|
||||
background-color: var(--accent-color-dim);
|
||||
}
|
||||
100% {
|
||||
background-color: var(--accent-color);
|
||||
}
|
||||
}
|
||||
|
@ -2,38 +2,40 @@ import {
|
||||
IconButtonProps,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
PlacementWithLogical,
|
||||
TooltipProps,
|
||||
forwardRef,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
interface Props extends IconButtonProps {
|
||||
tooltip?: string;
|
||||
tooltipPlacement?: PlacementWithLogical | undefined;
|
||||
export type IAIIconButtonProps = IconButtonProps & {
|
||||
styleClass?: string;
|
||||
}
|
||||
tooltip?: string;
|
||||
tooltipProps?: Omit<TooltipProps, 'children'>;
|
||||
asCheckbox?: boolean;
|
||||
isChecked?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reusable customized button component. Originally was more customized - now probably unecessary.
|
||||
*/
|
||||
const IAIIconButton = (props: Props) => {
|
||||
const IAIIconButton = forwardRef((props: IAIIconButtonProps, forwardedRef) => {
|
||||
const {
|
||||
tooltip = '',
|
||||
tooltipPlacement = 'bottom',
|
||||
styleClass,
|
||||
onClick,
|
||||
cursor,
|
||||
tooltipProps,
|
||||
asCheckbox,
|
||||
isChecked,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Tooltip label={tooltip} hasArrow placement={tooltipPlacement}>
|
||||
<Tooltip label={tooltip} hasArrow {...tooltipProps}>
|
||||
<IconButton
|
||||
className={`icon-button ${styleClass}`}
|
||||
ref={forwardedRef}
|
||||
className={`invokeai__icon-button ${styleClass}`}
|
||||
data-as-checkbox={asCheckbox}
|
||||
data-selected={isChecked !== undefined ? isChecked : undefined}
|
||||
style={props.onClick ? { cursor: 'pointer' } : {}}
|
||||
{...rest}
|
||||
cursor={cursor ? cursor : onClick ? 'pointer' : 'unset'}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default IAIIconButton;
|
||||
|
@ -1,7 +1,6 @@
|
||||
.invokeai__number-input-form-control {
|
||||
display: grid;
|
||||
grid-template-columns: max-content auto;
|
||||
column-gap: 1rem;
|
||||
align-items: center;
|
||||
|
||||
.invokeai__number-input-form-label {
|
||||
@ -11,6 +10,7 @@
|
||||
margin-bottom: 0;
|
||||
flex-grow: 2;
|
||||
white-space: nowrap;
|
||||
padding-right: 1rem;
|
||||
|
||||
&[data-focus] + .invokeai__number-input-root {
|
||||
outline: none;
|
||||
|
@ -123,13 +123,15 @@ const IAINumberInput = (props: Props) => {
|
||||
}
|
||||
{...formControlProps}
|
||||
>
|
||||
<FormLabel
|
||||
className="invokeai__number-input-form-label"
|
||||
style={{ display: label ? 'block' : 'none' }}
|
||||
{...formLabelProps}
|
||||
>
|
||||
{label}
|
||||
</FormLabel>
|
||||
{label && (
|
||||
<FormLabel
|
||||
className="invokeai__number-input-form-label"
|
||||
style={{ display: label ? 'block' : 'none' }}
|
||||
{...formLabelProps}
|
||||
>
|
||||
{label}
|
||||
</FormLabel>
|
||||
)}
|
||||
<NumberInput
|
||||
className="invokeai__number-input-root"
|
||||
value={valueAsString}
|
||||
@ -145,19 +147,18 @@ const IAINumberInput = (props: Props) => {
|
||||
textAlign={textAlign}
|
||||
{...numberInputFieldProps}
|
||||
/>
|
||||
<div
|
||||
className="invokeai__number-input-stepper"
|
||||
style={showStepper ? { display: 'block' } : { display: 'none' }}
|
||||
>
|
||||
<NumberIncrementStepper
|
||||
{...numberInputStepperProps}
|
||||
className="invokeai__number-input-stepper-button"
|
||||
/>
|
||||
<NumberDecrementStepper
|
||||
{...numberInputStepperProps}
|
||||
className="invokeai__number-input-stepper-button"
|
||||
/>
|
||||
</div>
|
||||
{showStepper && (
|
||||
<div className="invokeai__number-input-stepper">
|
||||
<NumberIncrementStepper
|
||||
{...numberInputStepperProps}
|
||||
className="invokeai__number-input-stepper-button"
|
||||
/>
|
||||
<NumberDecrementStepper
|
||||
{...numberInputStepperProps}
|
||||
className="invokeai__number-input-stepper-button"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</NumberInput>
|
||||
</FormControl>
|
||||
</Tooltip>
|
||||
|
@ -3,13 +3,14 @@ import {
|
||||
PopoverArrow,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
Box,
|
||||
BoxProps,
|
||||
} from '@chakra-ui/react';
|
||||
import { PopoverProps } from '@chakra-ui/react';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
type IAIPopoverProps = PopoverProps & {
|
||||
triggerComponent: ReactNode;
|
||||
triggerContainerProps?: BoxProps;
|
||||
children: ReactNode;
|
||||
styleClass?: string;
|
||||
hasArrow?: boolean;
|
||||
@ -23,11 +24,10 @@ const IAIPopover = (props: IAIPopoverProps) => {
|
||||
hasArrow = true,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Popover {...rest}>
|
||||
<PopoverTrigger>
|
||||
<Box>{triggerComponent}</Box>
|
||||
</PopoverTrigger>
|
||||
<PopoverTrigger>{triggerComponent}</PopoverTrigger>
|
||||
<PopoverContent className={`invokeai__popover-content ${styleClass}`}>
|
||||
{hasArrow && <PopoverArrow className={'invokeai__popover-arrow'} />}
|
||||
{children}
|
||||
|
@ -1,8 +1,7 @@
|
||||
@use '../../styles/Mixins/' as *;
|
||||
|
||||
.invokeai__select {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, max-content);
|
||||
display: flex;
|
||||
column-gap: 1rem;
|
||||
align-items: center;
|
||||
width: max-content;
|
||||
|
@ -1,17 +1,17 @@
|
||||
import { FormControl, FormLabel, Select, SelectProps } from '@chakra-ui/react';
|
||||
import { MouseEvent } from 'react';
|
||||
|
||||
interface Props extends SelectProps {
|
||||
type IAISelectProps = SelectProps & {
|
||||
label: string;
|
||||
styleClass?: string;
|
||||
validValues:
|
||||
| Array<number | string>
|
||||
| Array<{ key: string; value: string | number }>;
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Customized Chakra FormControl + Select multi-part component.
|
||||
*/
|
||||
const IAISelect = (props: Props) => {
|
||||
const IAISelect = (props: IAISelectProps) => {
|
||||
const {
|
||||
label,
|
||||
isDisabled,
|
||||
@ -33,19 +33,19 @@ const IAISelect = (props: Props) => {
|
||||
}}
|
||||
>
|
||||
<FormLabel
|
||||
className="invokeai__select-label"
|
||||
fontSize={fontSize}
|
||||
marginBottom={1}
|
||||
flexGrow={2}
|
||||
whiteSpace="nowrap"
|
||||
className="invokeai__select-label"
|
||||
>
|
||||
{label}
|
||||
</FormLabel>
|
||||
<Select
|
||||
className="invokeai__select-picker"
|
||||
fontSize={fontSize}
|
||||
size={size}
|
||||
{...rest}
|
||||
className="invokeai__select-picker"
|
||||
>
|
||||
{validValues.map((opt) => {
|
||||
return typeof opt === 'string' || typeof opt === 'number' ? (
|
||||
@ -53,7 +53,11 @@ const IAISelect = (props: Props) => {
|
||||
{opt}
|
||||
</option>
|
||||
) : (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
<option
|
||||
key={opt.value}
|
||||
value={opt.value}
|
||||
className="invokeai__select-option"
|
||||
>
|
||||
{opt.key}
|
||||
</option>
|
||||
);
|
||||
|
@ -22,8 +22,6 @@ const IAISwitch = (props: Props) => {
|
||||
const {
|
||||
label,
|
||||
isDisabled = false,
|
||||
// fontSize = 'md',
|
||||
// size = 'md',
|
||||
width = 'auto',
|
||||
formControlProps,
|
||||
formLabelProps,
|
||||
@ -39,17 +37,11 @@ const IAISwitch = (props: Props) => {
|
||||
>
|
||||
<FormLabel
|
||||
className="invokeai__switch-form-label"
|
||||
// fontSize={fontSize}
|
||||
whiteSpace="nowrap"
|
||||
{...formLabelProps}
|
||||
>
|
||||
{label}
|
||||
<Switch
|
||||
className="invokeai__switch-root"
|
||||
// size={size}
|
||||
// className="switch-button"
|
||||
{...rest}
|
||||
/>
|
||||
<Switch className="invokeai__switch-root" {...rest} />
|
||||
</FormLabel>
|
||||
</FormControl>
|
||||
);
|
||||
|
39
frontend/src/common/components/ImageUploadOverlay.tsx
Normal file
39
frontend/src/common/components/ImageUploadOverlay.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { Heading } from '@chakra-ui/react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
type ImageUploadOverlayProps = {
|
||||
isDragAccept: boolean;
|
||||
isDragReject: boolean;
|
||||
overlaySecondaryText: string;
|
||||
setIsHandlingUpload: (isHandlingUpload: boolean) => void;
|
||||
};
|
||||
|
||||
const ImageUploadOverlay = (props: ImageUploadOverlayProps) => {
|
||||
const {
|
||||
isDragAccept,
|
||||
isDragReject,
|
||||
overlaySecondaryText,
|
||||
setIsHandlingUpload,
|
||||
} = props;
|
||||
|
||||
useHotkeys('esc', () => {
|
||||
setIsHandlingUpload(false);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="dropzone-container">
|
||||
{isDragAccept && (
|
||||
<div className="dropzone-overlay is-drag-accept">
|
||||
<Heading size={'lg'}>Upload Image{overlaySecondaryText}</Heading>
|
||||
</div>
|
||||
)}
|
||||
{isDragReject && (
|
||||
<div className="dropzone-overlay is-drag-reject">
|
||||
<Heading size={'lg'}>Invalid Upload</Heading>
|
||||
<Heading size={'md'}>Must be single JPEG or PNG image</Heading>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ImageUploadOverlay;
|
@ -16,9 +16,10 @@
|
||||
row-gap: 1rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--background-color);
|
||||
|
||||
&.is-drag-accept {
|
||||
box-shadow: inset 0 0 20rem 1rem var(--status-good-color);
|
||||
box-shadow: inset 0 0 20rem 1rem var(--accent-color);
|
||||
}
|
||||
|
||||
&.is-drag-reject {
|
||||
@ -32,6 +33,7 @@
|
||||
}
|
||||
|
||||
.image-uploader-button-outer {
|
||||
min-width: 20rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { useCallback, ReactNode, useState, useEffect } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { FileRejection, useDropzone } from 'react-dropzone';
|
||||
import { Heading, Spinner, useToast } from '@chakra-ui/react';
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { uploadImage } from '../../app/socketio/actions';
|
||||
import { ImageUploadDestination, UploadImagePayload } from '../../app/invokeai';
|
||||
import { ImageUploaderTriggerContext } from '../../app/contexts/ImageUploaderTriggerContext';
|
||||
import { activeTabNameSelector } from '../../features/options/optionsSelectors';
|
||||
import { tabDict } from '../../features/tabs/InvokeTabs';
|
||||
import ImageUploadOverlay from './ImageUploadOverlay';
|
||||
|
||||
type ImageUploaderProps = {
|
||||
children: ReactNode;
|
||||
@ -71,6 +73,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
||||
accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] },
|
||||
noClick: true,
|
||||
onDrop,
|
||||
onDragOver: () => setIsHandlingUpload(true),
|
||||
maxFiles: 1,
|
||||
});
|
||||
|
||||
@ -128,30 +131,22 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
||||
};
|
||||
}, [dispatch, toast, activeTabName]);
|
||||
|
||||
const overlaySecondaryText = ['img2img', 'inpainting'].includes(activeTabName)
|
||||
? ` to ${tabDict[activeTabName as keyof typeof tabDict].tooltip}`
|
||||
: ``;
|
||||
|
||||
return (
|
||||
<ImageUploaderTriggerContext.Provider value={open}>
|
||||
<div {...getRootProps({ style: {} })}>
|
||||
<input {...getInputProps()} />
|
||||
{children}
|
||||
{isDragActive && (
|
||||
<div className="dropzone-container">
|
||||
{isDragAccept && (
|
||||
<div className="dropzone-overlay is-drag-accept">
|
||||
<Heading size={'lg'}>Drop Images</Heading>
|
||||
</div>
|
||||
)}
|
||||
{isDragReject && (
|
||||
<div className="dropzone-overlay is-drag-reject">
|
||||
<Heading size={'lg'}>Invalid Upload</Heading>
|
||||
<Heading size={'md'}>Must be single JPEG or PNG image</Heading>
|
||||
</div>
|
||||
)}
|
||||
{isHandlingUpload && (
|
||||
<div className="dropzone-overlay is-handling-upload">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isDragActive && isHandlingUpload && (
|
||||
<ImageUploadOverlay
|
||||
isDragAccept={isDragAccept}
|
||||
isDragReject={isDragReject}
|
||||
overlaySecondaryText={overlaySecondaryText}
|
||||
setIsHandlingUpload={setIsHandlingUpload}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ImageUploaderTriggerContext.Provider>
|
||||
|
19
frontend/src/common/components/ImageUploaderIconButton.tsx
Normal file
19
frontend/src/common/components/ImageUploaderIconButton.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { useContext } from 'react';
|
||||
import { FaUpload } from 'react-icons/fa';
|
||||
import { ImageUploaderTriggerContext } from '../../app/contexts/ImageUploaderTriggerContext';
|
||||
import IAIIconButton from './IAIIconButton';
|
||||
|
||||
const ImageUploaderIconButton = () => {
|
||||
const openImageUploader = useContext(ImageUploaderTriggerContext);
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
aria-label="Upload Image"
|
||||
tooltip="Upload Image"
|
||||
icon={<FaUpload />}
|
||||
onClick={openImageUploader || undefined}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageUploaderIconButton;
|
@ -62,11 +62,13 @@ export const frontendToBackendParameters = (
|
||||
shouldRandomizeSeed,
|
||||
} = optionsState;
|
||||
|
||||
const { shouldDisplayInProgressType } = systemState;
|
||||
const { shouldDisplayInProgressType, saveIntermediatesInterval } =
|
||||
systemState;
|
||||
|
||||
const generationParameters: { [k: string]: any } = {
|
||||
prompt,
|
||||
iterations,
|
||||
iterations:
|
||||
shouldRandomizeSeed || shouldGenerateVariations ? iterations : 1,
|
||||
steps,
|
||||
cfg_scale: cfgScale,
|
||||
threshold,
|
||||
@ -76,7 +78,8 @@ export const frontendToBackendParameters = (
|
||||
sampler_name: sampler,
|
||||
seed,
|
||||
progress_images: shouldDisplayInProgressType === 'full-res',
|
||||
progress_latents: shouldDisplayInProgressType === 'latents'
|
||||
progress_latents: shouldDisplayInProgressType === 'latents',
|
||||
save_intermediates: saveIntermediatesInterval,
|
||||
};
|
||||
|
||||
generationParameters.seed = shouldRandomizeSeed
|
||||
|
25
frontend/src/features/gallery/CurrentImageButtons.scss
Normal file
25
frontend/src/features/gallery/CurrentImageButtons.scss
Normal file
@ -0,0 +1,25 @@
|
||||
.current-image-options {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
column-gap: 0.5em;
|
||||
|
||||
.current-image-send-to-popover,
|
||||
.current-image-postprocessing-popover {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 0.5rem;
|
||||
max-width: 25rem;
|
||||
}
|
||||
|
||||
.chakra-popover__popper {
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
.delete-image-btn {
|
||||
svg {
|
||||
fill: var(--btn-delete-image);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import * as InvokeAI from '../../app/invokeai';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../app/store';
|
||||
import {
|
||||
@ -10,6 +8,7 @@ import {
|
||||
setActiveTab,
|
||||
setAllParameters,
|
||||
setInitialImage,
|
||||
setPrompt,
|
||||
setSeed,
|
||||
setShouldShowImageDetails,
|
||||
} from '../options/optionsSlice';
|
||||
@ -18,27 +17,30 @@ import { SystemState } from '../system/systemSlice';
|
||||
import IAIButton from '../../common/components/IAIButton';
|
||||
import { runESRGAN, runFacetool } from '../../app/socketio/actions';
|
||||
import IAIIconButton from '../../common/components/IAIIconButton';
|
||||
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';
|
||||
import { FaCopy, FaPaintBrush, FaSeedling } from 'react-icons/fa';
|
||||
import { setImageToInpaint } from '../tabs/Inpainting/inpaintingSlice';
|
||||
import { ButtonGroup, Link, useClipboard, useToast } from '@chakra-ui/react';
|
||||
import {
|
||||
FaAsterisk,
|
||||
FaCode,
|
||||
FaCopy,
|
||||
FaDownload,
|
||||
FaExpandArrowsAlt,
|
||||
FaGrinStars,
|
||||
FaQuoteRight,
|
||||
FaSeedling,
|
||||
FaShare,
|
||||
FaShareAlt,
|
||||
FaTrash,
|
||||
} from 'react-icons/fa';
|
||||
import {
|
||||
setImageToInpaint,
|
||||
setNeedsCache,
|
||||
} from '../tabs/Inpainting/inpaintingSlice';
|
||||
import { GalleryState } from './gallerySlice';
|
||||
import { activeTabNameSelector } from '../options/optionsSelectors';
|
||||
|
||||
const intermediateImageSelector = createSelector(
|
||||
(state: RootState) => state.gallery,
|
||||
(gallery: GalleryState) => gallery.intermediateImage,
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: (a, b) =>
|
||||
(a === undefined && b === undefined) || a.uuid === b.uuid,
|
||||
},
|
||||
}
|
||||
);
|
||||
import IAIPopover from '../../common/components/IAIPopover';
|
||||
|
||||
const systemSelector = createSelector(
|
||||
[
|
||||
@ -59,7 +61,7 @@ const systemSelector = createSelector(
|
||||
const { upscalingLevel, facetoolStrength, shouldShowImageDetails } =
|
||||
options;
|
||||
|
||||
const { intermediateImage } = gallery;
|
||||
const { intermediateImage, currentImage } = gallery;
|
||||
|
||||
return {
|
||||
isProcessing,
|
||||
@ -68,7 +70,8 @@ const systemSelector = createSelector(
|
||||
isESRGANAvailable,
|
||||
upscalingLevel,
|
||||
facetoolStrength,
|
||||
intermediateImage,
|
||||
shouldDisableToolbarButtons: Boolean(intermediateImage) || !currentImage,
|
||||
currentImage,
|
||||
shouldShowImageDetails,
|
||||
activeTabName,
|
||||
};
|
||||
@ -80,15 +83,11 @@ const systemSelector = createSelector(
|
||||
}
|
||||
);
|
||||
|
||||
type CurrentImageButtonsProps = {
|
||||
image: InvokeAI.Image;
|
||||
};
|
||||
|
||||
/**
|
||||
* Row of buttons for common actions:
|
||||
* Use as init image, use all params, use seed, upscale, fix faces, details, delete.
|
||||
*/
|
||||
const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
||||
const CurrentImageButtons = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const {
|
||||
isProcessing,
|
||||
@ -97,22 +96,37 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
||||
isESRGANAvailable,
|
||||
upscalingLevel,
|
||||
facetoolStrength,
|
||||
intermediateImage,
|
||||
shouldDisableToolbarButtons,
|
||||
shouldShowImageDetails,
|
||||
activeTabName,
|
||||
currentImage,
|
||||
} = useAppSelector(systemSelector);
|
||||
|
||||
const { onCopy } = useClipboard(
|
||||
currentImage ? window.location.toString() + currentImage.url : ''
|
||||
);
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const handleClickUseAsInitialImage = () => {
|
||||
dispatch(setInitialImage(image));
|
||||
dispatch(setActiveTab(1));
|
||||
if (!currentImage) return;
|
||||
dispatch(setInitialImage(currentImage));
|
||||
dispatch(setActiveTab('img2img'));
|
||||
};
|
||||
|
||||
const handleCopyImageLink = () => {
|
||||
onCopy();
|
||||
toast({
|
||||
title: 'Image Link Copied',
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
});
|
||||
};
|
||||
|
||||
useHotkeys(
|
||||
'shift+i',
|
||||
() => {
|
||||
if (image) {
|
||||
if (currentImage) {
|
||||
handleClickUseAsInitialImage();
|
||||
toast({
|
||||
title: 'Sent To Image To Image',
|
||||
@ -130,16 +144,20 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
||||
});
|
||||
}
|
||||
},
|
||||
[image]
|
||||
[currentImage]
|
||||
);
|
||||
|
||||
const handleClickUseAllParameters = () =>
|
||||
image.metadata && dispatch(setAllParameters(image.metadata));
|
||||
const handleClickUseAllParameters = () => {
|
||||
if (!currentImage) return;
|
||||
currentImage.metadata && dispatch(setAllParameters(currentImage.metadata));
|
||||
};
|
||||
|
||||
useHotkeys(
|
||||
'a',
|
||||
() => {
|
||||
if (['txt2img', 'img2img'].includes(image?.metadata?.image?.type)) {
|
||||
if (
|
||||
['txt2img', 'img2img'].includes(currentImage?.metadata?.image?.type)
|
||||
) {
|
||||
handleClickUseAllParameters();
|
||||
toast({
|
||||
title: 'Parameters Set',
|
||||
@ -157,15 +175,18 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
||||
});
|
||||
}
|
||||
},
|
||||
[image]
|
||||
[currentImage]
|
||||
);
|
||||
|
||||
const handleClickUseSeed = () =>
|
||||
image.metadata && dispatch(setSeed(image.metadata.image.seed));
|
||||
const handleClickUseSeed = () => {
|
||||
currentImage?.metadata &&
|
||||
dispatch(setSeed(currentImage.metadata.image.seed));
|
||||
};
|
||||
|
||||
useHotkeys(
|
||||
's',
|
||||
() => {
|
||||
if (image?.metadata?.image?.seed) {
|
||||
if (currentImage?.metadata?.image?.seed) {
|
||||
handleClickUseSeed();
|
||||
toast({
|
||||
title: 'Seed Set',
|
||||
@ -183,16 +204,47 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
||||
});
|
||||
}
|
||||
},
|
||||
[image]
|
||||
[currentImage]
|
||||
);
|
||||
|
||||
const handleClickUpscale = () => dispatch(runESRGAN(image));
|
||||
const handleClickUsePrompt = () =>
|
||||
currentImage?.metadata?.image?.prompt &&
|
||||
dispatch(setPrompt(currentImage.metadata.image.prompt));
|
||||
|
||||
useHotkeys(
|
||||
'p',
|
||||
() => {
|
||||
if (currentImage?.metadata?.image?.prompt) {
|
||||
handleClickUsePrompt();
|
||||
toast({
|
||||
title: 'Prompt Set',
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: 'Prompt Not Set',
|
||||
description: 'Could not find prompt for this image.',
|
||||
status: 'error',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
[currentImage]
|
||||
);
|
||||
|
||||
const handleClickUpscale = () => {
|
||||
currentImage && dispatch(runESRGAN(currentImage));
|
||||
};
|
||||
|
||||
useHotkeys(
|
||||
'u',
|
||||
() => {
|
||||
if (
|
||||
isESRGANAvailable &&
|
||||
Boolean(!intermediateImage) &&
|
||||
!shouldDisableToolbarButtons &&
|
||||
isConnected &&
|
||||
!isProcessing &&
|
||||
upscalingLevel
|
||||
@ -208,23 +260,25 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
||||
}
|
||||
},
|
||||
[
|
||||
image,
|
||||
currentImage,
|
||||
isESRGANAvailable,
|
||||
intermediateImage,
|
||||
shouldDisableToolbarButtons,
|
||||
isConnected,
|
||||
isProcessing,
|
||||
upscalingLevel,
|
||||
]
|
||||
);
|
||||
|
||||
const handleClickFixFaces = () => dispatch(runFacetool(image));
|
||||
const handleClickFixFaces = () => {
|
||||
currentImage && dispatch(runFacetool(currentImage));
|
||||
};
|
||||
|
||||
useHotkeys(
|
||||
'r',
|
||||
() => {
|
||||
if (
|
||||
isGFPGANAvailable &&
|
||||
Boolean(!intermediateImage) &&
|
||||
!shouldDisableToolbarButtons &&
|
||||
isConnected &&
|
||||
!isProcessing &&
|
||||
facetoolStrength
|
||||
@ -240,9 +294,9 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
||||
}
|
||||
},
|
||||
[
|
||||
image,
|
||||
currentImage,
|
||||
isGFPGANAvailable,
|
||||
intermediateImage,
|
||||
shouldDisableToolbarButtons,
|
||||
isConnected,
|
||||
isProcessing,
|
||||
facetoolStrength,
|
||||
@ -253,10 +307,13 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
||||
dispatch(setShouldShowImageDetails(!shouldShowImageDetails));
|
||||
|
||||
const handleSendToInpainting = () => {
|
||||
dispatch(setImageToInpaint(image));
|
||||
if (activeTabName !== 'inpainting') {
|
||||
dispatch(setActiveTab('inpainting'));
|
||||
}
|
||||
if (!currentImage) return;
|
||||
|
||||
dispatch(setImageToInpaint(currentImage));
|
||||
|
||||
dispatch(setActiveTab('inpainting'));
|
||||
dispatch(setNeedsCache(true));
|
||||
|
||||
toast({
|
||||
title: 'Sent to Inpainting',
|
||||
status: 'success',
|
||||
@ -268,7 +325,7 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
||||
useHotkeys(
|
||||
'i',
|
||||
() => {
|
||||
if (image) {
|
||||
if (currentImage) {
|
||||
handleClickShowImageDetails();
|
||||
} else {
|
||||
toast({
|
||||
@ -279,111 +336,141 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
||||
});
|
||||
}
|
||||
},
|
||||
[image, shouldShowImageDetails]
|
||||
[currentImage, shouldShowImageDetails]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="current-image-options">
|
||||
<IAIIconButton
|
||||
icon={<MdImage />}
|
||||
tooltip="Send To Image To Image"
|
||||
aria-label="Send To Image To Image"
|
||||
onClick={handleClickUseAsInitialImage}
|
||||
/>
|
||||
<ButtonGroup isAttached={true}>
|
||||
<IAIPopover
|
||||
trigger="hover"
|
||||
triggerComponent={
|
||||
<IAIIconButton aria-label="Send to..." icon={<FaShareAlt />} />
|
||||
}
|
||||
>
|
||||
<div className="current-image-send-to-popover">
|
||||
<IAIButton
|
||||
size={'sm'}
|
||||
onClick={handleClickUseAsInitialImage}
|
||||
leftIcon={<FaShare />}
|
||||
>
|
||||
Send to Image to Image
|
||||
</IAIButton>
|
||||
<IAIButton
|
||||
size={'sm'}
|
||||
onClick={handleSendToInpainting}
|
||||
leftIcon={<FaShare />}
|
||||
>
|
||||
Send to Inpainting
|
||||
</IAIButton>
|
||||
<IAIButton
|
||||
size={'sm'}
|
||||
onClick={handleCopyImageLink}
|
||||
leftIcon={<FaCopy />}
|
||||
>
|
||||
Copy Link to Image
|
||||
</IAIButton>
|
||||
|
||||
<IAIButton leftIcon={<FaDownload />} size={'sm'}>
|
||||
<Link download={true} href={currentImage?.url}>
|
||||
Download Image
|
||||
</Link>
|
||||
</IAIButton>
|
||||
</div>
|
||||
</IAIPopover>
|
||||
</ButtonGroup>
|
||||
|
||||
<ButtonGroup isAttached={true}>
|
||||
<IAIIconButton
|
||||
icon={<FaQuoteRight />}
|
||||
tooltip="Use Prompt"
|
||||
aria-label="Use Prompt"
|
||||
isDisabled={!currentImage?.metadata?.image?.prompt}
|
||||
onClick={handleClickUsePrompt}
|
||||
/>
|
||||
|
||||
<IAIIconButton
|
||||
icon={<FaSeedling />}
|
||||
tooltip="Use Seed"
|
||||
aria-label="Use Seed"
|
||||
isDisabled={!currentImage?.metadata?.image?.seed}
|
||||
onClick={handleClickUseSeed}
|
||||
/>
|
||||
|
||||
<IAIIconButton
|
||||
icon={<FaAsterisk />}
|
||||
tooltip="Use All"
|
||||
aria-label="Use All"
|
||||
isDisabled={
|
||||
!['txt2img', 'img2img'].includes(
|
||||
currentImage?.metadata?.image?.type
|
||||
)
|
||||
}
|
||||
onClick={handleClickUseAllParameters}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
|
||||
<ButtonGroup isAttached={true}>
|
||||
<IAIPopover
|
||||
trigger="hover"
|
||||
triggerComponent={
|
||||
<IAIIconButton icon={<FaGrinStars />} aria-label="Restore Faces" />
|
||||
}
|
||||
>
|
||||
<div className="current-image-postprocessing-popover">
|
||||
<FaceRestoreOptions />
|
||||
<IAIButton
|
||||
isDisabled={
|
||||
!isGFPGANAvailable ||
|
||||
!currentImage ||
|
||||
!(isConnected && !isProcessing) ||
|
||||
!facetoolStrength
|
||||
}
|
||||
onClick={handleClickFixFaces}
|
||||
>
|
||||
Restore Faces
|
||||
</IAIButton>
|
||||
</div>
|
||||
</IAIPopover>
|
||||
|
||||
<IAIPopover
|
||||
trigger="hover"
|
||||
triggerComponent={
|
||||
<IAIIconButton icon={<FaExpandArrowsAlt />} aria-label="Upscale" />
|
||||
}
|
||||
>
|
||||
<div className="current-image-postprocessing-popover">
|
||||
<UpscaleOptions />
|
||||
<IAIButton
|
||||
isDisabled={
|
||||
!isESRGANAvailable ||
|
||||
!currentImage ||
|
||||
!(isConnected && !isProcessing) ||
|
||||
!upscalingLevel
|
||||
}
|
||||
onClick={handleClickUpscale}
|
||||
>
|
||||
Upscale Image
|
||||
</IAIButton>
|
||||
</div>
|
||||
</IAIPopover>
|
||||
</ButtonGroup>
|
||||
|
||||
<IAIIconButton
|
||||
icon={<FaPaintBrush />}
|
||||
tooltip="Send To Inpainting"
|
||||
aria-label="Send To Inpainting"
|
||||
onClick={handleSendToInpainting}
|
||||
/>
|
||||
|
||||
<IAIIconButton
|
||||
icon={<FaCopy />}
|
||||
tooltip="Use All"
|
||||
aria-label="Use All"
|
||||
isDisabled={
|
||||
!['txt2img', 'img2img'].includes(image?.metadata?.image?.type)
|
||||
}
|
||||
onClick={handleClickUseAllParameters}
|
||||
/>
|
||||
|
||||
<IAIIconButton
|
||||
icon={<FaSeedling />}
|
||||
tooltip="Use Seed"
|
||||
aria-label="Use Seed"
|
||||
isDisabled={!image?.metadata?.image?.seed}
|
||||
onClick={handleClickUseSeed}
|
||||
/>
|
||||
|
||||
{/* <IAIButton
|
||||
label="Use All"
|
||||
isDisabled={
|
||||
!['txt2img', 'img2img'].includes(image?.metadata?.image?.type)
|
||||
}
|
||||
onClick={handleClickUseAllParameters}
|
||||
/>
|
||||
|
||||
<IAIButton
|
||||
label="Use Seed"
|
||||
isDisabled={!image?.metadata?.image?.seed}
|
||||
onClick={handleClickUseSeed}
|
||||
/> */}
|
||||
|
||||
<InvokePopover
|
||||
title="Restore Faces"
|
||||
popoverOptions={<FaceRestoreOptions />}
|
||||
actionButton={
|
||||
<IAIButton
|
||||
label={'Restore Faces'}
|
||||
isDisabled={
|
||||
!isGFPGANAvailable ||
|
||||
Boolean(intermediateImage) ||
|
||||
!(isConnected && !isProcessing) ||
|
||||
!facetoolStrength
|
||||
}
|
||||
onClick={handleClickFixFaces}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<IAIIconButton icon={<MdFace />} aria-label="Restore Faces" />
|
||||
</InvokePopover>
|
||||
|
||||
<InvokePopover
|
||||
title="Upscale"
|
||||
styleClass="upscale-popover"
|
||||
popoverOptions={<UpscaleOptions />}
|
||||
actionButton={
|
||||
<IAIButton
|
||||
label={'Upscale Image'}
|
||||
isDisabled={
|
||||
!isESRGANAvailable ||
|
||||
Boolean(intermediateImage) ||
|
||||
!(isConnected && !isProcessing) ||
|
||||
!upscalingLevel
|
||||
}
|
||||
onClick={handleClickUpscale}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<IAIIconButton icon={<MdHd />} aria-label="Upscale" />
|
||||
</InvokePopover>
|
||||
|
||||
<IAIIconButton
|
||||
icon={<MdInfo />}
|
||||
icon={<FaCode />}
|
||||
tooltip="Details"
|
||||
aria-label="Details"
|
||||
data-selected={shouldShowImageDetails}
|
||||
onClick={handleClickShowImageDetails}
|
||||
/>
|
||||
|
||||
<DeleteImageModal image={image}>
|
||||
<DeleteImageModal image={currentImage}>
|
||||
<IAIIconButton
|
||||
icon={<MdDelete />}
|
||||
icon={<FaTrash />}
|
||||
tooltip="Delete Image"
|
||||
aria-label="Delete Image"
|
||||
isDisabled={
|
||||
Boolean(intermediateImage) || !isConnected || isProcessing
|
||||
}
|
||||
isDisabled={!currentImage || !isConnected || isProcessing}
|
||||
className="delete-image-btn"
|
||||
/>
|
||||
</DeleteImageModal>
|
||||
</div>
|
||||
|
@ -9,18 +9,6 @@
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.current-image-options {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
column-gap: 0.5rem;
|
||||
|
||||
.chakra-popover__popper {
|
||||
z-index: 11;
|
||||
}
|
||||
}
|
||||
|
||||
.current-image-preview {
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
@ -50,6 +38,7 @@
|
||||
justify-content: space-between;
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
@ -19,10 +19,9 @@ export const currentImageDisplaySelector = createSelector(
|
||||
const { shouldShowImageDetails } = options;
|
||||
|
||||
return {
|
||||
currentImage,
|
||||
intermediateImage,
|
||||
activeTabName,
|
||||
shouldShowImageDetails,
|
||||
hasAnImageToDisplay: currentImage || intermediateImage,
|
||||
};
|
||||
},
|
||||
{
|
||||
@ -36,18 +35,16 @@ export const currentImageDisplaySelector = createSelector(
|
||||
* Displays the current image if there is one, plus associated actions.
|
||||
*/
|
||||
const CurrentImageDisplay = () => {
|
||||
const { currentImage, intermediateImage, activeTabName } = useAppSelector(
|
||||
const { hasAnImageToDisplay, activeTabName } = useAppSelector(
|
||||
currentImageDisplaySelector
|
||||
);
|
||||
|
||||
const imageToDisplay = intermediateImage || currentImage;
|
||||
|
||||
return (
|
||||
<div className="current-image-area" data-tab-name={activeTabName}>
|
||||
{imageToDisplay ? (
|
||||
{hasAnImageToDisplay ? (
|
||||
<>
|
||||
<CurrentImageButtons image={imageToDisplay} />
|
||||
<CurrentImagePreview imageToDisplay={imageToDisplay} />
|
||||
<CurrentImageButtons />
|
||||
<CurrentImagePreview />
|
||||
</>
|
||||
) : (
|
||||
<div className="current-image-display-placeholder">
|
||||
|
@ -2,8 +2,12 @@ import { IconButton, Image } from '@chakra-ui/react';
|
||||
import { useState } from 'react';
|
||||
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { GalleryState, selectNextImage, selectPrevImage } from './gallerySlice';
|
||||
import * as InvokeAI from '../../app/invokeai';
|
||||
import {
|
||||
GalleryCategory,
|
||||
GalleryState,
|
||||
selectNextImage,
|
||||
selectPrevImage,
|
||||
} from './gallerySlice';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import _ from 'lodash';
|
||||
import { OptionsState } from '../options/optionsSlice';
|
||||
@ -12,20 +16,29 @@ import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
|
||||
export const imagesSelector = createSelector(
|
||||
[(state: RootState) => state.gallery, (state: RootState) => state.options],
|
||||
(gallery: GalleryState, options: OptionsState) => {
|
||||
const { currentCategory } = gallery;
|
||||
const { currentCategory, currentImage, intermediateImage } = gallery;
|
||||
const { shouldShowImageDetails } = options;
|
||||
|
||||
const tempImages = gallery.categories[currentCategory].images;
|
||||
const tempImages =
|
||||
gallery.categories[
|
||||
currentImage ? (currentImage.category as GalleryCategory) : 'result'
|
||||
].images;
|
||||
const currentImageIndex = tempImages.findIndex(
|
||||
(i) => i.uuid === gallery?.currentImage?.uuid
|
||||
);
|
||||
const imagesLength = tempImages.length;
|
||||
|
||||
return {
|
||||
imageToDisplay: intermediateImage ? intermediateImage : currentImage,
|
||||
isIntermediate: intermediateImage,
|
||||
currentCategory,
|
||||
isOnFirstImage: currentImageIndex === 0,
|
||||
isOnLastImage:
|
||||
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
|
||||
shouldShowImageDetails,
|
||||
shouldShowPrevImageButton: currentImageIndex === 0,
|
||||
shouldShowNextImageButton:
|
||||
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
|
||||
};
|
||||
},
|
||||
{
|
||||
@ -35,16 +48,16 @@ export const imagesSelector = createSelector(
|
||||
}
|
||||
);
|
||||
|
||||
interface CurrentImagePreviewProps {
|
||||
imageToDisplay: InvokeAI.Image;
|
||||
}
|
||||
|
||||
export default function CurrentImagePreview(props: CurrentImagePreviewProps) {
|
||||
const { imageToDisplay } = props;
|
||||
export default function CurrentImagePreview() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { isOnFirstImage, isOnLastImage, shouldShowImageDetails } =
|
||||
useAppSelector(imagesSelector);
|
||||
const {
|
||||
isOnFirstImage,
|
||||
isOnLastImage,
|
||||
shouldShowImageDetails,
|
||||
imageToDisplay,
|
||||
isIntermediate,
|
||||
} = useAppSelector(imagesSelector);
|
||||
|
||||
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] =
|
||||
useState<boolean>(false);
|
||||
@ -67,11 +80,13 @@ export default function CurrentImagePreview(props: CurrentImagePreviewProps) {
|
||||
|
||||
return (
|
||||
<div className={'current-image-preview'}>
|
||||
<Image
|
||||
src={imageToDisplay.url}
|
||||
width={imageToDisplay.width}
|
||||
height={imageToDisplay.height}
|
||||
/>
|
||||
{imageToDisplay && (
|
||||
<Image
|
||||
src={imageToDisplay.url}
|
||||
width={isIntermediate ? imageToDisplay.width : undefined}
|
||||
height={isIntermediate ? imageToDisplay.height : undefined}
|
||||
/>
|
||||
)}
|
||||
{!shouldShowImageDetails && (
|
||||
<div className="current-image-next-prev-buttons">
|
||||
<div
|
||||
@ -104,7 +119,7 @@ export default function CurrentImagePreview(props: CurrentImagePreviewProps) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{shouldShowImageDetails && (
|
||||
{shouldShowImageDetails && imageToDisplay && (
|
||||
<ImageMetadataViewer
|
||||
image={imageToDisplay}
|
||||
styleClass="current-image-metadata"
|
||||
|
@ -28,12 +28,18 @@ import { RootState } from '../../app/store';
|
||||
import { setShouldConfirmOnDelete, SystemState } from '../system/systemSlice';
|
||||
import * as InvokeAI from '../../app/invokeai';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import _ from 'lodash';
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
const { shouldConfirmOnDelete, isConnected, isProcessing } = system;
|
||||
return { shouldConfirmOnDelete, isConnected, isProcessing };
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
interface DeleteImageModalProps {
|
||||
@ -44,7 +50,7 @@ interface DeleteImageModalProps {
|
||||
/**
|
||||
* The image to delete.
|
||||
*/
|
||||
image: InvokeAI.Image;
|
||||
image?: InvokeAI.Image;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -67,7 +73,7 @@ const DeleteImageModal = forwardRef(
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (isConnected && !isProcessing) {
|
||||
if (isConnected && !isProcessing && image) {
|
||||
dispatch(deleteImage(image));
|
||||
}
|
||||
onClose();
|
||||
@ -89,7 +95,7 @@ const DeleteImageModal = forwardRef(
|
||||
<>
|
||||
{cloneElement(children, {
|
||||
// TODO: This feels wrong.
|
||||
onClick: handleClickDelete,
|
||||
onClick: image ? handleClickDelete : undefined,
|
||||
ref: ref,
|
||||
})}
|
||||
|
||||
|
@ -19,8 +19,6 @@
|
||||
}
|
||||
|
||||
.image-gallery-wrapper {
|
||||
z-index: 100;
|
||||
|
||||
&[data-pinned='false'] {
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
|
@ -69,9 +69,9 @@ export default function ImageGallery() {
|
||||
if (!shouldPinGallery) return;
|
||||
|
||||
if (activeTabName === 'inpainting') {
|
||||
dispatch(setGalleryWidth(220));
|
||||
setGalleryMinWidth(220);
|
||||
setGalleryMaxWidth(220);
|
||||
dispatch(setGalleryWidth(190));
|
||||
setGalleryMinWidth(190);
|
||||
setGalleryMaxWidth(190);
|
||||
} else if (activeTabName === 'img2img') {
|
||||
dispatch(
|
||||
setGalleryWidth(Math.min(Math.max(Number(galleryWidth), 0), 490))
|
||||
@ -163,6 +163,15 @@ export default function ImageGallery() {
|
||||
[shouldPinGallery]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'esc',
|
||||
() => {
|
||||
if (shouldPinGallery) return;
|
||||
dispatch(setShouldShowGallery(false));
|
||||
},
|
||||
[shouldPinGallery]
|
||||
);
|
||||
|
||||
const IMAGE_SIZE_STEP = 32;
|
||||
|
||||
useHotkeys(
|
||||
@ -261,6 +270,7 @@ export default function ImageGallery() {
|
||||
>
|
||||
<div
|
||||
className="image-gallery-wrapper"
|
||||
style={{ zIndex: shouldPinGallery ? 1 : 100 }}
|
||||
data-pinned={shouldPinGallery}
|
||||
ref={galleryRef}
|
||||
onMouseLeave={!shouldPinGallery ? setCloseGalleryTimer : undefined}
|
||||
|
@ -32,10 +32,12 @@ import {
|
||||
setUpscalingStrength,
|
||||
setWidth,
|
||||
setInitialImage,
|
||||
setShouldShowImageDetails,
|
||||
} from '../../options/optionsSlice';
|
||||
import promptToString from '../../../common/util/promptToString';
|
||||
import { seedWeightsToString } from '../../../common/util/seedWeightPairs';
|
||||
import { FaCopy } from 'react-icons/fa';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
type MetadataItemProps = {
|
||||
isLink?: boolean;
|
||||
@ -107,7 +109,10 @@ const memoEqualityCheck = (
|
||||
const ImageMetadataViewer = memo(
|
||||
({ image, styleClass }: ImageMetadataViewerProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
// const jsonBgColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.100');
|
||||
|
||||
useHotkeys('esc', () => {
|
||||
dispatch(setShouldShowImageDetails(false));
|
||||
});
|
||||
|
||||
const metadata = image?.metadata?.image || {};
|
||||
const {
|
||||
|
@ -1,35 +0,0 @@
|
||||
.popover-content {
|
||||
background-color: var(--background-color-secondary) !important;
|
||||
border: none !important;
|
||||
border-top: 0px;
|
||||
background-color: var(--tab-hover-color);
|
||||
border-radius: 0 0 0.4rem 0.4rem;
|
||||
}
|
||||
|
||||
.popover-arrow {
|
||||
background: var(--tab-hover-color) !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.popover-options {
|
||||
background: var(--tab-panel-bg);
|
||||
border-radius: 0 0 0.4rem 0.4rem;
|
||||
border: 2px solid var(--tab-hover-color);
|
||||
padding: 0.75rem 1rem 0.75rem 1rem;
|
||||
display: grid;
|
||||
grid-auto-rows: max-content;
|
||||
grid-row-gap: 0.5rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.popover-header {
|
||||
background: var(--tab-hover-color);
|
||||
border-radius: 0.4rem 0.4rem 0 0;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
padding-left: 1rem !important;
|
||||
}
|
||||
|
||||
.upscale-popover {
|
||||
width: 23rem !important;
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
import {
|
||||
Box,
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverContent,
|
||||
PopoverHeader,
|
||||
PopoverTrigger,
|
||||
} from '@chakra-ui/react';
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
type PopoverProps = {
|
||||
title?: string;
|
||||
delay?: number;
|
||||
styleClass?: string;
|
||||
popoverOptions?: ReactNode;
|
||||
actionButton?: ReactNode;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const InvokePopover = ({
|
||||
title = 'Popup',
|
||||
styleClass,
|
||||
delay = 50,
|
||||
popoverOptions,
|
||||
actionButton,
|
||||
children,
|
||||
}: PopoverProps) => {
|
||||
return (
|
||||
<Popover trigger={'hover'} closeDelay={delay}>
|
||||
<PopoverTrigger>
|
||||
<Box>{children}</Box>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className={`popover-content ${styleClass}`}>
|
||||
<PopoverArrow className="popover-arrow" />
|
||||
<PopoverHeader className="popover-header">{title}</PopoverHeader>
|
||||
<div className="popover-options">
|
||||
{popoverOptions ? popoverOptions : null}
|
||||
{actionButton}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default InvokePopover;
|
@ -4,6 +4,7 @@ import { activeTabNameSelector } from '../options/optionsSelectors';
|
||||
import { OptionsState } from '../options/optionsSlice';
|
||||
import { SystemState } from '../system/systemSlice';
|
||||
import { GalleryState } from './gallerySlice';
|
||||
import _ from 'lodash';
|
||||
|
||||
export const imageGallerySelector = createSelector(
|
||||
[
|
||||
@ -43,6 +44,11 @@ export const imageGallerySelector = createSelector(
|
||||
currentCategory,
|
||||
galleryWidth,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@ -65,5 +71,10 @@ export const hoverableImageSelector = createSelector(
|
||||
galleryImageMinimumWidth: gallery.galleryImageMinimumWidth,
|
||||
activeTabName,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
@ -1,205 +0,0 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { BiHide, BiReset, BiShow } from 'react-icons/bi';
|
||||
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAICheckbox from '../../../../common/components/IAICheckbox';
|
||||
import IAIIconButton from '../../../../common/components/IAIIconButton';
|
||||
|
||||
import IAINumberInput from '../../../../common/components/IAINumberInput';
|
||||
import IAISlider from '../../../../common/components/IAISlider';
|
||||
import { roundDownToMultiple } from '../../../../common/util/roundDownToMultiple';
|
||||
import {
|
||||
InpaintingState,
|
||||
setBoundingBoxDimensions,
|
||||
setShouldLockBoundingBox,
|
||||
setShouldShowBoundingBox,
|
||||
setShouldShowBoundingBoxFill,
|
||||
} from '../../../tabs/Inpainting/inpaintingSlice';
|
||||
|
||||
const boundingBoxDimensionsSelector = createSelector(
|
||||
(state: RootState) => state.inpainting,
|
||||
(inpainting: InpaintingState) => {
|
||||
const {
|
||||
canvasDimensions,
|
||||
boundingBoxDimensions,
|
||||
shouldShowBoundingBox,
|
||||
shouldShowBoundingBoxFill,
|
||||
pastLines,
|
||||
futureLines,
|
||||
shouldLockBoundingBox,
|
||||
} = inpainting;
|
||||
return {
|
||||
canvasDimensions,
|
||||
boundingBoxDimensions,
|
||||
shouldShowBoundingBox,
|
||||
shouldShowBoundingBoxFill,
|
||||
pastLines,
|
||||
futureLines,
|
||||
shouldLockBoundingBox,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const BoundingBoxSettings = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const {
|
||||
canvasDimensions,
|
||||
boundingBoxDimensions,
|
||||
shouldShowBoundingBox,
|
||||
shouldShowBoundingBoxFill,
|
||||
shouldLockBoundingBox,
|
||||
} = useAppSelector(boundingBoxDimensionsSelector);
|
||||
|
||||
const handleChangeBoundingBoxWidth = (v: number) => {
|
||||
dispatch(
|
||||
setBoundingBoxDimensions({
|
||||
...boundingBoxDimensions,
|
||||
width: Math.floor(v),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleChangeBoundingBoxHeight = (v: number) => {
|
||||
dispatch(
|
||||
setBoundingBoxDimensions({
|
||||
...boundingBoxDimensions,
|
||||
height: Math.floor(v),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleChangeShouldShowBoundingBoxFill = () => {
|
||||
dispatch(setShouldShowBoundingBoxFill(!shouldShowBoundingBoxFill));
|
||||
};
|
||||
|
||||
const handleChangeShouldLockBoundingBox = () => {
|
||||
dispatch(setShouldLockBoundingBox(!shouldLockBoundingBox));
|
||||
};
|
||||
|
||||
const handleResetWidth = () => {
|
||||
dispatch(
|
||||
setBoundingBoxDimensions({
|
||||
...boundingBoxDimensions,
|
||||
width: Math.floor(canvasDimensions.width),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleResetHeight = () => {
|
||||
dispatch(
|
||||
setBoundingBoxDimensions({
|
||||
...boundingBoxDimensions,
|
||||
height: Math.floor(canvasDimensions.height),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleShowBoundingBox = () =>
|
||||
dispatch(setShouldShowBoundingBox(!shouldShowBoundingBox));
|
||||
|
||||
return (
|
||||
<div className="inpainting-bounding-box-settings">
|
||||
<div className="inpainting-bounding-box-header">
|
||||
<p>Inpaint Box</p>
|
||||
<IAIIconButton
|
||||
aria-label="Toggle Bounding Box Visibility"
|
||||
icon={
|
||||
shouldShowBoundingBox ? <BiShow size={22} /> : <BiHide size={22} />
|
||||
}
|
||||
onClick={handleShowBoundingBox}
|
||||
background={'none'}
|
||||
padding={0}
|
||||
/>
|
||||
</div>
|
||||
<div className="inpainting-bounding-box-settings-items">
|
||||
<div className="inpainting-bounding-box-dimensions-slider-numberinput">
|
||||
<IAISlider
|
||||
label="Box W"
|
||||
min={64}
|
||||
max={roundDownToMultiple(canvasDimensions.width, 64)}
|
||||
step={64}
|
||||
value={boundingBoxDimensions.width}
|
||||
onChange={handleChangeBoundingBoxWidth}
|
||||
width={'5rem'}
|
||||
/>
|
||||
<IAINumberInput
|
||||
value={boundingBoxDimensions.width}
|
||||
onChange={handleChangeBoundingBoxWidth}
|
||||
min={64}
|
||||
max={roundDownToMultiple(canvasDimensions.width, 64)}
|
||||
step={64}
|
||||
width={'5rem'}
|
||||
/>
|
||||
<IAIIconButton
|
||||
size={'sm'}
|
||||
aria-label={'Reset Width'}
|
||||
tooltip={'Reset Width'}
|
||||
onClick={handleResetWidth}
|
||||
icon={<BiReset />}
|
||||
styleClass="inpainting-bounding-box-reset-icon-btn"
|
||||
isDisabled={canvasDimensions.width === boundingBoxDimensions.width}
|
||||
/>
|
||||
</div>
|
||||
<div className="inpainting-bounding-box-dimensions-slider-numberinput">
|
||||
<IAISlider
|
||||
label="Box H"
|
||||
min={64}
|
||||
max={roundDownToMultiple(canvasDimensions.height, 64)}
|
||||
step={64}
|
||||
value={boundingBoxDimensions.height}
|
||||
onChange={handleChangeBoundingBoxHeight}
|
||||
width={'5rem'}
|
||||
/>
|
||||
<IAINumberInput
|
||||
value={boundingBoxDimensions.height}
|
||||
onChange={handleChangeBoundingBoxHeight}
|
||||
min={64}
|
||||
max={roundDownToMultiple(canvasDimensions.height, 64)}
|
||||
step={64}
|
||||
padding="0"
|
||||
width={'5rem'}
|
||||
/>
|
||||
<IAIIconButton
|
||||
size={'sm'}
|
||||
aria-label={'Reset Height'}
|
||||
tooltip={'Reset Height'}
|
||||
onClick={handleResetHeight}
|
||||
icon={<BiReset />}
|
||||
styleClass="inpainting-bounding-box-reset-icon-btn"
|
||||
isDisabled={
|
||||
canvasDimensions.height === boundingBoxDimensions.height
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Flex alignItems={'center'} justifyContent={'space-between'}>
|
||||
<IAICheckbox
|
||||
label="Darken Outside Box"
|
||||
isChecked={shouldShowBoundingBoxFill}
|
||||
onChange={handleChangeShouldShowBoundingBoxFill}
|
||||
styleClass="inpainting-bounding-box-darken"
|
||||
/>
|
||||
<IAICheckbox
|
||||
label="Lock Bounding Box"
|
||||
isChecked={shouldLockBoundingBox}
|
||||
onChange={handleChangeShouldLockBoundingBox}
|
||||
styleClass="inpainting-bounding-box-darken"
|
||||
/>
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoundingBoxSettings;
|
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../../app/store';
|
||||
import IAICheckbox from '../../../../../common/components/IAICheckbox';
|
||||
import { setShouldShowBoundingBoxFill } from '../../../../tabs/Inpainting/inpaintingSlice';
|
||||
|
||||
export default function BoundingBoxDarkenOutside() {
|
||||
const dispatch = useAppDispatch();
|
||||
const shouldShowBoundingBoxFill = useAppSelector(
|
||||
(state: RootState) => state.inpainting.shouldShowBoundingBoxFill
|
||||
);
|
||||
|
||||
const handleChangeShouldShowBoundingBoxFill = () => {
|
||||
dispatch(setShouldShowBoundingBoxFill(!shouldShowBoundingBoxFill));
|
||||
};
|
||||
|
||||
return (
|
||||
<IAICheckbox
|
||||
label="Darken Outside Box"
|
||||
isChecked={shouldShowBoundingBoxFill}
|
||||
onChange={handleChangeShouldShowBoundingBoxFill}
|
||||
styleClass="inpainting-bounding-box-darken"
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
import React from 'react';
|
||||
import IAISlider from '../../../../../common/components/IAISlider';
|
||||
import IAINumberInput from '../../../../../common/components/IAINumberInput';
|
||||
import IAIIconButton from '../../../../../common/components/IAIIconButton';
|
||||
import { BiReset } from 'react-icons/bi';
|
||||
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../../app/store';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import {
|
||||
InpaintingState,
|
||||
setBoundingBoxDimensions,
|
||||
} from '../../../../tabs/Inpainting/inpaintingSlice';
|
||||
|
||||
import { roundDownToMultiple } from '../../../../../common/util/roundDownToMultiple';
|
||||
import _ from 'lodash';
|
||||
|
||||
const boundingBoxDimensionsSelector = createSelector(
|
||||
(state: RootState) => state.inpainting,
|
||||
(inpainting: InpaintingState) => {
|
||||
const { canvasDimensions, boundingBoxDimensions, shouldLockBoundingBox } =
|
||||
inpainting;
|
||||
return {
|
||||
canvasDimensions,
|
||||
boundingBoxDimensions,
|
||||
shouldLockBoundingBox,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
type BoundingBoxDimensionSlidersType = {
|
||||
dimension: 'width' | 'height';
|
||||
};
|
||||
|
||||
export default function BoundingBoxDimensionSlider(
|
||||
props: BoundingBoxDimensionSlidersType
|
||||
) {
|
||||
const { dimension } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
const { shouldLockBoundingBox, canvasDimensions, boundingBoxDimensions } =
|
||||
useAppSelector(boundingBoxDimensionsSelector);
|
||||
|
||||
const canvasDimension = canvasDimensions[dimension];
|
||||
const boundingBoxDimension = boundingBoxDimensions[dimension];
|
||||
|
||||
const handleBoundingBoxDimension = (v: number) => {
|
||||
if (dimension == 'width') {
|
||||
dispatch(
|
||||
setBoundingBoxDimensions({
|
||||
...boundingBoxDimensions,
|
||||
width: Math.floor(v),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (dimension == 'height') {
|
||||
dispatch(
|
||||
setBoundingBoxDimensions({
|
||||
...boundingBoxDimensions,
|
||||
height: Math.floor(v),
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetDimension = () => {
|
||||
if (dimension == 'width') {
|
||||
dispatch(
|
||||
setBoundingBoxDimensions({
|
||||
...boundingBoxDimensions,
|
||||
width: Math.floor(canvasDimension),
|
||||
})
|
||||
);
|
||||
}
|
||||
if (dimension == 'height') {
|
||||
dispatch(
|
||||
setBoundingBoxDimensions({
|
||||
...boundingBoxDimensions,
|
||||
height: Math.floor(canvasDimension),
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="inpainting-bounding-box-dimensions-slider-numberinput">
|
||||
<IAISlider
|
||||
isDisabled={shouldLockBoundingBox}
|
||||
label="Box H"
|
||||
min={64}
|
||||
max={roundDownToMultiple(canvasDimension, 64)}
|
||||
step={64}
|
||||
value={boundingBoxDimension}
|
||||
onChange={handleBoundingBoxDimension}
|
||||
width={'5rem'}
|
||||
/>
|
||||
<IAINumberInput
|
||||
isDisabled={shouldLockBoundingBox}
|
||||
value={boundingBoxDimension}
|
||||
onChange={handleBoundingBoxDimension}
|
||||
min={64}
|
||||
max={roundDownToMultiple(canvasDimension, 64)}
|
||||
step={64}
|
||||
padding="0"
|
||||
width={'5rem'}
|
||||
/>
|
||||
<IAIIconButton
|
||||
size={'sm'}
|
||||
aria-label={'Reset Height'}
|
||||
tooltip={'Reset Height'}
|
||||
onClick={handleResetDimension}
|
||||
icon={<BiReset />}
|
||||
styleClass="inpainting-bounding-box-reset-icon-btn"
|
||||
isDisabled={
|
||||
shouldLockBoundingBox || canvasDimension === boundingBoxDimension
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../../app/store';
|
||||
import IAICheckbox from '../../../../../common/components/IAICheckbox';
|
||||
import { setShouldLockBoundingBox } from '../../../../tabs/Inpainting/inpaintingSlice';
|
||||
|
||||
export default function BoundingBoxLock() {
|
||||
const shouldLockBoundingBox = useAppSelector(
|
||||
(state: RootState) => state.inpainting.shouldLockBoundingBox
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleChangeShouldLockBoundingBox = () => {
|
||||
dispatch(setShouldLockBoundingBox(!shouldLockBoundingBox));
|
||||
};
|
||||
return (
|
||||
<IAICheckbox
|
||||
label="Lock Bounding Box"
|
||||
isChecked={shouldLockBoundingBox}
|
||||
onChange={handleChangeShouldLockBoundingBox}
|
||||
styleClass="inpainting-bounding-box-darken"
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import BoundingBoxDarkenOutside from './BoundingBoxDarkenOutside';
|
||||
import BoundingBoxDimensionSlider from './BoundingBoxDimensionSlider';
|
||||
import BoundingBoxLock from './BoundingBoxLock';
|
||||
import BoundingBoxVisibility from './BoundingBoxVisibility';
|
||||
|
||||
const BoundingBoxSettings = () => {
|
||||
return (
|
||||
<div className="inpainting-bounding-box-settings">
|
||||
<div className="inpainting-bounding-box-header">
|
||||
<p>Inpaint Box</p>
|
||||
<BoundingBoxVisibility />
|
||||
</div>
|
||||
<div className="inpainting-bounding-box-settings-items">
|
||||
<BoundingBoxDimensionSlider dimension="width" />
|
||||
<BoundingBoxDimensionSlider dimension="height" />
|
||||
<Flex alignItems={'center'} justifyContent={'space-between'}>
|
||||
<BoundingBoxDarkenOutside />
|
||||
<BoundingBoxLock />
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoundingBoxSettings;
|
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { BiHide, BiShow } from 'react-icons/bi';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../../app/store';
|
||||
import IAIIconButton from '../../../../../common/components/IAIIconButton';
|
||||
import { setShouldShowBoundingBox } from '../../../../tabs/Inpainting/inpaintingSlice';
|
||||
|
||||
export default function BoundingBoxVisibility() {
|
||||
const shouldShowBoundingBox = useAppSelector(
|
||||
(state: RootState) => state.inpainting.shouldShowBoundingBox
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleShowBoundingBox = () =>
|
||||
dispatch(setShouldShowBoundingBox(!shouldShowBoundingBox));
|
||||
return (
|
||||
<IAIIconButton
|
||||
aria-label="Toggle Bounding Box Visibility"
|
||||
icon={shouldShowBoundingBox ? <BiShow size={22} /> : <BiHide size={22} />}
|
||||
onClick={handleShowBoundingBox}
|
||||
background={'none'}
|
||||
padding={0}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAIButton from '../../../../common/components/IAIButton';
|
||||
import {
|
||||
InpaintingState,
|
||||
setClearBrushHistory,
|
||||
} from '../../../tabs/Inpainting/inpaintingSlice';
|
||||
import _ from 'lodash';
|
||||
|
||||
const clearBrushHistorySelector = createSelector(
|
||||
(state: RootState) => state.inpainting,
|
||||
(inpainting: InpaintingState) => {
|
||||
const { pastLines, futureLines } = inpainting;
|
||||
return {
|
||||
mayClearBrushHistory:
|
||||
futureLines.length > 0 || pastLines.length > 0 ? false : true,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default function ClearBrushHistory() {
|
||||
const dispatch = useAppDispatch();
|
||||
const toast = useToast();
|
||||
|
||||
const { mayClearBrushHistory } = useAppSelector(clearBrushHistorySelector);
|
||||
|
||||
const handleClearBrushHistory = () => {
|
||||
dispatch(setClearBrushHistory());
|
||||
toast({
|
||||
title: 'Brush Stroke History Cleared',
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
});
|
||||
};
|
||||
return (
|
||||
<IAIButton
|
||||
onClick={handleClearBrushHistory}
|
||||
tooltip="Clears brush stroke history"
|
||||
disabled={mayClearBrushHistory}
|
||||
styleClass="inpainting-options-btn"
|
||||
>
|
||||
Clear Brush History
|
||||
</IAIButton>
|
||||
);
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAINumberInput from '../../../../common/components/IAINumberInput';
|
||||
import _ from 'lodash';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import {
|
||||
InpaintingState,
|
||||
setInpaintReplace,
|
||||
setShouldUseInpaintReplace,
|
||||
} from '../../../tabs/Inpainting/inpaintingSlice';
|
||||
import IAISwitch from '../../../../common/components/IAISwitch';
|
||||
|
||||
const inpaintReplaceSelector = createSelector(
|
||||
(state: RootState) => state.inpainting,
|
||||
(inpainting: InpaintingState) => {
|
||||
const { inpaintReplace, shouldUseInpaintReplace } = inpainting;
|
||||
return {
|
||||
inpaintReplace,
|
||||
shouldUseInpaintReplace,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default function InpaintReplace() {
|
||||
const { inpaintReplace, shouldUseInpaintReplace } = useAppSelector(
|
||||
inpaintReplaceSelector
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 1rem 0 0.2rem',
|
||||
}}
|
||||
>
|
||||
<IAINumberInput
|
||||
label="Inpaint Replace"
|
||||
value={inpaintReplace}
|
||||
min={0}
|
||||
max={1.0}
|
||||
step={0.05}
|
||||
width={'auto'}
|
||||
formControlProps={{ style: { paddingRight: '1rem' } }}
|
||||
isInteger={false}
|
||||
isDisabled={!shouldUseInpaintReplace}
|
||||
onChange={(v: number) => {
|
||||
dispatch(setInpaintReplace(v));
|
||||
}}
|
||||
/>
|
||||
<IAISwitch
|
||||
isChecked={shouldUseInpaintReplace}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldUseInpaintReplace(e.target.checked))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,96 +1,13 @@
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAIButton from '../../../../common/components/IAIButton';
|
||||
import {
|
||||
InpaintingState,
|
||||
setClearBrushHistory,
|
||||
setInpaintReplace,
|
||||
setShouldUseInpaintReplace,
|
||||
} from '../../../tabs/Inpainting/inpaintingSlice';
|
||||
import BoundingBoxSettings from './BoundingBoxSettings';
|
||||
import _ from 'lodash';
|
||||
import IAINumberInput from '../../../../common/components/IAINumberInput';
|
||||
import IAISwitch from '../../../../common/components/IAISwitch';
|
||||
|
||||
const inpaintingSelector = createSelector(
|
||||
(state: RootState) => state.inpainting,
|
||||
(inpainting: InpaintingState) => {
|
||||
const { pastLines, futureLines, inpaintReplace, shouldUseInpaintReplace } =
|
||||
inpainting;
|
||||
return {
|
||||
pastLines,
|
||||
futureLines,
|
||||
inpaintReplace,
|
||||
shouldUseInpaintReplace,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
import BoundingBoxSettings from './BoundingBoxSettings/BoundingBoxSettings';
|
||||
import InpaintReplace from './InpaintReplace';
|
||||
import ClearBrushHistory from './ClearBrushHistory';
|
||||
|
||||
export default function InpaintingSettings() {
|
||||
const dispatch = useAppDispatch();
|
||||
const toast = useToast();
|
||||
|
||||
const { pastLines, futureLines, inpaintReplace, shouldUseInpaintReplace } =
|
||||
useAppSelector(inpaintingSelector);
|
||||
|
||||
const handleClearBrushHistory = () => {
|
||||
dispatch(setClearBrushHistory());
|
||||
toast({
|
||||
title: 'Brush Stroke History Cleared',
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 1rem 0 0.2rem',
|
||||
}}
|
||||
>
|
||||
<IAINumberInput
|
||||
label="Inpaint Replace"
|
||||
value={inpaintReplace}
|
||||
min={0}
|
||||
max={1.0}
|
||||
step={0.05}
|
||||
width={'auto'}
|
||||
formControlProps={{ style: { paddingRight: '1rem' } }}
|
||||
isInteger={false}
|
||||
isDisabled={!shouldUseInpaintReplace}
|
||||
onChange={(v: number) => {
|
||||
dispatch(setInpaintReplace(v));
|
||||
}}
|
||||
/>
|
||||
<IAISwitch
|
||||
isChecked={shouldUseInpaintReplace}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldUseInpaintReplace(e.target.checked))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<InpaintReplace />
|
||||
<BoundingBoxSettings />
|
||||
<IAIButton
|
||||
label="Clear Brush History"
|
||||
onClick={handleClearBrushHistory}
|
||||
tooltip="Clears brush stroke history"
|
||||
disabled={futureLines.length > 0 || pastLines.length > 0 ? false : true}
|
||||
styleClass="inpainting-options-btn"
|
||||
/>
|
||||
<ClearBrushHistory />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import { setHeight } from '../optionsSlice';
|
||||
import { fontSize } from './MainOptions';
|
||||
|
||||
export default function MainHeight() {
|
||||
const { height } = useAppSelector((state: RootState) => state.options);
|
||||
const height = useAppSelector((state: RootState) => state.options.height);
|
||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
@ -1,13 +1,33 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import IAINumberInput from '../../../common/components/IAINumberInput';
|
||||
import { setIterations } from '../optionsSlice';
|
||||
import { mayGenerateMultipleImagesSelector } from '../optionsSelectors';
|
||||
import { OptionsState, setIterations } from '../optionsSlice';
|
||||
import { fontSize, inputWidth } from './MainOptions';
|
||||
|
||||
const mainIterationsSelector = createSelector(
|
||||
[(state: RootState) => state.options, mayGenerateMultipleImagesSelector],
|
||||
(options: OptionsState, mayGenerateMultipleImages) => {
|
||||
const { iterations } = options;
|
||||
|
||||
return {
|
||||
iterations,
|
||||
mayGenerateMultipleImages,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default function MainIterations() {
|
||||
const dispatch = useAppDispatch();
|
||||
const iterations = useAppSelector(
|
||||
(state: RootState) => state.options.iterations
|
||||
const { iterations, mayGenerateMultipleImages } = useAppSelector(
|
||||
mainIterationsSelector
|
||||
);
|
||||
|
||||
const handleChangeIterations = (v: number) => dispatch(setIterations(v));
|
||||
@ -18,6 +38,7 @@ export default function MainIterations() {
|
||||
step={1}
|
||||
min={1}
|
||||
max={9999}
|
||||
isDisabled={!mayGenerateMultipleImages}
|
||||
onChange={handleChangeIterations}
|
||||
value={iterations}
|
||||
width={inputWidth}
|
||||
|
@ -19,6 +19,7 @@
|
||||
|
||||
.main-option-block {
|
||||
border-radius: 0.5rem;
|
||||
display: grid !important;
|
||||
grid-template-columns: auto !important;
|
||||
row-gap: 0.4rem;
|
||||
|
||||
|
@ -2,14 +2,14 @@ import React, { ChangeEvent } from 'react';
|
||||
import { WIDTHS } from '../../../app/constants';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import IAISelect from '../../../common/components/IAISelect';
|
||||
import { tabMap } from '../../tabs/InvokeTabs';
|
||||
import { activeTabNameSelector } from '../optionsSelectors';
|
||||
import { setWidth } from '../optionsSlice';
|
||||
import { fontSize } from './MainOptions';
|
||||
|
||||
export default function MainWidth() {
|
||||
const { width, activeTab } = useAppSelector(
|
||||
(state: RootState) => state.options
|
||||
);
|
||||
const width = useAppSelector((state: RootState) => state.options.width);
|
||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleChangeWidth = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||
@ -17,7 +17,7 @@ export default function MainWidth() {
|
||||
|
||||
return (
|
||||
<IAISelect
|
||||
isDisabled={tabMap[activeTab] === 'inpainting'}
|
||||
isDisabled={activeTabName === 'inpainting'}
|
||||
label="Width"
|
||||
value={width}
|
||||
flexGrow={1}
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { MdCancel } from 'react-icons/md';
|
||||
import { cancelProcessing } from '../../../app/socketio/actions';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import IAIIconButton from '../../../common/components/IAIIconButton';
|
||||
import IAIIconButton, {
|
||||
IAIIconButtonProps,
|
||||
} from '../../../common/components/IAIIconButton';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { SystemState } from '../../system/systemSlice';
|
||||
import _ from 'lodash';
|
||||
import { IAIButtonProps } from '../../../common/components/IAIButton';
|
||||
|
||||
const cancelButtonSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
@ -24,7 +25,9 @@ const cancelButtonSelector = createSelector(
|
||||
}
|
||||
);
|
||||
|
||||
export default function CancelButton(props: Omit<IAIButtonProps, 'label'>) {
|
||||
export default function CancelButton(
|
||||
props: Omit<IAIIconButtonProps, 'aria-label'>
|
||||
) {
|
||||
const { ...rest } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
const { isProcessing, isConnected, isCancelable } =
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { ListItem, UnorderedList } from '@chakra-ui/react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { FaPlay } from 'react-icons/fa';
|
||||
import { readinessSelector } from '../../../app/selectors/readinessSelector';
|
||||
@ -6,17 +7,21 @@ import { useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import IAIButton, {
|
||||
IAIButtonProps,
|
||||
} from '../../../common/components/IAIButton';
|
||||
import IAIIconButton from '../../../common/components/IAIIconButton';
|
||||
import IAIIconButton, {
|
||||
IAIIconButtonProps,
|
||||
} from '../../../common/components/IAIIconButton';
|
||||
import IAIPopover from '../../../common/components/IAIPopover';
|
||||
import { activeTabNameSelector } from '../optionsSelectors';
|
||||
|
||||
interface InvokeButton extends Omit<IAIButtonProps, 'label'> {
|
||||
interface InvokeButton
|
||||
extends Omit<IAIButtonProps | IAIIconButtonProps, 'aria-label'> {
|
||||
iconButton?: boolean;
|
||||
}
|
||||
|
||||
export default function InvokeButton(props: InvokeButton) {
|
||||
const { iconButton = false, ...rest } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
const isReady = useAppSelector(readinessSelector);
|
||||
const { isReady, reasonsWhyNotReady } = useAppSelector(readinessSelector);
|
||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||
|
||||
const handleClickGenerate = () => {
|
||||
@ -33,27 +38,62 @@ export default function InvokeButton(props: InvokeButton) {
|
||||
[isReady, activeTabName]
|
||||
);
|
||||
|
||||
return iconButton ? (
|
||||
<IAIIconButton
|
||||
aria-label="Invoke"
|
||||
type="submit"
|
||||
icon={<FaPlay />}
|
||||
isDisabled={!isReady}
|
||||
onClick={handleClickGenerate}
|
||||
className="invoke-btn invoke"
|
||||
tooltip="Invoke"
|
||||
tooltipPlacement="bottom"
|
||||
{...rest}
|
||||
/>
|
||||
) : (
|
||||
<IAIButton
|
||||
label="Invoke"
|
||||
aria-label="Invoke"
|
||||
type="submit"
|
||||
isDisabled={!isReady}
|
||||
onClick={handleClickGenerate}
|
||||
className="invoke-btn"
|
||||
{...rest}
|
||||
/>
|
||||
const buttonComponent = (
|
||||
<div style={{ flexGrow: 4 }}>
|
||||
{iconButton ? (
|
||||
<IAIIconButton
|
||||
aria-label="Invoke"
|
||||
type="submit"
|
||||
icon={<FaPlay />}
|
||||
isDisabled={!isReady}
|
||||
onClick={handleClickGenerate}
|
||||
className="invoke-btn invoke"
|
||||
tooltip="Invoke"
|
||||
tooltipProps={{ placement: 'bottom' }}
|
||||
{...rest}
|
||||
/>
|
||||
) : (
|
||||
<IAIButton
|
||||
aria-label="Invoke"
|
||||
type="submit"
|
||||
isDisabled={!isReady}
|
||||
onClick={handleClickGenerate}
|
||||
className="invoke-btn"
|
||||
{...rest}
|
||||
>
|
||||
Invoke
|
||||
</IAIButton>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return isReady ? (
|
||||
buttonComponent
|
||||
) : (
|
||||
<IAIPopover trigger="hover" triggerComponent={buttonComponent}>
|
||||
{reasonsWhyNotReady && (
|
||||
<UnorderedList>
|
||||
{reasonsWhyNotReady.map((reason, i) => (
|
||||
<ListItem key={i}>{reason}</ListItem>
|
||||
))}
|
||||
</UnorderedList>
|
||||
)}
|
||||
</IAIPopover>
|
||||
);
|
||||
|
||||
// return isReady ? (
|
||||
// buttonComponent
|
||||
// ) : (
|
||||
// <IAIPopover trigger="hover" triggerComponent={buttonComponent}>
|
||||
// {reasonsWhyNotReady ? (
|
||||
// <UnorderedList>
|
||||
// {reasonsWhyNotReady.map((reason, i) => (
|
||||
// <ListItem key={i}>{reason}</ListItem>
|
||||
// ))}
|
||||
// </UnorderedList>
|
||||
// ) : (
|
||||
// 'test'
|
||||
// )}
|
||||
// </IAIPopover>
|
||||
// );
|
||||
}
|
||||
|
@ -15,9 +15,11 @@ const LoopbackButton = () => {
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
aria-label="Loopback"
|
||||
tooltip="Loopback"
|
||||
data-selected={shouldLoopback}
|
||||
aria-label="Toggle Loopback"
|
||||
tooltip="Toggle Loopback"
|
||||
styleClass="loopback-btn"
|
||||
asCheckbox={true}
|
||||
isChecked={shouldLoopback}
|
||||
icon={<FaRecycle />}
|
||||
onClick={() => {
|
||||
dispatch(setShouldLoopback(!shouldLoopback));
|
||||
|
@ -7,6 +7,7 @@
|
||||
|
||||
.invoke-btn {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
svg {
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
@ -25,3 +26,34 @@
|
||||
// $btn-width: 3rem
|
||||
);
|
||||
}
|
||||
|
||||
.loopback-btn {
|
||||
&[data-as-checkbox='true'] {
|
||||
background-color: var(--btn-grey);
|
||||
border: 3px solid var(--btn-grey);
|
||||
svg {
|
||||
fill: var(--text-color);
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--btn-grey);
|
||||
border-color: var(--btn-checkbox-border-hover);
|
||||
svg {
|
||||
fill: var(--text-color);
|
||||
}
|
||||
}
|
||||
&[data-selected='true'] {
|
||||
border-color: var(--accent-color);
|
||||
background-color: var(--btn-grey);
|
||||
svg {
|
||||
fill: var(--text-color);
|
||||
}
|
||||
&:hover {
|
||||
border-color: var(--accent-color);
|
||||
background-color: var(--btn-grey);
|
||||
svg {
|
||||
fill: var(--text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ const promptInputSelector = createSelector(
|
||||
const PromptInput = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { prompt, activeTabName } = useAppSelector(promptInputSelector);
|
||||
const isReady = useAppSelector(readinessSelector);
|
||||
const { isReady } = useAppSelector(readinessSelector);
|
||||
|
||||
const promptRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
|
@ -13,3 +13,17 @@ export const activeTabNameSelector = createSelector(
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export const mayGenerateMultipleImagesSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => {
|
||||
const { shouldRandomizeSeed, shouldGenerateVariations } = options;
|
||||
|
||||
return shouldRandomizeSeed || shouldGenerateVariations;
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
@ -1,10 +1,3 @@
|
||||
.console-resizable {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.console {
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
@ -48,7 +41,7 @@
|
||||
position: fixed !important;
|
||||
left: 0.5rem;
|
||||
bottom: 0.5rem;
|
||||
z-index: 21;
|
||||
z-index: 10000;
|
||||
|
||||
&:hover {
|
||||
background: var(--console-icon-button-bg-color-hover) !important;
|
||||
@ -67,7 +60,7 @@
|
||||
position: fixed !important;
|
||||
left: 0.5rem;
|
||||
bottom: 3rem;
|
||||
z-index: 21;
|
||||
z-index: 10000;
|
||||
|
||||
&:hover {
|
||||
background: var(--console-icon-button-bg-color-hover) !important;
|
||||
|
@ -75,6 +75,10 @@ const Console = () => {
|
||||
[shouldShowLogViewer]
|
||||
);
|
||||
|
||||
useHotkeys('esc', () => {
|
||||
dispatch(setShouldShowLogViewer(false));
|
||||
});
|
||||
|
||||
const handleOnScroll = () => {
|
||||
if (!viewerRef.current) return;
|
||||
if (
|
||||
@ -99,7 +103,7 @@ const Console = () => {
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
zIndex: 20,
|
||||
zIndex: 9999,
|
||||
}}
|
||||
maxHeight={'90vh'}
|
||||
>
|
||||
|
@ -73,15 +73,20 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
|
||||
|
||||
const generalHotkeys = [
|
||||
{
|
||||
title: 'Set Parameters',
|
||||
desc: 'Use all parameters of the current image',
|
||||
hotkey: 'A',
|
||||
title: 'Set Prompt',
|
||||
desc: 'Use the prompt of the current image',
|
||||
hotkey: 'P',
|
||||
},
|
||||
{
|
||||
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' },
|
||||
{
|
||||
@ -95,6 +100,7 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
|
||||
hotkey: 'Shift+I',
|
||||
},
|
||||
{ title: 'Delete Image', desc: 'Delete the current image', hotkey: 'Del' },
|
||||
{ title: 'Close Panels', desc: 'Closes open panels', hotkey: 'Esc' },
|
||||
];
|
||||
|
||||
const galleryHotkeys = [
|
||||
@ -194,12 +200,12 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
|
||||
{
|
||||
title: 'Lock Bounding Box',
|
||||
desc: 'Locks the bounding box',
|
||||
hotkey: 'M',
|
||||
hotkey: 'Shift+Q',
|
||||
},
|
||||
{
|
||||
title: 'Quick Toggle Lock Bounding Box',
|
||||
desc: 'Hold to toggle locking the bounding box',
|
||||
hotkey: 'Space',
|
||||
hotkey: 'Q',
|
||||
},
|
||||
{
|
||||
title: 'Expand Inpainting Area',
|
||||
|
@ -1,24 +1,37 @@
|
||||
.model-list {
|
||||
.chakra-accordion {
|
||||
display: grid;
|
||||
row-gap: 0.5rem;
|
||||
}
|
||||
// .chakra-accordion {
|
||||
// display: grid;
|
||||
// row-gap: 0.5rem;
|
||||
// }
|
||||
|
||||
.chakra-accordion__item {
|
||||
border: none;
|
||||
border-radius: 0.3rem;
|
||||
background-color: var(--tab-hover-color);
|
||||
}
|
||||
// .chakra-accordion__item {
|
||||
// border: none;
|
||||
// }
|
||||
|
||||
// button {
|
||||
// border-radius: 0.3rem !important;
|
||||
|
||||
// &[aria-expanded='true'] {
|
||||
// // background-color: var(--tab-hover-color);
|
||||
// border-radius: 0.3rem;
|
||||
// }
|
||||
// }
|
||||
|
||||
.model-list-accordion {
|
||||
outline: none;
|
||||
padding: 0.25rem;
|
||||
|
||||
button {
|
||||
border-radius: 0.3rem !important;
|
||||
|
||||
&[aria-expanded='true'] {
|
||||
background-color: var(--tab-hover-color);
|
||||
border-radius: 0.3rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
&:hover {
|
||||
background-color: unset;
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.model-list-button {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -64,6 +77,9 @@
|
||||
}
|
||||
}
|
||||
.model-list-item-load-btn {
|
||||
button {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -73,31 +73,33 @@ const ModelList = () => {
|
||||
const { models } = useAppSelector(modelListSelector);
|
||||
|
||||
return (
|
||||
<div className="model-list">
|
||||
<Accordion allowToggle>
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<div className="model-list-button">
|
||||
<h2>Models</h2>
|
||||
<AccordionIcon />
|
||||
</div>
|
||||
</AccordionButton>
|
||||
<Accordion
|
||||
allowToggle
|
||||
className="model-list-accordion"
|
||||
variant={'unstyled'}
|
||||
>
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<div className="model-list-button">
|
||||
<h2>Models</h2>
|
||||
<AccordionIcon />
|
||||
</div>
|
||||
</AccordionButton>
|
||||
|
||||
<AccordionPanel>
|
||||
<div className="model-list-list">
|
||||
{models.map((model, i) => (
|
||||
<ModelListItem
|
||||
key={i}
|
||||
name={model.name}
|
||||
status={model.status}
|
||||
description={model.description}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
<AccordionPanel>
|
||||
<div className="model-list-list">
|
||||
{models.map((model, i) => (
|
||||
<ModelListItem
|
||||
key={i}
|
||||
name={model.name}
|
||||
status={model.status}
|
||||
description={model.description}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -14,18 +14,22 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import _, { isEqual } from 'lodash';
|
||||
import { cloneElement, ReactElement } from 'react';
|
||||
import { RootState, useAppSelector } from '../../../app/store';
|
||||
import { ChangeEvent, cloneElement, ReactElement } from 'react';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import { persistor } from '../../../main';
|
||||
import {
|
||||
InProgressImageType,
|
||||
setSaveIntermediatesInterval,
|
||||
setShouldConfirmOnDelete,
|
||||
setShouldDisplayGuides,
|
||||
setShouldDisplayInProgressType,
|
||||
SystemState,
|
||||
} from '../systemSlice';
|
||||
import ModelList from './ModelList';
|
||||
import { SettingsModalItem, SettingsModalSelectItem } from './SettingsModalItem';
|
||||
import { IN_PROGRESS_IMAGE_TYPES } from '../../../app/constants';
|
||||
import IAISwitch from '../../../common/components/IAISwitch';
|
||||
import IAISelect from '../../../common/components/IAISelect';
|
||||
import IAINumberInput from '../../../common/components/IAINumberInput';
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
@ -60,6 +64,14 @@ type SettingsModalProps = {
|
||||
* Secondary post-reset modal is included here.
|
||||
*/
|
||||
const SettingsModal = ({ children }: SettingsModalProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const saveIntermediatesInterval = useAppSelector(
|
||||
(state: RootState) => state.system.saveIntermediatesInterval
|
||||
);
|
||||
|
||||
const steps = useAppSelector((state: RootState) => state.options.steps);
|
||||
|
||||
const {
|
||||
isOpen: isSettingsModalOpen,
|
||||
onOpen: onSettingsModalOpen,
|
||||
@ -89,6 +101,12 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleChangeIntermediateSteps = (value: number) => {
|
||||
if (value > steps) value = steps;
|
||||
if (value < 1) value = 1;
|
||||
dispatch(setSaveIntermediatesInterval(value));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{cloneElement(children, {
|
||||
@ -101,31 +119,62 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
|
||||
<ModalHeader className="settings-modal-header">Settings</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody className="settings-modal-content">
|
||||
<ModelList />
|
||||
<div className="settings-modal-items">
|
||||
|
||||
<SettingsModalSelectItem
|
||||
settingTitle="Display In-Progress Images"
|
||||
validValues={IN_PROGRESS_IMAGE_TYPES}
|
||||
defaultValue={shouldDisplayInProgressType}
|
||||
dispatcher={setShouldDisplayInProgressType}
|
||||
/>
|
||||
|
||||
<SettingsModalItem
|
||||
settingTitle="Confirm on Delete"
|
||||
<div className="settings-modal-item">
|
||||
<ModelList />
|
||||
</div>
|
||||
<div
|
||||
className="settings-modal-item"
|
||||
style={{ gridAutoFlow: 'row', rowGap: '0.5rem' }}
|
||||
>
|
||||
<IAISelect
|
||||
label={'Display In-Progress Images'}
|
||||
validValues={IN_PROGRESS_IMAGE_TYPES}
|
||||
value={shouldDisplayInProgressType}
|
||||
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
|
||||
dispatch(
|
||||
setShouldDisplayInProgressType(
|
||||
e.target.value as InProgressImageType
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
{shouldDisplayInProgressType === 'full-res' && (
|
||||
<IAINumberInput
|
||||
label="Save images every n steps"
|
||||
min={1}
|
||||
max={steps}
|
||||
step={1}
|
||||
onChange={handleChangeIntermediateSteps}
|
||||
value={saveIntermediatesInterval}
|
||||
width="auto"
|
||||
textAlign="center"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<IAISwitch
|
||||
styleClass="settings-modal-item"
|
||||
label={'Confirm on Delete'}
|
||||
isChecked={shouldConfirmOnDelete}
|
||||
dispatcher={setShouldConfirmOnDelete}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldConfirmOnDelete(e.target.checked))
|
||||
}
|
||||
/>
|
||||
|
||||
<SettingsModalItem
|
||||
settingTitle="Display Help Icons"
|
||||
<IAISwitch
|
||||
styleClass="settings-modal-item"
|
||||
label={'Display Help Icons'}
|
||||
isChecked={shouldDisplayGuides}
|
||||
dispatcher={setShouldDisplayGuides}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldDisplayGuides(e.target.checked))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="settings-modal-reset">
|
||||
<Heading size={'md'}>Reset Web UI</Heading>
|
||||
<Button colorScheme="red" onClick={handleClickResetWebUI}>
|
||||
Reset Web UI
|
||||
</Button>
|
||||
<Text>
|
||||
Resetting the web UI only resets the browser's local cache of
|
||||
your images and remembered settings. It does not delete any
|
||||
@ -136,9 +185,6 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
|
||||
isn't working, please try resetting before submitting an issue
|
||||
on GitHub.
|
||||
</Text>
|
||||
<Button colorScheme="red" onClick={handleClickResetWebUI}>
|
||||
Reset Web UI
|
||||
</Button>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
|
@ -1,50 +0,0 @@
|
||||
import { useAppDispatch } from '../../../app/store';
|
||||
import IAISelect from '../../../common/components/IAISelect';
|
||||
import IAISwitch from '../../../common/components/IAISwitch';
|
||||
|
||||
export function SettingsModalItem({
|
||||
settingTitle,
|
||||
isChecked,
|
||||
dispatcher,
|
||||
}: {
|
||||
settingTitle: string;
|
||||
isChecked: boolean;
|
||||
dispatcher: any;
|
||||
}) {
|
||||
const dispatch = useAppDispatch();
|
||||
return (
|
||||
<IAISwitch
|
||||
styleClass="settings-modal-item"
|
||||
label={settingTitle}
|
||||
isChecked={isChecked}
|
||||
onChange={(e) => dispatch(dispatcher(e.target.checked))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function SettingsModalSelectItem({
|
||||
settingTitle,
|
||||
validValues,
|
||||
defaultValue,
|
||||
dispatcher,
|
||||
}: {
|
||||
settingTitle: string;
|
||||
validValues:
|
||||
Array<number | string>
|
||||
| Array<{ key: string; value: string | number }>;
|
||||
defaultValue: string;
|
||||
dispatcher: any;
|
||||
}) {
|
||||
const dispatch = useAppDispatch();
|
||||
return (
|
||||
<IAISelect
|
||||
styleClass="settings-modal-item"
|
||||
label={settingTitle}
|
||||
validValues={validValues}
|
||||
defaultValue={defaultValue}
|
||||
onChange={(e) => dispatch(dispatcher(e.target.value))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -1,11 +1,19 @@
|
||||
import { IconButton, Link, Tooltip, useColorMode } from '@chakra-ui/react';
|
||||
import { Link, useColorMode } from '@chakra-ui/react';
|
||||
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
import { FaSun, FaMoon, FaGithub, FaDiscord } from 'react-icons/fa';
|
||||
import { MdHelp, MdKeyboard, MdSettings } from 'react-icons/md';
|
||||
import {
|
||||
FaSun,
|
||||
FaMoon,
|
||||
FaGithub,
|
||||
FaDiscord,
|
||||
FaBug,
|
||||
FaKeyboard,
|
||||
FaWrench,
|
||||
} from 'react-icons/fa';
|
||||
|
||||
import InvokeAILogo from '../../assets/images/logo.png';
|
||||
import IAIIconButton from '../../common/components/IAIIconButton';
|
||||
|
||||
import HotkeysModal from './HotkeysModal/HotkeysModal';
|
||||
|
||||
@ -26,11 +34,6 @@ const SiteHeader = () => {
|
||||
[colorMode, toggleColorMode]
|
||||
);
|
||||
|
||||
const colorModeIcon = colorMode == 'light' ? <FaMoon /> : <FaSun />;
|
||||
|
||||
// Make FaMoon and FaSun icon apparent size consistent
|
||||
const colorModeIconFontSize = colorMode == 'light' ? 18 : 20;
|
||||
|
||||
return (
|
||||
<div className="site-header">
|
||||
<div className="site-header-left-side">
|
||||
@ -44,78 +47,79 @@ const SiteHeader = () => {
|
||||
<StatusIndicator />
|
||||
|
||||
<HotkeysModal>
|
||||
<IconButton
|
||||
<IAIIconButton
|
||||
aria-label="Hotkeys"
|
||||
variant="link"
|
||||
fontSize={24}
|
||||
tooltip="Hotkeys"
|
||||
size={'sm'}
|
||||
icon={<MdKeyboard />}
|
||||
variant="link"
|
||||
data-variant="link"
|
||||
fontSize={20}
|
||||
icon={<FaKeyboard />}
|
||||
/>
|
||||
</HotkeysModal>
|
||||
|
||||
<Tooltip hasArrow label="Theme" placement={'bottom'}>
|
||||
<IconButton
|
||||
aria-label="Toggle Dark Mode"
|
||||
onClick={toggleColorMode}
|
||||
variant="link"
|
||||
size={'sm'}
|
||||
fontSize={colorModeIconFontSize}
|
||||
icon={colorModeIcon}
|
||||
/>
|
||||
</Tooltip>
|
||||
<IAIIconButton
|
||||
aria-label="Toggle Dark Mode"
|
||||
tooltip="Dark Mode"
|
||||
onClick={toggleColorMode}
|
||||
variant="link"
|
||||
data-variant="link"
|
||||
fontSize={20}
|
||||
size={'sm'}
|
||||
icon={colorMode === 'light' ? <FaMoon /> : <FaSun />}
|
||||
/>
|
||||
|
||||
<Tooltip hasArrow label="Report Bug" placement={'bottom'}>
|
||||
<IconButton
|
||||
aria-label="Link to Github Issues"
|
||||
variant="link"
|
||||
fontSize={23}
|
||||
size={'sm'}
|
||||
icon={
|
||||
<Link
|
||||
isExternal
|
||||
href="http://github.com/invoke-ai/InvokeAI/issues"
|
||||
>
|
||||
<MdHelp />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
<IAIIconButton
|
||||
aria-label="Report Bug"
|
||||
tooltip="Report Bug"
|
||||
variant="link"
|
||||
data-variant="link"
|
||||
fontSize={20}
|
||||
size={'sm'}
|
||||
icon={
|
||||
<Link isExternal href="http://github.com/invoke-ai/InvokeAI/issues">
|
||||
<FaBug />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
<Tooltip hasArrow label="Github" placement={'bottom'}>
|
||||
<IconButton
|
||||
aria-label="Link to Github Repo"
|
||||
variant="link"
|
||||
fontSize={20}
|
||||
size={'sm'}
|
||||
icon={
|
||||
<Link isExternal href="http://github.com/invoke-ai/InvokeAI">
|
||||
<FaGithub />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
<IAIIconButton
|
||||
aria-label="Link to Github Repo"
|
||||
tooltip="Github"
|
||||
variant="link"
|
||||
data-variant="link"
|
||||
fontSize={20}
|
||||
size={'sm'}
|
||||
icon={
|
||||
<Link isExternal href="http://github.com/invoke-ai/InvokeAI">
|
||||
<FaGithub />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
<Tooltip hasArrow label="Discord" placement={'bottom'}>
|
||||
<IconButton
|
||||
aria-label="Link to Discord Server"
|
||||
variant="link"
|
||||
fontSize={20}
|
||||
size={'sm'}
|
||||
icon={
|
||||
<Link isExternal href="https://discord.gg/ZmtBAhwWhy">
|
||||
<FaDiscord />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
<IAIIconButton
|
||||
aria-label="Link to Discord Server"
|
||||
tooltip="Discord"
|
||||
variant="link"
|
||||
data-variant="link"
|
||||
fontSize={20}
|
||||
size={'sm'}
|
||||
icon={
|
||||
<Link isExternal href="https://discord.gg/ZmtBAhwWhy">
|
||||
<FaDiscord />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
<SettingsModal>
|
||||
<IconButton
|
||||
<IAIIconButton
|
||||
aria-label="Settings"
|
||||
tooltip="Settings"
|
||||
variant="link"
|
||||
fontSize={24}
|
||||
data-variant="link"
|
||||
fontSize={20}
|
||||
size={'sm'}
|
||||
icon={<MdSettings />}
|
||||
icon={<FaWrench />}
|
||||
/>
|
||||
</SettingsModal>
|
||||
</div>
|
||||
|
@ -15,10 +15,17 @@ export interface Log {
|
||||
[index: number]: LogEntry;
|
||||
}
|
||||
|
||||
export type ReadinessPayload = {
|
||||
isReady: boolean;
|
||||
reasonsWhyNotReady: string[];
|
||||
};
|
||||
|
||||
export type InProgressImageType = 'none' | 'full-res' | 'latents';
|
||||
|
||||
export interface SystemState
|
||||
extends InvokeAI.SystemStatus,
|
||||
InvokeAI.SystemConfig {
|
||||
shouldDisplayInProgressType: string;
|
||||
shouldDisplayInProgressType: InProgressImageType;
|
||||
log: Array<LogEntry>;
|
||||
shouldShowLogViewer: boolean;
|
||||
isGFPGANAvailable: boolean;
|
||||
@ -36,14 +43,15 @@ export interface SystemState
|
||||
shouldDisplayGuides: boolean;
|
||||
wasErrorSeen: boolean;
|
||||
isCancelable: boolean;
|
||||
saveIntermediatesInterval: number;
|
||||
}
|
||||
|
||||
const initialSystemState = {
|
||||
const initialSystemState: SystemState = {
|
||||
isConnected: false,
|
||||
isProcessing: false,
|
||||
log: [],
|
||||
shouldShowLogViewer: false,
|
||||
shouldDisplayInProgressType: "none",
|
||||
shouldDisplayInProgressType: 'latents',
|
||||
shouldDisplayGuides: true,
|
||||
isGFPGANAvailable: true,
|
||||
isESRGANAvailable: true,
|
||||
@ -65,15 +73,17 @@ const initialSystemState = {
|
||||
hasError: false,
|
||||
wasErrorSeen: true,
|
||||
isCancelable: true,
|
||||
saveIntermediatesInterval: 5,
|
||||
};
|
||||
|
||||
const initialState: SystemState = initialSystemState;
|
||||
|
||||
export const systemSlice = createSlice({
|
||||
name: 'system',
|
||||
initialState,
|
||||
initialState: initialSystemState,
|
||||
reducers: {
|
||||
setShouldDisplayInProgressType: (state, action: PayloadAction<string>) => {
|
||||
setShouldDisplayInProgressType: (
|
||||
state,
|
||||
action: PayloadAction<InProgressImageType>
|
||||
) => {
|
||||
state.shouldDisplayInProgressType = action.payload;
|
||||
},
|
||||
setIsProcessing: (state, action: PayloadAction<boolean>) => {
|
||||
@ -178,6 +188,9 @@ export const systemSlice = createSlice({
|
||||
state.isProcessing = true;
|
||||
state.currentStatusHasSteps = false;
|
||||
},
|
||||
setSaveIntermediatesInterval: (state, action: PayloadAction<number>) => {
|
||||
state.saveIntermediatesInterval = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -200,6 +213,7 @@ export const {
|
||||
setModelList,
|
||||
setIsCancelable,
|
||||
modelChangeRequested,
|
||||
setSaveIntermediatesInterval,
|
||||
} = systemSlice.actions;
|
||||
|
||||
export default systemSlice.reducer;
|
||||
|
@ -13,7 +13,7 @@ const FloatingGalleryButton = () => {
|
||||
return (
|
||||
<IAIIconButton
|
||||
tooltip="Show Gallery (G)"
|
||||
tooltipPlacement="top"
|
||||
tooltipProps={{ placement: 'top' }}
|
||||
aria-label="Show Gallery"
|
||||
styleClass="floating-show-hide-button right"
|
||||
onMouseOver={handleShowGallery}
|
||||
|
@ -36,7 +36,7 @@ const FloatingOptionsPanelButtons = () => {
|
||||
<div className="show-hide-button-options">
|
||||
<IAIIconButton
|
||||
tooltip="Show Options Panel (O)"
|
||||
tooltipPlacement="top"
|
||||
tooltipProps={{ placement: 'top' }}
|
||||
aria-label="Show Options Panel"
|
||||
onClick={handleShowOptionsPanel}
|
||||
>
|
||||
|
@ -1,20 +1,22 @@
|
||||
import { IconButton, Image, useToast } from '@chakra-ui/react';
|
||||
import React, { SyntheticEvent } from 'react';
|
||||
import { MdClear } from 'react-icons/md';
|
||||
import { Image, useToast } from '@chakra-ui/react';
|
||||
import { SyntheticEvent } from 'react';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import ImageUploaderIconButton from '../../../common/components/ImageUploaderIconButton';
|
||||
import { clearInitialImage } from '../../options/optionsSlice';
|
||||
|
||||
export default function InitImagePreview() {
|
||||
const { initialImage } = useAppSelector((state: RootState) => state.options);
|
||||
const initialImage = useAppSelector(
|
||||
(state: RootState) => state.options.initialImage
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const handleClickResetInitialImage = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
dispatch(clearInitialImage());
|
||||
};
|
||||
// const handleClickResetInitialImage = (e: SyntheticEvent) => {
|
||||
// e.stopPropagation();
|
||||
// dispatch(clearInitialImage());
|
||||
// };
|
||||
|
||||
const alertMissingInitImage = () => {
|
||||
toast({
|
||||
@ -29,13 +31,15 @@ export default function InitImagePreview() {
|
||||
return (
|
||||
<>
|
||||
<div className="init-image-preview-header">
|
||||
{/* <div className="init-image-preview-header"> */}
|
||||
<h2>Initial Image</h2>
|
||||
<IconButton
|
||||
{/* <IconButton
|
||||
isDisabled={!initialImage}
|
||||
aria-label={'Reset Initial Image'}
|
||||
onClick={handleClickResetInitialImage}
|
||||
icon={<MdClear />}
|
||||
/>
|
||||
/> */}
|
||||
<ImageUploaderIconButton />
|
||||
</div>
|
||||
{initialImage && (
|
||||
<div className="init-image-preview">
|
||||
|
@ -11,7 +11,11 @@
|
||||
.inpainting-settings {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 1rem;
|
||||
column-gap: 0.5rem;
|
||||
|
||||
svg {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.inpainting-buttons-group {
|
||||
display: flex;
|
||||
@ -29,10 +33,10 @@
|
||||
margin-left: 1rem !important;
|
||||
}
|
||||
|
||||
.inpainting-slider-numberinput {
|
||||
.inpainting-brush-options {
|
||||
display: flex;
|
||||
column-gap: 1rem;
|
||||
align-items: center;
|
||||
column-gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,33 +49,23 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.inpainting-canvas-wrapper {
|
||||
.inpainting-canvas-spiner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.inpainting-canvas-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
.inpainting-alerts {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
column-gap: 0.5rem;
|
||||
z-index: 2;
|
||||
padding: 0.5rem;
|
||||
pointer-events: none;
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
|
||||
div {
|
||||
background-color: var(--accent-color);
|
||||
color: var(--text-color);
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
.inpainting-canvas-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.inpainting-canvas-stage {
|
||||
|
@ -33,7 +33,7 @@ import InpaintingBoundingBoxPreview, {
|
||||
InpaintingBoundingBoxPreviewOverlay,
|
||||
} from './components/InpaintingBoundingBoxPreview';
|
||||
import { KonvaEventObject } from 'konva/lib/Node';
|
||||
import KeyboardEventManager from './components/KeyboardEventManager';
|
||||
import KeyboardEventManager from './KeyboardEventManager';
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
|
||||
// Use a closure allow other components to use these things... not ideal...
|
||||
@ -56,8 +56,8 @@ const InpaintingCanvas = () => {
|
||||
shouldShowBoundingBox,
|
||||
shouldShowBoundingBoxFill,
|
||||
isDrawing,
|
||||
shouldLockBoundingBox,
|
||||
boundingBoxDimensions,
|
||||
isModifyingBoundingBox,
|
||||
stageCursor,
|
||||
} = useAppSelector(inpaintingCanvasSelector);
|
||||
|
||||
const toast = useToast();
|
||||
@ -113,7 +113,7 @@ const InpaintingCanvas = () => {
|
||||
if (
|
||||
!scaledCursorPosition ||
|
||||
!maskLayerRef.current ||
|
||||
!shouldLockBoundingBox
|
||||
isModifyingBoundingBox
|
||||
)
|
||||
return;
|
||||
|
||||
@ -127,7 +127,7 @@ const InpaintingCanvas = () => {
|
||||
points: [scaledCursorPosition.x, scaledCursorPosition.y],
|
||||
})
|
||||
);
|
||||
}, [dispatch, brushSize, tool, shouldLockBoundingBox]);
|
||||
}, [dispatch, brushSize, tool, isModifyingBoundingBox]);
|
||||
|
||||
/**
|
||||
*
|
||||
@ -143,20 +143,20 @@ const InpaintingCanvas = () => {
|
||||
|
||||
dispatch(setCursorPosition(scaledCursorPosition));
|
||||
|
||||
if (!maskLayerRef.current || !shouldLockBoundingBox) {
|
||||
if (!maskLayerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastCursorPosition.current = scaledCursorPosition;
|
||||
|
||||
if (!isDrawing) return;
|
||||
if (!isDrawing || isModifyingBoundingBox) return;
|
||||
|
||||
didMouseMoveRef.current = true;
|
||||
// Extend the current line
|
||||
dispatch(
|
||||
addPointToCurrentLine([scaledCursorPosition.x, scaledCursorPosition.y])
|
||||
);
|
||||
}, [dispatch, isDrawing, shouldLockBoundingBox]);
|
||||
}, [dispatch, isDrawing, isModifyingBoundingBox]);
|
||||
|
||||
/**
|
||||
*
|
||||
@ -170,7 +170,7 @@ const InpaintingCanvas = () => {
|
||||
if (
|
||||
!scaledCursorPosition ||
|
||||
!maskLayerRef.current ||
|
||||
!shouldLockBoundingBox
|
||||
isModifyingBoundingBox
|
||||
)
|
||||
return;
|
||||
|
||||
@ -187,7 +187,7 @@ const InpaintingCanvas = () => {
|
||||
didMouseMoveRef.current = false;
|
||||
}
|
||||
dispatch(setIsDrawing(false));
|
||||
}, [dispatch, isDrawing, shouldLockBoundingBox]);
|
||||
}, [dispatch, isDrawing, isModifyingBoundingBox]);
|
||||
|
||||
/**
|
||||
*
|
||||
@ -214,7 +214,7 @@ const InpaintingCanvas = () => {
|
||||
if (
|
||||
!scaledCursorPosition ||
|
||||
!maskLayerRef.current ||
|
||||
!shouldLockBoundingBox
|
||||
isModifyingBoundingBox
|
||||
)
|
||||
return;
|
||||
|
||||
@ -230,93 +230,78 @@ const InpaintingCanvas = () => {
|
||||
);
|
||||
}
|
||||
},
|
||||
[dispatch, brushSize, tool, shouldLockBoundingBox]
|
||||
[dispatch, brushSize, tool, isModifyingBoundingBox]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="inpainting-canvas-wrapper" tabIndex={1}>
|
||||
<div className="inpainting-alerts">
|
||||
{!shouldShowMask && (
|
||||
<div style={{ pointerEvents: 'none' }}>Mask Hidden (H)</div>
|
||||
)}
|
||||
{shouldInvertMask && (
|
||||
<div style={{ pointerEvents: 'none' }}>Mask Inverted (Shift+M)</div>
|
||||
)}
|
||||
{!shouldLockBoundingBox && (
|
||||
<div style={{ pointerEvents: 'none' }}>
|
||||
{`Transforming Bounding Box ${boundingBoxDimensions.width}x${boundingBoxDimensions.height} (M)`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{canvasBgImage && (
|
||||
<Stage
|
||||
width={Math.floor(canvasBgImage.width * stageScale)}
|
||||
height={Math.floor(canvasBgImage.height * stageScale)}
|
||||
scale={{ x: stageScale, y: stageScale }}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseOut={handleMouseOutCanvas}
|
||||
onMouseLeave={handleMouseOutCanvas}
|
||||
style={{ cursor: shouldShowMask ? 'none' : 'default' }}
|
||||
className="inpainting-canvas-stage checkerboard"
|
||||
ref={stageRef}
|
||||
>
|
||||
{!shouldInvertMask && !shouldShowCheckboardTransparency && (
|
||||
<Layer name={'image-layer'} listening={false}>
|
||||
<KonvaImage listening={false} image={canvasBgImage} />
|
||||
</Layer>
|
||||
)}
|
||||
{shouldShowMask && (
|
||||
<>
|
||||
<Layer
|
||||
name={'mask-layer'}
|
||||
listening={false}
|
||||
opacity={
|
||||
shouldShowCheckboardTransparency || shouldInvertMask
|
||||
? 1
|
||||
: maskColor.a
|
||||
}
|
||||
ref={maskLayerRef}
|
||||
>
|
||||
<InpaintingCanvasLines />
|
||||
|
||||
{shouldLockBoundingBox && <InpaintingCanvasBrushPreview />}
|
||||
|
||||
{shouldInvertMask && (
|
||||
<KonvaImage
|
||||
image={canvasBgImage}
|
||||
listening={false}
|
||||
globalCompositeOperation="source-in"
|
||||
/>
|
||||
)}
|
||||
{!shouldInvertMask && shouldShowCheckboardTransparency && (
|
||||
<KonvaImage
|
||||
image={canvasBgImage}
|
||||
listening={false}
|
||||
globalCompositeOperation="source-out"
|
||||
/>
|
||||
)}
|
||||
<div className="inpainting-canvas-container">
|
||||
<div className="inpainting-canvas-wrapper">
|
||||
{canvasBgImage && (
|
||||
<Stage
|
||||
width={Math.floor(canvasBgImage.width * stageScale)}
|
||||
height={Math.floor(canvasBgImage.height * stageScale)}
|
||||
scale={{ x: stageScale, y: stageScale }}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseOut={handleMouseOutCanvas}
|
||||
onMouseLeave={handleMouseOutCanvas}
|
||||
style={{ ...(stageCursor ? { cursor: stageCursor } : {}) }}
|
||||
className="inpainting-canvas-stage checkerboard"
|
||||
ref={stageRef}
|
||||
>
|
||||
{!shouldInvertMask && !shouldShowCheckboardTransparency && (
|
||||
<Layer name={'image-layer'} listening={false}>
|
||||
<KonvaImage listening={false} image={canvasBgImage} />
|
||||
</Layer>
|
||||
{shouldShowMask && (
|
||||
)}
|
||||
{shouldShowMask && (
|
||||
<>
|
||||
<Layer
|
||||
name={'mask-layer'}
|
||||
listening={false}
|
||||
opacity={
|
||||
shouldShowCheckboardTransparency || shouldInvertMask
|
||||
? 1
|
||||
: maskColor.a
|
||||
}
|
||||
ref={maskLayerRef}
|
||||
>
|
||||
<InpaintingCanvasLines />
|
||||
|
||||
<InpaintingCanvasBrushPreview />
|
||||
|
||||
{shouldInvertMask && (
|
||||
<KonvaImage
|
||||
image={canvasBgImage}
|
||||
listening={false}
|
||||
globalCompositeOperation="source-in"
|
||||
/>
|
||||
)}
|
||||
{!shouldInvertMask && shouldShowCheckboardTransparency && (
|
||||
<KonvaImage
|
||||
image={canvasBgImage}
|
||||
listening={false}
|
||||
globalCompositeOperation="source-out"
|
||||
/>
|
||||
)}
|
||||
</Layer>
|
||||
<Layer>
|
||||
{shouldShowBoundingBoxFill && shouldShowBoundingBox && (
|
||||
<InpaintingBoundingBoxPreviewOverlay />
|
||||
)}
|
||||
{shouldShowBoundingBox && <InpaintingBoundingBoxPreview />}
|
||||
{shouldLockBoundingBox && (
|
||||
<InpaintingCanvasBrushPreviewOutline />
|
||||
)}
|
||||
|
||||
<InpaintingCanvasBrushPreviewOutline />
|
||||
</Layer>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Stage>
|
||||
)}
|
||||
<Cacher />
|
||||
<KeyboardEventManager />
|
||||
</>
|
||||
)}
|
||||
</Stage>
|
||||
)}
|
||||
<Cacher />
|
||||
<KeyboardEventManager />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,29 @@
|
||||
.inpainting-alerts {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
margin: 0.5rem;
|
||||
|
||||
button {
|
||||
background-color: var(--inpainting-alerts-bg);
|
||||
|
||||
svg {
|
||||
fill: var(--inpainting-alerts-icon-color);
|
||||
}
|
||||
|
||||
&[data-selected='true'] {
|
||||
background-color: var(--inpainting-alerts-bg-active);
|
||||
svg {
|
||||
fill: var(--inpainting-alerts-icon-active);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-alert='true'] {
|
||||
background-color: var(--inpainting-alerts-bg-alert);
|
||||
svg {
|
||||
fill: var(--inpainting-alerts-icon-alert);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
const inpaintingCanvasStatusIconsSelector = createSelector(
|
||||
(state: RootState) => state.inpainting,
|
||||
(inpainting: InpaintingState) => {
|
||||
const {
|
||||
shouldShowMask,
|
||||
shouldInvertMask,
|
||||
shouldLockBoundingBox,
|
||||
shouldShowBoundingBox,
|
||||
boundingBoxDimensions,
|
||||
} = inpainting;
|
||||
|
||||
return {
|
||||
shouldShowMask,
|
||||
shouldInvertMask,
|
||||
shouldLockBoundingBox,
|
||||
shouldShowBoundingBox,
|
||||
isBoundingBoxTooSmall:
|
||||
boundingBoxDimensions.width < 512 || boundingBoxDimensions.height < 512,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
import { ButtonGroup, IconButton, Tooltip } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { BiHide, BiShow } from 'react-icons/bi';
|
||||
import { GiResize } from 'react-icons/gi';
|
||||
import { BsBoundingBox } from 'react-icons/bs';
|
||||
import { FaLock, FaUnlock } from 'react-icons/fa';
|
||||
import { MdInvertColors, MdInvertColorsOff } from 'react-icons/md';
|
||||
import { RootState, useAppSelector } from '../../../app/store';
|
||||
import { InpaintingState } from './inpaintingSlice';
|
||||
import { MouseEvent, useRef, useState } from 'react';
|
||||
|
||||
const InpaintingCanvasStatusIcons = () => {
|
||||
const {
|
||||
shouldShowMask,
|
||||
shouldInvertMask,
|
||||
shouldLockBoundingBox,
|
||||
shouldShowBoundingBox,
|
||||
isBoundingBoxTooSmall,
|
||||
} = useAppSelector(inpaintingCanvasStatusIconsSelector);
|
||||
|
||||
const [shouldAcceptPointerEvents, setShouldAcceptPointerEvents] =
|
||||
useState<boolean>(false);
|
||||
const timeoutRef = useRef<number>(0);
|
||||
|
||||
const handleMouseOver = () => {
|
||||
if (!shouldAcceptPointerEvents) {
|
||||
timeoutRef.current = window.setTimeout(
|
||||
() => setShouldAcceptPointerEvents(true),
|
||||
1000
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseOut = () => {
|
||||
if (!shouldAcceptPointerEvents) {
|
||||
setShouldAcceptPointerEvents(false);
|
||||
window.clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="inpainting-alerts"
|
||||
onMouseOver={handleMouseOver}
|
||||
onMouseOut={handleMouseOut}
|
||||
onMouseLeave={handleMouseOut}
|
||||
onBlur={handleMouseOut}
|
||||
>
|
||||
<ButtonGroup
|
||||
isAttached
|
||||
pointerEvents={shouldAcceptPointerEvents ? 'auto' : 'none'}
|
||||
>
|
||||
<Tooltip label="Mask Hidden">
|
||||
<IconButton
|
||||
aria-label="Show/HideMask"
|
||||
size="xs"
|
||||
variant={'ghost'}
|
||||
fontSize={'1rem'}
|
||||
data-selected={!shouldShowMask}
|
||||
icon={shouldShowMask ? <BiShow /> : <BiHide />}
|
||||
/>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
aria-label="Invert Mask"
|
||||
variant={'ghost'}
|
||||
size="xs"
|
||||
fontSize={'1rem'}
|
||||
data-selected={shouldInvertMask}
|
||||
icon={shouldInvertMask ? <MdInvertColors /> : <MdInvertColorsOff />}
|
||||
/>
|
||||
<IconButton
|
||||
aria-label="Bounding Box Lock"
|
||||
size="xs"
|
||||
variant={'ghost'}
|
||||
data-selected={shouldLockBoundingBox}
|
||||
icon={shouldLockBoundingBox ? <FaLock /> : <FaUnlock />}
|
||||
/>
|
||||
<IconButton
|
||||
aria-label="Bounding Box Lock"
|
||||
size="xs"
|
||||
variant={'ghost'}
|
||||
data-alert={!shouldShowBoundingBox}
|
||||
icon={<BsBoundingBox />}
|
||||
/>
|
||||
<IconButton
|
||||
aria-label="Under 512x512"
|
||||
size="xs"
|
||||
variant={'ghost'}
|
||||
data-alert={isBoundingBoxTooSmall}
|
||||
icon={<GiResize />}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InpaintingCanvasStatusIcons;
|
@ -1,442 +1,38 @@
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import {
|
||||
FaEraser,
|
||||
FaMask,
|
||||
FaPaintBrush,
|
||||
FaPalette,
|
||||
FaPlus,
|
||||
FaRedo,
|
||||
FaTrash,
|
||||
FaUndo,
|
||||
} from 'react-icons/fa';
|
||||
import { BiHide, BiShow } from 'react-icons/bi';
|
||||
import { VscSplitHorizontal } from 'react-icons/vsc';
|
||||
import { useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import IAIIconButton from '../../../common/components/IAIIconButton';
|
||||
import {
|
||||
clearMask,
|
||||
redo,
|
||||
setMaskColor,
|
||||
setBrushSize,
|
||||
setShouldShowBrushPreview,
|
||||
setTool,
|
||||
undo,
|
||||
setShouldShowMask,
|
||||
setShouldInvertMask,
|
||||
setNeedsCache,
|
||||
toggleShouldLockBoundingBox,
|
||||
clearImageToInpaint,
|
||||
} from './inpaintingSlice';
|
||||
|
||||
import { MdInvertColors, MdInvertColorsOff } from 'react-icons/md';
|
||||
import IAISlider from '../../../common/components/IAISlider';
|
||||
import IAINumberInput from '../../../common/components/IAINumberInput';
|
||||
import { inpaintingControlsSelector } from './inpaintingSliceSelectors';
|
||||
import IAIPopover from '../../../common/components/IAIPopover';
|
||||
import IAIColorPicker from '../../../common/components/IAIColorPicker';
|
||||
import { RgbaColor } from 'react-colorful';
|
||||
import { setShowDualDisplay } from '../../options/optionsSlice';
|
||||
import { useState } from 'react';
|
||||
import InpaintingBrushControl from './InpaintingControls/InpaintingBrushControl';
|
||||
import InpaintingEraserControl from './InpaintingControls/InpaintingEraserControl';
|
||||
import InpaintingUndoControl from './InpaintingControls/InpaintingUndoControl';
|
||||
import InpaintingRedoControl from './InpaintingControls/InpaintingRedoControl';
|
||||
import { ButtonGroup } from '@chakra-ui/react';
|
||||
import InpaintingMaskClear from './InpaintingControls/InpaintingMaskControls/InpaintingMaskClear';
|
||||
import InpaintingMaskVisibilityControl from './InpaintingControls/InpaintingMaskControls/InpaintingMaskVisibilityControl';
|
||||
import InpaintingMaskInvertControl from './InpaintingControls/InpaintingMaskControls/InpaintingMaskInvertControl';
|
||||
import InpaintingLockBoundingBoxControl from './InpaintingControls/InpaintingLockBoundingBoxControl';
|
||||
import InpaintingShowHideBoundingBoxControl from './InpaintingControls/InpaintingShowHideBoundingBoxControl';
|
||||
import ImageUploaderIconButton from '../../../common/components/ImageUploaderIconButton';
|
||||
|
||||
const InpaintingControls = () => {
|
||||
const {
|
||||
tool,
|
||||
brushSize,
|
||||
maskColor,
|
||||
shouldInvertMask,
|
||||
shouldShowMask,
|
||||
canUndo,
|
||||
canRedo,
|
||||
isMaskEmpty,
|
||||
activeTabName,
|
||||
showDualDisplay,
|
||||
} = useAppSelector(inpaintingControlsSelector);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const toast = useToast();
|
||||
|
||||
// Button State Controllers
|
||||
const [maskOptionsOpen, setMaskOptionsOpen] = useState<boolean>(false);
|
||||
|
||||
/**
|
||||
* Hotkeys
|
||||
*/
|
||||
|
||||
// Decrease brush size
|
||||
useHotkeys(
|
||||
'[',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
if (brushSize - 5 > 0) {
|
||||
handleChangeBrushSize(brushSize - 5);
|
||||
} else {
|
||||
handleChangeBrushSize(1);
|
||||
}
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask, brushSize]
|
||||
);
|
||||
|
||||
// Increase brush size
|
||||
useHotkeys(
|
||||
']',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleChangeBrushSize(brushSize + 5);
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask, brushSize]
|
||||
);
|
||||
|
||||
// Decrease mask opacity
|
||||
useHotkeys(
|
||||
'shift+[',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleChangeMaskColor({
|
||||
...maskColor,
|
||||
a: Math.max(maskColor.a - 0.05, 0),
|
||||
});
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask, maskColor.a]
|
||||
);
|
||||
|
||||
// Increase mask opacity
|
||||
useHotkeys(
|
||||
'shift+]',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleChangeMaskColor({
|
||||
...maskColor,
|
||||
a: Math.min(maskColor.a + 0.05, 100),
|
||||
});
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask, maskColor.a]
|
||||
);
|
||||
|
||||
// Set tool to eraser
|
||||
useHotkeys(
|
||||
'e',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
if (activeTabName !== 'inpainting' || !shouldShowMask) return;
|
||||
handleSelectEraserTool();
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask]
|
||||
);
|
||||
|
||||
// Set tool to brush
|
||||
useHotkeys(
|
||||
'b',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleSelectBrushTool();
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask]
|
||||
);
|
||||
|
||||
// Toggle lock bounding box
|
||||
useHotkeys(
|
||||
'm',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
dispatch(toggleShouldLockBoundingBox());
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask]
|
||||
);
|
||||
|
||||
// Undo
|
||||
useHotkeys(
|
||||
'cmd+z, control+z',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleUndo();
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask && canUndo,
|
||||
},
|
||||
[activeTabName, shouldShowMask, canUndo]
|
||||
);
|
||||
|
||||
// Redo
|
||||
useHotkeys(
|
||||
'cmd+shift+z, control+shift+z, control+y, cmd+y',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleRedo();
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask && canRedo,
|
||||
},
|
||||
[activeTabName, shouldShowMask, canRedo]
|
||||
);
|
||||
|
||||
// Show/hide mask
|
||||
useHotkeys(
|
||||
'h',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleToggleShouldShowMask();
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting',
|
||||
},
|
||||
[activeTabName, shouldShowMask]
|
||||
);
|
||||
|
||||
// Invert mask
|
||||
useHotkeys(
|
||||
'shift+m',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleToggleShouldInvertMask();
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldInvertMask, shouldShowMask]
|
||||
);
|
||||
|
||||
// Clear mask
|
||||
useHotkeys(
|
||||
'shift+c',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleClearMask();
|
||||
toast({
|
||||
title: 'Mask Cleared',
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
});
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask && !isMaskEmpty,
|
||||
},
|
||||
[activeTabName, isMaskEmpty, shouldShowMask]
|
||||
);
|
||||
|
||||
// Toggle split view
|
||||
useHotkeys(
|
||||
'shift+j',
|
||||
() => {
|
||||
handleDualDisplay();
|
||||
},
|
||||
[showDualDisplay]
|
||||
);
|
||||
|
||||
const handleClearMask = () => {
|
||||
dispatch(clearMask());
|
||||
};
|
||||
|
||||
const handleSelectEraserTool = () => dispatch(setTool('eraser'));
|
||||
|
||||
const handleSelectBrushTool = () => dispatch(setTool('brush'));
|
||||
|
||||
const handleChangeBrushSize = (v: number) => {
|
||||
dispatch(setShouldShowBrushPreview(true));
|
||||
dispatch(setBrushSize(v));
|
||||
};
|
||||
|
||||
const handleToggleShouldShowMask = () =>
|
||||
dispatch(setShouldShowMask(!shouldShowMask));
|
||||
|
||||
const handleToggleShouldInvertMask = () =>
|
||||
dispatch(setShouldInvertMask(!shouldInvertMask));
|
||||
|
||||
const handleShowBrushPreview = () => {
|
||||
dispatch(setShouldShowBrushPreview(true));
|
||||
};
|
||||
|
||||
const handleHideBrushPreview = () => {
|
||||
dispatch(setShouldShowBrushPreview(false));
|
||||
};
|
||||
|
||||
const handleChangeMaskColor = (newColor: RgbaColor) => {
|
||||
dispatch(setMaskColor(newColor));
|
||||
};
|
||||
|
||||
const handleUndo = () => dispatch(undo());
|
||||
|
||||
const handleRedo = () => dispatch(redo());
|
||||
|
||||
const handleDualDisplay = () => {
|
||||
dispatch(setShowDualDisplay(!showDualDisplay));
|
||||
dispatch(setNeedsCache(true));
|
||||
};
|
||||
|
||||
const handleClearImage = () => {
|
||||
dispatch(clearImageToInpaint());
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="inpainting-settings">
|
||||
<div className="inpainting-buttons-group">
|
||||
<IAIPopover
|
||||
trigger="hover"
|
||||
onOpen={handleShowBrushPreview}
|
||||
onClose={handleHideBrushPreview}
|
||||
triggerComponent={
|
||||
<IAIIconButton
|
||||
aria-label="Brush (B)"
|
||||
tooltip="Brush (B)"
|
||||
icon={<FaPaintBrush />}
|
||||
onClick={handleSelectBrushTool}
|
||||
data-selected={tool === 'brush'}
|
||||
isDisabled={!shouldShowMask}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="inpainting-slider-numberinput">
|
||||
<IAISlider
|
||||
label="Brush Size"
|
||||
value={brushSize}
|
||||
onChange={handleChangeBrushSize}
|
||||
min={1}
|
||||
max={200}
|
||||
width="100px"
|
||||
focusThumbOnChange={false}
|
||||
isDisabled={!shouldShowMask}
|
||||
/>
|
||||
<IAINumberInput
|
||||
value={brushSize}
|
||||
onChange={handleChangeBrushSize}
|
||||
width={'80px'}
|
||||
min={1}
|
||||
max={999}
|
||||
isDisabled={!shouldShowMask}
|
||||
/>
|
||||
</div>
|
||||
</IAIPopover>
|
||||
<IAIIconButton
|
||||
aria-label="Eraser (E)"
|
||||
tooltip="Eraser (E)"
|
||||
icon={<FaEraser />}
|
||||
onClick={handleSelectEraserTool}
|
||||
data-selected={tool === 'eraser'}
|
||||
isDisabled={!shouldShowMask}
|
||||
/>
|
||||
</div>
|
||||
<div className="inpainting-buttons-group">
|
||||
<IAIPopover
|
||||
trigger="click"
|
||||
onOpen={() => setMaskOptionsOpen(true)}
|
||||
onClose={() => setMaskOptionsOpen(false)}
|
||||
triggerComponent={
|
||||
<IAIIconButton
|
||||
aria-label="Mask Options"
|
||||
tooltip="Mask Options"
|
||||
icon={<FaMask />}
|
||||
cursor={'pointer'}
|
||||
data-selected={maskOptionsOpen}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="inpainting-button-dropdown">
|
||||
<IAIIconButton
|
||||
aria-label="Hide/Show Mask (H)"
|
||||
tooltip="Hide/Show Mask (H)"
|
||||
data-selected={!shouldShowMask}
|
||||
icon={
|
||||
shouldShowMask ? <BiShow size={22} /> : <BiHide size={22} />
|
||||
}
|
||||
onClick={handleToggleShouldShowMask}
|
||||
/>
|
||||
<IAIIconButton
|
||||
tooltip="Invert Mask Display (Shift+M)"
|
||||
aria-label="Invert Mask Display (Shift+M)"
|
||||
data-selected={shouldInvertMask}
|
||||
icon={
|
||||
shouldInvertMask ? (
|
||||
<MdInvertColors size={22} />
|
||||
) : (
|
||||
<MdInvertColorsOff size={22} />
|
||||
)
|
||||
}
|
||||
onClick={handleToggleShouldInvertMask}
|
||||
isDisabled={!shouldShowMask}
|
||||
/>
|
||||
<IAIPopover
|
||||
trigger="hover"
|
||||
placement="right"
|
||||
styleClass="inpainting-color-picker"
|
||||
triggerComponent={
|
||||
<IAIIconButton
|
||||
aria-label="Mask Color"
|
||||
tooltip="Mask Color"
|
||||
icon={<FaPalette />}
|
||||
isDisabled={!shouldShowMask}
|
||||
cursor={'pointer'}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<IAIColorPicker
|
||||
color={maskColor}
|
||||
onChange={handleChangeMaskColor}
|
||||
/>
|
||||
</IAIPopover>
|
||||
</div>
|
||||
</IAIPopover>
|
||||
<IAIIconButton
|
||||
aria-label="Clear Mask (Shift+C)"
|
||||
tooltip="Clear Mask (Shift+C)"
|
||||
icon={<FaPlus size={18} style={{ transform: 'rotate(45deg)' }} />}
|
||||
onClick={handleClearMask}
|
||||
isDisabled={isMaskEmpty || !shouldShowMask}
|
||||
/>
|
||||
</div>
|
||||
<div className="inpainting-buttons-group">
|
||||
<IAIIconButton
|
||||
aria-label="Undo"
|
||||
tooltip="Undo"
|
||||
icon={<FaUndo />}
|
||||
onClick={handleUndo}
|
||||
isDisabled={!canUndo || !shouldShowMask}
|
||||
/>
|
||||
<IAIIconButton
|
||||
aria-label="Redo"
|
||||
tooltip="Redo"
|
||||
icon={<FaRedo />}
|
||||
onClick={handleRedo}
|
||||
isDisabled={!canRedo || !shouldShowMask}
|
||||
/>
|
||||
</div>
|
||||
<ButtonGroup isAttached={true}>
|
||||
<InpaintingBrushControl />
|
||||
<InpaintingEraserControl />
|
||||
</ButtonGroup>
|
||||
|
||||
<div className="inpainting-buttons-group">
|
||||
<IAIIconButton
|
||||
aria-label="Clear Image"
|
||||
tooltip="Clear Image"
|
||||
icon={<FaTrash size={16} />}
|
||||
onClick={handleClearImage}
|
||||
/>
|
||||
</div>
|
||||
<IAIIconButton
|
||||
aria-label="Split Layout (Shift+J)"
|
||||
tooltip="Split Layout (Shift+J)"
|
||||
icon={<VscSplitHorizontal />}
|
||||
data-selected={showDualDisplay}
|
||||
onClick={handleDualDisplay}
|
||||
/>
|
||||
<ButtonGroup isAttached={true}>
|
||||
<InpaintingMaskVisibilityControl />
|
||||
<InpaintingMaskInvertControl />
|
||||
<InpaintingLockBoundingBoxControl />
|
||||
<InpaintingShowHideBoundingBoxControl />
|
||||
<InpaintingMaskClear />
|
||||
</ButtonGroup>
|
||||
|
||||
<ButtonGroup isAttached={true}>
|
||||
<InpaintingUndoControl />
|
||||
<InpaintingRedoControl />
|
||||
</ButtonGroup>
|
||||
<ButtonGroup isAttached={true}>
|
||||
<ImageUploaderIconButton />
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,150 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import React from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { FaPaintBrush } from 'react-icons/fa';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAIIconButton from '../../../../common/components/IAIIconButton';
|
||||
import IAINumberInput from '../../../../common/components/IAINumberInput';
|
||||
import IAIPopover from '../../../../common/components/IAIPopover';
|
||||
import IAISlider from '../../../../common/components/IAISlider';
|
||||
import { activeTabNameSelector } from '../../../options/optionsSelectors';
|
||||
|
||||
import {
|
||||
InpaintingState,
|
||||
setBrushSize,
|
||||
setShouldShowBrushPreview,
|
||||
setTool,
|
||||
} from '../inpaintingSlice';
|
||||
|
||||
import _ from 'lodash';
|
||||
import InpaintingMaskColorPicker from './InpaintingMaskControls/InpaintingMaskColorPicker';
|
||||
|
||||
const inpaintingBrushSelector = createSelector(
|
||||
[(state: RootState) => state.inpainting, activeTabNameSelector],
|
||||
(inpainting: InpaintingState, activeTabName) => {
|
||||
const { tool, brushSize, shouldShowMask } = inpainting;
|
||||
|
||||
return {
|
||||
tool,
|
||||
brushSize,
|
||||
shouldShowMask,
|
||||
activeTabName,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default function InpaintingBrushControl() {
|
||||
const dispatch = useAppDispatch();
|
||||
const { tool, brushSize, shouldShowMask, activeTabName } = useAppSelector(
|
||||
inpaintingBrushSelector
|
||||
);
|
||||
|
||||
const handleSelectBrushTool = () => dispatch(setTool('brush'));
|
||||
|
||||
const handleShowBrushPreview = () => {
|
||||
dispatch(setShouldShowBrushPreview(true));
|
||||
};
|
||||
|
||||
const handleHideBrushPreview = () => {
|
||||
dispatch(setShouldShowBrushPreview(false));
|
||||
};
|
||||
|
||||
const handleChangeBrushSize = (v: number) => {
|
||||
dispatch(setShouldShowBrushPreview(true));
|
||||
dispatch(setBrushSize(v));
|
||||
};
|
||||
|
||||
// Hotkeys
|
||||
|
||||
// Decrease brush size
|
||||
useHotkeys(
|
||||
'[',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
if (brushSize - 5 > 0) {
|
||||
handleChangeBrushSize(brushSize - 5);
|
||||
} else {
|
||||
handleChangeBrushSize(1);
|
||||
}
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask, brushSize]
|
||||
);
|
||||
|
||||
// Increase brush size
|
||||
useHotkeys(
|
||||
']',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleChangeBrushSize(brushSize + 5);
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask, brushSize]
|
||||
);
|
||||
|
||||
// Set tool to brush
|
||||
useHotkeys(
|
||||
'b',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleSelectBrushTool();
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask]
|
||||
);
|
||||
|
||||
return (
|
||||
<IAIPopover
|
||||
trigger="hover"
|
||||
onOpen={handleShowBrushPreview}
|
||||
onClose={handleHideBrushPreview}
|
||||
triggerComponent={
|
||||
<IAIIconButton
|
||||
aria-label="Brush (B)"
|
||||
tooltip="Brush (B)"
|
||||
icon={<FaPaintBrush />}
|
||||
onClick={handleSelectBrushTool}
|
||||
data-selected={tool === 'brush'}
|
||||
isDisabled={!shouldShowMask}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="inpainting-brush-options">
|
||||
<IAISlider
|
||||
label="Brush Size"
|
||||
value={brushSize}
|
||||
onChange={handleChangeBrushSize}
|
||||
min={1}
|
||||
max={200}
|
||||
width="100px"
|
||||
focusThumbOnChange={false}
|
||||
isDisabled={!shouldShowMask}
|
||||
/>
|
||||
<IAINumberInput
|
||||
value={brushSize}
|
||||
onChange={handleChangeBrushSize}
|
||||
width={'80px'}
|
||||
min={1}
|
||||
max={999}
|
||||
isDisabled={!shouldShowMask}
|
||||
/>
|
||||
<InpaintingMaskColorPicker />
|
||||
</div>
|
||||
</IAIPopover>
|
||||
);
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { FaTrash } from 'react-icons/fa';
|
||||
import { useAppDispatch } from '../../../../app/store';
|
||||
import IAIIconButton from '../../../../common/components/IAIIconButton';
|
||||
import { clearImageToInpaint } from '../inpaintingSlice';
|
||||
|
||||
export default function InpaintingClearImageControl() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleClearImage = () => {
|
||||
dispatch(clearImageToInpaint());
|
||||
};
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
aria-label="Clear Image"
|
||||
tooltip="Clear Image"
|
||||
icon={<FaTrash size={16} />}
|
||||
onClick={handleClearImage}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import React from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { FaEraser } from 'react-icons/fa';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAIIconButton from '../../../../common/components/IAIIconButton';
|
||||
import { InpaintingState, setTool } from '../inpaintingSlice';
|
||||
|
||||
import _ from 'lodash';
|
||||
import { activeTabNameSelector } from '../../../options/optionsSelectors';
|
||||
|
||||
const inpaintingEraserSelector = createSelector(
|
||||
[(state: RootState) => state.inpainting, activeTabNameSelector],
|
||||
(inpainting: InpaintingState, activeTabName) => {
|
||||
const { tool, shouldShowMask } = inpainting;
|
||||
|
||||
return {
|
||||
tool,
|
||||
shouldShowMask,
|
||||
activeTabName,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default function InpaintingEraserControl() {
|
||||
const { tool, shouldShowMask, activeTabName } = useAppSelector(
|
||||
inpaintingEraserSelector
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleSelectEraserTool = () => dispatch(setTool('eraser'));
|
||||
|
||||
// Hotkeys
|
||||
// Set tool to eraser
|
||||
useHotkeys(
|
||||
'e',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
if (activeTabName !== 'inpainting' || !shouldShowMask) return;
|
||||
handleSelectEraserTool();
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask]
|
||||
);
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
aria-label="Eraser (E)"
|
||||
tooltip="Eraser (E)"
|
||||
icon={<FaEraser />}
|
||||
onClick={handleSelectEraserTool}
|
||||
data-selected={tool === 'eraser'}
|
||||
isDisabled={!shouldShowMask}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import { FaLock, FaUnlock } from 'react-icons/fa';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAIIconButton from '../../../../common/components/IAIIconButton';
|
||||
import { setShouldLockBoundingBox } from '../inpaintingSlice';
|
||||
|
||||
const InpaintingLockBoundingBoxControl = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const shouldLockBoundingBox = useAppSelector(
|
||||
(state: RootState) => state.inpainting.shouldLockBoundingBox
|
||||
);
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
aria-label="Lock Inpainting Box"
|
||||
tooltip="Lock Inpainting Box"
|
||||
icon={shouldLockBoundingBox ? <FaLock /> : <FaUnlock />}
|
||||
data-selected={shouldLockBoundingBox}
|
||||
onClick={() => {
|
||||
dispatch(setShouldLockBoundingBox(!shouldLockBoundingBox));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default InpaintingLockBoundingBoxControl;
|
@ -0,0 +1,38 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FaMask } from 'react-icons/fa';
|
||||
|
||||
import IAIIconButton from '../../../../common/components/IAIIconButton';
|
||||
import IAIPopover from '../../../../common/components/IAIPopover';
|
||||
|
||||
import InpaintingMaskVisibilityControl from './InpaintingMaskControls/InpaintingMaskVisibilityControl';
|
||||
import InpaintingMaskInvertControl from './InpaintingMaskControls/InpaintingMaskInvertControl';
|
||||
import InpaintingMaskColorPicker from './InpaintingMaskControls/InpaintingMaskColorPicker';
|
||||
|
||||
export default function InpaintingMaskControl() {
|
||||
const [maskOptionsOpen, setMaskOptionsOpen] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IAIPopover
|
||||
trigger="hover"
|
||||
onOpen={() => setMaskOptionsOpen(true)}
|
||||
onClose={() => setMaskOptionsOpen(false)}
|
||||
triggerComponent={
|
||||
<IAIIconButton
|
||||
aria-label="Mask Options"
|
||||
tooltip="Mask Options"
|
||||
icon={<FaMask />}
|
||||
cursor={'pointer'}
|
||||
data-selected={maskOptionsOpen}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="inpainting-button-dropdown">
|
||||
<InpaintingMaskVisibilityControl />
|
||||
<InpaintingMaskInvertControl />
|
||||
<InpaintingMaskColorPicker />
|
||||
</div>
|
||||
</IAIPopover>
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import React from 'react';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../../app/store';
|
||||
import IAIIconButton from '../../../../../common/components/IAIIconButton';
|
||||
import { activeTabNameSelector } from '../../../../options/optionsSelectors';
|
||||
import { clearMask, InpaintingState } from '../../inpaintingSlice';
|
||||
|
||||
import _ from 'lodash';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
|
||||
const inpaintingMaskClearSelector = createSelector(
|
||||
[(state: RootState) => state.inpainting, activeTabNameSelector],
|
||||
(inpainting: InpaintingState, activeTabName) => {
|
||||
const { shouldShowMask, lines } = inpainting;
|
||||
|
||||
return { shouldShowMask, activeTabName, isMaskEmpty: lines.length === 0 };
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default function InpaintingMaskClear() {
|
||||
const { shouldShowMask, activeTabName, isMaskEmpty } = useAppSelector(
|
||||
inpaintingMaskClearSelector
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const toast = useToast();
|
||||
|
||||
const handleClearMask = () => {
|
||||
dispatch(clearMask());
|
||||
};
|
||||
|
||||
// Clear mask
|
||||
useHotkeys(
|
||||
'shift+c',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleClearMask();
|
||||
toast({
|
||||
title: 'Mask Cleared',
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
});
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask && !isMaskEmpty,
|
||||
},
|
||||
[activeTabName, isMaskEmpty, shouldShowMask]
|
||||
);
|
||||
return (
|
||||
<IAIIconButton
|
||||
aria-label="Clear Mask (Shift+C)"
|
||||
tooltip="Clear Mask (Shift+C)"
|
||||
icon={<FaPlus size={20} style={{ transform: 'rotate(45deg)' }} />}
|
||||
onClick={handleClearMask}
|
||||
isDisabled={isMaskEmpty || !shouldShowMask}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
import React from 'react';
|
||||
import { RgbaColor } from 'react-colorful';
|
||||
import { FaPalette } from 'react-icons/fa';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../../app/store';
|
||||
import IAIColorPicker from '../../../../../common/components/IAIColorPicker';
|
||||
import IAIIconButton from '../../../../../common/components/IAIIconButton';
|
||||
import IAIPopover from '../../../../../common/components/IAIPopover';
|
||||
import { InpaintingState, setMaskColor } from '../../inpaintingSlice';
|
||||
|
||||
import _ from 'lodash';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { activeTabNameSelector } from '../../../../options/optionsSelectors';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
const inpaintingMaskColorPickerSelector = createSelector(
|
||||
[(state: RootState) => state.inpainting, activeTabNameSelector],
|
||||
(inpainting: InpaintingState, activeTabName) => {
|
||||
const { shouldShowMask, maskColor } = inpainting;
|
||||
|
||||
return { shouldShowMask, maskColor, activeTabName };
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default function InpaintingMaskColorPicker() {
|
||||
const { shouldShowMask, maskColor, activeTabName } = useAppSelector(
|
||||
inpaintingMaskColorPickerSelector
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
const handleChangeMaskColor = (newColor: RgbaColor) => {
|
||||
dispatch(setMaskColor(newColor));
|
||||
};
|
||||
|
||||
// Hotkeys
|
||||
// Decrease mask opacity
|
||||
useHotkeys(
|
||||
'shift+[',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleChangeMaskColor({
|
||||
...maskColor,
|
||||
a: Math.max(maskColor.a - 0.05, 0),
|
||||
});
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask, maskColor.a]
|
||||
);
|
||||
|
||||
// Increase mask opacity
|
||||
useHotkeys(
|
||||
'shift+]',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleChangeMaskColor({
|
||||
...maskColor,
|
||||
a: Math.min(maskColor.a + 0.05, 100),
|
||||
});
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask, maskColor.a]
|
||||
);
|
||||
|
||||
return (
|
||||
<IAIPopover
|
||||
trigger="hover"
|
||||
styleClass="inpainting-color-picker"
|
||||
triggerComponent={
|
||||
<IAIIconButton
|
||||
aria-label="Mask Color"
|
||||
icon={<FaPalette />}
|
||||
isDisabled={!shouldShowMask}
|
||||
cursor={'pointer'}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<IAIColorPicker color={maskColor} onChange={handleChangeMaskColor} />
|
||||
</IAIPopover>
|
||||
);
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import React from 'react';
|
||||
import { MdInvertColors, MdInvertColorsOff } from 'react-icons/md';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../../app/store';
|
||||
import IAIIconButton from '../../../../../common/components/IAIIconButton';
|
||||
import { InpaintingState, setShouldInvertMask } from '../../inpaintingSlice';
|
||||
|
||||
import _ from 'lodash';
|
||||
import { activeTabNameSelector } from '../../../../options/optionsSelectors';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
const inpaintingMaskInvertSelector = createSelector(
|
||||
[(state: RootState) => state.inpainting, activeTabNameSelector],
|
||||
(inpainting: InpaintingState, activeTabName) => {
|
||||
const { shouldShowMask, shouldInvertMask } = inpainting;
|
||||
|
||||
return { shouldInvertMask, shouldShowMask, activeTabName };
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default function InpaintingMaskInvertControl() {
|
||||
const { shouldInvertMask, shouldShowMask, activeTabName } = useAppSelector(
|
||||
inpaintingMaskInvertSelector
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleToggleShouldInvertMask = () =>
|
||||
dispatch(setShouldInvertMask(!shouldInvertMask));
|
||||
|
||||
// Invert mask
|
||||
useHotkeys(
|
||||
'shift+m',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleToggleShouldInvertMask();
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldInvertMask, shouldShowMask]
|
||||
);
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
tooltip="Invert Mask Display (Shift+M)"
|
||||
aria-label="Invert Mask Display (Shift+M)"
|
||||
data-selected={shouldInvertMask}
|
||||
icon={
|
||||
shouldInvertMask ? (
|
||||
<MdInvertColors size={22} />
|
||||
) : (
|
||||
<MdInvertColorsOff size={22} />
|
||||
)
|
||||
}
|
||||
onClick={handleToggleShouldInvertMask}
|
||||
isDisabled={!shouldShowMask}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { BiHide, BiShow } from 'react-icons/bi';
|
||||
import { createSelector } from 'reselect';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../../app/store';
|
||||
import IAIIconButton from '../../../../../common/components/IAIIconButton';
|
||||
import { activeTabNameSelector } from '../../../../options/optionsSelectors';
|
||||
import { InpaintingState, setShouldShowMask } from '../../inpaintingSlice';
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
const inpaintingMaskVisibilitySelector = createSelector(
|
||||
[(state: RootState) => state.inpainting, activeTabNameSelector],
|
||||
(inpainting: InpaintingState, activeTabName) => {
|
||||
const { shouldShowMask } = inpainting;
|
||||
|
||||
return { shouldShowMask, activeTabName };
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default function InpaintingMaskVisibilityControl() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { shouldShowMask, activeTabName } = useAppSelector(
|
||||
inpaintingMaskVisibilitySelector
|
||||
);
|
||||
|
||||
const handleToggleShouldShowMask = () =>
|
||||
dispatch(setShouldShowMask(!shouldShowMask));
|
||||
// Hotkeys
|
||||
// Show/hide mask
|
||||
useHotkeys(
|
||||
'h',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleToggleShouldShowMask();
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting',
|
||||
},
|
||||
[activeTabName, shouldShowMask]
|
||||
);
|
||||
return (
|
||||
<IAIIconButton
|
||||
aria-label="Hide Mask (H)"
|
||||
tooltip="Hide Mask (H)"
|
||||
data-alert={!shouldShowMask}
|
||||
icon={shouldShowMask ? <BiShow size={22} /> : <BiHide size={22} />}
|
||||
onClick={handleToggleShouldShowMask}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import React from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { FaRedo } from 'react-icons/fa';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAIIconButton from '../../../../common/components/IAIIconButton';
|
||||
import { activeTabNameSelector } from '../../../options/optionsSelectors';
|
||||
import { InpaintingState, redo } from '../inpaintingSlice';
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
const inpaintingRedoSelector = createSelector(
|
||||
[(state: RootState) => state.inpainting, activeTabNameSelector],
|
||||
(inpainting: InpaintingState, activeTabName) => {
|
||||
const { futureLines, shouldShowMask } = inpainting;
|
||||
|
||||
return {
|
||||
canRedo: futureLines.length > 0,
|
||||
shouldShowMask,
|
||||
activeTabName,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default function InpaintingRedoControl() {
|
||||
const dispatch = useAppDispatch();
|
||||
const { canRedo, shouldShowMask, activeTabName } = useAppSelector(
|
||||
inpaintingRedoSelector
|
||||
);
|
||||
|
||||
const handleRedo = () => dispatch(redo());
|
||||
|
||||
// Hotkeys
|
||||
|
||||
// Redo
|
||||
useHotkeys(
|
||||
'cmd+shift+z, control+shift+z, control+y, cmd+y',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleRedo();
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask && canRedo,
|
||||
},
|
||||
[activeTabName, shouldShowMask, canRedo]
|
||||
);
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
aria-label="Redo"
|
||||
tooltip="Redo"
|
||||
icon={<FaRedo />}
|
||||
onClick={handleRedo}
|
||||
isDisabled={!canRedo || !shouldShowMask}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import { FaVectorSquare } from 'react-icons/fa';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAIIconButton from '../../../../common/components/IAIIconButton';
|
||||
import { setShouldShowBoundingBox } from '../inpaintingSlice';
|
||||
|
||||
const InpaintingShowHideBoundingBoxControl = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const shouldShowBoundingBox = useAppSelector(
|
||||
(state: RootState) => state.inpainting.shouldShowBoundingBox
|
||||
);
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
aria-label="Hide Inpainting Box"
|
||||
tooltip="Hide Inpainting Box"
|
||||
icon={<FaVectorSquare />}
|
||||
data-alert={!shouldShowBoundingBox}
|
||||
onClick={() => {
|
||||
dispatch(setShouldShowBoundingBox(!shouldShowBoundingBox));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default InpaintingShowHideBoundingBoxControl;
|
@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { VscSplitHorizontal } from 'react-icons/vsc';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAIIconButton from '../../../../common/components/IAIIconButton';
|
||||
import { setShowDualDisplay } from '../../../options/optionsSlice';
|
||||
import { setNeedsCache } from '../inpaintingSlice';
|
||||
|
||||
export default function InpaintingSplitLayoutControl() {
|
||||
const dispatch = useAppDispatch();
|
||||
const showDualDisplay = useAppSelector(
|
||||
(state: RootState) => state.options.showDualDisplay
|
||||
);
|
||||
|
||||
const handleDualDisplay = () => {
|
||||
dispatch(setShowDualDisplay(!showDualDisplay));
|
||||
dispatch(setNeedsCache(true));
|
||||
};
|
||||
|
||||
// Hotkeys
|
||||
// Toggle split view
|
||||
useHotkeys(
|
||||
'shift+j',
|
||||
() => {
|
||||
handleDualDisplay();
|
||||
},
|
||||
[showDualDisplay]
|
||||
);
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
aria-label="Split Layout (Shift+J)"
|
||||
tooltip="Split Layout (Shift+J)"
|
||||
icon={<VscSplitHorizontal />}
|
||||
data-selected={showDualDisplay}
|
||||
onClick={handleDualDisplay}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import React from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { FaUndo } from 'react-icons/fa';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAIIconButton from '../../../../common/components/IAIIconButton';
|
||||
import { InpaintingState, undo } from '../inpaintingSlice';
|
||||
|
||||
import _ from 'lodash';
|
||||
import { activeTabNameSelector } from '../../../options/optionsSelectors';
|
||||
|
||||
const inpaintingUndoSelector = createSelector(
|
||||
[(state: RootState) => state.inpainting, activeTabNameSelector],
|
||||
(inpainting: InpaintingState, activeTabName) => {
|
||||
const { pastLines, shouldShowMask } = inpainting;
|
||||
|
||||
return {
|
||||
canUndo: pastLines.length > 0,
|
||||
shouldShowMask,
|
||||
activeTabName,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default function InpaintingUndoControl() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { canUndo, shouldShowMask, activeTabName } = useAppSelector(
|
||||
inpaintingUndoSelector
|
||||
);
|
||||
|
||||
const handleUndo = () => dispatch(undo());
|
||||
|
||||
// Hotkeys
|
||||
// Undo
|
||||
useHotkeys(
|
||||
'cmd+z, control+z',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
handleUndo();
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask && canUndo,
|
||||
},
|
||||
[activeTabName, shouldShowMask, canUndo]
|
||||
);
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
aria-label="Undo"
|
||||
tooltip="Undo"
|
||||
icon={<FaUndo />}
|
||||
onClick={handleUndo}
|
||||
isDisabled={!canUndo || !shouldShowMask}
|
||||
/>
|
||||
);
|
||||
}
|
@ -1,19 +1,17 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import _ from 'lodash';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import { activeTabNameSelector } from '../../../options/optionsSelectors';
|
||||
import { OptionsState } from '../../../options/optionsSlice';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import { activeTabNameSelector } from '../../options/optionsSelectors';
|
||||
import { OptionsState } from '../../options/optionsSlice';
|
||||
import {
|
||||
InpaintingState,
|
||||
setIsDrawing,
|
||||
setIsSpacebarHeld,
|
||||
setShouldLockBoundingBox,
|
||||
toggleShouldLockBoundingBox,
|
||||
toggleTool,
|
||||
} from '../inpaintingSlice';
|
||||
} from './inpaintingSlice';
|
||||
|
||||
const keyboardEventManagerSelector = createSelector(
|
||||
[
|
||||
@ -22,13 +20,18 @@ const keyboardEventManagerSelector = createSelector(
|
||||
activeTabNameSelector,
|
||||
],
|
||||
(options: OptionsState, inpainting: InpaintingState, activeTabName) => {
|
||||
const { shouldShowMask, cursorPosition, shouldLockBoundingBox } =
|
||||
inpainting;
|
||||
const {
|
||||
shouldShowMask,
|
||||
cursorPosition,
|
||||
shouldLockBoundingBox,
|
||||
shouldShowBoundingBox,
|
||||
} = inpainting;
|
||||
return {
|
||||
activeTabName,
|
||||
shouldShowMask,
|
||||
isCursorOnCanvas: Boolean(cursorPosition),
|
||||
shouldLockBoundingBox,
|
||||
shouldShowBoundingBox,
|
||||
};
|
||||
},
|
||||
{
|
||||
@ -45,15 +48,30 @@ const KeyboardEventManager = () => {
|
||||
activeTabName,
|
||||
isCursorOnCanvas,
|
||||
shouldLockBoundingBox,
|
||||
shouldShowBoundingBox,
|
||||
} = useAppSelector(keyboardEventManagerSelector);
|
||||
|
||||
const wasLastEventOverCanvas = useRef<boolean>(false);
|
||||
const lastEvent = useRef<KeyboardEvent | null>(null);
|
||||
|
||||
// Toggle lock bounding box
|
||||
useHotkeys(
|
||||
'shift+q',
|
||||
(e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
dispatch(toggleShouldLockBoundingBox());
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask]
|
||||
);
|
||||
|
||||
// Manages hold-style keyboard shortcuts
|
||||
useEffect(() => {
|
||||
const listener = (e: KeyboardEvent) => {
|
||||
if (
|
||||
!['x', ' '].includes(e.key) ||
|
||||
!['x', 'q'].includes(e.key) ||
|
||||
activeTabName !== 'inpainting' ||
|
||||
!shouldShowMask
|
||||
) {
|
||||
@ -91,13 +109,10 @@ const KeyboardEventManager = () => {
|
||||
dispatch(toggleTool());
|
||||
break;
|
||||
}
|
||||
case ' ': {
|
||||
if (!shouldShowMask) break;
|
||||
|
||||
if (e.type === 'keydown') {
|
||||
dispatch(setIsDrawing(false));
|
||||
}
|
||||
dispatch(setShouldLockBoundingBox(!shouldLockBoundingBox));
|
||||
case 'q': {
|
||||
if (!shouldShowMask || !shouldShowBoundingBox) break;
|
||||
dispatch(setIsSpacebarHeld(e.type === 'keydown'));
|
||||
dispatch(setShouldLockBoundingBox(e.type !== 'keydown'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -119,6 +134,7 @@ const KeyboardEventManager = () => {
|
||||
shouldShowMask,
|
||||
isCursorOnCanvas,
|
||||
shouldLockBoundingBox,
|
||||
shouldShowBoundingBox,
|
||||
]);
|
||||
|
||||
return null;
|
@ -33,6 +33,9 @@ const Cacher = () => {
|
||||
futureLines,
|
||||
needsCache,
|
||||
isDrawing,
|
||||
isTransformingBoundingBox,
|
||||
isMovingBoundingBox,
|
||||
shouldShowBoundingBox,
|
||||
} = useAppSelector((state: RootState) => state.inpainting);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@ -58,12 +61,15 @@ const Cacher = () => {
|
||||
imageToInpaint,
|
||||
shouldShowBrush,
|
||||
shouldShowBoundingBoxFill,
|
||||
shouldShowBoundingBox,
|
||||
shouldLockBoundingBox,
|
||||
stageScale,
|
||||
pastLines,
|
||||
futureLines,
|
||||
needsCache,
|
||||
isDrawing,
|
||||
isTransformingBoundingBox,
|
||||
isMovingBoundingBox,
|
||||
]);
|
||||
|
||||
/**
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import Konva from 'konva';
|
||||
import { Context } from 'konva/lib/Context';
|
||||
import { KonvaEventObject } from 'konva/lib/Node';
|
||||
import { Box } from 'konva/lib/shapes/Transformer';
|
||||
import { Vector2d } from 'konva/lib/types';
|
||||
@ -12,11 +13,13 @@ import {
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import { roundToMultiple } from '../../../../common/util/roundDownToMultiple';
|
||||
import { stageRef } from '../InpaintingCanvas';
|
||||
import {
|
||||
InpaintingState,
|
||||
setBoundingBoxCoordinate,
|
||||
setBoundingBoxDimensions,
|
||||
setIsMouseOverBoundingBox,
|
||||
setIsMovingBoundingBox,
|
||||
setIsTransformingBoundingBox,
|
||||
} from '../inpaintingSlice';
|
||||
import { rgbaColorToString } from '../util/colorToString';
|
||||
import {
|
||||
@ -35,6 +38,11 @@ const boundingBoxPreviewSelector = createSelector(
|
||||
stageScale,
|
||||
imageToInpaint,
|
||||
shouldLockBoundingBox,
|
||||
isDrawing,
|
||||
isTransformingBoundingBox,
|
||||
isMovingBoundingBox,
|
||||
isMouseOverBoundingBox,
|
||||
isSpacebarHeld,
|
||||
} = inpainting;
|
||||
return {
|
||||
boundingBoxCoordinate,
|
||||
@ -46,6 +54,11 @@ const boundingBoxPreviewSelector = createSelector(
|
||||
dash: DASH_WIDTH / stageScale, // scale dash lengths
|
||||
strokeWidth: 1 / stageScale, // scale stroke thickness
|
||||
shouldLockBoundingBox,
|
||||
isDrawing,
|
||||
isTransformingBoundingBox,
|
||||
isMouseOverBoundingBox,
|
||||
isMovingBoundingBox,
|
||||
isSpacebarHeld,
|
||||
};
|
||||
},
|
||||
{
|
||||
@ -93,10 +106,14 @@ const InpaintingBoundingBoxPreview = () => {
|
||||
const {
|
||||
boundingBoxCoordinate,
|
||||
boundingBoxDimensions,
|
||||
strokeWidth,
|
||||
stageScale,
|
||||
imageToInpaint,
|
||||
shouldLockBoundingBox,
|
||||
isDrawing,
|
||||
isTransformingBoundingBox,
|
||||
isMovingBoundingBox,
|
||||
isMouseOverBoundingBox,
|
||||
isSpacebarHeld,
|
||||
} = useAppSelector(boundingBoxPreviewSelector);
|
||||
|
||||
const transformerRef = useRef<Konva.Transformer>(null);
|
||||
@ -108,15 +125,6 @@ const InpaintingBoundingBoxPreview = () => {
|
||||
transformerRef.current.getLayer()?.batchDraw();
|
||||
}, [shouldLockBoundingBox]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
const container = stageRef.current?.container();
|
||||
if (!container) return;
|
||||
container.style.cursor = 'unset';
|
||||
},
|
||||
[shouldLockBoundingBox]
|
||||
);
|
||||
|
||||
const scaledStep = 64 * stageScale;
|
||||
|
||||
const handleOnDragMove = useCallback(
|
||||
@ -269,6 +277,35 @@ const InpaintingBoundingBoxPreview = () => {
|
||||
[imageToInpaint, stageScale]
|
||||
);
|
||||
|
||||
const handleStartedTransforming = (e: KonvaEventObject<MouseEvent>) => {
|
||||
e.cancelBubble = true;
|
||||
e.evt.stopImmediatePropagation();
|
||||
console.log("Started transform")
|
||||
dispatch(setIsTransformingBoundingBox(true));
|
||||
};
|
||||
|
||||
const handleEndedTransforming = (e: KonvaEventObject<MouseEvent>) => {
|
||||
dispatch(setIsTransformingBoundingBox(false));
|
||||
dispatch(setIsMouseOverBoundingBox(false));
|
||||
};
|
||||
|
||||
const handleStartedMoving = (e: KonvaEventObject<MouseEvent>) => {
|
||||
e.cancelBubble = true;
|
||||
e.evt.stopImmediatePropagation();
|
||||
dispatch(setIsMovingBoundingBox(true));
|
||||
};
|
||||
|
||||
const handleEndedModifying = (e: KonvaEventObject<MouseEvent>) => {
|
||||
dispatch(setIsTransformingBoundingBox(false));
|
||||
dispatch(setIsMovingBoundingBox(false));
|
||||
dispatch(setIsMouseOverBoundingBox(false));
|
||||
};
|
||||
|
||||
const spacebarHeldHitFunc = (context: Context, shape: Konva.Shape) => {
|
||||
context.rect(0, 0, imageToInpaint?.width, imageToInpaint?.height);
|
||||
context.fillShape(shape);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Rect
|
||||
@ -277,23 +314,28 @@ const InpaintingBoundingBoxPreview = () => {
|
||||
width={boundingBoxDimensions.width}
|
||||
height={boundingBoxDimensions.height}
|
||||
ref={shapeRef}
|
||||
stroke={'white'}
|
||||
strokeWidth={strokeWidth}
|
||||
listening={!shouldLockBoundingBox}
|
||||
onMouseEnter={(e) => {
|
||||
const container = e?.target?.getStage()?.container();
|
||||
if (!container) return;
|
||||
container.style.cursor = shouldLockBoundingBox ? 'none' : 'move';
|
||||
stroke={isMouseOverBoundingBox ? 'rgba(255,255,255,0.3)' : 'white'}
|
||||
strokeWidth={Math.floor((isMouseOverBoundingBox ? 8 : 1) / stageScale)}
|
||||
fillEnabled={isSpacebarHeld}
|
||||
hitFunc={isSpacebarHeld ? spacebarHeldHitFunc : undefined}
|
||||
hitStrokeWidth={Math.floor(13 / stageScale)}
|
||||
listening={!isDrawing && !shouldLockBoundingBox}
|
||||
onMouseOver={() => {
|
||||
dispatch(setIsMouseOverBoundingBox(true));
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
const container = e?.target?.getStage()?.container();
|
||||
if (!container) return;
|
||||
container.style.cursor = shouldLockBoundingBox ? 'none' : 'default';
|
||||
onMouseOut={() => {
|
||||
!isTransformingBoundingBox &&
|
||||
!isMovingBoundingBox &&
|
||||
dispatch(setIsMouseOverBoundingBox(false));
|
||||
}}
|
||||
draggable={!shouldLockBoundingBox}
|
||||
onMouseDown={handleStartedMoving}
|
||||
onMouseUp={handleEndedModifying}
|
||||
draggable={true}
|
||||
onDragMove={handleOnDragMove}
|
||||
dragBoundFunc={dragBoundFunc}
|
||||
onTransform={handleOnTransform}
|
||||
onDragEnd={handleEndedModifying}
|
||||
onTransformEnd={handleEndedTransforming}
|
||||
/>
|
||||
<Transformer
|
||||
ref={transformerRef}
|
||||
@ -308,10 +350,22 @@ const InpaintingBoundingBoxPreview = () => {
|
||||
flipEnabled={false}
|
||||
ignoreStroke={true}
|
||||
keepRatio={false}
|
||||
listening={!shouldLockBoundingBox}
|
||||
listening={!isDrawing && !shouldLockBoundingBox}
|
||||
onMouseDown={handleStartedTransforming}
|
||||
onMouseUp={handleEndedTransforming}
|
||||
enabledAnchors={shouldLockBoundingBox ? [] : undefined}
|
||||
boundBoxFunc={boundBoxFunc}
|
||||
anchorDragBoundFunc={anchorDragBoundFunc}
|
||||
onDragEnd={handleEndedModifying}
|
||||
onTransformEnd={handleEndedTransforming}
|
||||
onMouseOver={() => {
|
||||
dispatch(setIsMouseOverBoundingBox(true));
|
||||
}}
|
||||
onMouseOut={() => {
|
||||
!isTransformingBoundingBox &&
|
||||
!isMovingBoundingBox &&
|
||||
dispatch(setIsMouseOverBoundingBox(false));
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -11,22 +11,28 @@ const inpaintingCanvasBrushPreviewSelector = createSelector(
|
||||
const {
|
||||
cursorPosition,
|
||||
canvasDimensions: { width, height },
|
||||
shouldShowBrushPreview,
|
||||
brushSize,
|
||||
maskColor,
|
||||
tool,
|
||||
shouldShowBrush,
|
||||
isMovingBoundingBox,
|
||||
isTransformingBoundingBox,
|
||||
} = inpainting;
|
||||
|
||||
return {
|
||||
cursorPosition,
|
||||
width,
|
||||
height,
|
||||
shouldShowBrushPreview,
|
||||
brushSize,
|
||||
maskColorString: rgbaColorToRgbString(maskColor),
|
||||
tool,
|
||||
shouldShowBrush,
|
||||
shouldDrawBrushPreview:
|
||||
!(
|
||||
isMovingBoundingBox ||
|
||||
isTransformingBoundingBox ||
|
||||
!cursorPosition
|
||||
) && shouldShowBrush,
|
||||
};
|
||||
},
|
||||
{
|
||||
@ -44,16 +50,13 @@ const InpaintingCanvasBrushPreview = () => {
|
||||
cursorPosition,
|
||||
width,
|
||||
height,
|
||||
shouldShowBrushPreview,
|
||||
brushSize,
|
||||
maskColorString,
|
||||
tool,
|
||||
shouldShowBrush,
|
||||
shouldDrawBrushPreview,
|
||||
} = useAppSelector(inpaintingCanvasBrushPreviewSelector);
|
||||
|
||||
if (!shouldShowBrush || !(cursorPosition || shouldShowBrushPreview)) {
|
||||
return null;
|
||||
}
|
||||
if (!shouldDrawBrushPreview) return null;
|
||||
|
||||
return (
|
||||
<Circle
|
||||
|
@ -4,26 +4,34 @@ import { Circle } from 'react-konva';
|
||||
import { RootState, useAppSelector } from '../../../../app/store';
|
||||
import { InpaintingState } from '../inpaintingSlice';
|
||||
|
||||
const inpaintingCanvasBrushPreviewSelector = createSelector(
|
||||
const inpaintingCanvasBrushPrevieOutlineSelector = createSelector(
|
||||
(state: RootState) => state.inpainting,
|
||||
(inpainting: InpaintingState) => {
|
||||
const {
|
||||
cursorPosition,
|
||||
canvasDimensions: { width, height },
|
||||
shouldShowBrushPreview,
|
||||
brushSize,
|
||||
stageScale,
|
||||
tool,
|
||||
shouldShowBrush,
|
||||
isMovingBoundingBox,
|
||||
isTransformingBoundingBox,
|
||||
stageScale,
|
||||
} = inpainting;
|
||||
|
||||
return {
|
||||
cursorPosition,
|
||||
width,
|
||||
height,
|
||||
shouldShowBrushPreview,
|
||||
brushSize,
|
||||
tool,
|
||||
strokeWidth: 1 / stageScale, // scale stroke thickness
|
||||
shouldShowBrush,
|
||||
radius: 1 / stageScale, // scale stroke thickness
|
||||
shouldDrawBrushPreview:
|
||||
!(
|
||||
isMovingBoundingBox ||
|
||||
isTransformingBoundingBox ||
|
||||
!cursorPosition
|
||||
) && shouldShowBrush,
|
||||
};
|
||||
},
|
||||
{
|
||||
@ -41,15 +49,13 @@ const InpaintingCanvasBrushPreviewOutline = () => {
|
||||
cursorPosition,
|
||||
width,
|
||||
height,
|
||||
shouldShowBrushPreview,
|
||||
brushSize,
|
||||
shouldDrawBrushPreview,
|
||||
strokeWidth,
|
||||
shouldShowBrush,
|
||||
} = useAppSelector(inpaintingCanvasBrushPreviewSelector);
|
||||
|
||||
if (!shouldShowBrush || !(cursorPosition || shouldShowBrushPreview))
|
||||
return null;
|
||||
radius,
|
||||
} = useAppSelector(inpaintingCanvasBrushPrevieOutlineSelector);
|
||||
|
||||
if (!shouldDrawBrushPreview) return null;
|
||||
return (
|
||||
<>
|
||||
<Circle
|
||||
@ -64,7 +70,7 @@ const InpaintingCanvasBrushPreviewOutline = () => {
|
||||
<Circle
|
||||
x={cursorPosition ? cursorPosition.x : width / 2}
|
||||
y={cursorPosition ? cursorPosition.y : height / 2}
|
||||
radius={1}
|
||||
radius={radius}
|
||||
fill={'rgba(0,0,0,1)'}
|
||||
listening={false}
|
||||
/>
|
||||
|
@ -51,9 +51,13 @@ export interface InpaintingState {
|
||||
needsCache: boolean;
|
||||
stageScale: number;
|
||||
isDrawing: boolean;
|
||||
isTransformingBoundingBox: boolean;
|
||||
isMouseOverBoundingBox: boolean;
|
||||
isMovingBoundingBox: boolean;
|
||||
shouldUseInpaintReplace: boolean;
|
||||
inpaintReplace: number;
|
||||
shouldLockBoundingBox: boolean;
|
||||
isSpacebarHeld: boolean;
|
||||
}
|
||||
|
||||
const initialInpaintingState: InpaintingState = {
|
||||
@ -63,7 +67,7 @@ const initialInpaintingState: InpaintingState = {
|
||||
canvasDimensions: { width: 0, height: 0 },
|
||||
boundingBoxDimensions: { width: 512, height: 512 },
|
||||
boundingBoxCoordinate: { x: 0, y: 0 },
|
||||
boundingBoxPreviewFill: { r: 0, g: 0, b: 0, a: 0.7 },
|
||||
boundingBoxPreviewFill: { r: 0, g: 0, b: 0, a: 0.5 },
|
||||
shouldShowBoundingBox: true,
|
||||
shouldShowBoundingBoxFill: true,
|
||||
cursorPosition: null,
|
||||
@ -77,10 +81,14 @@ const initialInpaintingState: InpaintingState = {
|
||||
shouldShowBrushPreview: false,
|
||||
needsCache: false,
|
||||
isDrawing: false,
|
||||
isTransformingBoundingBox: false,
|
||||
isMouseOverBoundingBox: false,
|
||||
isMovingBoundingBox: false,
|
||||
stageScale: 1,
|
||||
shouldUseInpaintReplace: false,
|
||||
inpaintReplace: 1,
|
||||
inpaintReplace: 0.1,
|
||||
shouldLockBoundingBox: true,
|
||||
isSpacebarHeld: false,
|
||||
};
|
||||
|
||||
const initialState: InpaintingState = initialInpaintingState;
|
||||
@ -319,6 +327,18 @@ export const inpaintingSlice = createSlice({
|
||||
setShouldShowBoundingBox: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldShowBoundingBox = action.payload;
|
||||
},
|
||||
setIsTransformingBoundingBox: (state, action: PayloadAction<boolean>) => {
|
||||
state.isTransformingBoundingBox = action.payload;
|
||||
},
|
||||
setIsMovingBoundingBox: (state, action: PayloadAction<boolean>) => {
|
||||
state.isMovingBoundingBox = action.payload;
|
||||
},
|
||||
setIsMouseOverBoundingBox: (state, action: PayloadAction<boolean>) => {
|
||||
state.isMouseOverBoundingBox = action.payload;
|
||||
},
|
||||
setIsSpacebarHeld: (state, action: PayloadAction<boolean>) => {
|
||||
state.isSpacebarHeld = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -354,6 +374,10 @@ export const {
|
||||
setInpaintReplace,
|
||||
setShouldLockBoundingBox,
|
||||
toggleShouldLockBoundingBox,
|
||||
setIsMovingBoundingBox,
|
||||
setIsTransformingBoundingBox,
|
||||
setIsMouseOverBoundingBox,
|
||||
setIsSpacebarHeld,
|
||||
} = inpaintingSlice.actions;
|
||||
|
||||
export default inpaintingSlice.reducer;
|
||||
|
@ -78,7 +78,23 @@ export const inpaintingCanvasSelector = createSelector(
|
||||
isDrawing,
|
||||
shouldLockBoundingBox,
|
||||
boundingBoxDimensions,
|
||||
isTransformingBoundingBox,
|
||||
isMouseOverBoundingBox,
|
||||
isMovingBoundingBox,
|
||||
} = inpainting;
|
||||
|
||||
let stageCursor: string | undefined = '';
|
||||
|
||||
if (isTransformingBoundingBox) {
|
||||
stageCursor = undefined;
|
||||
} else if (isMovingBoundingBox || isMouseOverBoundingBox) {
|
||||
stageCursor = 'move';
|
||||
} else if (shouldShowMask) {
|
||||
stageCursor = 'none';
|
||||
} else {
|
||||
stageCursor = 'default';
|
||||
}
|
||||
|
||||
return {
|
||||
tool,
|
||||
brushSize,
|
||||
@ -93,6 +109,10 @@ export const inpaintingCanvasSelector = createSelector(
|
||||
isDrawing,
|
||||
shouldLockBoundingBox,
|
||||
boundingBoxDimensions,
|
||||
isTransformingBoundingBox,
|
||||
isModifyingBoundingBox: isTransformingBoundingBox || isMovingBoundingBox,
|
||||
stageCursor,
|
||||
isMouseOverBoundingBox,
|
||||
};
|
||||
},
|
||||
{
|
||||
|
@ -101,7 +101,7 @@ const generateMask = (
|
||||
new Konva.Image({ image: image, globalCompositeOperation: 'source-out' })
|
||||
);
|
||||
|
||||
const maskDataURL = stage.toDataURL();
|
||||
const maskDataURL = stage.toDataURL({ ...boundingBox });
|
||||
|
||||
return { maskDataURL, isMaskEmpty };
|
||||
};
|
||||
|
@ -67,6 +67,15 @@ const InvokeOptionsPanel = (props: Props) => {
|
||||
[shouldShowOptionsPanel]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'esc',
|
||||
() => {
|
||||
if (shouldPinOptionsPanel) return;
|
||||
dispatch(setShouldShowOptionsPanel(false));
|
||||
},
|
||||
[shouldPinOptionsPanel]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'shift+o',
|
||||
() => {
|
||||
@ -74,7 +83,6 @@ const InvokeOptionsPanel = (props: Props) => {
|
||||
},
|
||||
[shouldPinOptionsPanel]
|
||||
);
|
||||
//
|
||||
|
||||
const handleCloseOptionsPanel = useCallback(() => {
|
||||
if (shouldPinOptionsPanel) return;
|
||||
@ -112,12 +120,6 @@ const InvokeOptionsPanel = (props: Props) => {
|
||||
dispatch(setNeedsCache(true));
|
||||
};
|
||||
|
||||
// // set gallery scroll position
|
||||
// useEffect(() => {
|
||||
// if (!optionsPanelContainerRef.current) return;
|
||||
// optionsPanelContainerRef.current.scrollTop = optionsPanelScrollPosition;
|
||||
// }, [optionsPanelScrollPosition, shouldShowOptionsPanel]);
|
||||
|
||||
return (
|
||||
<CSSTransition
|
||||
nodeRef={optionsPanelRef}
|
||||
@ -170,7 +172,6 @@ const InvokeOptionsPanel = (props: Props) => {
|
||||
</h1>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -15,9 +15,10 @@ import TextToImageIcon from '../../common/icons/TextToImageIcon';
|
||||
import { setActiveTab } from '../options/optionsSlice';
|
||||
import ImageToImageWorkarea from './ImageToImage';
|
||||
import InpaintingWorkarea from './Inpainting';
|
||||
import { setNeedsCache } from './Inpainting/inpaintingSlice';
|
||||
import TextToImageWorkarea from './TextToImage';
|
||||
|
||||
export const tab_dict = {
|
||||
export const tabDict = {
|
||||
txt2img: {
|
||||
title: <TextToImageIcon fill={'black'} boxSize={'2.5rem'} />,
|
||||
workarea: <TextToImageWorkarea />,
|
||||
@ -50,8 +51,8 @@ export const tab_dict = {
|
||||
},
|
||||
};
|
||||
|
||||
// Array where index maps to the key of tab_dict
|
||||
export const tabMap = _.map(tab_dict, (tab, key) => key);
|
||||
// Array where index maps to the key of tabDict
|
||||
export const tabMap = _.map(tabDict, (tab, key) => key);
|
||||
|
||||
// Use tabMap to generate a union type of tab names
|
||||
const tabMapTypes = [...tabMap] as const;
|
||||
@ -73,6 +74,7 @@ export default function InvokeTabs() {
|
||||
|
||||
useHotkeys('3', () => {
|
||||
dispatch(setActiveTab(2));
|
||||
dispatch(setNeedsCache(true));
|
||||
});
|
||||
|
||||
useHotkeys('4', () => {
|
||||
@ -89,15 +91,15 @@ export default function InvokeTabs() {
|
||||
|
||||
const renderTabs = () => {
|
||||
const tabsToRender: ReactElement[] = [];
|
||||
Object.keys(tab_dict).forEach((key) => {
|
||||
Object.keys(tabDict).forEach((key) => {
|
||||
tabsToRender.push(
|
||||
<Tooltip
|
||||
key={key}
|
||||
hasArrow
|
||||
label={tab_dict[key as keyof typeof tab_dict].tooltip}
|
||||
label={tabDict[key as keyof typeof tabDict].tooltip}
|
||||
placement={'right'}
|
||||
>
|
||||
<Tab>{tab_dict[key as keyof typeof tab_dict].title}</Tab>
|
||||
<Tab>{tabDict[key as keyof typeof tabDict].title}</Tab>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
@ -106,10 +108,10 @@ export default function InvokeTabs() {
|
||||
|
||||
const renderTabPanels = () => {
|
||||
const tabPanelsToRender: ReactElement[] = [];
|
||||
Object.keys(tab_dict).forEach((key) => {
|
||||
Object.keys(tabDict).forEach((key) => {
|
||||
tabPanelsToRender.push(
|
||||
<TabPanel className="app-tabs-panel" key={key}>
|
||||
{tab_dict[key as keyof typeof tab_dict].workarea}
|
||||
{tabDict[key as keyof typeof tabDict].workarea}
|
||||
</TabPanel>
|
||||
);
|
||||
});
|
||||
@ -125,6 +127,7 @@ export default function InvokeTabs() {
|
||||
index={activeTab}
|
||||
onChange={(index: number) => {
|
||||
dispatch(setActiveTab(index));
|
||||
dispatch(setNeedsCache(true));
|
||||
}}
|
||||
>
|
||||
<div className="app-tabs-list">{renderTabs()}</div>
|
||||
|
@ -7,21 +7,26 @@
|
||||
|
||||
.workarea-main {
|
||||
display: flex;
|
||||
column-gap: 0.5rem;
|
||||
column-gap: 1rem;
|
||||
height: 100%;
|
||||
|
||||
.workarea-children-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.workarea-split-view {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
// height: $app-content-height;
|
||||
background-color: var(--background-color-secondary);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.workarea-single-view {
|
||||
width: 100%;
|
||||
// height: $app-content-height;
|
||||
height: 100%;
|
||||
background-color: var(--background-color-secondary);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
@ -45,3 +50,22 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.workarea-split-button {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 20;
|
||||
|
||||
&[data-selected='true'] {
|
||||
top: 0;
|
||||
right: 0;
|
||||
svg {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
svg {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user