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 = (
|
base_path = (
|
||||||
self.result_path if category == "result" else self.init_image_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(
|
image_paths = sorted(
|
||||||
paths, key=lambda x: os.path.getmtime(x), reverse=True
|
paths, key=lambda x: os.path.getmtime(x), reverse=True
|
||||||
@ -203,13 +206,19 @@ class InvokeAIWebServer:
|
|||||||
image_array = []
|
image_array = []
|
||||||
|
|
||||||
for path in image_paths:
|
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
|
(width, height) = Image.open(path).size
|
||||||
|
|
||||||
image_array.append(
|
image_array.append(
|
||||||
{
|
{
|
||||||
"url": self.get_url_from_image_path(path),
|
"url": self.get_url_from_image_path(path),
|
||||||
"mtime": os.path.getmtime(path),
|
"mtime": os.path.getmtime(path),
|
||||||
"metadata": metadata["sd-metadata"],
|
"metadata": sd_metadata,
|
||||||
"width": width,
|
"width": width,
|
||||||
"height": height,
|
"height": height,
|
||||||
"category": category,
|
"category": category,
|
||||||
@ -236,7 +245,9 @@ class InvokeAIWebServer:
|
|||||||
self.result_path if category == "result" else self.init_image_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(
|
image_paths = sorted(
|
||||||
paths, key=lambda x: os.path.getmtime(x), reverse=True
|
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_paths = image_paths[slice(0, page_size)]
|
||||||
|
|
||||||
image_array = []
|
image_array = []
|
||||||
|
|
||||||
for path in image_paths:
|
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
|
(width, height) = Image.open(path).size
|
||||||
|
|
||||||
@ -264,7 +278,7 @@ class InvokeAIWebServer:
|
|||||||
{
|
{
|
||||||
"url": self.get_url_from_image_path(path),
|
"url": self.get_url_from_image_path(path),
|
||||||
"mtime": os.path.getmtime(path),
|
"mtime": os.path.getmtime(path),
|
||||||
"metadata": metadata["sd-metadata"],
|
"metadata": sd_metadata,
|
||||||
"width": width,
|
"width": width,
|
||||||
"height": height,
|
"height": height,
|
||||||
"category": category,
|
"category": category,
|
||||||
@ -573,11 +587,7 @@ class InvokeAIWebServer:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# crop the mask image
|
generation_parameters["init_mask"] = mask_image
|
||||||
cropped_mask_image = copy_image_from_bounding_box(
|
|
||||||
mask_image, **generation_parameters["bounding_box"]
|
|
||||||
)
|
|
||||||
generation_parameters["init_mask"] = cropped_mask_image
|
|
||||||
|
|
||||||
totalSteps = self.calculate_real_steps(
|
totalSteps = self.calculate_real_steps(
|
||||||
steps=generation_parameters["steps"],
|
steps=generation_parameters["steps"],
|
||||||
@ -605,8 +615,9 @@ class InvokeAIWebServer:
|
|||||||
progress.set_current_status_has_steps(True)
|
progress.set_current_status_has_steps(True)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
generation_parameters['progress_images'] and step % 5 == 0 \
|
generation_parameters["progress_images"]
|
||||||
and step < generation_parameters['steps'] - 1
|
and step % generation_parameters['save_intermediates'] == 0
|
||||||
|
and step < generation_parameters["steps"] - 1
|
||||||
):
|
):
|
||||||
image = self.generate.sample_to_image(sample)
|
image = self.generate.sample_to_image(sample)
|
||||||
metadata = self.parameters_to_generated_image_metadata(
|
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)
|
image = self.generate.sample_to_lowres_estimated_image(sample)
|
||||||
(width, height) = image.size
|
(width, height) = image.size
|
||||||
width *= 8
|
width *= 8
|
||||||
height *= 8
|
height *= 8
|
||||||
buffered = io.BytesIO()
|
buffered = io.BytesIO()
|
||||||
image.save(buffered, format="PNG")
|
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(
|
self.socketio.emit(
|
||||||
"intermediateResult",
|
"intermediateResult",
|
||||||
{
|
{
|
||||||
@ -654,7 +667,7 @@ class InvokeAIWebServer:
|
|||||||
"metadata": {},
|
"metadata": {},
|
||||||
"width": width,
|
"width": width,
|
||||||
"height": height,
|
"height": height,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
self.socketio.emit("progressUpdate", progress.to_formatted_dict())
|
self.socketio.emit("progressUpdate", progress.to_formatted_dict())
|
||||||
@ -672,6 +685,14 @@ class InvokeAIWebServer:
|
|||||||
step_index = 1
|
step_index = 1
|
||||||
nonlocal prior_variations
|
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")
|
progress.set_current_status("Generation Complete")
|
||||||
|
|
||||||
self.socketio.emit("progressUpdate", progress.to_formatted_dict())
|
self.socketio.emit("progressUpdate", progress.to_formatted_dict())
|
||||||
@ -760,14 +781,6 @@ class InvokeAIWebServer:
|
|||||||
self.socketio.emit("progressUpdate", progress.to_formatted_dict())
|
self.socketio.emit("progressUpdate", progress.to_formatted_dict())
|
||||||
eventlet.sleep(0)
|
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
|
# restore the stashed URLS and discard the paths, we are about to send the result to client
|
||||||
if "init_img" in all_parameters:
|
if "init_img" in all_parameters:
|
||||||
all_parameters["init_img"] = init_img_url
|
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" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>InvokeAI - A Stable Diffusion Toolkit</title>
|
<title>InvokeAI - A Stable Diffusion Toolkit</title>
|
||||||
<link rel="shortcut icon" type="icon" href="./assets/favicon.0d253ced.ico" />
|
<link rel="shortcut icon" type="icon" href="./assets/favicon.0d253ced.ico" />
|
||||||
<script type="module" crossorigin src="./assets/index.cc049b93.js"></script>
|
<script type="module" crossorigin src="./assets/index.bf9dd1fc.js"></script>
|
||||||
<link rel="stylesheet" href="./assets/index.52c8231e.css">
|
<link rel="stylesheet" href="./assets/index.f9f4c989.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
import ProgressBar from '../features/system/ProgressBar';
|
import ProgressBar from '../features/system/ProgressBar';
|
||||||
import SiteHeader from '../features/system/SiteHeader';
|
import SiteHeader from '../features/system/SiteHeader';
|
||||||
import Console from '../features/system/Console';
|
import Console from '../features/system/Console';
|
||||||
import Loading from '../Loading';
|
|
||||||
import { useAppDispatch } from './store';
|
import { useAppDispatch } from './store';
|
||||||
import { requestSystemConfig } from './socketio/actions';
|
import { requestSystemConfig } from './socketio/actions';
|
||||||
import { keepGUIAlive } from './utils';
|
import { keepGUIAlive } from './utils';
|
||||||
@ -79,17 +78,14 @@ const appSelector = createSelector(
|
|||||||
const App = () => {
|
const App = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const [isReady, setIsReady] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const { shouldShowGalleryButton, shouldShowOptionsPanelButton } =
|
const { shouldShowGalleryButton, shouldShowOptionsPanelButton } =
|
||||||
useAppSelector(appSelector);
|
useAppSelector(appSelector);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(requestSystemConfig());
|
dispatch(requestSystemConfig());
|
||||||
setIsReady(true);
|
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
return isReady ? (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<ImageUploader>
|
<ImageUploader>
|
||||||
<ProgressBar />
|
<ProgressBar />
|
||||||
@ -104,8 +100,6 @@ const App = () => {
|
|||||||
{shouldShowOptionsPanelButton && <FloatingOptionsPanelButtons />}
|
{shouldShowOptionsPanelButton && <FloatingOptionsPanelButtons />}
|
||||||
</ImageUploader>
|
</ImageUploader>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<Loading />
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
// TODO: use Enums?
|
// TODO: use Enums?
|
||||||
|
|
||||||
|
import { InProgressImageType } from '../features/system/systemSlice';
|
||||||
|
|
||||||
// Valid samplers
|
// Valid samplers
|
||||||
export const SAMPLERS: Array<string> = [
|
export const SAMPLERS: Array<string> = [
|
||||||
'ddim',
|
'ddim',
|
||||||
@ -38,8 +40,11 @@ export const NUMPY_RAND_MAX = 4294967295;
|
|||||||
|
|
||||||
export const FACETOOL_TYPES = ['gfpgan', 'codeformer'] as const;
|
export const FACETOOL_TYPES = ['gfpgan', 'codeformer'] as const;
|
||||||
|
|
||||||
export const IN_PROGRESS_IMAGE_TYPES: Array<{ key: string; value: string }> = [
|
export const IN_PROGRESS_IMAGE_TYPES: Array<{
|
||||||
{ key: "None", value: 'none'},
|
key: string;
|
||||||
{ key: "Fast", value: 'latents' },
|
value: InProgressImageType;
|
||||||
{ key: "Accurate", value: 'full-res' }
|
}> = [
|
||||||
|
{ key: 'None', value: 'none' },
|
||||||
|
{ key: 'Fast', value: 'latents' },
|
||||||
|
{ key: 'Accurate', value: 'full-res' },
|
||||||
];
|
];
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { RootState } from '../../app/store';
|
import { RootState } from '../store';
|
||||||
import { activeTabNameSelector } from '../../features/options/optionsSelectors';
|
import { activeTabNameSelector } from '../../features/options/optionsSelectors';
|
||||||
import { OptionsState } from '../../features/options/optionsSlice';
|
import { OptionsState } from '../../features/options/optionsSlice';
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ export const readinessSelector = createSelector(
|
|||||||
prompt,
|
prompt,
|
||||||
shouldGenerateVariations,
|
shouldGenerateVariations,
|
||||||
seedWeights,
|
seedWeights,
|
||||||
maskPath,
|
// maskPath,
|
||||||
initialImage,
|
initialImage,
|
||||||
seed,
|
seed,
|
||||||
} = options;
|
} = options;
|
||||||
@ -34,33 +34,45 @@ export const readinessSelector = createSelector(
|
|||||||
|
|
||||||
const { imageToInpaint } = inpainting;
|
const { imageToInpaint } = inpainting;
|
||||||
|
|
||||||
|
let isReady = true;
|
||||||
|
const reasonsWhyNotReady: string[] = [];
|
||||||
|
|
||||||
// Cannot generate without a prompt
|
// Cannot generate without a prompt
|
||||||
if (!prompt || Boolean(prompt.match(/^[\s\r\n]+$/))) {
|
if (!prompt || Boolean(prompt.match(/^[\s\r\n]+$/))) {
|
||||||
return false;
|
isReady = false;
|
||||||
|
reasonsWhyNotReady.push('Missing prompt');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeTabName === 'img2img' && !initialImage) {
|
if (activeTabName === 'img2img' && !initialImage) {
|
||||||
return false;
|
isReady = false;
|
||||||
|
reasonsWhyNotReady.push('No initial image selected');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeTabName === 'inpainting' && !imageToInpaint) {
|
if (activeTabName === 'inpainting' && !imageToInpaint) {
|
||||||
return false;
|
isReady = false;
|
||||||
|
reasonsWhyNotReady.push('No inpainting image selected');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cannot generate with a mask without img2img
|
// // We don't use mask paths now.
|
||||||
if (maskPath && !initialImage) {
|
// // Cannot generate with a mask without img2img
|
||||||
return false;
|
// if (maskPath && !initialImage) {
|
||||||
}
|
// isReady = false;
|
||||||
|
// reasonsWhyNotReady.push(
|
||||||
|
// 'On ImageToImage tab, but no mask is provided.'
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
// TODO: job queue
|
// TODO: job queue
|
||||||
// Cannot generate if already processing an image
|
// Cannot generate if already processing an image
|
||||||
if (isProcessing) {
|
if (isProcessing) {
|
||||||
return false;
|
isReady = false;
|
||||||
|
reasonsWhyNotReady.push('System Busy');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cannot generate if not connected
|
// Cannot generate if not connected
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
return false;
|
isReady = false;
|
||||||
|
reasonsWhyNotReady.push('System Disconnected');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cannot generate variations without valid seed weights
|
// Cannot generate variations without valid seed weights
|
||||||
@ -68,11 +80,12 @@ export const readinessSelector = createSelector(
|
|||||||
shouldGenerateVariations &&
|
shouldGenerateVariations &&
|
||||||
(!(validateSeedWeights(seedWeights) || seedWeights === '') || seed === -1)
|
(!(validateSeedWeights(seedWeights) || seedWeights === '') || seed === -1)
|
||||||
) {
|
) {
|
||||||
return false;
|
isReady = false;
|
||||||
|
reasonsWhyNotReady.push('Seed-Weights badly formatted.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// All good
|
// All good
|
||||||
return true;
|
return { isReady, reasonsWhyNotReady };
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
memoizeOptions: {
|
memoizeOptions: {
|
||||||
|
@ -146,12 +146,14 @@ const makeSocketIOListeners = (
|
|||||||
...data,
|
...data,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
dispatch(
|
if (!data.isBase64) {
|
||||||
addLogEntry({
|
dispatch(
|
||||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
addLogEntry({
|
||||||
message: `Intermediate image generated: ${data.url}`,
|
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||||
})
|
message: `Intermediate image generated: ${data.url}`,
|
||||||
);
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
@ -5,12 +5,16 @@ import type { TypedUseSelectorHook } from 'react-redux';
|
|||||||
import { persistReducer } from 'redux-persist';
|
import { persistReducer } from 'redux-persist';
|
||||||
import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web
|
import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web
|
||||||
|
|
||||||
import optionsReducer from '../features/options/optionsSlice';
|
import optionsReducer, { OptionsState } from '../features/options/optionsSlice';
|
||||||
import galleryReducer from '../features/gallery/gallerySlice';
|
import galleryReducer, { GalleryState } from '../features/gallery/gallerySlice';
|
||||||
import inpaintingReducer from '../features/tabs/Inpainting/inpaintingSlice';
|
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 { 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.
|
* redux-persist provides an easy and reliable way to persist state across reloads.
|
||||||
@ -33,12 +37,14 @@ import { socketioMiddleware } from './socketio/middleware';
|
|||||||
const rootPersistConfig = {
|
const rootPersistConfig = {
|
||||||
key: 'root',
|
key: 'root',
|
||||||
storage,
|
storage,
|
||||||
|
stateReconciler: autoMergeLevel2,
|
||||||
blacklist: ['gallery', 'system', 'inpainting'],
|
blacklist: ['gallery', 'system', 'inpainting'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const systemPersistConfig = {
|
const systemPersistConfig = {
|
||||||
key: 'system',
|
key: 'system',
|
||||||
storage,
|
storage,
|
||||||
|
stateReconciler: autoMergeLevel2,
|
||||||
blacklist: [
|
blacklist: [
|
||||||
'isCancelable',
|
'isCancelable',
|
||||||
'isConnected',
|
'isConnected',
|
||||||
@ -58,6 +64,7 @@ const systemPersistConfig = {
|
|||||||
const galleryPersistConfig = {
|
const galleryPersistConfig = {
|
||||||
key: 'gallery',
|
key: 'gallery',
|
||||||
storage,
|
storage,
|
||||||
|
stateReconciler: autoMergeLevel2,
|
||||||
whitelist: [
|
whitelist: [
|
||||||
'galleryWidth',
|
'galleryWidth',
|
||||||
'shouldPinGallery',
|
'shouldPinGallery',
|
||||||
@ -71,17 +78,26 @@ const galleryPersistConfig = {
|
|||||||
const inpaintingPersistConfig = {
|
const inpaintingPersistConfig = {
|
||||||
key: 'inpainting',
|
key: 'inpainting',
|
||||||
storage,
|
storage,
|
||||||
|
stateReconciler: autoMergeLevel2,
|
||||||
blacklist: ['pastLines', 'futuresLines', 'cursorPosition'],
|
blacklist: ['pastLines', 'futuresLines', 'cursorPosition'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const reducers = combineReducers({
|
const reducers = combineReducers({
|
||||||
options: optionsReducer,
|
options: optionsReducer,
|
||||||
gallery: persistReducer(galleryPersistConfig, galleryReducer),
|
gallery: persistReducer<GalleryState>(galleryPersistConfig, galleryReducer),
|
||||||
system: persistReducer(systemPersistConfig, systemReducer),
|
system: persistReducer<SystemState>(systemPersistConfig, systemReducer),
|
||||||
inpainting: persistReducer(inpaintingPersistConfig, inpaintingReducer),
|
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
|
// Continue with store setup
|
||||||
export const store = configureStore({
|
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 {
|
export interface IAIButtonProps extends ButtonProps {
|
||||||
label: string;
|
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
|
tooltipProps?: Omit<TooltipProps, 'children'>;
|
||||||
styleClass?: string;
|
styleClass?: string;
|
||||||
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const IAIButton = forwardRef((props: IAIButtonProps, forwardedRef) => {
|
||||||
* Reusable customized button component.
|
const { children, tooltip = '', tooltipProps, styleClass, ...rest } = props;
|
||||||
*/
|
|
||||||
const IAIButton = (props: IAIButtonProps) => {
|
|
||||||
const { label, tooltip = '', styleClass, ...rest } = props;
|
|
||||||
return (
|
return (
|
||||||
<Tooltip label={tooltip}>
|
<Tooltip label={tooltip} {...tooltipProps}>
|
||||||
<Button className={styleClass ? styleClass : ''} {...rest}>
|
<Button
|
||||||
{label}
|
ref={forwardedRef}
|
||||||
|
className={['invokeai__button', styleClass].join(' ')}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export default IAIButton;
|
export default IAIButton;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
@use '../../styles/Mixins/' as *;
|
@use '../../styles/Mixins/' as *;
|
||||||
|
|
||||||
.icon-button {
|
.invokeai__icon-button {
|
||||||
background-color: var(--btn-grey);
|
background-color: var(--btn-grey);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
@ -8,13 +8,68 @@
|
|||||||
background-color: var(--btn-grey-hover);
|
background-color: var(--btn-grey-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-selected=true] {
|
&[data-selected='true'] {
|
||||||
background-color: var(--accent-color);
|
background-color: var(--accent-color);
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--accent-color-hover);
|
background-color: var(--accent-color-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&[disabled] {
|
&[disabled] {
|
||||||
cursor: not-allowed;
|
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,
|
IconButtonProps,
|
||||||
IconButton,
|
IconButton,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
PlacementWithLogical,
|
TooltipProps,
|
||||||
|
forwardRef,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
interface Props extends IconButtonProps {
|
export type IAIIconButtonProps = IconButtonProps & {
|
||||||
tooltip?: string;
|
|
||||||
tooltipPlacement?: PlacementWithLogical | undefined;
|
|
||||||
styleClass?: string;
|
styleClass?: string;
|
||||||
}
|
tooltip?: string;
|
||||||
|
tooltipProps?: Omit<TooltipProps, 'children'>;
|
||||||
|
asCheckbox?: boolean;
|
||||||
|
isChecked?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
const IAIIconButton = forwardRef((props: IAIIconButtonProps, forwardedRef) => {
|
||||||
* Reusable customized button component. Originally was more customized - now probably unecessary.
|
|
||||||
*/
|
|
||||||
const IAIIconButton = (props: Props) => {
|
|
||||||
const {
|
const {
|
||||||
tooltip = '',
|
tooltip = '',
|
||||||
tooltipPlacement = 'bottom',
|
|
||||||
styleClass,
|
styleClass,
|
||||||
onClick,
|
tooltipProps,
|
||||||
cursor,
|
asCheckbox,
|
||||||
|
isChecked,
|
||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip label={tooltip} hasArrow placement={tooltipPlacement}>
|
<Tooltip label={tooltip} hasArrow {...tooltipProps}>
|
||||||
<IconButton
|
<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}
|
{...rest}
|
||||||
cursor={cursor ? cursor : onClick ? 'pointer' : 'unset'}
|
|
||||||
onClick={onClick}
|
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export default IAIIconButton;
|
export default IAIIconButton;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
.invokeai__number-input-form-control {
|
.invokeai__number-input-form-control {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: max-content auto;
|
grid-template-columns: max-content auto;
|
||||||
column-gap: 1rem;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.invokeai__number-input-form-label {
|
.invokeai__number-input-form-label {
|
||||||
@ -11,6 +10,7 @@
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
flex-grow: 2;
|
flex-grow: 2;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
padding-right: 1rem;
|
||||||
|
|
||||||
&[data-focus] + .invokeai__number-input-root {
|
&[data-focus] + .invokeai__number-input-root {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
@ -123,13 +123,15 @@ const IAINumberInput = (props: Props) => {
|
|||||||
}
|
}
|
||||||
{...formControlProps}
|
{...formControlProps}
|
||||||
>
|
>
|
||||||
<FormLabel
|
{label && (
|
||||||
className="invokeai__number-input-form-label"
|
<FormLabel
|
||||||
style={{ display: label ? 'block' : 'none' }}
|
className="invokeai__number-input-form-label"
|
||||||
{...formLabelProps}
|
style={{ display: label ? 'block' : 'none' }}
|
||||||
>
|
{...formLabelProps}
|
||||||
{label}
|
>
|
||||||
</FormLabel>
|
{label}
|
||||||
|
</FormLabel>
|
||||||
|
)}
|
||||||
<NumberInput
|
<NumberInput
|
||||||
className="invokeai__number-input-root"
|
className="invokeai__number-input-root"
|
||||||
value={valueAsString}
|
value={valueAsString}
|
||||||
@ -145,19 +147,18 @@ const IAINumberInput = (props: Props) => {
|
|||||||
textAlign={textAlign}
|
textAlign={textAlign}
|
||||||
{...numberInputFieldProps}
|
{...numberInputFieldProps}
|
||||||
/>
|
/>
|
||||||
<div
|
{showStepper && (
|
||||||
className="invokeai__number-input-stepper"
|
<div className="invokeai__number-input-stepper">
|
||||||
style={showStepper ? { display: 'block' } : { display: 'none' }}
|
<NumberIncrementStepper
|
||||||
>
|
{...numberInputStepperProps}
|
||||||
<NumberIncrementStepper
|
className="invokeai__number-input-stepper-button"
|
||||||
{...numberInputStepperProps}
|
/>
|
||||||
className="invokeai__number-input-stepper-button"
|
<NumberDecrementStepper
|
||||||
/>
|
{...numberInputStepperProps}
|
||||||
<NumberDecrementStepper
|
className="invokeai__number-input-stepper-button"
|
||||||
{...numberInputStepperProps}
|
/>
|
||||||
className="invokeai__number-input-stepper-button"
|
</div>
|
||||||
/>
|
)}
|
||||||
</div>
|
|
||||||
</NumberInput>
|
</NumberInput>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -3,13 +3,14 @@ import {
|
|||||||
PopoverArrow,
|
PopoverArrow,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
Box,
|
BoxProps,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { PopoverProps } from '@chakra-ui/react';
|
import { PopoverProps } from '@chakra-ui/react';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
type IAIPopoverProps = PopoverProps & {
|
type IAIPopoverProps = PopoverProps & {
|
||||||
triggerComponent: ReactNode;
|
triggerComponent: ReactNode;
|
||||||
|
triggerContainerProps?: BoxProps;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
styleClass?: string;
|
styleClass?: string;
|
||||||
hasArrow?: boolean;
|
hasArrow?: boolean;
|
||||||
@ -23,11 +24,10 @@ const IAIPopover = (props: IAIPopoverProps) => {
|
|||||||
hasArrow = true,
|
hasArrow = true,
|
||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover {...rest}>
|
<Popover {...rest}>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>{triggerComponent}</PopoverTrigger>
|
||||||
<Box>{triggerComponent}</Box>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className={`invokeai__popover-content ${styleClass}`}>
|
<PopoverContent className={`invokeai__popover-content ${styleClass}`}>
|
||||||
{hasArrow && <PopoverArrow className={'invokeai__popover-arrow'} />}
|
{hasArrow && <PopoverArrow className={'invokeai__popover-arrow'} />}
|
||||||
{children}
|
{children}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
@use '../../styles/Mixins/' as *;
|
@use '../../styles/Mixins/' as *;
|
||||||
|
|
||||||
.invokeai__select {
|
.invokeai__select {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: repeat(2, max-content);
|
|
||||||
column-gap: 1rem;
|
column-gap: 1rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import { FormControl, FormLabel, Select, SelectProps } from '@chakra-ui/react';
|
import { FormControl, FormLabel, Select, SelectProps } from '@chakra-ui/react';
|
||||||
import { MouseEvent } from 'react';
|
import { MouseEvent } from 'react';
|
||||||
|
|
||||||
interface Props extends SelectProps {
|
type IAISelectProps = SelectProps & {
|
||||||
label: string;
|
label: string;
|
||||||
styleClass?: string;
|
styleClass?: string;
|
||||||
validValues:
|
validValues:
|
||||||
| Array<number | string>
|
| Array<number | string>
|
||||||
| Array<{ key: string; value: string | number }>;
|
| Array<{ key: string; value: string | number }>;
|
||||||
}
|
};
|
||||||
/**
|
/**
|
||||||
* Customized Chakra FormControl + Select multi-part component.
|
* Customized Chakra FormControl + Select multi-part component.
|
||||||
*/
|
*/
|
||||||
const IAISelect = (props: Props) => {
|
const IAISelect = (props: IAISelectProps) => {
|
||||||
const {
|
const {
|
||||||
label,
|
label,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
@ -33,19 +33,19 @@ const IAISelect = (props: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FormLabel
|
<FormLabel
|
||||||
|
className="invokeai__select-label"
|
||||||
fontSize={fontSize}
|
fontSize={fontSize}
|
||||||
marginBottom={1}
|
marginBottom={1}
|
||||||
flexGrow={2}
|
flexGrow={2}
|
||||||
whiteSpace="nowrap"
|
whiteSpace="nowrap"
|
||||||
className="invokeai__select-label"
|
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
|
className="invokeai__select-picker"
|
||||||
fontSize={fontSize}
|
fontSize={fontSize}
|
||||||
size={size}
|
size={size}
|
||||||
{...rest}
|
{...rest}
|
||||||
className="invokeai__select-picker"
|
|
||||||
>
|
>
|
||||||
{validValues.map((opt) => {
|
{validValues.map((opt) => {
|
||||||
return typeof opt === 'string' || typeof opt === 'number' ? (
|
return typeof opt === 'string' || typeof opt === 'number' ? (
|
||||||
@ -53,7 +53,11 @@ const IAISelect = (props: Props) => {
|
|||||||
{opt}
|
{opt}
|
||||||
</option>
|
</option>
|
||||||
) : (
|
) : (
|
||||||
<option key={opt.value} value={opt.value}>
|
<option
|
||||||
|
key={opt.value}
|
||||||
|
value={opt.value}
|
||||||
|
className="invokeai__select-option"
|
||||||
|
>
|
||||||
{opt.key}
|
{opt.key}
|
||||||
</option>
|
</option>
|
||||||
);
|
);
|
||||||
|
@ -22,8 +22,6 @@ const IAISwitch = (props: Props) => {
|
|||||||
const {
|
const {
|
||||||
label,
|
label,
|
||||||
isDisabled = false,
|
isDisabled = false,
|
||||||
// fontSize = 'md',
|
|
||||||
// size = 'md',
|
|
||||||
width = 'auto',
|
width = 'auto',
|
||||||
formControlProps,
|
formControlProps,
|
||||||
formLabelProps,
|
formLabelProps,
|
||||||
@ -39,17 +37,11 @@ const IAISwitch = (props: Props) => {
|
|||||||
>
|
>
|
||||||
<FormLabel
|
<FormLabel
|
||||||
className="invokeai__switch-form-label"
|
className="invokeai__switch-form-label"
|
||||||
// fontSize={fontSize}
|
|
||||||
whiteSpace="nowrap"
|
whiteSpace="nowrap"
|
||||||
{...formLabelProps}
|
{...formLabelProps}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
<Switch
|
<Switch className="invokeai__switch-root" {...rest} />
|
||||||
className="invokeai__switch-root"
|
|
||||||
// size={size}
|
|
||||||
// className="switch-button"
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
</FormControl>
|
</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;
|
row-gap: 1rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
|
||||||
&.is-drag-accept {
|
&.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 {
|
&.is-drag-reject {
|
||||||
@ -32,6 +33,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.image-uploader-button-outer {
|
.image-uploader-button-outer {
|
||||||
|
min-width: 20rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import { useCallback, ReactNode, useState, useEffect } from 'react';
|
import { useCallback, ReactNode, useState, useEffect } from 'react';
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||||
import { FileRejection, useDropzone } from 'react-dropzone';
|
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 { uploadImage } from '../../app/socketio/actions';
|
||||||
import { ImageUploadDestination, UploadImagePayload } from '../../app/invokeai';
|
import { ImageUploadDestination, UploadImagePayload } from '../../app/invokeai';
|
||||||
import { ImageUploaderTriggerContext } from '../../app/contexts/ImageUploaderTriggerContext';
|
import { ImageUploaderTriggerContext } from '../../app/contexts/ImageUploaderTriggerContext';
|
||||||
import { activeTabNameSelector } from '../../features/options/optionsSelectors';
|
import { activeTabNameSelector } from '../../features/options/optionsSelectors';
|
||||||
|
import { tabDict } from '../../features/tabs/InvokeTabs';
|
||||||
|
import ImageUploadOverlay from './ImageUploadOverlay';
|
||||||
|
|
||||||
type ImageUploaderProps = {
|
type ImageUploaderProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@ -71,6 +73,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
|||||||
accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] },
|
accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] },
|
||||||
noClick: true,
|
noClick: true,
|
||||||
onDrop,
|
onDrop,
|
||||||
|
onDragOver: () => setIsHandlingUpload(true),
|
||||||
maxFiles: 1,
|
maxFiles: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -128,30 +131,22 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
|||||||
};
|
};
|
||||||
}, [dispatch, toast, activeTabName]);
|
}, [dispatch, toast, activeTabName]);
|
||||||
|
|
||||||
|
const overlaySecondaryText = ['img2img', 'inpainting'].includes(activeTabName)
|
||||||
|
? ` to ${tabDict[activeTabName as keyof typeof tabDict].tooltip}`
|
||||||
|
: ``;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ImageUploaderTriggerContext.Provider value={open}>
|
<ImageUploaderTriggerContext.Provider value={open}>
|
||||||
<div {...getRootProps({ style: {} })}>
|
<div {...getRootProps({ style: {} })}>
|
||||||
<input {...getInputProps()} />
|
<input {...getInputProps()} />
|
||||||
{children}
|
{children}
|
||||||
{isDragActive && (
|
{isDragActive && isHandlingUpload && (
|
||||||
<div className="dropzone-container">
|
<ImageUploadOverlay
|
||||||
{isDragAccept && (
|
isDragAccept={isDragAccept}
|
||||||
<div className="dropzone-overlay is-drag-accept">
|
isDragReject={isDragReject}
|
||||||
<Heading size={'lg'}>Drop Images</Heading>
|
overlaySecondaryText={overlaySecondaryText}
|
||||||
</div>
|
setIsHandlingUpload={setIsHandlingUpload}
|
||||||
)}
|
/>
|
||||||
{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>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ImageUploaderTriggerContext.Provider>
|
</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,
|
shouldRandomizeSeed,
|
||||||
} = optionsState;
|
} = optionsState;
|
||||||
|
|
||||||
const { shouldDisplayInProgressType } = systemState;
|
const { shouldDisplayInProgressType, saveIntermediatesInterval } =
|
||||||
|
systemState;
|
||||||
|
|
||||||
const generationParameters: { [k: string]: any } = {
|
const generationParameters: { [k: string]: any } = {
|
||||||
prompt,
|
prompt,
|
||||||
iterations,
|
iterations:
|
||||||
|
shouldRandomizeSeed || shouldGenerateVariations ? iterations : 1,
|
||||||
steps,
|
steps,
|
||||||
cfg_scale: cfgScale,
|
cfg_scale: cfgScale,
|
||||||
threshold,
|
threshold,
|
||||||
@ -76,7 +78,8 @@ export const frontendToBackendParameters = (
|
|||||||
sampler_name: sampler,
|
sampler_name: sampler,
|
||||||
seed,
|
seed,
|
||||||
progress_images: shouldDisplayInProgressType === 'full-res',
|
progress_images: shouldDisplayInProgressType === 'full-res',
|
||||||
progress_latents: shouldDisplayInProgressType === 'latents'
|
progress_latents: shouldDisplayInProgressType === 'latents',
|
||||||
|
save_intermediates: saveIntermediatesInterval,
|
||||||
};
|
};
|
||||||
|
|
||||||
generationParameters.seed = shouldRandomizeSeed
|
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 { createSelector } from '@reduxjs/toolkit';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
|
|
||||||
import * as InvokeAI from '../../app/invokeai';
|
|
||||||
|
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||||
import { RootState } from '../../app/store';
|
import { RootState } from '../../app/store';
|
||||||
import {
|
import {
|
||||||
@ -10,6 +8,7 @@ import {
|
|||||||
setActiveTab,
|
setActiveTab,
|
||||||
setAllParameters,
|
setAllParameters,
|
||||||
setInitialImage,
|
setInitialImage,
|
||||||
|
setPrompt,
|
||||||
setSeed,
|
setSeed,
|
||||||
setShouldShowImageDetails,
|
setShouldShowImageDetails,
|
||||||
} from '../options/optionsSlice';
|
} from '../options/optionsSlice';
|
||||||
@ -18,27 +17,30 @@ import { SystemState } from '../system/systemSlice';
|
|||||||
import IAIButton from '../../common/components/IAIButton';
|
import IAIButton from '../../common/components/IAIButton';
|
||||||
import { runESRGAN, runFacetool } from '../../app/socketio/actions';
|
import { runESRGAN, runFacetool } from '../../app/socketio/actions';
|
||||||
import IAIIconButton from '../../common/components/IAIIconButton';
|
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 UpscaleOptions from '../options/AdvancedOptions/Upscale/UpscaleOptions';
|
||||||
import FaceRestoreOptions from '../options/AdvancedOptions/FaceRestore/FaceRestoreOptions';
|
import FaceRestoreOptions from '../options/AdvancedOptions/FaceRestore/FaceRestoreOptions';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useToast } from '@chakra-ui/react';
|
import { ButtonGroup, Link, useClipboard, useToast } from '@chakra-ui/react';
|
||||||
import { FaCopy, FaPaintBrush, FaSeedling } from 'react-icons/fa';
|
import {
|
||||||
import { setImageToInpaint } from '../tabs/Inpainting/inpaintingSlice';
|
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 { GalleryState } from './gallerySlice';
|
||||||
import { activeTabNameSelector } from '../options/optionsSelectors';
|
import { activeTabNameSelector } from '../options/optionsSelectors';
|
||||||
|
import IAIPopover from '../../common/components/IAIPopover';
|
||||||
const intermediateImageSelector = createSelector(
|
|
||||||
(state: RootState) => state.gallery,
|
|
||||||
(gallery: GalleryState) => gallery.intermediateImage,
|
|
||||||
{
|
|
||||||
memoizeOptions: {
|
|
||||||
resultEqualityCheck: (a, b) =>
|
|
||||||
(a === undefined && b === undefined) || a.uuid === b.uuid,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const systemSelector = createSelector(
|
const systemSelector = createSelector(
|
||||||
[
|
[
|
||||||
@ -59,7 +61,7 @@ const systemSelector = createSelector(
|
|||||||
const { upscalingLevel, facetoolStrength, shouldShowImageDetails } =
|
const { upscalingLevel, facetoolStrength, shouldShowImageDetails } =
|
||||||
options;
|
options;
|
||||||
|
|
||||||
const { intermediateImage } = gallery;
|
const { intermediateImage, currentImage } = gallery;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isProcessing,
|
isProcessing,
|
||||||
@ -68,7 +70,8 @@ const systemSelector = createSelector(
|
|||||||
isESRGANAvailable,
|
isESRGANAvailable,
|
||||||
upscalingLevel,
|
upscalingLevel,
|
||||||
facetoolStrength,
|
facetoolStrength,
|
||||||
intermediateImage,
|
shouldDisableToolbarButtons: Boolean(intermediateImage) || !currentImage,
|
||||||
|
currentImage,
|
||||||
shouldShowImageDetails,
|
shouldShowImageDetails,
|
||||||
activeTabName,
|
activeTabName,
|
||||||
};
|
};
|
||||||
@ -80,15 +83,11 @@ const systemSelector = createSelector(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
type CurrentImageButtonsProps = {
|
|
||||||
image: InvokeAI.Image;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Row of buttons for common actions:
|
* Row of buttons for common actions:
|
||||||
* Use as init image, use all params, use seed, upscale, fix faces, details, delete.
|
* Use as init image, use all params, use seed, upscale, fix faces, details, delete.
|
||||||
*/
|
*/
|
||||||
const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
const CurrentImageButtons = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const {
|
const {
|
||||||
isProcessing,
|
isProcessing,
|
||||||
@ -97,22 +96,37 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
|||||||
isESRGANAvailable,
|
isESRGANAvailable,
|
||||||
upscalingLevel,
|
upscalingLevel,
|
||||||
facetoolStrength,
|
facetoolStrength,
|
||||||
intermediateImage,
|
shouldDisableToolbarButtons,
|
||||||
shouldShowImageDetails,
|
shouldShowImageDetails,
|
||||||
activeTabName,
|
currentImage,
|
||||||
} = useAppSelector(systemSelector);
|
} = useAppSelector(systemSelector);
|
||||||
|
|
||||||
|
const { onCopy } = useClipboard(
|
||||||
|
currentImage ? window.location.toString() + currentImage.url : ''
|
||||||
|
);
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
const handleClickUseAsInitialImage = () => {
|
const handleClickUseAsInitialImage = () => {
|
||||||
dispatch(setInitialImage(image));
|
if (!currentImage) return;
|
||||||
dispatch(setActiveTab(1));
|
dispatch(setInitialImage(currentImage));
|
||||||
|
dispatch(setActiveTab('img2img'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopyImageLink = () => {
|
||||||
|
onCopy();
|
||||||
|
toast({
|
||||||
|
title: 'Image Link Copied',
|
||||||
|
status: 'success',
|
||||||
|
duration: 2500,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'shift+i',
|
'shift+i',
|
||||||
() => {
|
() => {
|
||||||
if (image) {
|
if (currentImage) {
|
||||||
handleClickUseAsInitialImage();
|
handleClickUseAsInitialImage();
|
||||||
toast({
|
toast({
|
||||||
title: 'Sent To Image To Image',
|
title: 'Sent To Image To Image',
|
||||||
@ -130,16 +144,20 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[image]
|
[currentImage]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClickUseAllParameters = () =>
|
const handleClickUseAllParameters = () => {
|
||||||
image.metadata && dispatch(setAllParameters(image.metadata));
|
if (!currentImage) return;
|
||||||
|
currentImage.metadata && dispatch(setAllParameters(currentImage.metadata));
|
||||||
|
};
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'a',
|
'a',
|
||||||
() => {
|
() => {
|
||||||
if (['txt2img', 'img2img'].includes(image?.metadata?.image?.type)) {
|
if (
|
||||||
|
['txt2img', 'img2img'].includes(currentImage?.metadata?.image?.type)
|
||||||
|
) {
|
||||||
handleClickUseAllParameters();
|
handleClickUseAllParameters();
|
||||||
toast({
|
toast({
|
||||||
title: 'Parameters Set',
|
title: 'Parameters Set',
|
||||||
@ -157,15 +175,18 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[image]
|
[currentImage]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClickUseSeed = () =>
|
const handleClickUseSeed = () => {
|
||||||
image.metadata && dispatch(setSeed(image.metadata.image.seed));
|
currentImage?.metadata &&
|
||||||
|
dispatch(setSeed(currentImage.metadata.image.seed));
|
||||||
|
};
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
's',
|
's',
|
||||||
() => {
|
() => {
|
||||||
if (image?.metadata?.image?.seed) {
|
if (currentImage?.metadata?.image?.seed) {
|
||||||
handleClickUseSeed();
|
handleClickUseSeed();
|
||||||
toast({
|
toast({
|
||||||
title: 'Seed Set',
|
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(
|
useHotkeys(
|
||||||
'u',
|
'u',
|
||||||
() => {
|
() => {
|
||||||
if (
|
if (
|
||||||
isESRGANAvailable &&
|
isESRGANAvailable &&
|
||||||
Boolean(!intermediateImage) &&
|
!shouldDisableToolbarButtons &&
|
||||||
isConnected &&
|
isConnected &&
|
||||||
!isProcessing &&
|
!isProcessing &&
|
||||||
upscalingLevel
|
upscalingLevel
|
||||||
@ -208,23 +260,25 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
image,
|
currentImage,
|
||||||
isESRGANAvailable,
|
isESRGANAvailable,
|
||||||
intermediateImage,
|
shouldDisableToolbarButtons,
|
||||||
isConnected,
|
isConnected,
|
||||||
isProcessing,
|
isProcessing,
|
||||||
upscalingLevel,
|
upscalingLevel,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClickFixFaces = () => dispatch(runFacetool(image));
|
const handleClickFixFaces = () => {
|
||||||
|
currentImage && dispatch(runFacetool(currentImage));
|
||||||
|
};
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'r',
|
'r',
|
||||||
() => {
|
() => {
|
||||||
if (
|
if (
|
||||||
isGFPGANAvailable &&
|
isGFPGANAvailable &&
|
||||||
Boolean(!intermediateImage) &&
|
!shouldDisableToolbarButtons &&
|
||||||
isConnected &&
|
isConnected &&
|
||||||
!isProcessing &&
|
!isProcessing &&
|
||||||
facetoolStrength
|
facetoolStrength
|
||||||
@ -240,9 +294,9 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
image,
|
currentImage,
|
||||||
isGFPGANAvailable,
|
isGFPGANAvailable,
|
||||||
intermediateImage,
|
shouldDisableToolbarButtons,
|
||||||
isConnected,
|
isConnected,
|
||||||
isProcessing,
|
isProcessing,
|
||||||
facetoolStrength,
|
facetoolStrength,
|
||||||
@ -253,10 +307,13 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
|||||||
dispatch(setShouldShowImageDetails(!shouldShowImageDetails));
|
dispatch(setShouldShowImageDetails(!shouldShowImageDetails));
|
||||||
|
|
||||||
const handleSendToInpainting = () => {
|
const handleSendToInpainting = () => {
|
||||||
dispatch(setImageToInpaint(image));
|
if (!currentImage) return;
|
||||||
if (activeTabName !== 'inpainting') {
|
|
||||||
dispatch(setActiveTab('inpainting'));
|
dispatch(setImageToInpaint(currentImage));
|
||||||
}
|
|
||||||
|
dispatch(setActiveTab('inpainting'));
|
||||||
|
dispatch(setNeedsCache(true));
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: 'Sent to Inpainting',
|
title: 'Sent to Inpainting',
|
||||||
status: 'success',
|
status: 'success',
|
||||||
@ -268,7 +325,7 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
|||||||
useHotkeys(
|
useHotkeys(
|
||||||
'i',
|
'i',
|
||||||
() => {
|
() => {
|
||||||
if (image) {
|
if (currentImage) {
|
||||||
handleClickShowImageDetails();
|
handleClickShowImageDetails();
|
||||||
} else {
|
} else {
|
||||||
toast({
|
toast({
|
||||||
@ -279,111 +336,141 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[image, shouldShowImageDetails]
|
[currentImage, shouldShowImageDetails]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="current-image-options">
|
<div className="current-image-options">
|
||||||
<IAIIconButton
|
<ButtonGroup isAttached={true}>
|
||||||
icon={<MdImage />}
|
<IAIPopover
|
||||||
tooltip="Send To Image To Image"
|
trigger="hover"
|
||||||
aria-label="Send To Image To Image"
|
triggerComponent={
|
||||||
onClick={handleClickUseAsInitialImage}
|
<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
|
<IAIIconButton
|
||||||
icon={<FaPaintBrush />}
|
icon={<FaCode />}
|
||||||
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 />}
|
|
||||||
tooltip="Details"
|
tooltip="Details"
|
||||||
aria-label="Details"
|
aria-label="Details"
|
||||||
|
data-selected={shouldShowImageDetails}
|
||||||
onClick={handleClickShowImageDetails}
|
onClick={handleClickShowImageDetails}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DeleteImageModal image={image}>
|
<DeleteImageModal image={currentImage}>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
icon={<MdDelete />}
|
icon={<FaTrash />}
|
||||||
tooltip="Delete Image"
|
tooltip="Delete Image"
|
||||||
aria-label="Delete Image"
|
aria-label="Delete Image"
|
||||||
isDisabled={
|
isDisabled={!currentImage || !isConnected || isProcessing}
|
||||||
Boolean(intermediateImage) || !isConnected || isProcessing
|
className="delete-image-btn"
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</DeleteImageModal>
|
</DeleteImageModal>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,18 +9,6 @@
|
|||||||
border-radius: 0.5rem;
|
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 {
|
.current-image-preview {
|
||||||
position: relative;
|
position: relative;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -50,6 +38,7 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,10 +19,9 @@ export const currentImageDisplaySelector = createSelector(
|
|||||||
const { shouldShowImageDetails } = options;
|
const { shouldShowImageDetails } = options;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentImage,
|
|
||||||
intermediateImage,
|
|
||||||
activeTabName,
|
activeTabName,
|
||||||
shouldShowImageDetails,
|
shouldShowImageDetails,
|
||||||
|
hasAnImageToDisplay: currentImage || intermediateImage,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -36,18 +35,16 @@ export const currentImageDisplaySelector = createSelector(
|
|||||||
* Displays the current image if there is one, plus associated actions.
|
* Displays the current image if there is one, plus associated actions.
|
||||||
*/
|
*/
|
||||||
const CurrentImageDisplay = () => {
|
const CurrentImageDisplay = () => {
|
||||||
const { currentImage, intermediateImage, activeTabName } = useAppSelector(
|
const { hasAnImageToDisplay, activeTabName } = useAppSelector(
|
||||||
currentImageDisplaySelector
|
currentImageDisplaySelector
|
||||||
);
|
);
|
||||||
|
|
||||||
const imageToDisplay = intermediateImage || currentImage;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="current-image-area" data-tab-name={activeTabName}>
|
<div className="current-image-area" data-tab-name={activeTabName}>
|
||||||
{imageToDisplay ? (
|
{hasAnImageToDisplay ? (
|
||||||
<>
|
<>
|
||||||
<CurrentImageButtons image={imageToDisplay} />
|
<CurrentImageButtons />
|
||||||
<CurrentImagePreview imageToDisplay={imageToDisplay} />
|
<CurrentImagePreview />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="current-image-display-placeholder">
|
<div className="current-image-display-placeholder">
|
||||||
|
@ -2,8 +2,12 @@ import { IconButton, Image } from '@chakra-ui/react';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
|
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
|
||||||
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
||||||
import { GalleryState, selectNextImage, selectPrevImage } from './gallerySlice';
|
import {
|
||||||
import * as InvokeAI from '../../app/invokeai';
|
GalleryCategory,
|
||||||
|
GalleryState,
|
||||||
|
selectNextImage,
|
||||||
|
selectPrevImage,
|
||||||
|
} from './gallerySlice';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { OptionsState } from '../options/optionsSlice';
|
import { OptionsState } from '../options/optionsSlice';
|
||||||
@ -12,20 +16,29 @@ import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
|
|||||||
export const imagesSelector = createSelector(
|
export const imagesSelector = createSelector(
|
||||||
[(state: RootState) => state.gallery, (state: RootState) => state.options],
|
[(state: RootState) => state.gallery, (state: RootState) => state.options],
|
||||||
(gallery: GalleryState, options: OptionsState) => {
|
(gallery: GalleryState, options: OptionsState) => {
|
||||||
const { currentCategory } = gallery;
|
const { currentCategory, currentImage, intermediateImage } = gallery;
|
||||||
const { shouldShowImageDetails } = options;
|
const { shouldShowImageDetails } = options;
|
||||||
|
|
||||||
const tempImages = gallery.categories[currentCategory].images;
|
const tempImages =
|
||||||
|
gallery.categories[
|
||||||
|
currentImage ? (currentImage.category as GalleryCategory) : 'result'
|
||||||
|
].images;
|
||||||
const currentImageIndex = tempImages.findIndex(
|
const currentImageIndex = tempImages.findIndex(
|
||||||
(i) => i.uuid === gallery?.currentImage?.uuid
|
(i) => i.uuid === gallery?.currentImage?.uuid
|
||||||
);
|
);
|
||||||
const imagesLength = tempImages.length;
|
const imagesLength = tempImages.length;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
imageToDisplay: intermediateImage ? intermediateImage : currentImage,
|
||||||
|
isIntermediate: intermediateImage,
|
||||||
currentCategory,
|
currentCategory,
|
||||||
isOnFirstImage: currentImageIndex === 0,
|
isOnFirstImage: currentImageIndex === 0,
|
||||||
isOnLastImage:
|
isOnLastImage:
|
||||||
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
|
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
|
||||||
shouldShowImageDetails,
|
shouldShowImageDetails,
|
||||||
|
shouldShowPrevImageButton: currentImageIndex === 0,
|
||||||
|
shouldShowNextImageButton:
|
||||||
|
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -35,16 +48,16 @@ export const imagesSelector = createSelector(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
interface CurrentImagePreviewProps {
|
export default function CurrentImagePreview() {
|
||||||
imageToDisplay: InvokeAI.Image;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CurrentImagePreview(props: CurrentImagePreviewProps) {
|
|
||||||
const { imageToDisplay } = props;
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const { isOnFirstImage, isOnLastImage, shouldShowImageDetails } =
|
const {
|
||||||
useAppSelector(imagesSelector);
|
isOnFirstImage,
|
||||||
|
isOnLastImage,
|
||||||
|
shouldShowImageDetails,
|
||||||
|
imageToDisplay,
|
||||||
|
isIntermediate,
|
||||||
|
} = useAppSelector(imagesSelector);
|
||||||
|
|
||||||
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] =
|
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] =
|
||||||
useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
@ -67,11 +80,13 @@ export default function CurrentImagePreview(props: CurrentImagePreviewProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'current-image-preview'}>
|
<div className={'current-image-preview'}>
|
||||||
<Image
|
{imageToDisplay && (
|
||||||
src={imageToDisplay.url}
|
<Image
|
||||||
width={imageToDisplay.width}
|
src={imageToDisplay.url}
|
||||||
height={imageToDisplay.height}
|
width={isIntermediate ? imageToDisplay.width : undefined}
|
||||||
/>
|
height={isIntermediate ? imageToDisplay.height : undefined}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{!shouldShowImageDetails && (
|
{!shouldShowImageDetails && (
|
||||||
<div className="current-image-next-prev-buttons">
|
<div className="current-image-next-prev-buttons">
|
||||||
<div
|
<div
|
||||||
@ -104,7 +119,7 @@ export default function CurrentImagePreview(props: CurrentImagePreviewProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{shouldShowImageDetails && (
|
{shouldShowImageDetails && imageToDisplay && (
|
||||||
<ImageMetadataViewer
|
<ImageMetadataViewer
|
||||||
image={imageToDisplay}
|
image={imageToDisplay}
|
||||||
styleClass="current-image-metadata"
|
styleClass="current-image-metadata"
|
||||||
|
@ -28,12 +28,18 @@ import { RootState } from '../../app/store';
|
|||||||
import { setShouldConfirmOnDelete, SystemState } from '../system/systemSlice';
|
import { setShouldConfirmOnDelete, SystemState } from '../system/systemSlice';
|
||||||
import * as InvokeAI from '../../app/invokeai';
|
import * as InvokeAI from '../../app/invokeai';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
const systemSelector = createSelector(
|
const systemSelector = createSelector(
|
||||||
(state: RootState) => state.system,
|
(state: RootState) => state.system,
|
||||||
(system: SystemState) => {
|
(system: SystemState) => {
|
||||||
const { shouldConfirmOnDelete, isConnected, isProcessing } = system;
|
const { shouldConfirmOnDelete, isConnected, isProcessing } = system;
|
||||||
return { shouldConfirmOnDelete, isConnected, isProcessing };
|
return { shouldConfirmOnDelete, isConnected, isProcessing };
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
interface DeleteImageModalProps {
|
interface DeleteImageModalProps {
|
||||||
@ -44,7 +50,7 @@ interface DeleteImageModalProps {
|
|||||||
/**
|
/**
|
||||||
* The image to delete.
|
* The image to delete.
|
||||||
*/
|
*/
|
||||||
image: InvokeAI.Image;
|
image?: InvokeAI.Image;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -67,7 +73,7 @@ const DeleteImageModal = forwardRef(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
if (isConnected && !isProcessing) {
|
if (isConnected && !isProcessing && image) {
|
||||||
dispatch(deleteImage(image));
|
dispatch(deleteImage(image));
|
||||||
}
|
}
|
||||||
onClose();
|
onClose();
|
||||||
@ -89,7 +95,7 @@ const DeleteImageModal = forwardRef(
|
|||||||
<>
|
<>
|
||||||
{cloneElement(children, {
|
{cloneElement(children, {
|
||||||
// TODO: This feels wrong.
|
// TODO: This feels wrong.
|
||||||
onClick: handleClickDelete,
|
onClick: image ? handleClickDelete : undefined,
|
||||||
ref: ref,
|
ref: ref,
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
@ -19,8 +19,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.image-gallery-wrapper {
|
.image-gallery-wrapper {
|
||||||
z-index: 100;
|
|
||||||
|
|
||||||
&[data-pinned='false'] {
|
&[data-pinned='false'] {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
@ -69,9 +69,9 @@ export default function ImageGallery() {
|
|||||||
if (!shouldPinGallery) return;
|
if (!shouldPinGallery) return;
|
||||||
|
|
||||||
if (activeTabName === 'inpainting') {
|
if (activeTabName === 'inpainting') {
|
||||||
dispatch(setGalleryWidth(220));
|
dispatch(setGalleryWidth(190));
|
||||||
setGalleryMinWidth(220);
|
setGalleryMinWidth(190);
|
||||||
setGalleryMaxWidth(220);
|
setGalleryMaxWidth(190);
|
||||||
} else if (activeTabName === 'img2img') {
|
} else if (activeTabName === 'img2img') {
|
||||||
dispatch(
|
dispatch(
|
||||||
setGalleryWidth(Math.min(Math.max(Number(galleryWidth), 0), 490))
|
setGalleryWidth(Math.min(Math.max(Number(galleryWidth), 0), 490))
|
||||||
@ -163,6 +163,15 @@ export default function ImageGallery() {
|
|||||||
[shouldPinGallery]
|
[shouldPinGallery]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'esc',
|
||||||
|
() => {
|
||||||
|
if (shouldPinGallery) return;
|
||||||
|
dispatch(setShouldShowGallery(false));
|
||||||
|
},
|
||||||
|
[shouldPinGallery]
|
||||||
|
);
|
||||||
|
|
||||||
const IMAGE_SIZE_STEP = 32;
|
const IMAGE_SIZE_STEP = 32;
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
@ -261,6 +270,7 @@ export default function ImageGallery() {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="image-gallery-wrapper"
|
className="image-gallery-wrapper"
|
||||||
|
style={{ zIndex: shouldPinGallery ? 1 : 100 }}
|
||||||
data-pinned={shouldPinGallery}
|
data-pinned={shouldPinGallery}
|
||||||
ref={galleryRef}
|
ref={galleryRef}
|
||||||
onMouseLeave={!shouldPinGallery ? setCloseGalleryTimer : undefined}
|
onMouseLeave={!shouldPinGallery ? setCloseGalleryTimer : undefined}
|
||||||
|
@ -32,10 +32,12 @@ import {
|
|||||||
setUpscalingStrength,
|
setUpscalingStrength,
|
||||||
setWidth,
|
setWidth,
|
||||||
setInitialImage,
|
setInitialImage,
|
||||||
|
setShouldShowImageDetails,
|
||||||
} from '../../options/optionsSlice';
|
} from '../../options/optionsSlice';
|
||||||
import promptToString from '../../../common/util/promptToString';
|
import promptToString from '../../../common/util/promptToString';
|
||||||
import { seedWeightsToString } from '../../../common/util/seedWeightPairs';
|
import { seedWeightsToString } from '../../../common/util/seedWeightPairs';
|
||||||
import { FaCopy } from 'react-icons/fa';
|
import { FaCopy } from 'react-icons/fa';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
type MetadataItemProps = {
|
type MetadataItemProps = {
|
||||||
isLink?: boolean;
|
isLink?: boolean;
|
||||||
@ -107,7 +109,10 @@ const memoEqualityCheck = (
|
|||||||
const ImageMetadataViewer = memo(
|
const ImageMetadataViewer = memo(
|
||||||
({ image, styleClass }: ImageMetadataViewerProps) => {
|
({ image, styleClass }: ImageMetadataViewerProps) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
// const jsonBgColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.100');
|
|
||||||
|
useHotkeys('esc', () => {
|
||||||
|
dispatch(setShouldShowImageDetails(false));
|
||||||
|
});
|
||||||
|
|
||||||
const metadata = image?.metadata?.image || {};
|
const metadata = image?.metadata?.image || {};
|
||||||
const {
|
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 { OptionsState } from '../options/optionsSlice';
|
||||||
import { SystemState } from '../system/systemSlice';
|
import { SystemState } from '../system/systemSlice';
|
||||||
import { GalleryState } from './gallerySlice';
|
import { GalleryState } from './gallerySlice';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
export const imageGallerySelector = createSelector(
|
export const imageGallerySelector = createSelector(
|
||||||
[
|
[
|
||||||
@ -43,6 +44,11 @@ export const imageGallerySelector = createSelector(
|
|||||||
currentCategory,
|
currentCategory,
|
||||||
galleryWidth,
|
galleryWidth,
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -65,5 +71,10 @@ export const hoverableImageSelector = createSelector(
|
|||||||
galleryImageMinimumWidth: gallery.galleryImageMinimumWidth,
|
galleryImageMinimumWidth: gallery.galleryImageMinimumWidth,
|
||||||
activeTabName,
|
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 BoundingBoxSettings from './BoundingBoxSettings/BoundingBoxSettings';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import InpaintReplace from './InpaintReplace';
|
||||||
import React, { ChangeEvent } from 'react';
|
import ClearBrushHistory from './ClearBrushHistory';
|
||||||
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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export default function InpaintingSettings() {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<InpaintReplace />
|
||||||
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>
|
|
||||||
<BoundingBoxSettings />
|
<BoundingBoxSettings />
|
||||||
<IAIButton
|
<ClearBrushHistory />
|
||||||
label="Clear Brush History"
|
|
||||||
onClick={handleClearBrushHistory}
|
|
||||||
tooltip="Clears brush stroke history"
|
|
||||||
disabled={futureLines.length > 0 || pastLines.length > 0 ? false : true}
|
|
||||||
styleClass="inpainting-options-btn"
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import { setHeight } from '../optionsSlice';
|
|||||||
import { fontSize } from './MainOptions';
|
import { fontSize } from './MainOptions';
|
||||||
|
|
||||||
export default function MainHeight() {
|
export default function MainHeight() {
|
||||||
const { height } = useAppSelector((state: RootState) => state.options);
|
const height = useAppSelector((state: RootState) => state.options.height);
|
||||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
@ -1,13 +1,33 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import _ from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||||
import IAINumberInput from '../../../common/components/IAINumberInput';
|
import IAINumberInput from '../../../common/components/IAINumberInput';
|
||||||
import { setIterations } from '../optionsSlice';
|
import { mayGenerateMultipleImagesSelector } from '../optionsSelectors';
|
||||||
|
import { OptionsState, setIterations } from '../optionsSlice';
|
||||||
import { fontSize, inputWidth } from './MainOptions';
|
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() {
|
export default function MainIterations() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const iterations = useAppSelector(
|
const { iterations, mayGenerateMultipleImages } = useAppSelector(
|
||||||
(state: RootState) => state.options.iterations
|
mainIterationsSelector
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChangeIterations = (v: number) => dispatch(setIterations(v));
|
const handleChangeIterations = (v: number) => dispatch(setIterations(v));
|
||||||
@ -18,6 +38,7 @@ export default function MainIterations() {
|
|||||||
step={1}
|
step={1}
|
||||||
min={1}
|
min={1}
|
||||||
max={9999}
|
max={9999}
|
||||||
|
isDisabled={!mayGenerateMultipleImages}
|
||||||
onChange={handleChangeIterations}
|
onChange={handleChangeIterations}
|
||||||
value={iterations}
|
value={iterations}
|
||||||
width={inputWidth}
|
width={inputWidth}
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
.main-option-block {
|
.main-option-block {
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
|
display: grid !important;
|
||||||
grid-template-columns: auto !important;
|
grid-template-columns: auto !important;
|
||||||
row-gap: 0.4rem;
|
row-gap: 0.4rem;
|
||||||
|
|
||||||
|
@ -2,14 +2,14 @@ import React, { ChangeEvent } from 'react';
|
|||||||
import { WIDTHS } from '../../../app/constants';
|
import { WIDTHS } from '../../../app/constants';
|
||||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||||
import IAISelect from '../../../common/components/IAISelect';
|
import IAISelect from '../../../common/components/IAISelect';
|
||||||
import { tabMap } from '../../tabs/InvokeTabs';
|
import { activeTabNameSelector } from '../optionsSelectors';
|
||||||
import { setWidth } from '../optionsSlice';
|
import { setWidth } from '../optionsSlice';
|
||||||
import { fontSize } from './MainOptions';
|
import { fontSize } from './MainOptions';
|
||||||
|
|
||||||
export default function MainWidth() {
|
export default function MainWidth() {
|
||||||
const { width, activeTab } = useAppSelector(
|
const width = useAppSelector((state: RootState) => state.options.width);
|
||||||
(state: RootState) => state.options
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
);
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const handleChangeWidth = (e: ChangeEvent<HTMLSelectElement>) =>
|
const handleChangeWidth = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||||
@ -17,7 +17,7 @@ export default function MainWidth() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<IAISelect
|
<IAISelect
|
||||||
isDisabled={tabMap[activeTab] === 'inpainting'}
|
isDisabled={activeTabName === 'inpainting'}
|
||||||
label="Width"
|
label="Width"
|
||||||
value={width}
|
value={width}
|
||||||
flexGrow={1}
|
flexGrow={1}
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import { MdCancel } from 'react-icons/md';
|
import { MdCancel } from 'react-icons/md';
|
||||||
import { cancelProcessing } from '../../../app/socketio/actions';
|
import { cancelProcessing } from '../../../app/socketio/actions';
|
||||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
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 { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { SystemState } from '../../system/systemSlice';
|
import { SystemState } from '../../system/systemSlice';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { IAIButtonProps } from '../../../common/components/IAIButton';
|
|
||||||
|
|
||||||
const cancelButtonSelector = createSelector(
|
const cancelButtonSelector = createSelector(
|
||||||
(state: RootState) => state.system,
|
(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 { ...rest } = props;
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { isProcessing, isConnected, isCancelable } =
|
const { isProcessing, isConnected, isCancelable } =
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { ListItem, UnorderedList } from '@chakra-ui/react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { FaPlay } from 'react-icons/fa';
|
import { FaPlay } from 'react-icons/fa';
|
||||||
import { readinessSelector } from '../../../app/selectors/readinessSelector';
|
import { readinessSelector } from '../../../app/selectors/readinessSelector';
|
||||||
@ -6,17 +7,21 @@ import { useAppDispatch, useAppSelector } from '../../../app/store';
|
|||||||
import IAIButton, {
|
import IAIButton, {
|
||||||
IAIButtonProps,
|
IAIButtonProps,
|
||||||
} from '../../../common/components/IAIButton';
|
} 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';
|
import { activeTabNameSelector } from '../optionsSelectors';
|
||||||
|
|
||||||
interface InvokeButton extends Omit<IAIButtonProps, 'label'> {
|
interface InvokeButton
|
||||||
|
extends Omit<IAIButtonProps | IAIIconButtonProps, 'aria-label'> {
|
||||||
iconButton?: boolean;
|
iconButton?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function InvokeButton(props: InvokeButton) {
|
export default function InvokeButton(props: InvokeButton) {
|
||||||
const { iconButton = false, ...rest } = props;
|
const { iconButton = false, ...rest } = props;
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const isReady = useAppSelector(readinessSelector);
|
const { isReady, reasonsWhyNotReady } = useAppSelector(readinessSelector);
|
||||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
|
|
||||||
const handleClickGenerate = () => {
|
const handleClickGenerate = () => {
|
||||||
@ -33,27 +38,62 @@ export default function InvokeButton(props: InvokeButton) {
|
|||||||
[isReady, activeTabName]
|
[isReady, activeTabName]
|
||||||
);
|
);
|
||||||
|
|
||||||
return iconButton ? (
|
const buttonComponent = (
|
||||||
<IAIIconButton
|
<div style={{ flexGrow: 4 }}>
|
||||||
aria-label="Invoke"
|
{iconButton ? (
|
||||||
type="submit"
|
<IAIIconButton
|
||||||
icon={<FaPlay />}
|
aria-label="Invoke"
|
||||||
isDisabled={!isReady}
|
type="submit"
|
||||||
onClick={handleClickGenerate}
|
icon={<FaPlay />}
|
||||||
className="invoke-btn invoke"
|
isDisabled={!isReady}
|
||||||
tooltip="Invoke"
|
onClick={handleClickGenerate}
|
||||||
tooltipPlacement="bottom"
|
className="invoke-btn invoke"
|
||||||
{...rest}
|
tooltip="Invoke"
|
||||||
/>
|
tooltipProps={{ placement: 'bottom' }}
|
||||||
) : (
|
{...rest}
|
||||||
<IAIButton
|
/>
|
||||||
label="Invoke"
|
) : (
|
||||||
aria-label="Invoke"
|
<IAIButton
|
||||||
type="submit"
|
aria-label="Invoke"
|
||||||
isDisabled={!isReady}
|
type="submit"
|
||||||
onClick={handleClickGenerate}
|
isDisabled={!isReady}
|
||||||
className="invoke-btn"
|
onClick={handleClickGenerate}
|
||||||
{...rest}
|
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 (
|
return (
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label="Loopback"
|
aria-label="Toggle Loopback"
|
||||||
tooltip="Loopback"
|
tooltip="Toggle Loopback"
|
||||||
data-selected={shouldLoopback}
|
styleClass="loopback-btn"
|
||||||
|
asCheckbox={true}
|
||||||
|
isChecked={shouldLoopback}
|
||||||
icon={<FaRecycle />}
|
icon={<FaRecycle />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dispatch(setShouldLoopback(!shouldLoopback));
|
dispatch(setShouldLoopback(!shouldLoopback));
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
.invoke-btn {
|
.invoke-btn {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
width: 100%;
|
||||||
svg {
|
svg {
|
||||||
width: 18px !important;
|
width: 18px !important;
|
||||||
height: 18px !important;
|
height: 18px !important;
|
||||||
@ -25,3 +26,34 @@
|
|||||||
// $btn-width: 3rem
|
// $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 PromptInput = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { prompt, activeTabName } = useAppSelector(promptInputSelector);
|
const { prompt, activeTabName } = useAppSelector(promptInputSelector);
|
||||||
const isReady = useAppSelector(readinessSelector);
|
const { isReady } = useAppSelector(readinessSelector);
|
||||||
|
|
||||||
const promptRef = useRef<HTMLTextAreaElement>(null);
|
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 {
|
.console {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -48,7 +41,7 @@
|
|||||||
position: fixed !important;
|
position: fixed !important;
|
||||||
left: 0.5rem;
|
left: 0.5rem;
|
||||||
bottom: 0.5rem;
|
bottom: 0.5rem;
|
||||||
z-index: 21;
|
z-index: 10000;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--console-icon-button-bg-color-hover) !important;
|
background: var(--console-icon-button-bg-color-hover) !important;
|
||||||
@ -67,7 +60,7 @@
|
|||||||
position: fixed !important;
|
position: fixed !important;
|
||||||
left: 0.5rem;
|
left: 0.5rem;
|
||||||
bottom: 3rem;
|
bottom: 3rem;
|
||||||
z-index: 21;
|
z-index: 10000;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--console-icon-button-bg-color-hover) !important;
|
background: var(--console-icon-button-bg-color-hover) !important;
|
||||||
|
@ -75,6 +75,10 @@ const Console = () => {
|
|||||||
[shouldShowLogViewer]
|
[shouldShowLogViewer]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useHotkeys('esc', () => {
|
||||||
|
dispatch(setShouldShowLogViewer(false));
|
||||||
|
});
|
||||||
|
|
||||||
const handleOnScroll = () => {
|
const handleOnScroll = () => {
|
||||||
if (!viewerRef.current) return;
|
if (!viewerRef.current) return;
|
||||||
if (
|
if (
|
||||||
@ -99,7 +103,7 @@ const Console = () => {
|
|||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
left: 0,
|
left: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
zIndex: 20,
|
zIndex: 9999,
|
||||||
}}
|
}}
|
||||||
maxHeight={'90vh'}
|
maxHeight={'90vh'}
|
||||||
>
|
>
|
||||||
|
@ -73,15 +73,20 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
|
|||||||
|
|
||||||
const generalHotkeys = [
|
const generalHotkeys = [
|
||||||
{
|
{
|
||||||
title: 'Set Parameters',
|
title: 'Set Prompt',
|
||||||
desc: 'Use all parameters of the current image',
|
desc: 'Use the prompt of the current image',
|
||||||
hotkey: 'A',
|
hotkey: 'P',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Set Seed',
|
title: 'Set Seed',
|
||||||
desc: 'Use the seed of the current image',
|
desc: 'Use the seed of the current image',
|
||||||
hotkey: 'S',
|
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: 'Restore Faces', desc: 'Restore the current image', hotkey: 'R' },
|
||||||
{ title: 'Upscale', desc: 'Upscale the current image', hotkey: 'U' },
|
{ title: 'Upscale', desc: 'Upscale the current image', hotkey: 'U' },
|
||||||
{
|
{
|
||||||
@ -95,6 +100,7 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
|
|||||||
hotkey: 'Shift+I',
|
hotkey: 'Shift+I',
|
||||||
},
|
},
|
||||||
{ title: 'Delete Image', desc: 'Delete the current image', hotkey: 'Del' },
|
{ title: 'Delete Image', desc: 'Delete the current image', hotkey: 'Del' },
|
||||||
|
{ title: 'Close Panels', desc: 'Closes open panels', hotkey: 'Esc' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const galleryHotkeys = [
|
const galleryHotkeys = [
|
||||||
@ -194,12 +200,12 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
|
|||||||
{
|
{
|
||||||
title: 'Lock Bounding Box',
|
title: 'Lock Bounding Box',
|
||||||
desc: 'Locks the bounding box',
|
desc: 'Locks the bounding box',
|
||||||
hotkey: 'M',
|
hotkey: 'Shift+Q',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Quick Toggle Lock Bounding Box',
|
title: 'Quick Toggle Lock Bounding Box',
|
||||||
desc: 'Hold to toggle locking the bounding box',
|
desc: 'Hold to toggle locking the bounding box',
|
||||||
hotkey: 'Space',
|
hotkey: 'Q',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Expand Inpainting Area',
|
title: 'Expand Inpainting Area',
|
||||||
|
@ -1,24 +1,37 @@
|
|||||||
.model-list {
|
// .chakra-accordion {
|
||||||
.chakra-accordion {
|
// display: grid;
|
||||||
display: grid;
|
// row-gap: 0.5rem;
|
||||||
row-gap: 0.5rem;
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
.chakra-accordion__item {
|
// .chakra-accordion__item {
|
||||||
border: none;
|
// border: none;
|
||||||
border-radius: 0.3rem;
|
// }
|
||||||
background-color: var(--tab-hover-color);
|
|
||||||
}
|
// 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 {
|
button {
|
||||||
border-radius: 0.3rem !important;
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
&[aria-expanded='true'] {
|
&:hover {
|
||||||
background-color: var(--tab-hover-color);
|
background-color: unset;
|
||||||
border-radius: 0.3rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.model-list-button {
|
.model-list-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -64,6 +77,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.model-list-item-load-btn {
|
.model-list-item-load-btn {
|
||||||
|
button {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,31 +73,33 @@ const ModelList = () => {
|
|||||||
const { models } = useAppSelector(modelListSelector);
|
const { models } = useAppSelector(modelListSelector);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="model-list">
|
<Accordion
|
||||||
<Accordion allowToggle>
|
allowToggle
|
||||||
<AccordionItem>
|
className="model-list-accordion"
|
||||||
<AccordionButton>
|
variant={'unstyled'}
|
||||||
<div className="model-list-button">
|
>
|
||||||
<h2>Models</h2>
|
<AccordionItem>
|
||||||
<AccordionIcon />
|
<AccordionButton>
|
||||||
</div>
|
<div className="model-list-button">
|
||||||
</AccordionButton>
|
<h2>Models</h2>
|
||||||
|
<AccordionIcon />
|
||||||
|
</div>
|
||||||
|
</AccordionButton>
|
||||||
|
|
||||||
<AccordionPanel>
|
<AccordionPanel>
|
||||||
<div className="model-list-list">
|
<div className="model-list-list">
|
||||||
{models.map((model, i) => (
|
{models.map((model, i) => (
|
||||||
<ModelListItem
|
<ModelListItem
|
||||||
key={i}
|
key={i}
|
||||||
name={model.name}
|
name={model.name}
|
||||||
status={model.status}
|
status={model.status}
|
||||||
description={model.description}
|
description={model.description}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -14,18 +14,22 @@ import {
|
|||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import _, { isEqual } from 'lodash';
|
import _, { isEqual } from 'lodash';
|
||||||
import { cloneElement, ReactElement } from 'react';
|
import { ChangeEvent, cloneElement, ReactElement } from 'react';
|
||||||
import { RootState, useAppSelector } from '../../../app/store';
|
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||||
import { persistor } from '../../../main';
|
import { persistor } from '../../../main';
|
||||||
import {
|
import {
|
||||||
|
InProgressImageType,
|
||||||
|
setSaveIntermediatesInterval,
|
||||||
setShouldConfirmOnDelete,
|
setShouldConfirmOnDelete,
|
||||||
setShouldDisplayGuides,
|
setShouldDisplayGuides,
|
||||||
setShouldDisplayInProgressType,
|
setShouldDisplayInProgressType,
|
||||||
SystemState,
|
SystemState,
|
||||||
} from '../systemSlice';
|
} from '../systemSlice';
|
||||||
import ModelList from './ModelList';
|
import ModelList from './ModelList';
|
||||||
import { SettingsModalItem, SettingsModalSelectItem } from './SettingsModalItem';
|
|
||||||
import { IN_PROGRESS_IMAGE_TYPES } from '../../../app/constants';
|
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(
|
const systemSelector = createSelector(
|
||||||
(state: RootState) => state.system,
|
(state: RootState) => state.system,
|
||||||
@ -60,6 +64,14 @@ type SettingsModalProps = {
|
|||||||
* Secondary post-reset modal is included here.
|
* Secondary post-reset modal is included here.
|
||||||
*/
|
*/
|
||||||
const SettingsModal = ({ children }: SettingsModalProps) => {
|
const SettingsModal = ({ children }: SettingsModalProps) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const saveIntermediatesInterval = useAppSelector(
|
||||||
|
(state: RootState) => state.system.saveIntermediatesInterval
|
||||||
|
);
|
||||||
|
|
||||||
|
const steps = useAppSelector((state: RootState) => state.options.steps);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isOpen: isSettingsModalOpen,
|
isOpen: isSettingsModalOpen,
|
||||||
onOpen: onSettingsModalOpen,
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{cloneElement(children, {
|
{cloneElement(children, {
|
||||||
@ -101,31 +119,62 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
|
|||||||
<ModalHeader className="settings-modal-header">Settings</ModalHeader>
|
<ModalHeader className="settings-modal-header">Settings</ModalHeader>
|
||||||
<ModalCloseButton />
|
<ModalCloseButton />
|
||||||
<ModalBody className="settings-modal-content">
|
<ModalBody className="settings-modal-content">
|
||||||
<ModelList />
|
|
||||||
<div className="settings-modal-items">
|
<div className="settings-modal-items">
|
||||||
|
<div className="settings-modal-item">
|
||||||
<SettingsModalSelectItem
|
<ModelList />
|
||||||
settingTitle="Display In-Progress Images"
|
</div>
|
||||||
validValues={IN_PROGRESS_IMAGE_TYPES}
|
<div
|
||||||
defaultValue={shouldDisplayInProgressType}
|
className="settings-modal-item"
|
||||||
dispatcher={setShouldDisplayInProgressType}
|
style={{ gridAutoFlow: 'row', rowGap: '0.5rem' }}
|
||||||
/>
|
>
|
||||||
|
<IAISelect
|
||||||
<SettingsModalItem
|
label={'Display In-Progress Images'}
|
||||||
settingTitle="Confirm on Delete"
|
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}
|
isChecked={shouldConfirmOnDelete}
|
||||||
dispatcher={setShouldConfirmOnDelete}
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
dispatch(setShouldConfirmOnDelete(e.target.checked))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
<IAISwitch
|
||||||
<SettingsModalItem
|
styleClass="settings-modal-item"
|
||||||
settingTitle="Display Help Icons"
|
label={'Display Help Icons'}
|
||||||
isChecked={shouldDisplayGuides}
|
isChecked={shouldDisplayGuides}
|
||||||
dispatcher={setShouldDisplayGuides}
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
dispatch(setShouldDisplayGuides(e.target.checked))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="settings-modal-reset">
|
<div className="settings-modal-reset">
|
||||||
<Heading size={'md'}>Reset Web UI</Heading>
|
<Heading size={'md'}>Reset Web UI</Heading>
|
||||||
|
<Button colorScheme="red" onClick={handleClickResetWebUI}>
|
||||||
|
Reset Web UI
|
||||||
|
</Button>
|
||||||
<Text>
|
<Text>
|
||||||
Resetting the web UI only resets the browser's local cache of
|
Resetting the web UI only resets the browser's local cache of
|
||||||
your images and remembered settings. It does not delete any
|
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
|
isn't working, please try resetting before submitting an issue
|
||||||
on GitHub.
|
on GitHub.
|
||||||
</Text>
|
</Text>
|
||||||
<Button colorScheme="red" onClick={handleClickResetWebUI}>
|
|
||||||
Reset Web UI
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</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 { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
import { FaSun, FaMoon, FaGithub, FaDiscord } from 'react-icons/fa';
|
import {
|
||||||
import { MdHelp, MdKeyboard, MdSettings } from 'react-icons/md';
|
FaSun,
|
||||||
|
FaMoon,
|
||||||
|
FaGithub,
|
||||||
|
FaDiscord,
|
||||||
|
FaBug,
|
||||||
|
FaKeyboard,
|
||||||
|
FaWrench,
|
||||||
|
} from 'react-icons/fa';
|
||||||
|
|
||||||
import InvokeAILogo from '../../assets/images/logo.png';
|
import InvokeAILogo from '../../assets/images/logo.png';
|
||||||
|
import IAIIconButton from '../../common/components/IAIIconButton';
|
||||||
|
|
||||||
import HotkeysModal from './HotkeysModal/HotkeysModal';
|
import HotkeysModal from './HotkeysModal/HotkeysModal';
|
||||||
|
|
||||||
@ -26,11 +34,6 @@ const SiteHeader = () => {
|
|||||||
[colorMode, toggleColorMode]
|
[colorMode, toggleColorMode]
|
||||||
);
|
);
|
||||||
|
|
||||||
const colorModeIcon = colorMode == 'light' ? <FaMoon /> : <FaSun />;
|
|
||||||
|
|
||||||
// Make FaMoon and FaSun icon apparent size consistent
|
|
||||||
const colorModeIconFontSize = colorMode == 'light' ? 18 : 20;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="site-header">
|
<div className="site-header">
|
||||||
<div className="site-header-left-side">
|
<div className="site-header-left-side">
|
||||||
@ -44,78 +47,79 @@ const SiteHeader = () => {
|
|||||||
<StatusIndicator />
|
<StatusIndicator />
|
||||||
|
|
||||||
<HotkeysModal>
|
<HotkeysModal>
|
||||||
<IconButton
|
<IAIIconButton
|
||||||
aria-label="Hotkeys"
|
aria-label="Hotkeys"
|
||||||
variant="link"
|
tooltip="Hotkeys"
|
||||||
fontSize={24}
|
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
icon={<MdKeyboard />}
|
variant="link"
|
||||||
|
data-variant="link"
|
||||||
|
fontSize={20}
|
||||||
|
icon={<FaKeyboard />}
|
||||||
/>
|
/>
|
||||||
</HotkeysModal>
|
</HotkeysModal>
|
||||||
|
|
||||||
<Tooltip hasArrow label="Theme" placement={'bottom'}>
|
<IAIIconButton
|
||||||
<IconButton
|
aria-label="Toggle Dark Mode"
|
||||||
aria-label="Toggle Dark Mode"
|
tooltip="Dark Mode"
|
||||||
onClick={toggleColorMode}
|
onClick={toggleColorMode}
|
||||||
variant="link"
|
variant="link"
|
||||||
size={'sm'}
|
data-variant="link"
|
||||||
fontSize={colorModeIconFontSize}
|
fontSize={20}
|
||||||
icon={colorModeIcon}
|
size={'sm'}
|
||||||
/>
|
icon={colorMode === 'light' ? <FaMoon /> : <FaSun />}
|
||||||
</Tooltip>
|
/>
|
||||||
|
|
||||||
<Tooltip hasArrow label="Report Bug" placement={'bottom'}>
|
<IAIIconButton
|
||||||
<IconButton
|
aria-label="Report Bug"
|
||||||
aria-label="Link to Github Issues"
|
tooltip="Report Bug"
|
||||||
variant="link"
|
variant="link"
|
||||||
fontSize={23}
|
data-variant="link"
|
||||||
size={'sm'}
|
fontSize={20}
|
||||||
icon={
|
size={'sm'}
|
||||||
<Link
|
icon={
|
||||||
isExternal
|
<Link isExternal href="http://github.com/invoke-ai/InvokeAI/issues">
|
||||||
href="http://github.com/invoke-ai/InvokeAI/issues"
|
<FaBug />
|
||||||
>
|
</Link>
|
||||||
<MdHelp />
|
}
|
||||||
</Link>
|
/>
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip hasArrow label="Github" placement={'bottom'}>
|
<IAIIconButton
|
||||||
<IconButton
|
aria-label="Link to Github Repo"
|
||||||
aria-label="Link to Github Repo"
|
tooltip="Github"
|
||||||
variant="link"
|
variant="link"
|
||||||
fontSize={20}
|
data-variant="link"
|
||||||
size={'sm'}
|
fontSize={20}
|
||||||
icon={
|
size={'sm'}
|
||||||
<Link isExternal href="http://github.com/invoke-ai/InvokeAI">
|
icon={
|
||||||
<FaGithub />
|
<Link isExternal href="http://github.com/invoke-ai/InvokeAI">
|
||||||
</Link>
|
<FaGithub />
|
||||||
}
|
</Link>
|
||||||
/>
|
}
|
||||||
</Tooltip>
|
/>
|
||||||
|
|
||||||
<Tooltip hasArrow label="Discord" placement={'bottom'}>
|
<IAIIconButton
|
||||||
<IconButton
|
aria-label="Link to Discord Server"
|
||||||
aria-label="Link to Discord Server"
|
tooltip="Discord"
|
||||||
variant="link"
|
variant="link"
|
||||||
fontSize={20}
|
data-variant="link"
|
||||||
size={'sm'}
|
fontSize={20}
|
||||||
icon={
|
size={'sm'}
|
||||||
<Link isExternal href="https://discord.gg/ZmtBAhwWhy">
|
icon={
|
||||||
<FaDiscord />
|
<Link isExternal href="https://discord.gg/ZmtBAhwWhy">
|
||||||
</Link>
|
<FaDiscord />
|
||||||
}
|
</Link>
|
||||||
/>
|
}
|
||||||
</Tooltip>
|
/>
|
||||||
|
|
||||||
<SettingsModal>
|
<SettingsModal>
|
||||||
<IconButton
|
<IAIIconButton
|
||||||
aria-label="Settings"
|
aria-label="Settings"
|
||||||
|
tooltip="Settings"
|
||||||
variant="link"
|
variant="link"
|
||||||
fontSize={24}
|
data-variant="link"
|
||||||
|
fontSize={20}
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
icon={<MdSettings />}
|
icon={<FaWrench />}
|
||||||
/>
|
/>
|
||||||
</SettingsModal>
|
</SettingsModal>
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,10 +15,17 @@ export interface Log {
|
|||||||
[index: number]: LogEntry;
|
[index: number]: LogEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ReadinessPayload = {
|
||||||
|
isReady: boolean;
|
||||||
|
reasonsWhyNotReady: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InProgressImageType = 'none' | 'full-res' | 'latents';
|
||||||
|
|
||||||
export interface SystemState
|
export interface SystemState
|
||||||
extends InvokeAI.SystemStatus,
|
extends InvokeAI.SystemStatus,
|
||||||
InvokeAI.SystemConfig {
|
InvokeAI.SystemConfig {
|
||||||
shouldDisplayInProgressType: string;
|
shouldDisplayInProgressType: InProgressImageType;
|
||||||
log: Array<LogEntry>;
|
log: Array<LogEntry>;
|
||||||
shouldShowLogViewer: boolean;
|
shouldShowLogViewer: boolean;
|
||||||
isGFPGANAvailable: boolean;
|
isGFPGANAvailable: boolean;
|
||||||
@ -36,14 +43,15 @@ export interface SystemState
|
|||||||
shouldDisplayGuides: boolean;
|
shouldDisplayGuides: boolean;
|
||||||
wasErrorSeen: boolean;
|
wasErrorSeen: boolean;
|
||||||
isCancelable: boolean;
|
isCancelable: boolean;
|
||||||
|
saveIntermediatesInterval: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialSystemState = {
|
const initialSystemState: SystemState = {
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
isProcessing: false,
|
isProcessing: false,
|
||||||
log: [],
|
log: [],
|
||||||
shouldShowLogViewer: false,
|
shouldShowLogViewer: false,
|
||||||
shouldDisplayInProgressType: "none",
|
shouldDisplayInProgressType: 'latents',
|
||||||
shouldDisplayGuides: true,
|
shouldDisplayGuides: true,
|
||||||
isGFPGANAvailable: true,
|
isGFPGANAvailable: true,
|
||||||
isESRGANAvailable: true,
|
isESRGANAvailable: true,
|
||||||
@ -65,15 +73,17 @@ const initialSystemState = {
|
|||||||
hasError: false,
|
hasError: false,
|
||||||
wasErrorSeen: true,
|
wasErrorSeen: true,
|
||||||
isCancelable: true,
|
isCancelable: true,
|
||||||
|
saveIntermediatesInterval: 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState: SystemState = initialSystemState;
|
|
||||||
|
|
||||||
export const systemSlice = createSlice({
|
export const systemSlice = createSlice({
|
||||||
name: 'system',
|
name: 'system',
|
||||||
initialState,
|
initialState: initialSystemState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setShouldDisplayInProgressType: (state, action: PayloadAction<string>) => {
|
setShouldDisplayInProgressType: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<InProgressImageType>
|
||||||
|
) => {
|
||||||
state.shouldDisplayInProgressType = action.payload;
|
state.shouldDisplayInProgressType = action.payload;
|
||||||
},
|
},
|
||||||
setIsProcessing: (state, action: PayloadAction<boolean>) => {
|
setIsProcessing: (state, action: PayloadAction<boolean>) => {
|
||||||
@ -178,6 +188,9 @@ export const systemSlice = createSlice({
|
|||||||
state.isProcessing = true;
|
state.isProcessing = true;
|
||||||
state.currentStatusHasSteps = false;
|
state.currentStatusHasSteps = false;
|
||||||
},
|
},
|
||||||
|
setSaveIntermediatesInterval: (state, action: PayloadAction<number>) => {
|
||||||
|
state.saveIntermediatesInterval = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -200,6 +213,7 @@ export const {
|
|||||||
setModelList,
|
setModelList,
|
||||||
setIsCancelable,
|
setIsCancelable,
|
||||||
modelChangeRequested,
|
modelChangeRequested,
|
||||||
|
setSaveIntermediatesInterval,
|
||||||
} = systemSlice.actions;
|
} = systemSlice.actions;
|
||||||
|
|
||||||
export default systemSlice.reducer;
|
export default systemSlice.reducer;
|
||||||
|
@ -13,7 +13,7 @@ const FloatingGalleryButton = () => {
|
|||||||
return (
|
return (
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
tooltip="Show Gallery (G)"
|
tooltip="Show Gallery (G)"
|
||||||
tooltipPlacement="top"
|
tooltipProps={{ placement: 'top' }}
|
||||||
aria-label="Show Gallery"
|
aria-label="Show Gallery"
|
||||||
styleClass="floating-show-hide-button right"
|
styleClass="floating-show-hide-button right"
|
||||||
onMouseOver={handleShowGallery}
|
onMouseOver={handleShowGallery}
|
||||||
|
@ -36,7 +36,7 @@ const FloatingOptionsPanelButtons = () => {
|
|||||||
<div className="show-hide-button-options">
|
<div className="show-hide-button-options">
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
tooltip="Show Options Panel (O)"
|
tooltip="Show Options Panel (O)"
|
||||||
tooltipPlacement="top"
|
tooltipProps={{ placement: 'top' }}
|
||||||
aria-label="Show Options Panel"
|
aria-label="Show Options Panel"
|
||||||
onClick={handleShowOptionsPanel}
|
onClick={handleShowOptionsPanel}
|
||||||
>
|
>
|
||||||
|
@ -1,20 +1,22 @@
|
|||||||
import { IconButton, Image, useToast } from '@chakra-ui/react';
|
import { Image, useToast } from '@chakra-ui/react';
|
||||||
import React, { SyntheticEvent } from 'react';
|
import { SyntheticEvent } from 'react';
|
||||||
import { MdClear } from 'react-icons/md';
|
|
||||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||||
|
import ImageUploaderIconButton from '../../../common/components/ImageUploaderIconButton';
|
||||||
import { clearInitialImage } from '../../options/optionsSlice';
|
import { clearInitialImage } from '../../options/optionsSlice';
|
||||||
|
|
||||||
export default function InitImagePreview() {
|
export default function InitImagePreview() {
|
||||||
const { initialImage } = useAppSelector((state: RootState) => state.options);
|
const initialImage = useAppSelector(
|
||||||
|
(state: RootState) => state.options.initialImage
|
||||||
|
);
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
const handleClickResetInitialImage = (e: SyntheticEvent) => {
|
// const handleClickResetInitialImage = (e: SyntheticEvent) => {
|
||||||
e.stopPropagation();
|
// e.stopPropagation();
|
||||||
dispatch(clearInitialImage());
|
// dispatch(clearInitialImage());
|
||||||
};
|
// };
|
||||||
|
|
||||||
const alertMissingInitImage = () => {
|
const alertMissingInitImage = () => {
|
||||||
toast({
|
toast({
|
||||||
@ -29,13 +31,15 @@ export default function InitImagePreview() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="init-image-preview-header">
|
<div className="init-image-preview-header">
|
||||||
|
{/* <div className="init-image-preview-header"> */}
|
||||||
<h2>Initial Image</h2>
|
<h2>Initial Image</h2>
|
||||||
<IconButton
|
{/* <IconButton
|
||||||
isDisabled={!initialImage}
|
isDisabled={!initialImage}
|
||||||
aria-label={'Reset Initial Image'}
|
aria-label={'Reset Initial Image'}
|
||||||
onClick={handleClickResetInitialImage}
|
onClick={handleClickResetInitialImage}
|
||||||
icon={<MdClear />}
|
icon={<MdClear />}
|
||||||
/>
|
/> */}
|
||||||
|
<ImageUploaderIconButton />
|
||||||
</div>
|
</div>
|
||||||
{initialImage && (
|
{initialImage && (
|
||||||
<div className="init-image-preview">
|
<div className="init-image-preview">
|
||||||
|
@ -11,7 +11,11 @@
|
|||||||
.inpainting-settings {
|
.inpainting-settings {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
column-gap: 1rem;
|
column-gap: 0.5rem;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
.inpainting-buttons-group {
|
.inpainting-buttons-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -29,10 +33,10 @@
|
|||||||
margin-left: 1rem !important;
|
margin-left: 1rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inpainting-slider-numberinput {
|
.inpainting-brush-options {
|
||||||
display: flex;
|
display: flex;
|
||||||
column-gap: 1rem;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
column-gap: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,33 +49,23 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inpainting-canvas-wrapper {
|
.inpainting-canvas-spiner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inpainting-canvas-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
position: relative;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
|
|
||||||
.inpainting-alerts {
|
.inpainting-canvas-wrapper {
|
||||||
position: absolute;
|
position: relative;
|
||||||
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-stage {
|
.inpainting-canvas-stage {
|
||||||
|
@ -33,7 +33,7 @@ import InpaintingBoundingBoxPreview, {
|
|||||||
InpaintingBoundingBoxPreviewOverlay,
|
InpaintingBoundingBoxPreviewOverlay,
|
||||||
} from './components/InpaintingBoundingBoxPreview';
|
} from './components/InpaintingBoundingBoxPreview';
|
||||||
import { KonvaEventObject } from 'konva/lib/Node';
|
import { KonvaEventObject } from 'konva/lib/Node';
|
||||||
import KeyboardEventManager from './components/KeyboardEventManager';
|
import KeyboardEventManager from './KeyboardEventManager';
|
||||||
import { useToast } from '@chakra-ui/react';
|
import { useToast } from '@chakra-ui/react';
|
||||||
|
|
||||||
// Use a closure allow other components to use these things... not ideal...
|
// Use a closure allow other components to use these things... not ideal...
|
||||||
@ -56,8 +56,8 @@ const InpaintingCanvas = () => {
|
|||||||
shouldShowBoundingBox,
|
shouldShowBoundingBox,
|
||||||
shouldShowBoundingBoxFill,
|
shouldShowBoundingBoxFill,
|
||||||
isDrawing,
|
isDrawing,
|
||||||
shouldLockBoundingBox,
|
isModifyingBoundingBox,
|
||||||
boundingBoxDimensions,
|
stageCursor,
|
||||||
} = useAppSelector(inpaintingCanvasSelector);
|
} = useAppSelector(inpaintingCanvasSelector);
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@ -113,7 +113,7 @@ const InpaintingCanvas = () => {
|
|||||||
if (
|
if (
|
||||||
!scaledCursorPosition ||
|
!scaledCursorPosition ||
|
||||||
!maskLayerRef.current ||
|
!maskLayerRef.current ||
|
||||||
!shouldLockBoundingBox
|
isModifyingBoundingBox
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -127,7 +127,7 @@ const InpaintingCanvas = () => {
|
|||||||
points: [scaledCursorPosition.x, scaledCursorPosition.y],
|
points: [scaledCursorPosition.x, scaledCursorPosition.y],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}, [dispatch, brushSize, tool, shouldLockBoundingBox]);
|
}, [dispatch, brushSize, tool, isModifyingBoundingBox]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -143,20 +143,20 @@ const InpaintingCanvas = () => {
|
|||||||
|
|
||||||
dispatch(setCursorPosition(scaledCursorPosition));
|
dispatch(setCursorPosition(scaledCursorPosition));
|
||||||
|
|
||||||
if (!maskLayerRef.current || !shouldLockBoundingBox) {
|
if (!maskLayerRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastCursorPosition.current = scaledCursorPosition;
|
lastCursorPosition.current = scaledCursorPosition;
|
||||||
|
|
||||||
if (!isDrawing) return;
|
if (!isDrawing || isModifyingBoundingBox) return;
|
||||||
|
|
||||||
didMouseMoveRef.current = true;
|
didMouseMoveRef.current = true;
|
||||||
// Extend the current line
|
// Extend the current line
|
||||||
dispatch(
|
dispatch(
|
||||||
addPointToCurrentLine([scaledCursorPosition.x, scaledCursorPosition.y])
|
addPointToCurrentLine([scaledCursorPosition.x, scaledCursorPosition.y])
|
||||||
);
|
);
|
||||||
}, [dispatch, isDrawing, shouldLockBoundingBox]);
|
}, [dispatch, isDrawing, isModifyingBoundingBox]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -170,7 +170,7 @@ const InpaintingCanvas = () => {
|
|||||||
if (
|
if (
|
||||||
!scaledCursorPosition ||
|
!scaledCursorPosition ||
|
||||||
!maskLayerRef.current ||
|
!maskLayerRef.current ||
|
||||||
!shouldLockBoundingBox
|
isModifyingBoundingBox
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -187,7 +187,7 @@ const InpaintingCanvas = () => {
|
|||||||
didMouseMoveRef.current = false;
|
didMouseMoveRef.current = false;
|
||||||
}
|
}
|
||||||
dispatch(setIsDrawing(false));
|
dispatch(setIsDrawing(false));
|
||||||
}, [dispatch, isDrawing, shouldLockBoundingBox]);
|
}, [dispatch, isDrawing, isModifyingBoundingBox]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -214,7 +214,7 @@ const InpaintingCanvas = () => {
|
|||||||
if (
|
if (
|
||||||
!scaledCursorPosition ||
|
!scaledCursorPosition ||
|
||||||
!maskLayerRef.current ||
|
!maskLayerRef.current ||
|
||||||
!shouldLockBoundingBox
|
isModifyingBoundingBox
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -230,93 +230,78 @@ const InpaintingCanvas = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dispatch, brushSize, tool, shouldLockBoundingBox]
|
[dispatch, brushSize, tool, isModifyingBoundingBox]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inpainting-canvas-wrapper" tabIndex={1}>
|
<div className="inpainting-canvas-container">
|
||||||
<div className="inpainting-alerts">
|
<div className="inpainting-canvas-wrapper">
|
||||||
{!shouldShowMask && (
|
{canvasBgImage && (
|
||||||
<div style={{ pointerEvents: 'none' }}>Mask Hidden (H)</div>
|
<Stage
|
||||||
)}
|
width={Math.floor(canvasBgImage.width * stageScale)}
|
||||||
{shouldInvertMask && (
|
height={Math.floor(canvasBgImage.height * stageScale)}
|
||||||
<div style={{ pointerEvents: 'none' }}>Mask Inverted (Shift+M)</div>
|
scale={{ x: stageScale, y: stageScale }}
|
||||||
)}
|
onMouseDown={handleMouseDown}
|
||||||
{!shouldLockBoundingBox && (
|
onMouseMove={handleMouseMove}
|
||||||
<div style={{ pointerEvents: 'none' }}>
|
onMouseEnter={handleMouseEnter}
|
||||||
{`Transforming Bounding Box ${boundingBoxDimensions.width}x${boundingBoxDimensions.height} (M)`}
|
onMouseUp={handleMouseUp}
|
||||||
</div>
|
onMouseOut={handleMouseOutCanvas}
|
||||||
)}
|
onMouseLeave={handleMouseOutCanvas}
|
||||||
</div>
|
style={{ ...(stageCursor ? { cursor: stageCursor } : {}) }}
|
||||||
|
className="inpainting-canvas-stage checkerboard"
|
||||||
{canvasBgImage && (
|
ref={stageRef}
|
||||||
<Stage
|
>
|
||||||
width={Math.floor(canvasBgImage.width * stageScale)}
|
{!shouldInvertMask && !shouldShowCheckboardTransparency && (
|
||||||
height={Math.floor(canvasBgImage.height * stageScale)}
|
<Layer name={'image-layer'} listening={false}>
|
||||||
scale={{ x: stageScale, y: stageScale }}
|
<KonvaImage listening={false} image={canvasBgImage} />
|
||||||
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"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Layer>
|
</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>
|
<Layer>
|
||||||
{shouldShowBoundingBoxFill && shouldShowBoundingBox && (
|
{shouldShowBoundingBoxFill && shouldShowBoundingBox && (
|
||||||
<InpaintingBoundingBoxPreviewOverlay />
|
<InpaintingBoundingBoxPreviewOverlay />
|
||||||
)}
|
)}
|
||||||
{shouldShowBoundingBox && <InpaintingBoundingBoxPreview />}
|
{shouldShowBoundingBox && <InpaintingBoundingBoxPreview />}
|
||||||
{shouldLockBoundingBox && (
|
|
||||||
<InpaintingCanvasBrushPreviewOutline />
|
<InpaintingCanvasBrushPreviewOutline />
|
||||||
)}
|
|
||||||
</Layer>
|
</Layer>
|
||||||
)}
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
</Stage>
|
||||||
</Stage>
|
)}
|
||||||
)}
|
<Cacher />
|
||||||
<Cacher />
|
<KeyboardEventManager />
|
||||||
<KeyboardEventManager />
|
</div>
|
||||||
</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 InpaintingBrushControl from './InpaintingControls/InpaintingBrushControl';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import InpaintingEraserControl from './InpaintingControls/InpaintingEraserControl';
|
||||||
import {
|
import InpaintingUndoControl from './InpaintingControls/InpaintingUndoControl';
|
||||||
FaEraser,
|
import InpaintingRedoControl from './InpaintingControls/InpaintingRedoControl';
|
||||||
FaMask,
|
import { ButtonGroup } from '@chakra-ui/react';
|
||||||
FaPaintBrush,
|
import InpaintingMaskClear from './InpaintingControls/InpaintingMaskControls/InpaintingMaskClear';
|
||||||
FaPalette,
|
import InpaintingMaskVisibilityControl from './InpaintingControls/InpaintingMaskControls/InpaintingMaskVisibilityControl';
|
||||||
FaPlus,
|
import InpaintingMaskInvertControl from './InpaintingControls/InpaintingMaskControls/InpaintingMaskInvertControl';
|
||||||
FaRedo,
|
import InpaintingLockBoundingBoxControl from './InpaintingControls/InpaintingLockBoundingBoxControl';
|
||||||
FaTrash,
|
import InpaintingShowHideBoundingBoxControl from './InpaintingControls/InpaintingShowHideBoundingBoxControl';
|
||||||
FaUndo,
|
import ImageUploaderIconButton from '../../../common/components/ImageUploaderIconButton';
|
||||||
} 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';
|
|
||||||
|
|
||||||
const InpaintingControls = () => {
|
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 (
|
return (
|
||||||
<div className="inpainting-settings">
|
<div className="inpainting-settings">
|
||||||
<div className="inpainting-buttons-group">
|
<ButtonGroup isAttached={true}>
|
||||||
<IAIPopover
|
<InpaintingBrushControl />
|
||||||
trigger="hover"
|
<InpaintingEraserControl />
|
||||||
onOpen={handleShowBrushPreview}
|
</ButtonGroup>
|
||||||
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>
|
|
||||||
|
|
||||||
<div className="inpainting-buttons-group">
|
<ButtonGroup isAttached={true}>
|
||||||
<IAIIconButton
|
<InpaintingMaskVisibilityControl />
|
||||||
aria-label="Clear Image"
|
<InpaintingMaskInvertControl />
|
||||||
tooltip="Clear Image"
|
<InpaintingLockBoundingBoxControl />
|
||||||
icon={<FaTrash size={16} />}
|
<InpaintingShowHideBoundingBoxControl />
|
||||||
onClick={handleClearImage}
|
<InpaintingMaskClear />
|
||||||
/>
|
</ButtonGroup>
|
||||||
</div>
|
|
||||||
<IAIIconButton
|
<ButtonGroup isAttached={true}>
|
||||||
aria-label="Split Layout (Shift+J)"
|
<InpaintingUndoControl />
|
||||||
tooltip="Split Layout (Shift+J)"
|
<InpaintingRedoControl />
|
||||||
icon={<VscSplitHorizontal />}
|
</ButtonGroup>
|
||||||
data-selected={showDualDisplay}
|
<ButtonGroup isAttached={true}>
|
||||||
onClick={handleDualDisplay}
|
<ImageUploaderIconButton />
|
||||||
/>
|
</ButtonGroup>
|
||||||
</div>
|
</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 { createSelector } from '@reduxjs/toolkit';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import {
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
RootState,
|
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||||
useAppDispatch,
|
import { activeTabNameSelector } from '../../options/optionsSelectors';
|
||||||
useAppSelector,
|
import { OptionsState } from '../../options/optionsSlice';
|
||||||
} from '../../../../app/store';
|
|
||||||
import { activeTabNameSelector } from '../../../options/optionsSelectors';
|
|
||||||
import { OptionsState } from '../../../options/optionsSlice';
|
|
||||||
import {
|
import {
|
||||||
InpaintingState,
|
InpaintingState,
|
||||||
setIsDrawing,
|
setIsSpacebarHeld,
|
||||||
setShouldLockBoundingBox,
|
setShouldLockBoundingBox,
|
||||||
|
toggleShouldLockBoundingBox,
|
||||||
toggleTool,
|
toggleTool,
|
||||||
} from '../inpaintingSlice';
|
} from './inpaintingSlice';
|
||||||
|
|
||||||
const keyboardEventManagerSelector = createSelector(
|
const keyboardEventManagerSelector = createSelector(
|
||||||
[
|
[
|
||||||
@ -22,13 +20,18 @@ const keyboardEventManagerSelector = createSelector(
|
|||||||
activeTabNameSelector,
|
activeTabNameSelector,
|
||||||
],
|
],
|
||||||
(options: OptionsState, inpainting: InpaintingState, activeTabName) => {
|
(options: OptionsState, inpainting: InpaintingState, activeTabName) => {
|
||||||
const { shouldShowMask, cursorPosition, shouldLockBoundingBox } =
|
const {
|
||||||
inpainting;
|
shouldShowMask,
|
||||||
|
cursorPosition,
|
||||||
|
shouldLockBoundingBox,
|
||||||
|
shouldShowBoundingBox,
|
||||||
|
} = inpainting;
|
||||||
return {
|
return {
|
||||||
activeTabName,
|
activeTabName,
|
||||||
shouldShowMask,
|
shouldShowMask,
|
||||||
isCursorOnCanvas: Boolean(cursorPosition),
|
isCursorOnCanvas: Boolean(cursorPosition),
|
||||||
shouldLockBoundingBox,
|
shouldLockBoundingBox,
|
||||||
|
shouldShowBoundingBox,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -45,15 +48,30 @@ const KeyboardEventManager = () => {
|
|||||||
activeTabName,
|
activeTabName,
|
||||||
isCursorOnCanvas,
|
isCursorOnCanvas,
|
||||||
shouldLockBoundingBox,
|
shouldLockBoundingBox,
|
||||||
|
shouldShowBoundingBox,
|
||||||
} = useAppSelector(keyboardEventManagerSelector);
|
} = useAppSelector(keyboardEventManagerSelector);
|
||||||
|
|
||||||
const wasLastEventOverCanvas = useRef<boolean>(false);
|
const wasLastEventOverCanvas = useRef<boolean>(false);
|
||||||
const lastEvent = useRef<KeyboardEvent | null>(null);
|
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(() => {
|
useEffect(() => {
|
||||||
const listener = (e: KeyboardEvent) => {
|
const listener = (e: KeyboardEvent) => {
|
||||||
if (
|
if (
|
||||||
!['x', ' '].includes(e.key) ||
|
!['x', 'q'].includes(e.key) ||
|
||||||
activeTabName !== 'inpainting' ||
|
activeTabName !== 'inpainting' ||
|
||||||
!shouldShowMask
|
!shouldShowMask
|
||||||
) {
|
) {
|
||||||
@ -91,13 +109,10 @@ const KeyboardEventManager = () => {
|
|||||||
dispatch(toggleTool());
|
dispatch(toggleTool());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ' ': {
|
case 'q': {
|
||||||
if (!shouldShowMask) break;
|
if (!shouldShowMask || !shouldShowBoundingBox) break;
|
||||||
|
dispatch(setIsSpacebarHeld(e.type === 'keydown'));
|
||||||
if (e.type === 'keydown') {
|
dispatch(setShouldLockBoundingBox(e.type !== 'keydown'));
|
||||||
dispatch(setIsDrawing(false));
|
|
||||||
}
|
|
||||||
dispatch(setShouldLockBoundingBox(!shouldLockBoundingBox));
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -119,6 +134,7 @@ const KeyboardEventManager = () => {
|
|||||||
shouldShowMask,
|
shouldShowMask,
|
||||||
isCursorOnCanvas,
|
isCursorOnCanvas,
|
||||||
shouldLockBoundingBox,
|
shouldLockBoundingBox,
|
||||||
|
shouldShowBoundingBox,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return null;
|
return null;
|
@ -33,6 +33,9 @@ const Cacher = () => {
|
|||||||
futureLines,
|
futureLines,
|
||||||
needsCache,
|
needsCache,
|
||||||
isDrawing,
|
isDrawing,
|
||||||
|
isTransformingBoundingBox,
|
||||||
|
isMovingBoundingBox,
|
||||||
|
shouldShowBoundingBox,
|
||||||
} = useAppSelector((state: RootState) => state.inpainting);
|
} = useAppSelector((state: RootState) => state.inpainting);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
@ -58,12 +61,15 @@ const Cacher = () => {
|
|||||||
imageToInpaint,
|
imageToInpaint,
|
||||||
shouldShowBrush,
|
shouldShowBrush,
|
||||||
shouldShowBoundingBoxFill,
|
shouldShowBoundingBoxFill,
|
||||||
|
shouldShowBoundingBox,
|
||||||
shouldLockBoundingBox,
|
shouldLockBoundingBox,
|
||||||
stageScale,
|
stageScale,
|
||||||
pastLines,
|
pastLines,
|
||||||
futureLines,
|
futureLines,
|
||||||
needsCache,
|
needsCache,
|
||||||
isDrawing,
|
isDrawing,
|
||||||
|
isTransformingBoundingBox,
|
||||||
|
isMovingBoundingBox,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
|
import { Context } from 'konva/lib/Context';
|
||||||
import { KonvaEventObject } from 'konva/lib/Node';
|
import { KonvaEventObject } from 'konva/lib/Node';
|
||||||
import { Box } from 'konva/lib/shapes/Transformer';
|
import { Box } from 'konva/lib/shapes/Transformer';
|
||||||
import { Vector2d } from 'konva/lib/types';
|
import { Vector2d } from 'konva/lib/types';
|
||||||
@ -12,11 +13,13 @@ import {
|
|||||||
useAppSelector,
|
useAppSelector,
|
||||||
} from '../../../../app/store';
|
} from '../../../../app/store';
|
||||||
import { roundToMultiple } from '../../../../common/util/roundDownToMultiple';
|
import { roundToMultiple } from '../../../../common/util/roundDownToMultiple';
|
||||||
import { stageRef } from '../InpaintingCanvas';
|
|
||||||
import {
|
import {
|
||||||
InpaintingState,
|
InpaintingState,
|
||||||
setBoundingBoxCoordinate,
|
setBoundingBoxCoordinate,
|
||||||
setBoundingBoxDimensions,
|
setBoundingBoxDimensions,
|
||||||
|
setIsMouseOverBoundingBox,
|
||||||
|
setIsMovingBoundingBox,
|
||||||
|
setIsTransformingBoundingBox,
|
||||||
} from '../inpaintingSlice';
|
} from '../inpaintingSlice';
|
||||||
import { rgbaColorToString } from '../util/colorToString';
|
import { rgbaColorToString } from '../util/colorToString';
|
||||||
import {
|
import {
|
||||||
@ -35,6 +38,11 @@ const boundingBoxPreviewSelector = createSelector(
|
|||||||
stageScale,
|
stageScale,
|
||||||
imageToInpaint,
|
imageToInpaint,
|
||||||
shouldLockBoundingBox,
|
shouldLockBoundingBox,
|
||||||
|
isDrawing,
|
||||||
|
isTransformingBoundingBox,
|
||||||
|
isMovingBoundingBox,
|
||||||
|
isMouseOverBoundingBox,
|
||||||
|
isSpacebarHeld,
|
||||||
} = inpainting;
|
} = inpainting;
|
||||||
return {
|
return {
|
||||||
boundingBoxCoordinate,
|
boundingBoxCoordinate,
|
||||||
@ -46,6 +54,11 @@ const boundingBoxPreviewSelector = createSelector(
|
|||||||
dash: DASH_WIDTH / stageScale, // scale dash lengths
|
dash: DASH_WIDTH / stageScale, // scale dash lengths
|
||||||
strokeWidth: 1 / stageScale, // scale stroke thickness
|
strokeWidth: 1 / stageScale, // scale stroke thickness
|
||||||
shouldLockBoundingBox,
|
shouldLockBoundingBox,
|
||||||
|
isDrawing,
|
||||||
|
isTransformingBoundingBox,
|
||||||
|
isMouseOverBoundingBox,
|
||||||
|
isMovingBoundingBox,
|
||||||
|
isSpacebarHeld,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -93,10 +106,14 @@ const InpaintingBoundingBoxPreview = () => {
|
|||||||
const {
|
const {
|
||||||
boundingBoxCoordinate,
|
boundingBoxCoordinate,
|
||||||
boundingBoxDimensions,
|
boundingBoxDimensions,
|
||||||
strokeWidth,
|
|
||||||
stageScale,
|
stageScale,
|
||||||
imageToInpaint,
|
imageToInpaint,
|
||||||
shouldLockBoundingBox,
|
shouldLockBoundingBox,
|
||||||
|
isDrawing,
|
||||||
|
isTransformingBoundingBox,
|
||||||
|
isMovingBoundingBox,
|
||||||
|
isMouseOverBoundingBox,
|
||||||
|
isSpacebarHeld,
|
||||||
} = useAppSelector(boundingBoxPreviewSelector);
|
} = useAppSelector(boundingBoxPreviewSelector);
|
||||||
|
|
||||||
const transformerRef = useRef<Konva.Transformer>(null);
|
const transformerRef = useRef<Konva.Transformer>(null);
|
||||||
@ -108,15 +125,6 @@ const InpaintingBoundingBoxPreview = () => {
|
|||||||
transformerRef.current.getLayer()?.batchDraw();
|
transformerRef.current.getLayer()?.batchDraw();
|
||||||
}, [shouldLockBoundingBox]);
|
}, [shouldLockBoundingBox]);
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() => () => {
|
|
||||||
const container = stageRef.current?.container();
|
|
||||||
if (!container) return;
|
|
||||||
container.style.cursor = 'unset';
|
|
||||||
},
|
|
||||||
[shouldLockBoundingBox]
|
|
||||||
);
|
|
||||||
|
|
||||||
const scaledStep = 64 * stageScale;
|
const scaledStep = 64 * stageScale;
|
||||||
|
|
||||||
const handleOnDragMove = useCallback(
|
const handleOnDragMove = useCallback(
|
||||||
@ -269,6 +277,35 @@ const InpaintingBoundingBoxPreview = () => {
|
|||||||
[imageToInpaint, stageScale]
|
[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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Rect
|
<Rect
|
||||||
@ -277,23 +314,28 @@ const InpaintingBoundingBoxPreview = () => {
|
|||||||
width={boundingBoxDimensions.width}
|
width={boundingBoxDimensions.width}
|
||||||
height={boundingBoxDimensions.height}
|
height={boundingBoxDimensions.height}
|
||||||
ref={shapeRef}
|
ref={shapeRef}
|
||||||
stroke={'white'}
|
stroke={isMouseOverBoundingBox ? 'rgba(255,255,255,0.3)' : 'white'}
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={Math.floor((isMouseOverBoundingBox ? 8 : 1) / stageScale)}
|
||||||
listening={!shouldLockBoundingBox}
|
fillEnabled={isSpacebarHeld}
|
||||||
onMouseEnter={(e) => {
|
hitFunc={isSpacebarHeld ? spacebarHeldHitFunc : undefined}
|
||||||
const container = e?.target?.getStage()?.container();
|
hitStrokeWidth={Math.floor(13 / stageScale)}
|
||||||
if (!container) return;
|
listening={!isDrawing && !shouldLockBoundingBox}
|
||||||
container.style.cursor = shouldLockBoundingBox ? 'none' : 'move';
|
onMouseOver={() => {
|
||||||
|
dispatch(setIsMouseOverBoundingBox(true));
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseOut={() => {
|
||||||
const container = e?.target?.getStage()?.container();
|
!isTransformingBoundingBox &&
|
||||||
if (!container) return;
|
!isMovingBoundingBox &&
|
||||||
container.style.cursor = shouldLockBoundingBox ? 'none' : 'default';
|
dispatch(setIsMouseOverBoundingBox(false));
|
||||||
}}
|
}}
|
||||||
draggable={!shouldLockBoundingBox}
|
onMouseDown={handleStartedMoving}
|
||||||
|
onMouseUp={handleEndedModifying}
|
||||||
|
draggable={true}
|
||||||
onDragMove={handleOnDragMove}
|
onDragMove={handleOnDragMove}
|
||||||
dragBoundFunc={dragBoundFunc}
|
dragBoundFunc={dragBoundFunc}
|
||||||
onTransform={handleOnTransform}
|
onTransform={handleOnTransform}
|
||||||
|
onDragEnd={handleEndedModifying}
|
||||||
|
onTransformEnd={handleEndedTransforming}
|
||||||
/>
|
/>
|
||||||
<Transformer
|
<Transformer
|
||||||
ref={transformerRef}
|
ref={transformerRef}
|
||||||
@ -308,10 +350,22 @@ const InpaintingBoundingBoxPreview = () => {
|
|||||||
flipEnabled={false}
|
flipEnabled={false}
|
||||||
ignoreStroke={true}
|
ignoreStroke={true}
|
||||||
keepRatio={false}
|
keepRatio={false}
|
||||||
listening={!shouldLockBoundingBox}
|
listening={!isDrawing && !shouldLockBoundingBox}
|
||||||
|
onMouseDown={handleStartedTransforming}
|
||||||
|
onMouseUp={handleEndedTransforming}
|
||||||
enabledAnchors={shouldLockBoundingBox ? [] : undefined}
|
enabledAnchors={shouldLockBoundingBox ? [] : undefined}
|
||||||
boundBoxFunc={boundBoxFunc}
|
boundBoxFunc={boundBoxFunc}
|
||||||
anchorDragBoundFunc={anchorDragBoundFunc}
|
anchorDragBoundFunc={anchorDragBoundFunc}
|
||||||
|
onDragEnd={handleEndedModifying}
|
||||||
|
onTransformEnd={handleEndedTransforming}
|
||||||
|
onMouseOver={() => {
|
||||||
|
dispatch(setIsMouseOverBoundingBox(true));
|
||||||
|
}}
|
||||||
|
onMouseOut={() => {
|
||||||
|
!isTransformingBoundingBox &&
|
||||||
|
!isMovingBoundingBox &&
|
||||||
|
dispatch(setIsMouseOverBoundingBox(false));
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -11,22 +11,28 @@ const inpaintingCanvasBrushPreviewSelector = createSelector(
|
|||||||
const {
|
const {
|
||||||
cursorPosition,
|
cursorPosition,
|
||||||
canvasDimensions: { width, height },
|
canvasDimensions: { width, height },
|
||||||
shouldShowBrushPreview,
|
|
||||||
brushSize,
|
brushSize,
|
||||||
maskColor,
|
maskColor,
|
||||||
tool,
|
tool,
|
||||||
shouldShowBrush,
|
shouldShowBrush,
|
||||||
|
isMovingBoundingBox,
|
||||||
|
isTransformingBoundingBox,
|
||||||
} = inpainting;
|
} = inpainting;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cursorPosition,
|
cursorPosition,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
shouldShowBrushPreview,
|
|
||||||
brushSize,
|
brushSize,
|
||||||
maskColorString: rgbaColorToRgbString(maskColor),
|
maskColorString: rgbaColorToRgbString(maskColor),
|
||||||
tool,
|
tool,
|
||||||
shouldShowBrush,
|
shouldShowBrush,
|
||||||
|
shouldDrawBrushPreview:
|
||||||
|
!(
|
||||||
|
isMovingBoundingBox ||
|
||||||
|
isTransformingBoundingBox ||
|
||||||
|
!cursorPosition
|
||||||
|
) && shouldShowBrush,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -44,16 +50,13 @@ const InpaintingCanvasBrushPreview = () => {
|
|||||||
cursorPosition,
|
cursorPosition,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
shouldShowBrushPreview,
|
|
||||||
brushSize,
|
brushSize,
|
||||||
maskColorString,
|
maskColorString,
|
||||||
tool,
|
tool,
|
||||||
shouldShowBrush,
|
shouldDrawBrushPreview,
|
||||||
} = useAppSelector(inpaintingCanvasBrushPreviewSelector);
|
} = useAppSelector(inpaintingCanvasBrushPreviewSelector);
|
||||||
|
|
||||||
if (!shouldShowBrush || !(cursorPosition || shouldShowBrushPreview)) {
|
if (!shouldDrawBrushPreview) return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Circle
|
<Circle
|
||||||
|
@ -4,26 +4,34 @@ import { Circle } from 'react-konva';
|
|||||||
import { RootState, useAppSelector } from '../../../../app/store';
|
import { RootState, useAppSelector } from '../../../../app/store';
|
||||||
import { InpaintingState } from '../inpaintingSlice';
|
import { InpaintingState } from '../inpaintingSlice';
|
||||||
|
|
||||||
const inpaintingCanvasBrushPreviewSelector = createSelector(
|
const inpaintingCanvasBrushPrevieOutlineSelector = createSelector(
|
||||||
(state: RootState) => state.inpainting,
|
(state: RootState) => state.inpainting,
|
||||||
(inpainting: InpaintingState) => {
|
(inpainting: InpaintingState) => {
|
||||||
const {
|
const {
|
||||||
cursorPosition,
|
cursorPosition,
|
||||||
canvasDimensions: { width, height },
|
canvasDimensions: { width, height },
|
||||||
shouldShowBrushPreview,
|
|
||||||
brushSize,
|
brushSize,
|
||||||
stageScale,
|
tool,
|
||||||
shouldShowBrush,
|
shouldShowBrush,
|
||||||
|
isMovingBoundingBox,
|
||||||
|
isTransformingBoundingBox,
|
||||||
|
stageScale,
|
||||||
} = inpainting;
|
} = inpainting;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cursorPosition,
|
cursorPosition,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
shouldShowBrushPreview,
|
|
||||||
brushSize,
|
brushSize,
|
||||||
|
tool,
|
||||||
strokeWidth: 1 / stageScale, // scale stroke thickness
|
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,
|
cursorPosition,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
shouldShowBrushPreview,
|
|
||||||
brushSize,
|
brushSize,
|
||||||
|
shouldDrawBrushPreview,
|
||||||
strokeWidth,
|
strokeWidth,
|
||||||
shouldShowBrush,
|
radius,
|
||||||
} = useAppSelector(inpaintingCanvasBrushPreviewSelector);
|
} = useAppSelector(inpaintingCanvasBrushPrevieOutlineSelector);
|
||||||
|
|
||||||
if (!shouldShowBrush || !(cursorPosition || shouldShowBrushPreview))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
|
if (!shouldDrawBrushPreview) return null;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Circle
|
<Circle
|
||||||
@ -64,7 +70,7 @@ const InpaintingCanvasBrushPreviewOutline = () => {
|
|||||||
<Circle
|
<Circle
|
||||||
x={cursorPosition ? cursorPosition.x : width / 2}
|
x={cursorPosition ? cursorPosition.x : width / 2}
|
||||||
y={cursorPosition ? cursorPosition.y : height / 2}
|
y={cursorPosition ? cursorPosition.y : height / 2}
|
||||||
radius={1}
|
radius={radius}
|
||||||
fill={'rgba(0,0,0,1)'}
|
fill={'rgba(0,0,0,1)'}
|
||||||
listening={false}
|
listening={false}
|
||||||
/>
|
/>
|
||||||
|
@ -51,9 +51,13 @@ export interface InpaintingState {
|
|||||||
needsCache: boolean;
|
needsCache: boolean;
|
||||||
stageScale: number;
|
stageScale: number;
|
||||||
isDrawing: boolean;
|
isDrawing: boolean;
|
||||||
|
isTransformingBoundingBox: boolean;
|
||||||
|
isMouseOverBoundingBox: boolean;
|
||||||
|
isMovingBoundingBox: boolean;
|
||||||
shouldUseInpaintReplace: boolean;
|
shouldUseInpaintReplace: boolean;
|
||||||
inpaintReplace: number;
|
inpaintReplace: number;
|
||||||
shouldLockBoundingBox: boolean;
|
shouldLockBoundingBox: boolean;
|
||||||
|
isSpacebarHeld: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialInpaintingState: InpaintingState = {
|
const initialInpaintingState: InpaintingState = {
|
||||||
@ -63,7 +67,7 @@ const initialInpaintingState: InpaintingState = {
|
|||||||
canvasDimensions: { width: 0, height: 0 },
|
canvasDimensions: { width: 0, height: 0 },
|
||||||
boundingBoxDimensions: { width: 512, height: 512 },
|
boundingBoxDimensions: { width: 512, height: 512 },
|
||||||
boundingBoxCoordinate: { x: 0, y: 0 },
|
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,
|
shouldShowBoundingBox: true,
|
||||||
shouldShowBoundingBoxFill: true,
|
shouldShowBoundingBoxFill: true,
|
||||||
cursorPosition: null,
|
cursorPosition: null,
|
||||||
@ -77,10 +81,14 @@ const initialInpaintingState: InpaintingState = {
|
|||||||
shouldShowBrushPreview: false,
|
shouldShowBrushPreview: false,
|
||||||
needsCache: false,
|
needsCache: false,
|
||||||
isDrawing: false,
|
isDrawing: false,
|
||||||
|
isTransformingBoundingBox: false,
|
||||||
|
isMouseOverBoundingBox: false,
|
||||||
|
isMovingBoundingBox: false,
|
||||||
stageScale: 1,
|
stageScale: 1,
|
||||||
shouldUseInpaintReplace: false,
|
shouldUseInpaintReplace: false,
|
||||||
inpaintReplace: 1,
|
inpaintReplace: 0.1,
|
||||||
shouldLockBoundingBox: true,
|
shouldLockBoundingBox: true,
|
||||||
|
isSpacebarHeld: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState: InpaintingState = initialInpaintingState;
|
const initialState: InpaintingState = initialInpaintingState;
|
||||||
@ -319,6 +327,18 @@ export const inpaintingSlice = createSlice({
|
|||||||
setShouldShowBoundingBox: (state, action: PayloadAction<boolean>) => {
|
setShouldShowBoundingBox: (state, action: PayloadAction<boolean>) => {
|
||||||
state.shouldShowBoundingBox = action.payload;
|
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,
|
setInpaintReplace,
|
||||||
setShouldLockBoundingBox,
|
setShouldLockBoundingBox,
|
||||||
toggleShouldLockBoundingBox,
|
toggleShouldLockBoundingBox,
|
||||||
|
setIsMovingBoundingBox,
|
||||||
|
setIsTransformingBoundingBox,
|
||||||
|
setIsMouseOverBoundingBox,
|
||||||
|
setIsSpacebarHeld,
|
||||||
} = inpaintingSlice.actions;
|
} = inpaintingSlice.actions;
|
||||||
|
|
||||||
export default inpaintingSlice.reducer;
|
export default inpaintingSlice.reducer;
|
||||||
|
@ -78,7 +78,23 @@ export const inpaintingCanvasSelector = createSelector(
|
|||||||
isDrawing,
|
isDrawing,
|
||||||
shouldLockBoundingBox,
|
shouldLockBoundingBox,
|
||||||
boundingBoxDimensions,
|
boundingBoxDimensions,
|
||||||
|
isTransformingBoundingBox,
|
||||||
|
isMouseOverBoundingBox,
|
||||||
|
isMovingBoundingBox,
|
||||||
} = inpainting;
|
} = inpainting;
|
||||||
|
|
||||||
|
let stageCursor: string | undefined = '';
|
||||||
|
|
||||||
|
if (isTransformingBoundingBox) {
|
||||||
|
stageCursor = undefined;
|
||||||
|
} else if (isMovingBoundingBox || isMouseOverBoundingBox) {
|
||||||
|
stageCursor = 'move';
|
||||||
|
} else if (shouldShowMask) {
|
||||||
|
stageCursor = 'none';
|
||||||
|
} else {
|
||||||
|
stageCursor = 'default';
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tool,
|
tool,
|
||||||
brushSize,
|
brushSize,
|
||||||
@ -93,6 +109,10 @@ export const inpaintingCanvasSelector = createSelector(
|
|||||||
isDrawing,
|
isDrawing,
|
||||||
shouldLockBoundingBox,
|
shouldLockBoundingBox,
|
||||||
boundingBoxDimensions,
|
boundingBoxDimensions,
|
||||||
|
isTransformingBoundingBox,
|
||||||
|
isModifyingBoundingBox: isTransformingBoundingBox || isMovingBoundingBox,
|
||||||
|
stageCursor,
|
||||||
|
isMouseOverBoundingBox,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -101,7 +101,7 @@ const generateMask = (
|
|||||||
new Konva.Image({ image: image, globalCompositeOperation: 'source-out' })
|
new Konva.Image({ image: image, globalCompositeOperation: 'source-out' })
|
||||||
);
|
);
|
||||||
|
|
||||||
const maskDataURL = stage.toDataURL();
|
const maskDataURL = stage.toDataURL({ ...boundingBox });
|
||||||
|
|
||||||
return { maskDataURL, isMaskEmpty };
|
return { maskDataURL, isMaskEmpty };
|
||||||
};
|
};
|
||||||
|
@ -67,6 +67,15 @@ const InvokeOptionsPanel = (props: Props) => {
|
|||||||
[shouldShowOptionsPanel]
|
[shouldShowOptionsPanel]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'esc',
|
||||||
|
() => {
|
||||||
|
if (shouldPinOptionsPanel) return;
|
||||||
|
dispatch(setShouldShowOptionsPanel(false));
|
||||||
|
},
|
||||||
|
[shouldPinOptionsPanel]
|
||||||
|
);
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'shift+o',
|
'shift+o',
|
||||||
() => {
|
() => {
|
||||||
@ -74,7 +83,6 @@ const InvokeOptionsPanel = (props: Props) => {
|
|||||||
},
|
},
|
||||||
[shouldPinOptionsPanel]
|
[shouldPinOptionsPanel]
|
||||||
);
|
);
|
||||||
//
|
|
||||||
|
|
||||||
const handleCloseOptionsPanel = useCallback(() => {
|
const handleCloseOptionsPanel = useCallback(() => {
|
||||||
if (shouldPinOptionsPanel) return;
|
if (shouldPinOptionsPanel) return;
|
||||||
@ -112,12 +120,6 @@ const InvokeOptionsPanel = (props: Props) => {
|
|||||||
dispatch(setNeedsCache(true));
|
dispatch(setNeedsCache(true));
|
||||||
};
|
};
|
||||||
|
|
||||||
// // set gallery scroll position
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (!optionsPanelContainerRef.current) return;
|
|
||||||
// optionsPanelContainerRef.current.scrollTop = optionsPanelScrollPosition;
|
|
||||||
// }, [optionsPanelScrollPosition, shouldShowOptionsPanel]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CSSTransition
|
<CSSTransition
|
||||||
nodeRef={optionsPanelRef}
|
nodeRef={optionsPanelRef}
|
||||||
@ -170,7 +172,6 @@ const InvokeOptionsPanel = (props: Props) => {
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,9 +15,10 @@ import TextToImageIcon from '../../common/icons/TextToImageIcon';
|
|||||||
import { setActiveTab } from '../options/optionsSlice';
|
import { setActiveTab } from '../options/optionsSlice';
|
||||||
import ImageToImageWorkarea from './ImageToImage';
|
import ImageToImageWorkarea from './ImageToImage';
|
||||||
import InpaintingWorkarea from './Inpainting';
|
import InpaintingWorkarea from './Inpainting';
|
||||||
|
import { setNeedsCache } from './Inpainting/inpaintingSlice';
|
||||||
import TextToImageWorkarea from './TextToImage';
|
import TextToImageWorkarea from './TextToImage';
|
||||||
|
|
||||||
export const tab_dict = {
|
export const tabDict = {
|
||||||
txt2img: {
|
txt2img: {
|
||||||
title: <TextToImageIcon fill={'black'} boxSize={'2.5rem'} />,
|
title: <TextToImageIcon fill={'black'} boxSize={'2.5rem'} />,
|
||||||
workarea: <TextToImageWorkarea />,
|
workarea: <TextToImageWorkarea />,
|
||||||
@ -50,8 +51,8 @@ export const tab_dict = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Array where index maps to the key of tab_dict
|
// Array where index maps to the key of tabDict
|
||||||
export const tabMap = _.map(tab_dict, (tab, key) => key);
|
export const tabMap = _.map(tabDict, (tab, key) => key);
|
||||||
|
|
||||||
// Use tabMap to generate a union type of tab names
|
// Use tabMap to generate a union type of tab names
|
||||||
const tabMapTypes = [...tabMap] as const;
|
const tabMapTypes = [...tabMap] as const;
|
||||||
@ -73,6 +74,7 @@ export default function InvokeTabs() {
|
|||||||
|
|
||||||
useHotkeys('3', () => {
|
useHotkeys('3', () => {
|
||||||
dispatch(setActiveTab(2));
|
dispatch(setActiveTab(2));
|
||||||
|
dispatch(setNeedsCache(true));
|
||||||
});
|
});
|
||||||
|
|
||||||
useHotkeys('4', () => {
|
useHotkeys('4', () => {
|
||||||
@ -89,15 +91,15 @@ export default function InvokeTabs() {
|
|||||||
|
|
||||||
const renderTabs = () => {
|
const renderTabs = () => {
|
||||||
const tabsToRender: ReactElement[] = [];
|
const tabsToRender: ReactElement[] = [];
|
||||||
Object.keys(tab_dict).forEach((key) => {
|
Object.keys(tabDict).forEach((key) => {
|
||||||
tabsToRender.push(
|
tabsToRender.push(
|
||||||
<Tooltip
|
<Tooltip
|
||||||
key={key}
|
key={key}
|
||||||
hasArrow
|
hasArrow
|
||||||
label={tab_dict[key as keyof typeof tab_dict].tooltip}
|
label={tabDict[key as keyof typeof tabDict].tooltip}
|
||||||
placement={'right'}
|
placement={'right'}
|
||||||
>
|
>
|
||||||
<Tab>{tab_dict[key as keyof typeof tab_dict].title}</Tab>
|
<Tab>{tabDict[key as keyof typeof tabDict].title}</Tab>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -106,10 +108,10 @@ export default function InvokeTabs() {
|
|||||||
|
|
||||||
const renderTabPanels = () => {
|
const renderTabPanels = () => {
|
||||||
const tabPanelsToRender: ReactElement[] = [];
|
const tabPanelsToRender: ReactElement[] = [];
|
||||||
Object.keys(tab_dict).forEach((key) => {
|
Object.keys(tabDict).forEach((key) => {
|
||||||
tabPanelsToRender.push(
|
tabPanelsToRender.push(
|
||||||
<TabPanel className="app-tabs-panel" key={key}>
|
<TabPanel className="app-tabs-panel" key={key}>
|
||||||
{tab_dict[key as keyof typeof tab_dict].workarea}
|
{tabDict[key as keyof typeof tabDict].workarea}
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -125,6 +127,7 @@ export default function InvokeTabs() {
|
|||||||
index={activeTab}
|
index={activeTab}
|
||||||
onChange={(index: number) => {
|
onChange={(index: number) => {
|
||||||
dispatch(setActiveTab(index));
|
dispatch(setActiveTab(index));
|
||||||
|
dispatch(setNeedsCache(true));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="app-tabs-list">{renderTabs()}</div>
|
<div className="app-tabs-list">{renderTabs()}</div>
|
||||||
|
@ -7,21 +7,26 @@
|
|||||||
|
|
||||||
.workarea-main {
|
.workarea-main {
|
||||||
display: flex;
|
display: flex;
|
||||||
column-gap: 0.5rem;
|
column-gap: 1rem;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
.workarea-children-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
.workarea-split-view {
|
.workarea-split-view {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
// height: $app-content-height;
|
|
||||||
background-color: var(--background-color-secondary);
|
background-color: var(--background-color-secondary);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workarea-single-view {
|
.workarea-single-view {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
// height: $app-content-height;
|
height: 100%;
|
||||||
background-color: var(--background-color-secondary);
|
background-color: var(--background-color-secondary);
|
||||||
border-radius: 0.5rem;
|
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