[WebUI] Localization Support (#2050)

* Initial Localization Implementation

* Fix Initial Spinner

* Language Picker Dropdown

* RU Localization Update

Co-Authored-By: Artur <83028930+netsvetaev@users.noreply.github.com>

* Fixed localization breaking themes

* useUpdateTranslation Hook

To force trigger translations for data objects

* Localize Tab Data

* Localize Prompt Input & Current Image Buttons

* Localize Gallery & Bug FIxes

Fix a bug where the delete image from the context menu wasn't working. Removed tooltips that were broken as they don't work in context menu.

* Fix localization breaking in production

* Add Toast Localization Support

* Localize Unified Canvas

* Localize WIP Tabs

* Localize Hotkeys

* Localize Settings

* RU Localization Update

Co-Authored-By: Artur <83028930+netsvetaev@users.noreply.github.com>

* Add Support for Italian and Portuguese

* Localize Toasts

* Fix width of language picker items

* Localize Backend Messages

* Disable Debug Messages

* Add Support for French

* Fix missing localization for a string in the SettingsModal

* Disable French

* Styling updates to normalize text and accommodate other langs

* Add Portuguese Brazilian

* Fix Hotkey headers not being localized.

* Fix styling issue on models tag in Settings

* Fix Slider Styling to accommodate different languages

* Fix slider styling in light mode.

* Add German

* Add Italian

* Add Polish

* Update Italian

* Localized Frontend Build

* Updated RU Translations

* Fresh Build with updated RU changes

* Bug Fixes and Loc Updates

* Updated Frontend Build

* Fresh Build

Co-authored-by: Artur <83028930+netsvetaev@users.noreply.github.com>
This commit is contained in:
blessedcoolant
2022-12-25 07:23:21 +13:00
committed by GitHub
parent f961e865f5
commit 1d34405f4f
234 changed files with 7114 additions and 1246 deletions

View File

@@ -1,6 +1,7 @@
import { AnyAction, MiddlewareAPI, Dispatch } from '@reduxjs/toolkit';
import { v4 as uuidv4 } from 'uuid';
import dateFormat from 'dateformat';
import i18n from 'i18n';
import * as InvokeAI from 'app/invokeai';
@@ -58,7 +59,7 @@ const makeSocketIOListeners = (
onConnect: () => {
try {
dispatch(setIsConnected(true));
dispatch(setCurrentStatus('Connected'));
dispatch(setCurrentStatus(i18n.t('common:statusConnected')));
dispatch(requestSystemConfig());
const gallery: GalleryState = getState().gallery;
@@ -83,7 +84,7 @@ const makeSocketIOListeners = (
onDisconnect: () => {
try {
dispatch(setIsConnected(false));
dispatch(setCurrentStatus('Disconnected'));
dispatch(setCurrentStatus(i18n.t('common:statusDisconnected')));
dispatch(
addLogEntry({
@@ -353,7 +354,7 @@ const makeSocketIOListeners = (
onModelChanged: (data: InvokeAI.ModelChangeResponse) => {
const { model_name, model_list } = data;
dispatch(setModelList(model_list));
dispatch(setCurrentStatus('Model Changed'));
dispatch(setCurrentStatus(i18n.t('common:statusModelChanged')));
dispatch(setIsProcessing(false));
dispatch(setIsCancelable(true));
dispatch(
@@ -381,7 +382,7 @@ const makeSocketIOListeners = (
onTempFolderEmptied: () => {
dispatch(
addToast({
title: 'Temp Folder Emptied',
title: i18n.t('toast:tempFoldersEmptied'),
status: 'success',
duration: 2500,
isClosable: true,

View File

@@ -6,7 +6,6 @@
.input-label {
color: var(--text-color-secondary);
margin-right: 0;
}
.input-entry {

View File

@@ -14,7 +14,7 @@ export default function IAIInput(props: IAIInputProps) {
label,
styleClass,
isDisabled = false,
fontSize = '1rem',
fontSize = 'sm',
width,
isInvalid,
...rest
@@ -25,12 +25,14 @@ export default function IAIInput(props: IAIInputProps) {
className={`input ${styleClass}`}
isInvalid={isInvalid}
isDisabled={isDisabled}
flexGrow={1}
>
<FormLabel
fontSize={fontSize}
marginBottom={1}
fontWeight="bold"
alignItems="center"
whiteSpace="nowrap"
marginBottom={0}
marginRight={0}
className="input-label"
>
{label}

View File

@@ -5,10 +5,6 @@
.invokeai__number-input-form-label {
color: var(--text-color-secondary);
margin-right: 0;
font-size: 1rem;
margin-bottom: 0;
white-space: nowrap;
&[data-focus] + .invokeai__number-input-root {
outline: none;

View File

@@ -44,7 +44,7 @@ interface Props extends Omit<NumberInputProps, 'onChange'> {
const IAINumberInput = (props: Props) => {
const {
label,
labelFontSize = '1rem',
labelFontSize = 'sm',
styleClass,
isDisabled = false,
showStepper = true,
@@ -130,6 +130,10 @@ const IAINumberInput = (props: Props) => {
className="invokeai__number-input-form-label"
style={{ display: label ? 'block' : 'none' }}
fontSize={labelFontSize}
fontWeight="bold"
marginRight={0}
marginBottom={0}
whiteSpace="nowrap"
{...formLabelProps}
>
{label}

View File

@@ -7,7 +7,6 @@
.invokeai__select-label {
color: var(--text-color-secondary);
margin-right: 0;
}
.invokeai__select-picker {

View File

@@ -28,7 +28,7 @@ const IAISelect = (props: IAISelectProps) => {
tooltip,
tooltipProps,
size = 'sm',
fontSize = 'md',
fontSize = 'sm',
styleClass,
...rest
} = props;
@@ -47,8 +47,9 @@ const IAISelect = (props: IAISelectProps) => {
<FormLabel
className="invokeai__select-label"
fontSize={fontSize}
marginBottom={1}
flexGrow={2}
fontWeight="bold"
marginRight={0}
marginBottom={0}
whiteSpace="nowrap"
>
{label}

View File

@@ -1,7 +1,6 @@
.invokeai__slider-component {
display: flex;
gap: 1rem;
align-items: center;
padding-bottom: 0.5rem;
border-radius: 0.5rem;
.invokeai__slider-component-label {
min-width: max-content;
@@ -26,7 +25,7 @@
.invokeai__slider-mark {
font-size: 0.75rem;
font-weight: bold;
color: var(--slider-color);
color: var(--slider-mark-color);
margin-top: 0.3rem;
}

View File

@@ -50,6 +50,7 @@ export type IAIFullSliderProps = {
isInputDisabled?: boolean;
tooltipSuffix?: string;
hideTooltip?: boolean;
isCompact?: boolean;
styleClass?: string;
sliderFormControlProps?: FormControlProps;
sliderFormLabelProps?: FormLabelProps;
@@ -83,6 +84,7 @@ export default function IAISlider(props: IAIFullSliderProps) {
inputReadOnly = true,
withReset = false,
hideTooltip = false,
isCompact = false,
handleReset,
isResetDisabled,
isSliderDisabled,
@@ -142,6 +144,18 @@ export default function IAISlider(props: IAIFullSliderProps) {
: `invokeai__slider-component`
}
data-markers={withSliderMarks}
style={
isCompact
? {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
columnGap: '1rem',
margin: 0,
padding: 0,
}
: {}
}
{...sliderFormControlProps}
>
<FormLabel
@@ -151,7 +165,7 @@ export default function IAISlider(props: IAIFullSliderProps) {
{label}
</FormLabel>
<HStack w={'100%'} gap={2}>
<HStack w={'100%'} gap={2} alignItems="center">
<Slider
aria-label={label}
value={value}

View File

@@ -1,33 +1,24 @@
.invokeai__switch-form-control {
.invokeai__switch-form-label {
display: flex;
column-gap: 1rem;
justify-content: space-between;
align-items: center;
color: var(--text-color-secondary);
font-size: 1rem;
margin-right: 0;
margin-bottom: 0.1rem;
white-space: nowrap;
width: auto;
}
.invokeai__switch-root {
.invokeai__switch-root {
span {
background-color: var(--switch-bg-color);
span {
background-color: var(--switch-bg-color);
background-color: var(--white);
}
}
&[data-checked] {
span {
background: var(--switch-bg-active-color);
span {
background-color: var(--white);
}
}
&[data-checked] {
span {
background: var(--switch-bg-active-color);
span {
background-color: var(--white);
}
}
}
}
}
}

View File

@@ -33,16 +33,26 @@ const IAISwitch = (props: Props) => {
isDisabled={isDisabled}
width={width}
className={`invokeai__switch-form-control ${styleClass}`}
display="flex"
columnGap="1rem"
alignItems="center"
justifyContent="space-between"
{...formControlProps}
>
<FormLabel
className="invokeai__switch-form-label"
whiteSpace="nowrap"
marginRight={0}
marginTop={0.5}
marginBottom={0.5}
fontSize="sm"
fontWeight="bold"
width="auto"
{...formLabelProps}
>
{label}
<Switch className="invokeai__switch-root" {...rest} />
</FormLabel>
<Switch className="invokeai__switch-root" {...rest} />
</FormControl>
);
};

View File

@@ -14,6 +14,7 @@ import { tabDict } from 'features/tabs/components/InvokeTabs';
import ImageUploadOverlay from './ImageUploadOverlay';
import { uploadImage } from 'features/gallery/store/thunks/uploadImage';
import useImageUploader from 'common/hooks/useImageUploader';
import { useTranslation } from 'react-i18next';
type ImageUploaderProps = {
children: ReactNode;
@@ -24,6 +25,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
const dispatch = useAppDispatch();
const activeTabName = useAppSelector(activeTabNameSelector);
const toast = useToast({});
const { t } = useTranslation();
const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false);
const { setOpenUploader } = useImageUploader();
@@ -35,13 +37,13 @@ const ImageUploader = (props: ImageUploaderProps) => {
''
);
toast({
title: 'Upload failed',
title: t('toast:uploadFailed'),
description: msg,
status: 'error',
isClosable: true,
});
},
[toast]
[t, toast]
);
const fileAcceptedCallback = useCallback(
@@ -103,8 +105,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
if (imageItems.length > 1) {
toast({
description:
'Multiple images pasted, may only upload one image at a time',
description: t('toast:uploadFailedMultipleImagesDesc'),
status: 'error',
isClosable: true,
});
@@ -115,7 +116,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
if (!file) {
toast({
description: 'Unable to load file',
description: t('toast:uploadFailedUnableToLoadDesc'),
status: 'error',
isClosable: true,
});
@@ -128,7 +129,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
return () => {
document.removeEventListener('paste', pasteImageListener);
};
}, [dispatch, toast, activeTabName]);
}, [t, dispatch, toast, activeTabName]);
const overlaySecondaryText = ['img2img', 'unifiedCanvas'].includes(
activeTabName

View File

@@ -1,13 +1,12 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
export default function NodesWIP() {
const { t } = useTranslation();
return (
<div className="work-in-progress nodes-work-in-progress">
<h1>Nodes</h1>
<p>
A node based system for the generation of images is under development
currently. Stay tuned for updates about this amazing feature.
</p>
<h1>{t('common:nodes')}</h1>
<p>{t('common:nodesDesc')}</p>
</div>
);
}

View File

@@ -1,24 +1,14 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
export const PostProcessingWIP = () => {
const { t } = useTranslation();
return (
<div className="work-in-progress post-processing-work-in-progress">
<h1>Post Processing</h1>
<p>
Invoke AI offers a wide variety of post processing features. Image
Upscaling and Face Restoration are already available in the WebUI. You
can access them from the Advanced Options menu of the Text To Image and
Image To Image tabs. You can also process images directly, using the
image action buttons above the current image display or in the viewer.
</p>
<p>
A dedicated UI will be released soon to facilitate more advanced post
processing workflows.
</p>
<p>
The Invoke AI Command Line Interface offers various other features
including Embiggen.
</p>
<h1>{t('common:postProcessing')}</h1>
<p>{t('common:postProcessDesc1')}</p>
<p>{t('common:postProcessDesc2')}</p>
<p>{t('common:postProcessDesc3')}</p>
</div>
);
};

View File

@@ -1,15 +1,16 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
export default function TrainingWIP() {
const { t } = useTranslation();
return (
<div className="work-in-progress nodes-work-in-progress">
<h1>Training</h1>
<h1>{t('common:training')}</h1>
<p>
A dedicated workflow for training your own embeddings and checkpoints
using Textual Inversion and Dreambooth from the web interface. <br />
{t('common:trainingDesc1')}
<br />
InvokeAI already supports training custom embeddings using Textual
Inversion using the main script.
<br />
{t('common:trainingDesc2')}
</p>
</div>
);

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
export default function useUpdateTranslations(fn: () => void) {
const { i18n } = useTranslation();
const currentLang = localStorage.getItem('i18nextLng');
React.useEffect(() => {
fn();
}, [fn]);
React.useEffect(() => {
i18n.on('languageChanged', () => {
fn();
});
}, [fn, i18n, currentLang]);
}

View File

@@ -2,30 +2,29 @@ import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAIAlertDialog from 'common/components/IAIAlertDialog';
import IAIButton from 'common/components/IAIButton';
import { clearCanvasHistory } from 'features/canvas/store/canvasSlice';
import { useTranslation } from 'react-i18next';
import { FaTrash } from 'react-icons/fa';
import { isStagingSelector } from '../store/canvasSelectors';
const ClearCanvasHistoryButtonModal = () => {
const isStaging = useAppSelector(isStagingSelector);
const dispatch = useAppDispatch();
const { t } = useTranslation();
return (
<IAIAlertDialog
title={'Clear Canvas History'}
title={t('unifiedcanvas:clearCanvasHistory')}
acceptCallback={() => dispatch(clearCanvasHistory())}
acceptButtonText={'Clear History'}
acceptButtonText={t('unifiedcanvas:clearHistory')}
triggerComponent={
<IAIButton size={'sm'} leftIcon={<FaTrash />} isDisabled={isStaging}>
Clear Canvas History
{t('unifiedcanvas:clearCanvasHistory')}
</IAIButton>
}
>
<p>
Clearing the canvas history leaves your current canvas intact, but
irreversibly clears the undo and redo history.
</p>
<p>{t('unifiedcanvas:clearCanvasHistoryMessage')}</p>
<br />
<p>Are you sure you want to clear the canvas history?</p>
<p>{t('unifiedcanvas:clearCanvasHistoryConfirm')}</p>
</IAIAlertDialog>
);
};

View File

@@ -24,6 +24,7 @@ import {
} from 'features/canvas/store/canvasSlice';
import { useHotkeys } from 'react-hotkeys-hook';
import { saveStagingAreaImageToGallery } from 'app/socketio/actions';
import { useTranslation } from 'react-i18next';
const selector = createSelector(
[canvasSelector],
@@ -61,6 +62,8 @@ const IAICanvasStagingAreaToolbar = () => {
shouldShowStagingImage,
} = useAppSelector(selector);
const { t } = useTranslation();
const handleMouseOver = useCallback(() => {
dispatch(setShouldShowStagingOutline(true));
}, [dispatch]);
@@ -121,31 +124,31 @@ const IAICanvasStagingAreaToolbar = () => {
>
<ButtonGroup isAttached>
<IAIIconButton
tooltip="Previous (Left)"
aria-label="Previous (Left)"
tooltip={`${t('unifiedcanvas:previous')} (Left)`}
aria-label={`${t('unifiedcanvas:previous')} (Left)`}
icon={<FaArrowLeft />}
onClick={handlePrevImage}
data-selected={true}
isDisabled={isOnFirstImage}
/>
<IAIIconButton
tooltip="Next (Right)"
aria-label="Next (Right)"
tooltip={`${t('unifiedcanvas:next')} (Right)`}
aria-label={`${t('unifiedcanvas:next')} (Right)`}
icon={<FaArrowRight />}
onClick={handleNextImage}
data-selected={true}
isDisabled={isOnLastImage}
/>
<IAIIconButton
tooltip="Accept (Enter)"
aria-label="Accept (Enter)"
tooltip={`${t('unifiedcanvas:accept')} (Enter)`}
aria-label={`${t('unifiedcanvas:accept')} (Enter)`}
icon={<FaCheck />}
onClick={handleAccept}
data-selected={true}
/>
<IAIIconButton
tooltip="Show/Hide"
aria-label="Show/Hide"
tooltip={t('unifiedcanvas:showHide')}
aria-label={t('unifiedcanvas:showHide')}
data-alert={!shouldShowStagingImage}
icon={shouldShowStagingImage ? <FaEye /> : <FaEyeSlash />}
onClick={() =>
@@ -154,8 +157,8 @@ const IAICanvasStagingAreaToolbar = () => {
data-selected={true}
/>
<IAIIconButton
tooltip="Save to Gallery"
aria-label="Save to Gallery"
tooltip={t('unifiedcanvas:saveToGallery')}
aria-label={t('unifiedcanvas:saveToGallery')}
icon={<FaSave />}
onClick={() =>
dispatch(
@@ -165,8 +168,8 @@ const IAICanvasStagingAreaToolbar = () => {
data-selected={true}
/>
<IAIIconButton
tooltip="Discard All"
aria-label="Discard All"
tooltip={t('unifiedcanvas:discardAll')}
aria-label={t('unifiedcanvas:discardAll')}
icon={<FaPlus style={{ transform: 'rotate(45deg)' }} />}
onClick={() => dispatch(discardStagedImages())}
data-selected={true}

View File

@@ -4,6 +4,7 @@ import _ from 'lodash';
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
import IAICanvasStatusTextCursorPos from './IAICanvasStatusText/IAICanvasStatusTextCursorPos';
import roundToHundreth from '../util/roundToHundreth';
import { useTranslation } from 'react-i18next';
const selector = createSelector(
[canvasSelector],
@@ -79,33 +80,45 @@ const IAICanvasStatusText = () => {
shouldShowBoundingBox,
} = useAppSelector(selector);
const { t } = useTranslation();
return (
<div className="canvas-status-text">
<div
style={{
color: activeLayerColor,
}}
>{`Active Layer: ${activeLayerString}`}</div>
<div>{`Canvas Scale: ${canvasScaleString}%`}</div>
>{`${t('unifiedcanvas:activeLayer')}: ${activeLayerString}`}</div>
<div>{`${t('unifiedcanvas:canvasScale')}: ${canvasScaleString}%`}</div>
{shouldShowBoundingBox && (
<div
style={{
color: boundingBoxColor,
}}
>{`Bounding Box: ${boundingBoxDimensionsString}`}</div>
>{`${t(
'unifiedcanvas:boundingBox'
)}: ${boundingBoxDimensionsString}`}</div>
)}
{shouldShowScaledBoundingBox && (
<div
style={{
color: boundingBoxColor,
}}
>{`Scaled Bounding Box: ${scaledBoundingBoxDimensionsString}`}</div>
>{`${t(
'unifiedcanvas:scaledBoundingBox'
)}: ${scaledBoundingBoxDimensionsString}`}</div>
)}
{shouldShowCanvasDebugInfo && (
<>
<div>{`Bounding Box Position: ${boundingBoxCoordinatesString}`}</div>
<div>{`Canvas Dimensions: ${canvasDimensionsString}`}</div>
<div>{`Canvas Position: ${canvasCoordinatesString}`}</div>
<div>{`${t(
'unifiedcanvas:boundingBoxPosition'
)}: ${boundingBoxCoordinatesString}`}</div>
<div>{`${t(
'unifiedcanvas:canvasDimensions'
)}: ${canvasDimensionsString}`}</div>
<div>{`${t(
'unifiedcanvas:canvasPosition'
)}: ${canvasCoordinatesString}`}</div>
<IAICanvasStatusTextCursorPos />
</>
)}

View File

@@ -4,6 +4,7 @@ import { canvasSelector } from 'features/canvas/store/canvasSelectors';
import React from 'react';
import _ from 'lodash';
import roundToHundreth from 'features/canvas/util/roundToHundreth';
import { useTranslation } from 'react-i18next';
const cursorPositionSelector = createSelector(
[canvasSelector],
@@ -29,6 +30,11 @@ const cursorPositionSelector = createSelector(
export default function IAICanvasStatusTextCursorPos() {
const { cursorCoordinatesString } = useAppSelector(cursorPositionSelector);
const { t } = useTranslation();
return <div>{`Cursor Position: ${cursorCoordinatesString}`}</div>;
return (
<div>{`${t(
'unifiedcanvas:cursorPosition'
)}: ${cursorCoordinatesString}`}</div>
);
}

View File

@@ -21,6 +21,7 @@ import {
} from 'features/canvas/store/canvasSelectors';
import { useHotkeys } from 'react-hotkeys-hook';
import { rgbaColorToString } from 'features/canvas/util/colorToString';
import { useTranslation } from 'react-i18next';
export const selector = createSelector(
[canvasSelector, isStagingSelector],
@@ -45,6 +46,8 @@ export const selector = createSelector(
);
const IAICanvasMaskOptions = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const {
layer,
maskColor,
@@ -104,8 +107,8 @@ const IAICanvasMaskOptions = () => {
triggerComponent={
<ButtonGroup>
<IAIIconButton
aria-label="Masking Options"
tooltip="Masking Options"
aria-label={t('unifiedcanvas:maskingOptions')}
tooltip={t('unifiedcanvas:maskingOptions')}
icon={<FaMask />}
style={
layer === 'mask'
@@ -119,12 +122,12 @@ const IAICanvasMaskOptions = () => {
>
<Flex direction={'column'} gap={'0.5rem'}>
<IAICheckbox
label="Enable Mask (H)"
label={`${t('unifiedcanvas:enableMask')} (H)`}
isChecked={isMaskEnabled}
onChange={handleToggleEnableMask}
/>
<IAICheckbox
label="Preserve Masked Area"
label={t('unifiedcanvas:preserveMaskedArea')}
isChecked={shouldPreserveMaskedArea}
onChange={(e) =>
dispatch(setShouldPreserveMaskedArea(e.target.checked))
@@ -136,7 +139,7 @@ const IAICanvasMaskOptions = () => {
onChange={(newColor) => dispatch(setMaskColor(newColor))}
/>
<IAIButton size={'sm'} leftIcon={<FaTrash />} onClick={handleClearMask}>
Clear Mask (Shift+C)
{t('unifiedcanvas:clearMask')} (Shift+C)
</IAIButton>
</Flex>
</IAIPopover>

View File

@@ -9,6 +9,7 @@ import { canvasSelector } from 'features/canvas/store/canvasSelectors';
import _ from 'lodash';
import { redo } from 'features/canvas/store/canvasSlice';
import { systemSelector } from 'features/system/store/systemSelectors';
import { useTranslation } from 'react-i18next';
const canvasRedoSelector = createSelector(
[canvasSelector, activeTabNameSelector, systemSelector],
@@ -31,6 +32,8 @@ export default function IAICanvasRedoButton() {
const dispatch = useAppDispatch();
const { canRedo, activeTabName } = useAppSelector(canvasRedoSelector);
const { t } = useTranslation();
const handleRedo = () => {
dispatch(redo());
};
@@ -49,8 +52,8 @@ export default function IAICanvasRedoButton() {
return (
<IAIIconButton
aria-label="Redo (Ctrl+Shift+Z)"
tooltip="Redo (Ctrl+Shift+Z)"
aria-label={`${t('unifiedcanvas:redo')} (Ctrl+Shift+Z)`}
tooltip={`${t('unifiedcanvas:redo')} (Ctrl+Shift+Z)`}
icon={<FaRedo />}
onClick={handleRedo}
isDisabled={!canRedo}

View File

@@ -21,6 +21,7 @@ import EmptyTempFolderButtonModal from 'features/system/components/ClearTempFold
import ClearCanvasHistoryButtonModal from '../ClearCanvasHistoryButtonModal';
import { ChangeEvent } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
export const canvasControlsSelector = createSelector(
[canvasSelector],
@@ -56,6 +57,8 @@ export const canvasControlsSelector = createSelector(
const IAICanvasSettingsButtonPopover = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const {
shouldAutoSave,
shouldCropToBoundingBoxOnSave,
@@ -87,58 +90,58 @@ const IAICanvasSettingsButtonPopover = () => {
trigger="hover"
triggerComponent={
<IAIIconButton
tooltip="Canvas Settings"
aria-label="Canvas Settings"
tooltip={t('unifiedcanvas:canvasSettings')}
aria-label={t('unifiedcanvas:canvasSettings')}
icon={<FaWrench />}
/>
}
>
<Flex direction={'column'} gap={'0.5rem'}>
<IAICheckbox
label="Show Intermediates"
label={t('unifiedcanvas:showIntermediates')}
isChecked={shouldShowIntermediates}
onChange={(e) =>
dispatch(setShouldShowIntermediates(e.target.checked))
}
/>
<IAICheckbox
label="Show Grid"
label={t('unifiedcanvas:showGrid')}
isChecked={shouldShowGrid}
onChange={(e) => dispatch(setShouldShowGrid(e.target.checked))}
/>
<IAICheckbox
label="Snap to Grid"
label={t('unifiedcanvas:snapToGrid')}
isChecked={shouldSnapToGrid}
onChange={handleChangeShouldSnapToGrid}
/>
<IAICheckbox
label="Darken Outside Selection"
label={t('unifiedcanvas:darkenOutsideSelection')}
isChecked={shouldDarkenOutsideBoundingBox}
onChange={(e) =>
dispatch(setShouldDarkenOutsideBoundingBox(e.target.checked))
}
/>
<IAICheckbox
label="Auto Save to Gallery"
label={t('unifiedcanvas:autoSaveToGallery')}
isChecked={shouldAutoSave}
onChange={(e) => dispatch(setShouldAutoSave(e.target.checked))}
/>
<IAICheckbox
label="Save Box Region Only"
label={t('unifiedcanvas:saveBoxRegionOnly')}
isChecked={shouldCropToBoundingBoxOnSave}
onChange={(e) =>
dispatch(setShouldCropToBoundingBoxOnSave(e.target.checked))
}
/>
<IAICheckbox
label="Limit Strokes to Box"
label={t('unifiedcanvas:limitStrokesToBox')}
isChecked={shouldRestrictStrokesToBox}
onChange={(e) =>
dispatch(setShouldRestrictStrokesToBox(e.target.checked))
}
/>
<IAICheckbox
label="Show Canvas Debug Info"
label={t('unifiedcanvas:showCanvasDebugInfo')}
isChecked={shouldShowCanvasDebugInfo}
onChange={(e) =>
dispatch(setShouldShowCanvasDebugInfo(e.target.checked))

View File

@@ -27,6 +27,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
import IAIPopover from 'common/components/IAIPopover';
import IAISlider from 'common/components/IAISlider';
import IAIColorPicker from 'common/components/IAIColorPicker';
import { useTranslation } from 'react-i18next';
export const selector = createSelector(
[canvasSelector, isStagingSelector, systemSelector],
@@ -52,6 +53,7 @@ export const selector = createSelector(
const IAICanvasToolChooserOptions = () => {
const dispatch = useAppDispatch();
const { tool, brushColor, brushSize, isStaging } = useAppSelector(selector);
const { t } = useTranslation();
useHotkeys(
['b'],
@@ -178,38 +180,38 @@ const IAICanvasToolChooserOptions = () => {
return (
<ButtonGroup isAttached>
<IAIIconButton
aria-label="Brush Tool (B)"
tooltip="Brush Tool (B)"
aria-label={`${t('unifiedcanvas:brush')} (B)`}
tooltip={`${t('unifiedcanvas:brush')} (B)`}
icon={<FaPaintBrush />}
data-selected={tool === 'brush' && !isStaging}
onClick={handleSelectBrushTool}
isDisabled={isStaging}
/>
<IAIIconButton
aria-label="Eraser Tool (E)"
tooltip="Eraser Tool (E)"
aria-label={`${t('unifiedcanvas:eraser')} (E)`}
tooltip={`${t('unifiedcanvas:eraser')} (E)`}
icon={<FaEraser />}
data-selected={tool === 'eraser' && !isStaging}
isDisabled={isStaging}
onClick={handleSelectEraserTool}
/>
<IAIIconButton
aria-label="Fill Bounding Box (Shift+F)"
tooltip="Fill Bounding Box (Shift+F)"
aria-label={`${t('unifiedcanvas:fillBoundingBox')} (Shift+F)`}
tooltip={`${t('unifiedcanvas:fillBoundingBox')} (Shift+F)`}
icon={<FaFillDrip />}
isDisabled={isStaging}
onClick={handleFillRect}
/>
<IAIIconButton
aria-label="Erase Bounding Box Area (Delete/Backspace)"
tooltip="Erase Bounding Box Area (Delete/Backspace)"
aria-label={`${t('unifiedcanvas:eraseBoundingBox')} (Del/Backspace)`}
tooltip={`${t('unifiedcanvas:eraseBoundingBox')} (Del/Backspace)`}
icon={<FaPlus style={{ transform: 'rotate(45deg)' }} />}
isDisabled={isStaging}
onClick={handleEraseBoundingBox}
/>
<IAIIconButton
aria-label="Color Picker (C)"
tooltip="Color Picker (C)"
aria-label={`${t('unifiedcanvas:colorPicker')} (C)`}
tooltip={`${t('unifiedcanvas:colorPicker')} (C)`}
icon={<FaEyeDropper />}
data-selected={tool === 'colorPicker' && !isStaging}
isDisabled={isStaging}
@@ -219,8 +221,8 @@ const IAICanvasToolChooserOptions = () => {
trigger="hover"
triggerComponent={
<IAIIconButton
aria-label="Brush Options"
tooltip="Brush Options"
aria-label={t('unifiedcanvas:brushOptions')}
tooltip={t('unifiedcanvas:brushOptions')}
icon={<FaSlidersH />}
/>
}
@@ -233,7 +235,7 @@ const IAICanvasToolChooserOptions = () => {
>
<Flex gap={'1rem'} justifyContent="space-between">
<IAISlider
label="Size"
label={t('unifiedcanvas:brushSize')}
value={brushSize}
withInput
onChange={(newSize) => dispatch(setBrushSize(newSize))}

View File

@@ -41,6 +41,7 @@ import {
LAYER_NAMES_DICT,
} from 'features/canvas/store/canvasTypes';
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
export const selector = createSelector(
[systemSelector, canvasSelector, isStagingSelector],
@@ -77,6 +78,8 @@ const IAICanvasOutpaintingControls = () => {
} = useAppSelector(selector);
const canvasBaseLayer = getCanvasBaseLayer();
const { t } = useTranslation();
const { openUploader } = useImageUploader();
useHotkeys(
@@ -221,7 +224,7 @@ const IAICanvasOutpaintingControls = () => {
return (
<div className="inpainting-settings">
<IAISelect
tooltip={'Layer (Q)'}
tooltip={`${t('unifiedcanvas:layer')} (Q)`}
tooltipProps={{ hasArrow: true, placement: 'top' }}
value={layer}
validValues={LAYER_NAMES_DICT}
@@ -234,15 +237,15 @@ const IAICanvasOutpaintingControls = () => {
<ButtonGroup isAttached>
<IAIIconButton
aria-label="Move Tool (V)"
tooltip="Move Tool (V)"
aria-label={`${t('unifiedcanvas:move')} (V)`}
tooltip={`${t('unifiedcanvas:move')} (V)`}
icon={<FaArrowsAlt />}
data-selected={tool === 'move' || isStaging}
onClick={handleSelectMoveTool}
/>
<IAIIconButton
aria-label="Reset View (R)"
tooltip="Reset View (R)"
aria-label={`${t('unifiedcanvas:resetView')} (R)`}
tooltip={`${t('unifiedcanvas:resetView')} (R)`}
icon={<FaCrosshairs />}
onClick={handleResetCanvasView}
/>
@@ -250,29 +253,29 @@ const IAICanvasOutpaintingControls = () => {
<ButtonGroup isAttached>
<IAIIconButton
aria-label="Merge Visible (Shift+M)"
tooltip="Merge Visible (Shift+M)"
aria-label={`${t('unifiedcanvas:mergeVisible')} (Shift+M)`}
tooltip={`${t('unifiedcanvas:mergeVisible')} (Shift+M)`}
icon={<FaLayerGroup />}
onClick={handleMergeVisible}
isDisabled={isStaging}
/>
<IAIIconButton
aria-label="Save to Gallery (Shift+S)"
tooltip="Save to Gallery (Shift+S)"
aria-label={`${t('unifiedcanvas:saveToGallery')} (Shift+S)`}
tooltip={`${t('unifiedcanvas:saveToGallery')} (Shift+S)`}
icon={<FaSave />}
onClick={handleSaveToGallery}
isDisabled={isStaging}
/>
<IAIIconButton
aria-label="Copy to Clipboard (Cmd/Ctrl+C)"
tooltip="Copy to Clipboard (Cmd/Ctrl+C)"
aria-label={`${t('unifiedcanvas:copyToClipboard')} (Cmd/Ctrl+C)`}
tooltip={`${t('unifiedcanvas:copyToClipboard')} (Cmd/Ctrl+C)`}
icon={<FaCopy />}
onClick={handleCopyImageToClipboard}
isDisabled={isStaging}
/>
<IAIIconButton
aria-label="Download as Image (Shift+D)"
tooltip="Download as Image (Shift+D)"
aria-label={`${t('unifiedcanvas:downloadAsImage')} (Shift+D)`}
tooltip={`${t('unifiedcanvas:downloadAsImage')} (Shift+D)`}
icon={<FaDownload />}
onClick={handleDownloadAsImage}
isDisabled={isStaging}
@@ -285,15 +288,15 @@ const IAICanvasOutpaintingControls = () => {
<ButtonGroup isAttached>
<IAIIconButton
aria-label="Upload"
tooltip="Upload"
aria-label={`${t('common:upload')}`}
tooltip={`${t('common:upload')}`}
icon={<FaUpload />}
onClick={openUploader}
isDisabled={isStaging}
/>
<IAIIconButton
aria-label="Clear Canvas"
tooltip="Clear Canvas"
aria-label={`${t('unifiedcanvas:clearCanvas')}`}
tooltip={`${t('unifiedcanvas:clearCanvas')}`}
icon={<FaTrash />}
onClick={handleResetCanvas}
style={{ backgroundColor: 'var(--btn-delete-image)' }}

View File

@@ -9,6 +9,7 @@ import _ from 'lodash';
import { activeTabNameSelector } from 'features/options/store/optionsSelectors';
import { undo } from 'features/canvas/store/canvasSlice';
import { systemSelector } from 'features/system/store/systemSelectors';
import { useTranslation } from 'react-i18next';
const canvasUndoSelector = createSelector(
[canvasSelector, activeTabNameSelector, systemSelector],
@@ -30,6 +31,8 @@ const canvasUndoSelector = createSelector(
export default function IAICanvasUndoButton() {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const { canUndo, activeTabName } = useAppSelector(canvasUndoSelector);
const handleUndo = () => {
@@ -50,8 +53,8 @@ export default function IAICanvasUndoButton() {
return (
<IAIIconButton
aria-label="Undo (Ctrl+Z)"
tooltip="Undo (Ctrl+Z)"
aria-label={`${t('unifiedcanvas:undo')} (Ctrl+Z)`}
tooltip={`${t('unifiedcanvas:undo')} (Ctrl+Z)`}
icon={<FaUndo />}
onClick={handleUndo}
isDisabled={!canUndo}

View File

@@ -16,6 +16,7 @@ import {
import { addImage } from 'features/gallery/store/gallerySlice';
import { setMergedCanvas } from '../canvasSlice';
import { CanvasState } from '../canvasTypes';
import i18n from 'i18n';
type MergeAndUploadCanvasConfig = {
cropVisible?: boolean;
@@ -114,7 +115,7 @@ export const mergeAndUploadCanvas =
downloadFile(url);
dispatch(
addToast({
title: 'Image Download Started',
title: i18n.t('toast:downloadImageStarted'),
status: 'success',
duration: 2500,
isClosable: true,
@@ -126,7 +127,7 @@ export const mergeAndUploadCanvas =
copyImage(url, width, height);
dispatch(
addToast({
title: 'Image Copied',
title: i18n.t('toast:imageCopied'),
status: 'success',
duration: 2500,
isClosable: true,
@@ -138,7 +139,7 @@ export const mergeAndUploadCanvas =
dispatch(addImage({ image: newImage, category: 'result' }));
dispatch(
addToast({
title: 'Image Saved to Gallery',
title: i18n.t('toast:imageSavedToGallery'),
status: 'success',
duration: 2500,
isClosable: true,
@@ -157,7 +158,7 @@ export const mergeAndUploadCanvas =
);
dispatch(
addToast({
title: 'Canvas Merged',
title: i18n.t('toast:canvasMerged'),
status: 'success',
duration: 2500,
isClosable: true,
@@ -166,6 +167,6 @@ export const mergeAndUploadCanvas =
}
dispatch(setIsProcessing(false));
dispatch(setCurrentStatus('Connected'));
dispatch(setCurrentStatus(i18n.t('common:statusConnected')));
dispatch(setIsCancelable(true));
};

View File

@@ -43,6 +43,7 @@ import {
import { GalleryState } from 'features/gallery/store/gallerySlice';
import { activeTabNameSelector } from 'features/options/store/optionsSelectors';
import IAIPopover from 'common/components/IAIPopover';
import { useTranslation } from 'react-i18next';
const systemSelector = createSelector(
[
@@ -111,6 +112,7 @@ const CurrentImageButtons = () => {
} = useAppSelector(systemSelector);
const toast = useToast();
const { t } = useTranslation();
const handleClickUseAsInitialImage = () => {
if (!currentImage) return;
@@ -126,7 +128,7 @@ const CurrentImageButtons = () => {
)
.then(() => {
toast({
title: 'Image Link Copied',
title: t('toast:imageLinkCopied'),
status: 'success',
duration: 2500,
isClosable: true,
@@ -140,15 +142,15 @@ const CurrentImageButtons = () => {
if (currentImage) {
handleClickUseAsInitialImage();
toast({
title: 'Sent To Image To Image',
title: t('toast:sentToImageToImage'),
status: 'success',
duration: 2500,
isClosable: true,
});
} else {
toast({
title: 'No Image Loaded',
description: 'No image found to send to image to image module.',
title: t('toast:imageNotLoaded'),
description: t('toast:imageNotLoadedDesc'),
status: 'error',
duration: 2500,
isClosable: true,
@@ -176,15 +178,15 @@ const CurrentImageButtons = () => {
) {
handleClickUseAllParameters();
toast({
title: 'Parameters Set',
title: t('toast:parametersSet'),
status: 'success',
duration: 2500,
isClosable: true,
});
} else {
toast({
title: 'Parameters Not Set',
description: 'No metadata found for this image.',
title: t('toast:parametersNotSet'),
description: t('toast:parametersNotSetDesc'),
status: 'error',
duration: 2500,
isClosable: true,
@@ -205,15 +207,15 @@ const CurrentImageButtons = () => {
if (currentImage?.metadata?.image?.seed) {
handleClickUseSeed();
toast({
title: 'Seed Set',
title: t('toast:seedSet'),
status: 'success',
duration: 2500,
isClosable: true,
});
} else {
toast({
title: 'Seed Not Set',
description: 'Could not find seed for this image.',
title: t('toast:seedNotSet'),
description: t('toast:seedNotSetDesc'),
status: 'error',
duration: 2500,
isClosable: true,
@@ -233,15 +235,15 @@ const CurrentImageButtons = () => {
if (currentImage?.metadata?.image?.prompt) {
handleClickUsePrompt();
toast({
title: 'Prompt Set',
title: t('toast:promptSet'),
status: 'success',
duration: 2500,
isClosable: true,
});
} else {
toast({
title: 'Prompt Not Set',
description: 'Could not find prompt for this image.',
title: t('toast:promptNotSet'),
description: t('toast:promptNotSetDesc'),
status: 'error',
duration: 2500,
isClosable: true,
@@ -268,7 +270,7 @@ const CurrentImageButtons = () => {
handleClickUpscale();
} else {
toast({
title: 'Upscaling Failed',
title: t('toast:upscalingFailed'),
status: 'error',
duration: 2500,
isClosable: true,
@@ -302,7 +304,7 @@ const CurrentImageButtons = () => {
handleClickFixFaces();
} else {
toast({
title: 'Face Restoration Failed',
title: t('toast:faceRestoreFailed'),
status: 'error',
duration: 2500,
isClosable: true,
@@ -334,7 +336,7 @@ const CurrentImageButtons = () => {
}
toast({
title: 'Sent to Unified Canvas',
title: t('toast:sentToUnifiedCanvas'),
status: 'success',
duration: 2500,
isClosable: true,
@@ -348,7 +350,7 @@ const CurrentImageButtons = () => {
handleClickShowImageDetails();
} else {
toast({
title: 'Failed to load metadata',
title: t('toast:metadataLoadFailed'),
status: 'error',
duration: 2500,
isClosable: true,
@@ -368,7 +370,10 @@ const CurrentImageButtons = () => {
<IAIPopover
trigger="hover"
triggerComponent={
<IAIIconButton aria-label="Send to..." icon={<FaShareAlt />} />
<IAIIconButton
aria-label={`${t('options:sendTo')}...`}
icon={<FaShareAlt />}
/>
}
>
<div className="current-image-send-to-popover">
@@ -377,35 +382,41 @@ const CurrentImageButtons = () => {
onClick={handleClickUseAsInitialImage}
leftIcon={<FaShare />}
>
Send to Image to Image
{t('options:sendToImg2Img')}
</IAIButton>
<IAIButton
size={'sm'}
onClick={handleSendToCanvas}
leftIcon={<FaShare />}
>
Send to Unified Canvas
{t('options:sendToUnifiedCanvas')}
</IAIButton>
<IAIButton
size={'sm'}
onClick={handleCopyImageLink}
leftIcon={<FaCopy />}
>
Copy Link to Image
{t('options:copyImageToLink')}
</IAIButton>
<IAIButton leftIcon={<FaDownload />} size={'sm'}>
<Link download={true} href={currentImage?.url}>
Download Image
{t('options:downloadImage')}
</Link>
</IAIButton>
</div>
</IAIPopover>
<IAIIconButton
icon={<FaExpand />}
tooltip={!isLightBoxOpen ? 'Open In Viewer (Z)' : 'Close Viewer (Z)'}
tooltip={
!isLightBoxOpen
? `${t('options:openInViewer')} (Z)`
: `${t('options:closeViewer')} (Z)`
}
aria-label={
!isLightBoxOpen ? 'Open In Viewer (Z)' : 'Close Viewer (Z)'
!isLightBoxOpen
? `${t('options:openInViewer')} (Z)`
: `${t('options:closeViewer')} (Z)`
}
data-selected={isLightBoxOpen}
onClick={handleLightBox}
@@ -415,24 +426,24 @@ const CurrentImageButtons = () => {
<ButtonGroup isAttached={true}>
<IAIIconButton
icon={<FaQuoteRight />}
tooltip="Use Prompt (P)"
aria-label="Use Prompt (P)"
tooltip={`${t('options:usePrompt')} (P)`}
aria-label={`${t('options:usePrompt')} (P)`}
isDisabled={!currentImage?.metadata?.image?.prompt}
onClick={handleClickUsePrompt}
/>
<IAIIconButton
icon={<FaSeedling />}
tooltip="Use Seed (S)"
aria-label="Use Seed (S)"
tooltip={`${t('options:useSeed')} (S)`}
aria-label={`${t('options:useSeed')} (S)`}
isDisabled={!currentImage?.metadata?.image?.seed}
onClick={handleClickUseSeed}
/>
<IAIIconButton
icon={<FaAsterisk />}
tooltip="Use All (A)"
aria-label="Use All (A)"
tooltip={`${t('options:useAll')} (A)`}
aria-label={`${t('options:useAll')} (A)`}
isDisabled={
!['txt2img', 'img2img'].includes(
currentImage?.metadata?.image?.type
@@ -446,7 +457,10 @@ const CurrentImageButtons = () => {
<IAIPopover
trigger="hover"
triggerComponent={
<IAIIconButton icon={<FaGrinStars />} aria-label="Restore Faces" />
<IAIIconButton
icon={<FaGrinStars />}
aria-label={t('options:restoreFaces')}
/>
}
>
<div className="current-image-postprocessing-popover">
@@ -460,7 +474,7 @@ const CurrentImageButtons = () => {
}
onClick={handleClickFixFaces}
>
Restore Faces
{t('options:restoreFaces')}
</IAIButton>
</div>
</IAIPopover>
@@ -468,7 +482,10 @@ const CurrentImageButtons = () => {
<IAIPopover
trigger="hover"
triggerComponent={
<IAIIconButton icon={<FaExpandArrowsAlt />} aria-label="Upscale" />
<IAIIconButton
icon={<FaExpandArrowsAlt />}
aria-label={t('options:upscale')}
/>
}
>
<div className="current-image-postprocessing-popover">
@@ -482,7 +499,7 @@ const CurrentImageButtons = () => {
}
onClick={handleClickUpscale}
>
Upscale Image
{t('options:upscaleImage')}
</IAIButton>
</div>
</IAIPopover>
@@ -491,8 +508,8 @@ const CurrentImageButtons = () => {
<ButtonGroup isAttached={true}>
<IAIIconButton
icon={<FaCode />}
tooltip="Info (I)"
aria-label="Info (I)"
tooltip={`${t('options:info')} (I)`}
aria-label={`${t('options:info')} (I)`}
data-selected={shouldShowImageDetails}
onClick={handleClickShowImageDetails}
/>
@@ -501,8 +518,8 @@ const CurrentImageButtons = () => {
<DeleteImageModal image={currentImage}>
<IAIIconButton
icon={<FaTrash />}
tooltip="Delete Image"
aria-label="Delete Image"
tooltip={`${t('options:deleteImage')} (Del)`}
aria-label={`${t('options:deleteImage')} (Del)`}
isDisabled={!currentImage || !isConnected || isProcessing}
style={{ backgroundColor: 'var(--btn-delete-image)' }}
/>

View File

@@ -1,11 +1,4 @@
import {
Box,
Icon,
IconButton,
Image,
Tooltip,
useToast,
} from '@chakra-ui/react';
import { Box, Icon, IconButton, Image, useToast } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import {
setCurrentImage,
@@ -30,6 +23,7 @@ import {
setInitialCanvasImage,
} from 'features/canvas/store/canvasSlice';
import { hoverableImageSelector } from 'features/gallery/store/gallerySliceSelectors';
import { useTranslation } from 'react-i18next';
interface HoverableImageProps {
image: InvokeAI.Image;
@@ -61,6 +55,8 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const toast = useToast();
const { t } = useTranslation();
const handleMouseOver = () => setIsHovered(true);
const handleMouseOut = () => setIsHovered(false);
@@ -68,7 +64,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const handleUsePrompt = () => {
image.metadata && dispatch(setPrompt(image.metadata.image.prompt));
toast({
title: 'Prompt Set',
title: t('toast:promptSet'),
status: 'success',
duration: 2500,
isClosable: true,
@@ -78,7 +74,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const handleUseSeed = () => {
image.metadata && dispatch(setSeed(image.metadata.image.seed));
toast({
title: 'Seed Set',
title: t('toast:seedSet'),
status: 'success',
duration: 2500,
isClosable: true,
@@ -92,7 +88,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
dispatch(setActiveTab('img2img'));
}
toast({
title: 'Sent to Image To Image',
title: t('toast:sentToImageToImage'),
status: 'success',
duration: 2500,
isClosable: true,
@@ -111,7 +107,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
}
toast({
title: 'Sent to Unified Canvas',
title: t('toast:sentToUnifiedCanvas'),
status: 'success',
duration: 2500,
isClosable: true,
@@ -121,7 +117,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const handleUseAllParameters = () => {
metadata && dispatch(setAllTextToImageParameters(metadata));
toast({
title: 'Parameters Set',
title: t('toast:parametersSet'),
status: 'success',
duration: 2500,
isClosable: true,
@@ -135,7 +131,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
dispatch(setActiveTab('img2img'));
dispatch(setAllImageToImageParameters(metadata));
toast({
title: 'Initial Image Set',
title: t('toast:initialImageSet'),
status: 'success',
duration: 2500,
isClosable: true,
@@ -144,8 +140,8 @@ const HoverableImage = memo((props: HoverableImageProps) => {
}
}
toast({
title: 'Initial Image Not Set',
description: 'Could not load initial image.',
title: t('toast:initialImageNotSet'),
description: t('toast:initialImageNotSetDesc'),
status: 'error',
duration: 2500,
isClosable: true,
@@ -202,18 +198,16 @@ const HoverableImage = memo((props: HoverableImageProps) => {
</div>
{isHovered && galleryImageMinimumWidth >= 64 && (
<div className="hoverable-image-delete-button">
<Tooltip label={'Delete image'} hasArrow>
<DeleteImageModal image={image}>
<IconButton
aria-label="Delete image"
icon={<FaTrashAlt />}
size="xs"
variant={'imageHoverIconButton'}
fontSize={14}
isDisabled={!mayDeleteImage}
/>
</DeleteImageModal>
</Tooltip>
<DeleteImageModal image={image}>
<IconButton
aria-label={t('options:deleteImage')}
icon={<FaTrashAlt />}
size="xs"
variant={'imageHoverIconButton'}
fontSize={14}
isDisabled={!mayDeleteImage}
/>
</DeleteImageModal>
</div>
)}
</Box>
@@ -226,20 +220,20 @@ const HoverableImage = memo((props: HoverableImageProps) => {
}}
>
<ContextMenu.Item onClickCapture={handleLightBox}>
Open In Viewer
{t('options:openInViewer')}
</ContextMenu.Item>
<ContextMenu.Item
onClickCapture={handleUsePrompt}
disabled={image?.metadata?.image?.prompt === undefined}
>
Use Prompt
{t('options:usePrompt')}
</ContextMenu.Item>
<ContextMenu.Item
onClickCapture={handleUseSeed}
disabled={image?.metadata?.image?.seed === undefined}
>
Use Seed
{t('options:useSeed')}
</ContextMenu.Item>
<ContextMenu.Item
onClickCapture={handleUseAllParameters}
@@ -247,25 +241,25 @@ const HoverableImage = memo((props: HoverableImageProps) => {
!['txt2img', 'img2img'].includes(image?.metadata?.image?.type)
}
>
Use All Parameters
{t('options:useAll')}
</ContextMenu.Item>
<ContextMenu.Item
onClickCapture={handleUseInitialImage}
disabled={image?.metadata?.image?.type !== 'img2img'}
>
{t('options:useInitImg')}
</ContextMenu.Item>
<Tooltip label="Load initial image used for this generation">
<ContextMenu.Item
onClickCapture={handleUseInitialImage}
disabled={image?.metadata?.image?.type !== 'img2img'}
>
Use Initial Image
</ContextMenu.Item>
</Tooltip>
<ContextMenu.Item onClickCapture={handleSendToImageToImage}>
Send to Image To Image
{t('options:sendToImg2Img')}
</ContextMenu.Item>
<ContextMenu.Item onClickCapture={handleSendToCanvas}>
Send to Unified Canvas
{t('options:sendToUnifiedCanvas')}
</ContextMenu.Item>
<ContextMenu.Item data-warning>
<DeleteImageModal image={image}>
<p>{t('options:deleteImage')}</p>
</DeleteImageModal>
</ContextMenu.Item>
<DeleteImageModal image={image}>
<ContextMenu.Item data-warning>Delete Image</ContextMenu.Item>
</DeleteImageModal>
</ContextMenu.Content>
</ContextMenu.Root>
);

View File

@@ -42,6 +42,7 @@ import { setDoesCanvasNeedScaling } from 'features/canvas/store/canvasSlice';
import _ from 'lodash';
import IAIButton from 'common/components/IAIButton';
import { InvokeTabName } from 'features/tabs/tabMap';
import { useTranslation } from 'react-i18next';
const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 320;
const GALLERY_IMAGE_WIDTH_OFFSET = 40;
@@ -63,6 +64,8 @@ const LIGHTBOX_GALLERY_WIDTH = 400;
export default function ImageGallery() {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const {
images,
currentCategory,
@@ -395,28 +398,28 @@ export default function ImageGallery() {
data-selected={currentCategory === 'result'}
onClick={() => dispatch(setCurrentCategory('result'))}
>
Generations
{t('gallery:generations')}
</IAIButton>
<IAIButton
size={'sm'}
data-selected={currentCategory === 'user'}
onClick={() => dispatch(setCurrentCategory('user'))}
>
Uploads
{t('gallery:uploads')}
</IAIButton>
</>
) : (
<>
<IAIIconButton
aria-label="Show Generations"
tooltip="Show Generations"
aria-label={t('gallery:showGenerations')}
tooltip={t('gallery:showGenerations')}
data-selected={currentCategory === 'result'}
icon={<FaImage />}
onClick={() => dispatch(setCurrentCategory('result'))}
/>
<IAIIconButton
aria-label="Show Uploads"
tooltip="Show Uploads"
aria-label={t('gallery:showUploads')}
tooltip={t('gallery:showUploads')}
data-selected={currentCategory === 'user'}
icon={<FaUser />}
onClick={() => dispatch(setCurrentCategory('user'))}
@@ -433,7 +436,7 @@ export default function ImageGallery() {
triggerComponent={
<IAIIconButton
size={'sm'}
aria-label={'Gallery Settings'}
aria-label={t('gallery:gallerySettings')}
icon={<FaWrench />}
className="image-gallery-icon-btn"
cursor={'pointer'}
@@ -448,12 +451,12 @@ export default function ImageGallery() {
min={32}
max={256}
hideTooltip={true}
label={'Image Size'}
label={t('gallery:galleryImageSize')}
/>
<IAIIconButton
size={'sm'}
aria-label={'Reset'}
tooltip={'Reset Size'}
aria-label={t('gallery:galleryImageResetSize')}
tooltip={t('gallery:galleryImageResetSize')}
onClick={() => dispatch(setGalleryImageMinimumWidth(64))}
icon={<BiReset />}
data-selected={shouldPinGallery}
@@ -462,7 +465,7 @@ export default function ImageGallery() {
</div>
<div>
<IAICheckbox
label="Maintain Aspect Ratio"
label={t('gallery:maintainAspectRatio')}
isChecked={galleryImageObjectFit === 'contain'}
onChange={() =>
dispatch(
@@ -477,7 +480,7 @@ export default function ImageGallery() {
</div>
<div>
<IAICheckbox
label="Auto-Switch to New Images"
label={t('gallery:autoSwitchNewImages')}
isChecked={shouldAutoSwitchToNewImages}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
dispatch(
@@ -488,7 +491,7 @@ export default function ImageGallery() {
</div>
<div>
<IAICheckbox
label="Single Column Layout"
label={t('gallery:singleColumnLayout')}
isChecked={shouldUseSingleGalleryColumn}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
dispatch(
@@ -503,8 +506,8 @@ export default function ImageGallery() {
<IAIIconButton
size={'sm'}
className={'image-gallery-icon-btn'}
aria-label={'Pin Gallery'}
tooltip={'Pin Gallery (Shift+G)'}
aria-label={t('gallery:pinGallery')}
tooltip={`${t('gallery:pinGallery')} (Shift+G)`}
onClick={handleSetShouldPinGallery}
icon={shouldPinGallery ? <BsPinAngleFill /> : <BsPinAngle />}
/>
@@ -534,13 +537,15 @@ export default function ImageGallery() {
isDisabled={!areMoreImagesAvailable}
className="image-gallery-load-more-btn"
>
{areMoreImagesAvailable ? 'Load More' : 'All Images Loaded'}
{areMoreImagesAvailable
? t('gallery:loadMore')
: t('gallery:allImagesLoaded')}
</Button>
</>
) : (
<div className="image-gallery-container-placeholder">
<MdPhotoLibrary />
<p>No Images In Gallery</p>
<p>{t('gallery:noImagesInGallery')}</p>
</div>
)}
</div>

View File

@@ -5,6 +5,7 @@ import IAISlider from 'common/components/IAISlider';
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
import _ from 'lodash';
import { useTranslation } from 'react-i18next';
const selector = createSelector(
canvasSelector,
@@ -27,6 +28,8 @@ const BoundingBoxSettings = () => {
const dispatch = useAppDispatch();
const { boundingBoxDimensions } = useAppSelector(selector);
const { t } = useTranslation();
const handleChangeWidth = (v: number) => {
dispatch(
setBoundingBoxDimensions({
@@ -66,7 +69,7 @@ const BoundingBoxSettings = () => {
return (
<Flex direction="column" gap="1rem">
<IAISlider
label={'Width'}
label={t('options:width')}
min={64}
max={1024}
step={64}
@@ -79,7 +82,7 @@ const BoundingBoxSettings = () => {
withReset
/>
<IAISlider
label={'Height'}
label={t('options:height')}
min={64}
max={1024}
step={64}
@@ -98,9 +101,10 @@ const BoundingBoxSettings = () => {
export default BoundingBoxSettings;
export const BoundingBoxSettingsHeader = () => {
const { t } = useTranslation();
return (
<Box flex="1" textAlign="left">
Bounding Box
{t('options:boundingBoxHeader')}
</Box>
);
};

View File

@@ -20,6 +20,7 @@ import {
import { systemSelector } from 'features/system/store/systemSelectors';
import _ from 'lodash';
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import InpaintReplace from './InpaintReplace';
const selector = createSelector(
@@ -61,6 +62,8 @@ const InfillAndScalingOptions = () => {
scaledBoundingBoxDimensions,
} = useAppSelector(selector);
const { t } = useTranslation();
const handleChangeScaledWidth = (v: number) => {
dispatch(
setScaledBoundingBoxDimensions({
@@ -106,7 +109,7 @@ const InfillAndScalingOptions = () => {
return (
<Flex direction="column" gap="1rem">
<IAISelect
label={'Scale Before Processing'}
label={t('options:scaleBeforeProcessing')}
validValues={BOUNDING_BOX_SCALES_DICT}
value={boundingBoxScale}
onChange={handleChangeBoundingBoxScaleMethod}
@@ -115,7 +118,7 @@ const InfillAndScalingOptions = () => {
isInputDisabled={!isManual}
isResetDisabled={!isManual}
isSliderDisabled={!isManual}
label={'Scaled W'}
label={t('options:scaledWidth')}
min={64}
max={1024}
step={64}
@@ -131,7 +134,7 @@ const InfillAndScalingOptions = () => {
isInputDisabled={!isManual}
isResetDisabled={!isManual}
isSliderDisabled={!isManual}
label={'Scaled H'}
label={t('options:scaledHeight')}
min={64}
max={1024}
step={64}
@@ -145,7 +148,7 @@ const InfillAndScalingOptions = () => {
/>
<InpaintReplace />
<IAISelect
label="Infill Method"
label={t('options:infillMethod')}
value={infillMethod}
validValues={availableInfillMethods}
onChange={(e) => dispatch(setInfillMethod(e.target.value))}
@@ -155,7 +158,7 @@ const InfillAndScalingOptions = () => {
isResetDisabled={infillMethod !== 'tile'}
isSliderDisabled={infillMethod !== 'tile'}
sliderMarkRightOffset={-4}
label={'Tile Size'}
label={t('options:tileSize')}
min={16}
max={64}
sliderNumberInputProps={{ max: 256 }}

View File

@@ -10,6 +10,7 @@ import {
setShouldUseInpaintReplace,
} from 'features/canvas/store/canvasSlice';
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
import { useTranslation } from 'react-i18next';
const selector = createSelector(
canvasSelector,
@@ -32,10 +33,12 @@ export default function InpaintReplace() {
const dispatch = useAppDispatch();
const { t } = useTranslation();
return (
<Flex alignItems={'center'} columnGap={'1rem'}>
<IAISlider
label="Inpaint Replace"
label={t('options:inpaintReplace')}
value={inpaintReplace}
onChange={(v: number) => {
dispatch(setInpaintReplace(v));
@@ -56,6 +59,7 @@ export default function InpaintReplace() {
onChange={(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldUseInpaintReplace(e.target.checked))
}
marginTop="1.25rem"
/>
</Flex>
);

View File

@@ -10,6 +10,7 @@ import {
setSeamStrength,
} from 'features/options/store/optionsSlice';
import _ from 'lodash';
import { useTranslation } from 'react-i18next';
const selector = createSelector(
[optionsSelector],
@@ -35,11 +36,13 @@ const SeamCorrectionOptions = () => {
const { seamSize, seamBlur, seamStrength, seamSteps } =
useAppSelector(selector);
const { t } = useTranslation();
return (
<Flex direction="column" gap="1rem">
<IAISlider
sliderMarkRightOffset={-6}
label={'Seam Size'}
label={t('options:seamSize')}
min={1}
max={256}
sliderNumberInputProps={{ max: 512 }}
@@ -54,7 +57,7 @@ const SeamCorrectionOptions = () => {
/>
<IAISlider
sliderMarkRightOffset={-4}
label={'Seam Blur'}
label={t('options:seamBlur')}
min={0}
max={64}
sliderNumberInputProps={{ max: 512 }}
@@ -70,8 +73,8 @@ const SeamCorrectionOptions = () => {
withReset
/>
<IAISlider
sliderMarkRightOffset={-2}
label={'Seam Strength'}
sliderMarkRightOffset={-7}
label={t('options:seamStrength')}
min={0.01}
max={0.99}
step={0.01}
@@ -88,7 +91,7 @@ const SeamCorrectionOptions = () => {
/>
<IAISlider
sliderMarkRightOffset={-4}
label={'Seam Steps'}
label={t('options:seamSteps')}
min={1}
max={32}
sliderNumberInputProps={{ max: 100 }}

View File

@@ -18,6 +18,7 @@ import IAINumberInput from 'common/components/IAINumberInput';
import IAISelect from 'common/components/IAISelect';
import { FACETOOL_TYPES } from 'app/constants';
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
const optionsSelector = createSelector(
(state: RootState) => state.options,
@@ -66,17 +67,19 @@ const FaceRestoreOptions = () => {
const handleChangeFacetoolType = (e: ChangeEvent<HTMLSelectElement>) =>
dispatch(setFacetoolType(e.target.value as FacetoolType));
const { t } = useTranslation();
return (
<Flex direction={'column'} gap={2}>
<IAISelect
label="Type"
label={t('options:type')}
validValues={FACETOOL_TYPES.concat()}
value={facetoolType}
onChange={handleChangeFacetoolType}
/>
<IAINumberInput
isDisabled={!isGFPGANAvailable}
label="Strength"
label={t('options:strength')}
step={0.05}
min={0}
max={1}
@@ -88,7 +91,7 @@ const FaceRestoreOptions = () => {
{facetoolType === 'codeformer' && (
<IAINumberInput
isDisabled={!isGFPGANAvailable}
label="Fidelity"
label={t('options:codeformerFidelity')}
step={0.05}
min={0}
max={1}

View File

@@ -3,6 +3,7 @@ import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISwitch from 'common/components/IAISwitch';
import { setShouldFitToWidthHeight } from 'features/options/store/optionsSlice';
import { useTranslation } from 'react-i18next';
export default function ImageFit() {
const dispatch = useAppDispatch();
@@ -14,9 +15,11 @@ export default function ImageFit() {
const handleChangeFit = (e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldFitToWidthHeight(e.target.checked));
const { t } = useTranslation();
return (
<IAISwitch
label="Fit Initial Image To Output Size"
label={t('options:imageFit')}
isChecked={shouldFitToWidthHeight}
onChange={handleChangeFit}
/>

View File

@@ -3,6 +3,7 @@ import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISlider from 'common/components/IAISlider';
import { setImg2imgStrength } from 'features/options/store/optionsSlice';
import { useTranslation } from 'react-i18next';
interface ImageToImageStrengthProps {
label?: string;
@@ -10,7 +11,8 @@ interface ImageToImageStrengthProps {
}
export default function ImageToImageStrength(props: ImageToImageStrengthProps) {
const { label = 'Strength', styleClass } = props;
const { t } = useTranslation();
const { label = `${t('options:strength')}`, styleClass } = props;
const img2imgStrength = useAppSelector(
(state: RootState) => state.options.img2imgStrength
);

View File

@@ -4,6 +4,7 @@ import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISwitch from 'common/components/IAISwitch';
import { setHiresFix } from 'features/options/store/optionsSlice';
import { useTranslation } from 'react-i18next';
/**
* Hires Fix Toggle
@@ -13,13 +14,15 @@ const HiresOptions = () => {
const hiresFix = useAppSelector((state: RootState) => state.options.hiresFix);
const { t } = useTranslation();
const handleChangeHiresFix = (e: ChangeEvent<HTMLInputElement>) =>
dispatch(setHiresFix(e.target.checked));
return (
<Flex gap={2} direction={'column'}>
<IAISwitch
label="High Res Optimization"
label={t('options:hiresOptim')}
fontSize={'md'}
isChecked={hiresFix}
onChange={handleChangeHiresFix}

View File

@@ -4,6 +4,7 @@ import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISwitch from 'common/components/IAISwitch';
import { setSeamless } from 'features/options/store/optionsSlice';
import { useTranslation } from 'react-i18next';
/**
* Seamless tiling toggle
@@ -16,10 +17,12 @@ const SeamlessOptions = () => {
const handleChangeSeamless = (e: ChangeEvent<HTMLInputElement>) =>
dispatch(setSeamless(e.target.checked));
const { t } = useTranslation();
return (
<Flex gap={2} direction={'column'}>
<IAISwitch
label="Seamless tiling"
label={t('options:seamlessTiling')}
fontSize={'md'}
isChecked={seamless}
onChange={handleChangeSeamless}

View File

@@ -3,16 +3,18 @@ import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAINumberInput from 'common/components/IAINumberInput';
import { setPerlin } from 'features/options/store/optionsSlice';
import { useTranslation } from 'react-i18next';
export default function Perlin() {
const dispatch = useAppDispatch();
const perlin = useAppSelector((state: RootState) => state.options.perlin);
const { t } = useTranslation();
const handleChangePerlin = (v: number) => dispatch(setPerlin(v));
return (
<IAINumberInput
label="Perlin Noise"
label={t('options:perlinNoise')}
min={0}
max={1}
step={0.05}

View File

@@ -5,9 +5,11 @@ import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISwitch from 'common/components/IAISwitch';
import { setShouldRandomizeSeed } from 'features/options/store/optionsSlice';
import { useTranslation } from 'react-i18next';
export default function RandomizeSeed() {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const shouldRandomizeSeed = useAppSelector(
(state: RootState) => state.options.shouldRandomizeSeed
@@ -18,7 +20,7 @@ export default function RandomizeSeed() {
return (
<IAISwitch
label="Randomize Seed"
label={t('options:randomizeSeed')}
isChecked={shouldRandomizeSeed}
onChange={handleChangeShouldRandomizeSeed}
/>

View File

@@ -4,6 +4,7 @@ import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAINumberInput from 'common/components/IAINumberInput';
import { setSeed } from 'features/options/store/optionsSlice';
import { useTranslation } from 'react-i18next';
export default function Seed() {
const seed = useAppSelector((state: RootState) => state.options.seed);
@@ -14,13 +15,15 @@ export default function Seed() {
(state: RootState) => state.options.shouldGenerateVariations
);
const { t } = useTranslation();
const dispatch = useAppDispatch();
const handleChangeSeed = (v: number) => dispatch(setSeed(v));
return (
<IAINumberInput
label="Seed"
label={t('options:seed')}
step={1}
precision={0}
flexGrow={1}

View File

@@ -5,12 +5,14 @@ import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import randomInt from 'common/util/randomInt';
import { setSeed } from 'features/options/store/optionsSlice';
import { useTranslation } from 'react-i18next';
export default function ShuffleSeed() {
const dispatch = useAppDispatch();
const shouldRandomizeSeed = useAppSelector(
(state: RootState) => state.options.shouldRandomizeSeed
);
const { t } = useTranslation();
const handleClickRandomizeSeed = () =>
dispatch(setSeed(randomInt(NUMPY_RAND_MIN, NUMPY_RAND_MAX)));
@@ -21,7 +23,7 @@ export default function ShuffleSeed() {
isDisabled={shouldRandomizeSeed}
onClick={handleClickRandomizeSeed}
>
<p>Shuffle</p>
<p>{t('options:shuffle')}</p>
</Button>
);
}

View File

@@ -3,18 +3,20 @@ import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAINumberInput from 'common/components/IAINumberInput';
import { setThreshold } from 'features/options/store/optionsSlice';
import { useTranslation } from 'react-i18next';
export default function Threshold() {
const dispatch = useAppDispatch();
const threshold = useAppSelector(
(state: RootState) => state.options.threshold
);
const { t } = useTranslation();
const handleChangeThreshold = (v: number) => dispatch(setThreshold(v));
return (
<IAINumberInput
label="Noise Threshold"
label={t('options:noiseThreshold')}
min={0}
max={1000}
step={0.1}

View File

@@ -15,6 +15,7 @@ import { SystemState } from 'features/system/store/systemSlice';
import { ChangeEvent } from 'react';
import IAINumberInput from 'common/components/IAINumberInput';
import IAISelect from 'common/components/IAISelect';
import { useTranslation } from 'react-i18next';
const optionsSelector = createSelector(
(state: RootState) => state.options,
@@ -54,6 +55,8 @@ const UpscaleOptions = () => {
const { isESRGANAvailable } = useAppSelector(systemSelector);
const { t } = useTranslation();
const handleChangeLevel = (e: ChangeEvent<HTMLSelectElement>) =>
dispatch(setUpscalingLevel(Number(e.target.value) as UpscalingLevel));
@@ -63,14 +66,14 @@ const UpscaleOptions = () => {
<div className="upscale-options">
<IAISelect
isDisabled={!isESRGANAvailable}
label="Scale"
label={t('options:scale')}
value={upscalingLevel}
onChange={handleChangeLevel}
validValues={UPSCALING_LEVELS}
/>
<IAINumberInput
isDisabled={!isESRGANAvailable}
label="Strength"
label={t('options:strength')}
step={0.05}
min={0}
max={1}

View File

@@ -4,6 +4,7 @@ import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAIInput from 'common/components/IAIInput';
import { validateSeedWeights } from 'common/util/seedWeightPairs';
import { setSeedWeights } from 'features/options/store/optionsSlice';
import { useTranslation } from 'react-i18next';
export default function SeedWeights() {
const seedWeights = useAppSelector(
@@ -14,6 +15,8 @@ export default function SeedWeights() {
(state: RootState) => state.options.shouldGenerateVariations
);
const { t } = useTranslation();
const dispatch = useAppDispatch();
const handleChangeSeedWeights = (e: ChangeEvent<HTMLInputElement>) =>
@@ -21,7 +24,7 @@ export default function SeedWeights() {
return (
<IAIInput
label={'Seed Weights'}
label={t('options:seedWeights')}
value={seedWeights}
isInvalid={
shouldGenerateVariations &&

View File

@@ -3,6 +3,7 @@ import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAINumberInput from 'common/components/IAINumberInput';
import { setVariationAmount } from 'features/options/store/optionsSlice';
import { useTranslation } from 'react-i18next';
export default function VariationAmount() {
const variationAmount = useAppSelector(
@@ -13,13 +14,15 @@ export default function VariationAmount() {
(state: RootState) => state.options.shouldGenerateVariations
);
const { t } = useTranslation();
const dispatch = useAppDispatch();
const handleChangevariationAmount = (v: number) =>
dispatch(setVariationAmount(v));
return (
<IAINumberInput
label="Variation Amount"
label={t('options:variationAmount')}
value={variationAmount}
step={0.01}
min={0}

View File

@@ -3,16 +3,18 @@ import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAINumberInput from 'common/components/IAINumberInput';
import { setCfgScale } from 'features/options/store/optionsSlice';
import { useTranslation } from 'react-i18next';
export default function MainCFGScale() {
const dispatch = useAppDispatch();
const cfgScale = useAppSelector((state: RootState) => state.options.cfgScale);
const { t } = useTranslation();
const handleChangeCfgScale = (v: number) => dispatch(setCfgScale(v));
return (
<IAINumberInput
label="CFG Scale"
label={t('options:cfgScale')}
step={0.5}
min={1.01}
max={200}

View File

@@ -6,11 +6,13 @@ import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISelect from 'common/components/IAISelect';
import { activeTabNameSelector } from 'features/options/store/optionsSelectors';
import { setHeight } from 'features/options/store/optionsSlice';
import { useTranslation } from 'react-i18next';
export default function MainHeight() {
const height = useAppSelector((state: RootState) => state.options.height);
const activeTabName = useAppSelector(activeTabNameSelector);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleChangeHeight = (e: ChangeEvent<HTMLSelectElement>) =>
dispatch(setHeight(Number(e.target.value)));
@@ -18,7 +20,7 @@ export default function MainHeight() {
return (
<IAISelect
isDisabled={activeTabName === 'unifiedCanvas'}
label="Height"
label={t('options:height')}
value={height}
flexGrow={1}
onChange={handleChangeHeight}

View File

@@ -8,6 +8,7 @@ import {
OptionsState,
setIterations,
} from 'features/options/store/optionsSlice';
import { useTranslation } from 'react-i18next';
const mainIterationsSelector = createSelector(
[(state: RootState) => state.options],
@@ -28,12 +29,13 @@ const mainIterationsSelector = createSelector(
export default function MainIterations() {
const dispatch = useAppDispatch();
const { iterations } = useAppSelector(mainIterationsSelector);
const { t } = useTranslation();
const handleChangeIterations = (v: number) => dispatch(setIterations(v));
return (
<IAINumberInput
label="Images"
label={t('options:images')}
step={1}
min={1}
max={9999}

View File

@@ -4,17 +4,19 @@ import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISelect from 'common/components/IAISelect';
import { setSampler } from 'features/options/store/optionsSlice';
import { useTranslation } from 'react-i18next';
export default function MainSampler() {
const sampler = useAppSelector((state: RootState) => state.options.sampler);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleChangeSampler = (e: ChangeEvent<HTMLSelectElement>) =>
dispatch(setSampler(e.target.value));
return (
<IAISelect
label="Sampler"
label={t('options:sampler')}
value={sampler}
onChange={handleChangeSampler}
validValues={SAMPLERS}

View File

@@ -3,16 +3,18 @@ import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAINumberInput from 'common/components/IAINumberInput';
import { setSteps } from 'features/options/store/optionsSlice';
import { useTranslation } from 'react-i18next';
export default function MainSteps() {
const dispatch = useAppDispatch();
const steps = useAppSelector((state: RootState) => state.options.steps);
const { t } = useTranslation();
const handleChangeSteps = (v: number) => dispatch(setSteps(v));
return (
<IAINumberInput
label="Steps"
label={t('options:steps')}
min={1}
max={9999}
step={1}

View File

@@ -5,10 +5,12 @@ import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISelect from 'common/components/IAISelect';
import { activeTabNameSelector } from 'features/options/store/optionsSelectors';
import { setWidth } from 'features/options/store/optionsSlice';
import { useTranslation } from 'react-i18next';
export default function MainWidth() {
const width = useAppSelector((state: RootState) => state.options.width);
const activeTabName = useAppSelector(activeTabNameSelector);
const { t } = useTranslation();
const dispatch = useAppDispatch();
@@ -18,7 +20,7 @@ export default function MainWidth() {
return (
<IAISelect
isDisabled={activeTabName === 'unifiedCanvas'}
label="Width"
label={t('options:width')}
value={width}
flexGrow={1}
onChange={handleChangeWidth}

View File

@@ -9,6 +9,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { createSelector } from '@reduxjs/toolkit';
import { SystemState } from 'features/system/store/systemSlice';
import _ from 'lodash';
import { useTranslation } from 'react-i18next';
const cancelButtonSelector = createSelector(
(state: RootState) => state.system,
@@ -35,6 +36,8 @@ export default function CancelButton(
useAppSelector(cancelButtonSelector);
const handleClickCancel = () => dispatch(cancelProcessing());
const { t } = useTranslation();
useHotkeys(
'shift+x',
() => {
@@ -48,8 +51,8 @@ export default function CancelButton(
return (
<IAIIconButton
icon={<MdCancel />}
tooltip="Cancel"
aria-label="Cancel"
tooltip={t('options:cancel')}
aria-label={t('options:cancel')}
isDisabled={!isConnected || !isProcessing || !isCancelable}
onClick={handleClickCancel}
styleClass="cancel-btn"

View File

@@ -8,6 +8,7 @@ import IAIIconButton, {
IAIIconButtonProps,
} from 'common/components/IAIIconButton';
import { activeTabNameSelector } from 'features/options/store/optionsSelectors';
import { useTranslation } from 'react-i18next';
interface InvokeButton
extends Omit<IAIButtonProps | IAIIconButtonProps, 'aria-label'> {
@@ -24,6 +25,8 @@ export default function InvokeButton(props: InvokeButton) {
dispatch(generateImage(activeTabName));
};
const { t } = useTranslation();
useHotkeys(
['ctrl+enter', 'meta+enter'],
() => {
@@ -41,19 +44,19 @@ export default function InvokeButton(props: InvokeButton) {
<div style={{ flexGrow: 4 }}>
{iconButton ? (
<IAIIconButton
aria-label="Invoke"
aria-label={t('options:invoke')}
type="submit"
icon={<FaPlay />}
isDisabled={!isReady}
onClick={handleClickGenerate}
className="invoke-btn"
tooltip="Invoke"
tooltip={t('options:invoke')}
tooltipProps={{ placement: 'bottom' }}
{...rest}
/>
) : (
<IAIButton
aria-label="Invoke"
aria-label={t('options:invoke')}
type="submit"
isDisabled={!isReady}
onClick={handleClickGenerate}

View File

@@ -7,6 +7,7 @@ import {
OptionsState,
setShouldLoopback,
} from 'features/options/store/optionsSlice';
import { useTranslation } from 'react-i18next';
const loopbackSelector = createSelector(
(state: RootState) => state.options,
@@ -17,10 +18,12 @@ const LoopbackButton = () => {
const dispatch = useAppDispatch();
const shouldLoopback = useAppSelector(loopbackSelector);
const { t } = useTranslation();
return (
<IAIIconButton
aria-label="Toggle Loopback"
tooltip="Toggle Loopback"
aria-label={t('options:toggleLoopback')}
tooltip={t('options:toggleLoopback')}
styleClass="loopback-btn"
asCheckbox={true}
isChecked={shouldLoopback}

View File

@@ -10,6 +10,7 @@ import _ from 'lodash';
import { useHotkeys } from 'react-hotkeys-hook';
import { activeTabNameSelector } from 'features/options/store/optionsSelectors';
import { readinessSelector } from 'app/selectors/readinessSelector';
import { useTranslation } from 'react-i18next';
const promptInputSelector = createSelector(
[(state: RootState) => state.options, activeTabNameSelector],
@@ -36,6 +37,8 @@ const PromptInput = () => {
const promptRef = useRef<HTMLTextAreaElement>(null);
const { t } = useTranslation();
const handleChangePrompt = (e: ChangeEvent<HTMLTextAreaElement>) => {
dispatch(setPrompt(e.target.value));
};
@@ -63,7 +66,7 @@ const PromptInput = () => {
<Textarea
id="prompt"
name="prompt"
placeholder="Type prompt here. [negative tokens], (upweight)++, (downweight)--, swap and blend are available (see docs)"
placeholder={t('options:promptPlaceholder')}
size={'lg'}
value={prompt}
onChange={handleChangePrompt}

View File

@@ -7,11 +7,13 @@ import {
clearCanvasHistory,
resetCanvas,
} from 'features/canvas/store/canvasSlice';
import { useTranslation } from 'react-i18next';
import { FaTrash } from 'react-icons/fa';
const EmptyTempFolderButtonModal = () => {
const isStaging = useAppSelector(isStagingSelector);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const acceptCallback = () => {
dispatch(emptyTempFolder());
@@ -21,22 +23,18 @@ const EmptyTempFolderButtonModal = () => {
return (
<IAIAlertDialog
title={'Empty Temp Image Folder'}
title={t('unifiedcanvas:emptyTempImageFolder')}
acceptCallback={acceptCallback}
acceptButtonText={'Empty Folder'}
acceptButtonText={t('unifiedcanvas:emptyFolder')}
triggerComponent={
<IAIButton leftIcon={<FaTrash />} size={'sm'} isDisabled={isStaging}>
Empty Temp Image Folder
{t('unifiedcanvas:emptyTempImageFolder')}
</IAIButton>
}
>
<p>
Emptying the temp image folder also fully resets the Unified Canvas.
This includes all undo/redo history, images in the staging area, and the
canvas base layer.
</p>
<p>{t('unifiedcanvas:emptyTempImagesFolderMessage')}</p>
<br />
<p>Are you sure you want to empty the temp folder?</p>
<p>{t('unifiedcanvas:emptyTempImagesFolderConfirm')}</p>
</IAIAlertDialog>
);
};

View File

@@ -11,6 +11,7 @@ import {
useDisclosure,
} from '@chakra-ui/react';
import React, { cloneElement, ReactElement } from 'react';
import { useTranslation } from 'react-i18next';
import HotkeysModalItem from './HotkeysModalItem';
type HotkeysModalProps = {
@@ -31,245 +32,267 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
onClose: onHotkeysModalClose,
} = useDisclosure();
const { t } = useTranslation();
const appHotkeys = [
{ title: 'Invoke', desc: 'Generate an image', hotkey: 'Ctrl+Enter' },
{ title: 'Cancel', desc: 'Cancel image generation', hotkey: 'Shift+X' },
{
title: 'Focus Prompt',
desc: 'Focus the prompt input area',
title: t('hotkeys:invoke.title'),
desc: t('hotkeys:invoke.desc'),
hotkey: 'Ctrl+Enter',
},
{
title: t('hotkeys:cancel.title'),
desc: t('hotkeys:cancel.desc'),
hotkey: 'Shift+X',
},
{
title: t('hotkeys:focusPrompt.title'),
desc: t('hotkeys:focusPrompt.desc'),
hotkey: 'Alt+A',
},
{
title: 'Toggle Options',
desc: 'Open and close the options panel',
title: t('hotkeys:toggleOptions.title'),
desc: t('hotkeys:toggleOptions.desc'),
hotkey: 'O',
},
{
title: 'Pin Options',
desc: 'Pin the options panel',
title: t('hotkeys:pinOptions.title'),
desc: t('hotkeys:pinOptions.desc'),
hotkey: 'Shift+O',
},
{
title: 'Toggle Viewer',
desc: 'Open and close Image Viewer',
title: t('hotkeys:toggleViewer.title'),
desc: t('hotkeys:toggleViewer.desc'),
hotkey: 'Z',
},
{
title: 'Toggle Gallery',
desc: 'Open and close the gallery drawer',
title: t('hotkeys:toggleGallery.title'),
desc: t('hotkeys:toggleGallery.desc'),
hotkey: 'G',
},
{
title: 'Maximize Workspace',
desc: 'Close panels and maximize work area',
title: t('hotkeys:maximizeWorkSpace.title'),
desc: t('hotkeys:maximizeWorkSpace.desc'),
hotkey: 'F',
},
{
title: 'Change Tabs',
desc: 'Switch to another workspace',
title: t('hotkeys:changeTabs.title'),
desc: t('hotkeys:changeTabs.desc'),
hotkey: '1-5',
},
{
title: 'Console Toggle',
desc: 'Open and close console',
title: t('hotkeys:consoleToggle.title'),
desc: t('hotkeys:consoleToggle.desc'),
hotkey: '`',
},
];
const generalHotkeys = [
{
title: 'Set Prompt',
desc: 'Use the prompt of the current image',
title: t('hotkeys:setPrompt.title'),
desc: t('hotkeys:setPrompt.desc'),
hotkey: 'P',
},
{
title: 'Set Seed',
desc: 'Use the seed of the current image',
title: t('hotkeys:setSeed.title'),
desc: t('hotkeys:setSeed.desc'),
hotkey: 'S',
},
{
title: 'Set Parameters',
desc: 'Use all parameters of the current image',
title: t('hotkeys:setParameters.title'),
desc: t('hotkeys:setParameters.desc'),
hotkey: 'A',
},
{
title: 'Restore Faces',
desc: 'Restore the current image',
title: t('hotkeys:restoreFaces.title'),
desc: t('hotkeys:restoreFaces.desc'),
hotkey: 'Shift+R',
},
{ title: 'Upscale', desc: 'Upscale the current image', hotkey: 'Shift+U' },
{
title: 'Show Info',
desc: 'Show metadata info of the current image',
title: t('hotkeys:upscale.title'),
desc: t('hotkeys:upscale.desc'),
hotkey: 'Shift+U',
},
{
title: t('hotkeys:showInfo.title'),
desc: t('hotkeys:showInfo.desc'),
hotkey: 'I',
},
{
title: 'Send To Image To Image',
desc: 'Send current image to Image to Image',
title: t('hotkeys:sendToImageToImage.title'),
desc: t('hotkeys:sendToImageToImage.desc'),
hotkey: 'Shift+I',
},
{ title: 'Delete Image', desc: 'Delete the current image', hotkey: 'Del' },
{ title: 'Close Panels', desc: 'Closes open panels', hotkey: 'Esc' },
{
title: t('hotkeys:deleteImage.title'),
desc: t('hotkeys:deleteImage.desc'),
hotkey: 'Del',
},
{
title: t('hotkeys:closePanels.title'),
desc: t('hotkeys:closePanels.desc'),
hotkey: 'Esc',
},
];
const galleryHotkeys = [
{
title: 'Previous Image',
desc: 'Display the previous image in gallery',
title: t('hotkeys:previousImage.title'),
desc: t('hotkeys:previousImage.desc'),
hotkey: 'Arrow Left',
},
{
title: 'Next Image',
desc: 'Display the next image in gallery',
title: t('hotkeys:nextImage.title'),
desc: t('hotkeys:nextImage.desc'),
hotkey: 'Arrow Right',
},
{
title: 'Toggle Gallery Pin',
desc: 'Pins and unpins the gallery to the UI',
title: t('hotkeys:toggleGalleryPin.title'),
desc: t('hotkeys:toggleGalleryPin.desc'),
hotkey: 'Shift+G',
},
{
title: 'Increase Gallery Image Size',
desc: 'Increases gallery thumbnails size',
title: t('hotkeys:increaseGalleryThumbSize.title'),
desc: t('hotkeys:increaseGalleryThumbSize.desc'),
hotkey: 'Shift+Up',
},
{
title: 'Decrease Gallery Image Size',
desc: 'Decreases gallery thumbnails size',
title: t('hotkeys:decreaseGalleryThumbSize.title'),
desc: t('hotkeys:decreaseGalleryThumbSize.desc'),
hotkey: 'Shift+Down',
},
];
const unifiedCanvasHotkeys = [
{
title: 'Select Brush',
desc: 'Selects the canvas brush',
title: t('hotkeys:selectBrush.title'),
desc: t('hotkeys:selectBrush.desc'),
hotkey: 'B',
},
{
title: 'Select Eraser',
desc: 'Selects the canvas eraser',
title: t('hotkeys:selectEraser.title'),
desc: t('hotkeys:selectEraser.desc'),
hotkey: 'E',
},
{
title: 'Decrease Brush Size',
desc: 'Decreases the size of the canvas brush/eraser',
title: t('hotkeys:decreaseBrushSize.title'),
desc: t('hotkeys:decreaseBrushSize.desc'),
hotkey: '[',
},
{
title: 'Increase Brush Size',
desc: 'Increases the size of the canvas brush/eraser',
title: t('hotkeys:increaseBrushSize.title'),
desc: t('hotkeys:increaseBrushSize.desc'),
hotkey: ']',
},
{
title: 'Decrease Brush Opacity',
desc: 'Decreases the opacity of the canvas brush',
title: t('hotkeys:decreaseBrushOpacity.title'),
desc: t('hotkeys:decreaseBrushOpacity.desc'),
hotkey: 'Shift + [',
},
{
title: 'Increase Brush Opacity',
desc: 'Increases the opacity of the canvas brush',
title: t('hotkeys:increaseBrushOpacity.title'),
desc: t('hotkeys:increaseBrushOpacity.desc'),
hotkey: 'Shift + ]',
},
{
title: 'Move Tool',
desc: 'Allows canvas navigation',
title: t('hotkeys:moveTool.title'),
desc: t('hotkeys:moveTool.desc'),
hotkey: 'V',
},
{
title: 'Fill Bounding Box',
desc: 'Fills the bounding box with brush color',
title: t('hotkeys:fillBoundingBox.title'),
desc: t('hotkeys:fillBoundingBox.desc'),
hotkey: 'Shift + F',
},
{
title: 'Erase Bounding Box',
desc: 'Erases the bounding box area',
title: t('hotkeys:eraseBoundingBox.title'),
desc: t('hotkeys:eraseBoundingBox.desc'),
hotkey: 'Delete / Backspace',
},
{
title: 'Select Color Picker',
desc: 'Selects the canvas color picker',
title: t('hotkeys:colorPicker.title'),
desc: t('hotkeys:colorPicker.desc'),
hotkey: 'C',
},
{
title: 'Toggle Snap',
desc: 'Toggles Snap to Grid',
title: t('hotkeys:toggleSnap.title'),
desc: t('hotkeys:toggleSnap.desc'),
hotkey: 'N',
},
{
title: 'Quick Toggle Move',
desc: 'Temporarily toggles Move mode',
title: t('hotkeys:quickToggleMove.title'),
desc: t('hotkeys:quickToggleMove.desc'),
hotkey: 'Hold Space',
},
{
title: 'Toggle Layer',
desc: 'Toggles mask/base layer selection',
title: t('hotkeys:toggleLayer.title'),
desc: t('hotkeys:toggleLayer.desc'),
hotkey: 'Q',
},
{
title: 'Clear Mask',
desc: 'Clear the entire mask',
title: t('hotkeys:clearMask.title'),
desc: t('hotkeys:clearMask.desc'),
hotkey: 'Shift+C',
},
{
title: 'Hide Mask',
desc: 'Hide and unhide mask',
title: t('hotkeys:hideMask.title'),
desc: t('hotkeys:hideMask.desc'),
hotkey: 'H',
},
{
title: 'Show/Hide Bounding Box',
desc: 'Toggle visibility of bounding box',
title: t('hotkeys:showHideBoundingBox.title'),
desc: t('hotkeys:showHideBoundingBox.desc'),
hotkey: 'Shift+H',
},
{
title: 'Merge Visible',
desc: 'Merge all visible layers of canvas',
title: t('hotkeys:mergeVisible.title'),
desc: t('hotkeys:mergeVisible.desc'),
hotkey: 'Shift+M',
},
{
title: 'Save To Gallery',
desc: 'Save current canvas to gallery',
title: t('hotkeys:saveToGallery.title'),
desc: t('hotkeys:saveToGallery.desc'),
hotkey: 'Shift+S',
},
{
title: 'Copy to Clipboard',
desc: 'Copy current canvas to clipboard',
title: t('hotkeys:copyToClipboard.title'),
desc: t('hotkeys:copyToClipboard.desc'),
hotkey: 'Ctrl+C',
},
{
title: 'Download Image',
desc: 'Download current canvas',
title: t('hotkeys:downloadImage.title'),
desc: t('hotkeys:downloadImage.desc'),
hotkey: 'Shift+D',
},
{
title: 'Undo Stroke',
desc: 'Undo a brush stroke',
title: t('hotkeys:undoStroke.title'),
desc: t('hotkeys:undoStroke.desc'),
hotkey: 'Ctrl+Z',
},
{
title: 'Redo Stroke',
desc: 'Redo a brush stroke',
title: t('hotkeys:redoStroke.title'),
desc: t('hotkeys:redoStroke.desc'),
hotkey: 'Ctrl+Shift+Z, Ctrl+Y',
},
{
title: 'Reset View',
desc: 'Reset Canvas View',
title: t('hotkeys:resetView.title'),
desc: t('hotkeys:resetView.desc'),
hotkey: 'R',
},
{
title: 'Previous Staging Image',
desc: 'Previous Staging Area Image',
title: t('hotkeys:previousStagingImage.title'),
desc: t('hotkeys:previousStagingImage.desc'),
hotkey: 'Arrow Left',
},
{
title: 'Next Staging Image',
desc: 'Next Staging Area Image',
title: t('hotkeys:nextStagingImage.title'),
desc: t('hotkeys:nextStagingImage.desc'),
hotkey: 'Arrow Right',
},
{
title: 'Accept Staging Image',
desc: 'Accept Current Staging Area Image',
title: t('hotkeys:acceptStagingImage.title'),
desc: t('hotkeys:acceptStagingImage.desc'),
hotkey: 'Enter',
},
];
@@ -308,7 +331,7 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
<Accordion allowMultiple>
<AccordionItem>
<AccordionButton className="hotkeys-modal-button">
<h2>App Hotkeys</h2>
<h2>{t('hotkeys:appHotkeys')}</h2>
<AccordionIcon />
</AccordionButton>
<AccordionPanel>
@@ -318,7 +341,7 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
<AccordionItem>
<AccordionButton className="hotkeys-modal-button">
<h2>General Hotkeys</h2>
<h2>{t('hotkeys:generalHotkeys')}</h2>
<AccordionIcon />
</AccordionButton>
<AccordionPanel>
@@ -328,7 +351,7 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
<AccordionItem>
<AccordionButton className="hotkeys-modal-button">
<h2>Gallery Hotkeys</h2>
<h2>{t('hotkeys:galleryHotkeys')}</h2>
<AccordionIcon />
</AccordionButton>
<AccordionPanel>
@@ -338,7 +361,7 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
<AccordionItem>
<AccordionButton className="hotkeys-modal-button">
<h2>Unified Canvas Hotkeys</h2>
<h2>{t('hotkeys:unifiedCanvasHotkeys')}</h2>
<AccordionIcon />
</AccordionButton>
<AccordionPanel>

View File

@@ -0,0 +1,63 @@
import type { ReactNode } from 'react';
import { VStack } from '@chakra-ui/react';
import IAIPopover from 'common/components/IAIPopover';
import React from 'react';
import IAIButton from 'common/components/IAIButton';
import { useTranslation } from 'react-i18next';
import IAIIconButton from 'common/components/IAIIconButton';
import { FaLanguage } from 'react-icons/fa';
export default function LanguagePicker() {
const { t, i18n } = useTranslation();
const LANGUAGES = {
en: t('common:langEnglish'),
ru: t('common:langRussian'),
it: t('common:langItalian'),
pt_br: t('common:langBrPortuguese'),
de: t('common:langGerman'),
pl: t('common:langPolish'),
};
const renderLanguagePicker = () => {
const languagesToRender: ReactNode[] = [];
Object.keys(LANGUAGES).forEach((lang) => {
languagesToRender.push(
<IAIButton
key={lang}
data-selected={localStorage.getItem('i18nextLng') === lang}
onClick={() => i18n.changeLanguage(lang)}
className="modal-close-btn lang-select-btn"
aria-label={LANGUAGES[lang as keyof typeof LANGUAGES]}
tooltip={LANGUAGES[lang as keyof typeof LANGUAGES]}
size="sm"
minWidth="200px"
>
{LANGUAGES[lang as keyof typeof LANGUAGES]}
</IAIButton>
);
});
return languagesToRender;
};
return (
<IAIPopover
trigger="hover"
triggerComponent={
<IAIIconButton
aria-label={t('common:languagePickerLabel')}
tooltip={t('common:languagePickerLabel')}
icon={<FaLanguage />}
size={'sm'}
variant="link"
data-variant="link"
fontSize={26}
/>
}
>
<VStack>{renderLanguagePicker()}</VStack>
</IAIPopover>
);
}

View File

@@ -7,6 +7,7 @@ import {
AccordionButton,
AccordionPanel,
AccordionIcon,
Text,
} from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import _ from 'lodash';
@@ -15,6 +16,7 @@ import { requestModelChange } from 'app/socketio/actions';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import { SystemState } from 'features/system/store/systemSlice';
import { useTranslation } from 'react-i18next';
type ModelListItemProps = {
name: string;
@@ -32,6 +34,7 @@ const ModelListItem = (props: ModelListItemProps) => {
const handleChangeModel = () => {
dispatch(requestModelChange(name));
};
const { t } = useTranslation();
return (
<div className="model-list-item">
<Tooltip label={description} hasArrow placement="bottom">
@@ -47,7 +50,7 @@ const ModelListItem = (props: ModelListItemProps) => {
onClick={handleChangeModel}
isDisabled={status === 'active' || isProcessing || !isConnected}
>
Load
{t('common:load')}
</Button>
</div>
</div>
@@ -72,6 +75,7 @@ const modelListSelector = createSelector(
const ModelList = () => {
const { models } = useAppSelector(modelListSelector);
const { t } = useTranslation();
return (
<Accordion
@@ -82,7 +86,13 @@ const ModelList = () => {
<AccordionItem>
<AccordionButton>
<div className="model-list-button">
<h2>Models</h2>
<Text
fontSize="sm"
fontWeight="bold"
color="var(--text-color-secondary)"
>
{t('settings:models')}
</Text>
<AccordionIcon />
</div>
</AccordionButton>

View File

@@ -34,6 +34,7 @@ import IAINumberInput from 'common/components/IAINumberInput';
import { systemSelector } from 'features/system/store/systemSelectors';
import { optionsSelector } from 'features/options/store/optionsSelectors';
import { setShouldUseCanvasBetaLayout } from 'features/options/store/optionsSlice';
import { useTranslation } from 'react-i18next';
const selector = createSelector(
[systemSelector, optionsSelector],
@@ -77,6 +78,7 @@ type SettingsModalProps = {
*/
const SettingsModal = ({ children }: SettingsModalProps) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const steps = useAppSelector((state: RootState) => state.options.steps);
@@ -124,10 +126,16 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
onClick: onSettingsModalOpen,
})}
<Modal isOpen={isSettingsModalOpen} onClose={onSettingsModalClose}>
<Modal
isOpen={isSettingsModalOpen}
onClose={onSettingsModalClose}
size="lg"
>
<ModalOverlay />
<ModalContent className="modal settings-modal">
<ModalHeader className="settings-modal-header">Settings</ModalHeader>
<ModalHeader className="settings-modal-header">
{t('common:settingsLabel')}
</ModalHeader>
<ModalCloseButton className="modal-close-btn" />
<ModalBody className="settings-modal-content">
<div className="settings-modal-items">
@@ -139,7 +147,7 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
style={{ gridAutoFlow: 'row', rowGap: '0.5rem' }}
>
<IAISelect
label={'Display In-Progress Images'}
label={t('settings:displayInProgress')}
validValues={IN_PROGRESS_IMAGE_TYPES}
value={shouldDisplayInProgressType}
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
@@ -152,7 +160,7 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
/>
{shouldDisplayInProgressType === 'full-res' && (
<IAINumberInput
label="Save images every n steps"
label={t('settings:saveSteps')}
min={1}
max={steps}
step={1}
@@ -165,7 +173,7 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
</div>
<IAISwitch
styleClass="settings-modal-item"
label={'Confirm on Delete'}
label={t('settings:confirmOnDelete')}
isChecked={shouldConfirmOnDelete}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldConfirmOnDelete(e.target.checked))
@@ -173,7 +181,7 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
/>
<IAISwitch
styleClass="settings-modal-item"
label={'Display Help Icons'}
label={t('settings:displayHelpIcons')}
isChecked={shouldDisplayGuides}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldDisplayGuides(e.target.checked))
@@ -181,7 +189,7 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
/>
<IAISwitch
styleClass="settings-modal-item"
label={'Use Canvas Beta Layout'}
label={t('settings:useCanvasBeta')}
isChecked={shouldUseCanvasBetaLayout}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldUseCanvasBetaLayout(e.target.checked))
@@ -193,7 +201,7 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
<h2 style={{ fontWeight: 'bold' }}>Developer</h2>
<IAISwitch
styleClass="settings-modal-item"
label={'Enable Image Debugging'}
label={t('settings:enableImageDebugging')}
isChecked={enableImageDebugging}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setEnableImageDebugging(e.target.checked))
@@ -202,26 +210,18 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
</div>
<div className="settings-modal-reset">
<Heading size={'md'}>Reset Web UI</Heading>
<Heading size={'md'}>{t('settings:resetWebUI')}</Heading>
<Button colorScheme="red" onClick={handleClickResetWebUI}>
Reset Web UI
{t('settings:resetWebUI')}
</Button>
<Text>
Resetting the web UI only resets the browser's local cache of
your images and remembered settings. It does not delete any
images from disk.
</Text>
<Text>
If images aren't showing up in the gallery or something else
isn't working, please try resetting before submitting an issue
on GitHub.
</Text>
<Text>{t('settings:resetWebUIDesc1')}</Text>
<Text>{t('settings:resetWebUIDesc2')}</Text>
</div>
</ModalBody>
<ModalFooter>
<Button onClick={onSettingsModalClose} className="modal-close-btn">
Close
{t('common:close')}
</Button>
</ModalFooter>
</ModalContent>
@@ -238,7 +238,7 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
<ModalBody pb={6} pt={6}>
<Flex justifyContent={'center'}>
<Text fontSize={'lg'}>
Web UI has been reset. Refresh the page to reload.
<Text>{t('settings:resetComplete')}</Text>
</Text>
</Flex>
</ModalBody>

View File

@@ -23,4 +23,13 @@
display: flex;
align-items: center;
column-gap: 0.5rem;
.lang-select-btn {
&[data-selected='true'] {
background-color: var(--accent-color);
&:hover {
background-color: var(--accent-color);
}
}
}
}

View File

@@ -17,11 +17,16 @@ import SettingsModal from './SettingsModal/SettingsModal';
import StatusIndicator from './StatusIndicator';
import ThemeChanger from './ThemeChanger';
import ModelSelect from './ModelSelect';
import LanguagePicker from './LanguagePicker';
import { useTranslation } from 'react-i18next';
/**
* Header, includes color mode toggle, settings button, status message.
*/
const SiteHeader = () => {
const { t } = useTranslation();
return (
<div className="site-header">
<div className="site-header-left-side">
@@ -38,8 +43,8 @@ const SiteHeader = () => {
<HotkeysModal>
<IAIIconButton
aria-label="Hotkeys"
tooltip="Hotkeys"
aria-label={t('common:hotkeysLabel')}
tooltip={t('common:hotkeysLabel')}
size={'sm'}
variant="link"
data-variant="link"
@@ -50,9 +55,11 @@ const SiteHeader = () => {
<ThemeChanger />
<LanguagePicker />
<IAIIconButton
aria-label="Report Bug"
tooltip="Report Bug"
aria-label={t('common:reportBugLabel')}
tooltip={t('common:reportBugLabel')}
variant="link"
data-variant="link"
fontSize={20}
@@ -65,8 +72,8 @@ const SiteHeader = () => {
/>
<IAIIconButton
aria-label="Link to Github Repo"
tooltip="Github"
aria-label={t('common:githubLabel')}
tooltip={t('common:githubLabel')}
variant="link"
data-variant="link"
fontSize={20}
@@ -79,8 +86,8 @@ const SiteHeader = () => {
/>
<IAIIconButton
aria-label="Link to Discord Server"
tooltip="Discord"
aria-label={t('common:discordLabel')}
tooltip={t('common:discordLabel')}
variant="link"
data-variant="link"
fontSize={20}
@@ -94,8 +101,8 @@ const SiteHeader = () => {
<SettingsModal>
<IAIIconButton
aria-label="Settings"
tooltip="Settings"
aria-label={t('common:settingsLabel')}
tooltip={t('common:settingsLabel')}
variant="link"
data-variant="link"
fontSize={20}

View File

@@ -4,6 +4,7 @@ import { isEqual } from 'lodash';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import { errorSeen, SystemState } from 'features/system/store/systemSlice';
import { useTranslation } from 'react-i18next';
const systemSelector = createSelector(
(state: RootState) => state.system,
@@ -34,6 +35,7 @@ const StatusIndicator = () => {
wasErrorSeen,
} = useAppSelector(systemSelector);
const dispatch = useAppDispatch();
const { t } = useTranslation();
let statusStyle;
if (isConnected && !hasError) {
@@ -45,14 +47,14 @@ const StatusIndicator = () => {
let statusMessage = currentStatus;
const intermediateStatuses = [
'generating',
'preparing',
'saving image',
'restoring faces',
'upscaling',
t('common:statusGenerating'),
t('common:statusPreparing'),
t('common:statusSavingImage'),
t('common:statusRestoringFaces'),
t('common:statusUpscaling'),
];
if (intermediateStatuses.includes(statusMessage.toLowerCase())) {
if (intermediateStatuses.includes(statusMessage)) {
statusStyle = 'status-working';
}
@@ -84,7 +86,7 @@ const StatusIndicator = () => {
onClick={handleClickStatusIndicator}
className={`status ${statusStyle}`}
>
{statusMessage}
{t(statusMessage as keyof typeof t)}
</Text>
</Tooltip>
);

View File

@@ -7,16 +7,23 @@ import IAIPopover from 'common/components/IAIPopover';
import IAIIconButton from 'common/components/IAIIconButton';
import { FaCheck, FaPalette } from 'react-icons/fa';
import IAIButton from 'common/components/IAIButton';
const THEMES = ['dark', 'light', 'green'];
import { useTranslation } from 'react-i18next';
import type { ReactNode } from 'react';
export default function ThemeChanger() {
const { t } = useTranslation();
const { setColorMode, colorMode } = useColorMode();
const dispatch = useAppDispatch();
const currentTheme = useAppSelector(
(state: RootState) => state.options.currentTheme
);
const THEMES = {
dark: t('common:darkTheme'),
light: t('common:lightTheme'),
green: t('common:greenTheme'),
};
useEffect(() => {
// syncs the redux store theme to the chakra's theme on startup and when
// setCurrentTheme is dispatched
@@ -29,12 +36,34 @@ export default function ThemeChanger() {
dispatch(setCurrentTheme(theme));
};
const renderThemeOptions = () => {
const themesToRender: ReactNode[] = [];
Object.keys(THEMES).forEach((theme) => {
themesToRender.push(
<IAIButton
style={{
width: '6rem',
}}
leftIcon={currentTheme === theme ? <FaCheck /> : undefined}
size={'sm'}
onClick={() => handleChangeTheme(theme)}
key={theme}
>
{THEMES[theme as keyof typeof THEMES]}
</IAIButton>
);
});
return themesToRender;
};
return (
<IAIPopover
trigger="hover"
triggerComponent={
<IAIIconButton
aria-label="Theme"
aria-label={t('common:themeLabel')}
size={'sm'}
variant="link"
data-variant="link"
@@ -43,21 +72,7 @@ export default function ThemeChanger() {
/>
}
>
<VStack align={'stretch'}>
{THEMES.map((theme) => (
<IAIButton
style={{
width: '6rem',
}}
leftIcon={currentTheme === theme ? <FaCheck /> : undefined}
size={'sm'}
onClick={() => handleChangeTheme(theme)}
key={theme}
>
{theme.charAt(0).toUpperCase() + theme.slice(1)}
</IAIButton>
))}
</VStack>
<VStack align={'stretch'}>{renderThemeOptions()}</VStack>
</IAIPopover>
);
}

View File

@@ -2,6 +2,7 @@ import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import { ExpandedIndex, UseToastOptions } from '@chakra-ui/react';
import * as InvokeAI from 'app/invokeai';
import i18n from 'i18n';
export type LogLevel = 'info' | 'warning' | 'error';
@@ -64,7 +65,9 @@ const initialSystemState: SystemState = {
totalSteps: 0,
currentIteration: 0,
totalIterations: 0,
currentStatus: 'Disconnected',
currentStatus: i18n.isInitialized
? i18n.t('common:statusDisconnected')
: 'Disconnected',
currentStatusHasSteps: false,
model: '',
model_id: '',
@@ -109,13 +112,15 @@ export const systemSlice = createSlice({
state.currentIteration = 0;
state.totalIterations = 0;
state.currentStatusHasSteps = false;
state.currentStatus = 'Error';
state.currentStatus = i18n.t('common:statusError');
state.wasErrorSeen = false;
},
errorSeen: (state) => {
state.hasError = false;
state.wasErrorSeen = true;
state.currentStatus = state.isConnected ? 'Connected' : 'Disconnected';
state.currentStatus = state.isConnected
? i18n.t('common:statusConnected')
: i18n.t('common:statusDisconnected');
},
addLogEntry: (
state,
@@ -176,7 +181,7 @@ export const systemSlice = createSlice({
state.currentIteration = 0;
state.totalIterations = 0;
state.currentStatusHasSteps = false;
state.currentStatus = 'Processing canceled';
state.currentStatus = i18n.t('common:statusProcessingCanceled');
},
generationRequested: (state) => {
state.isProcessing = true;
@@ -186,7 +191,7 @@ export const systemSlice = createSlice({
state.currentIteration = 0;
state.totalIterations = 0;
state.currentStatusHasSteps = false;
state.currentStatus = 'Preparing';
state.currentStatus = i18n.t('common:statusPreparing');
},
setModelList: (
state,
@@ -198,7 +203,7 @@ export const systemSlice = createSlice({
state.isCancelable = action.payload;
},
modelChangeRequested: (state) => {
state.currentStatus = 'Loading Model';
state.currentStatus = i18n.t('common:statusLoadingModel');
state.isCancelable = false;
state.isProcessing = true;
state.currentStatusHasSteps = false;

View File

@@ -18,34 +18,37 @@ import { setHiresFix } from 'features/options/store/optionsSlice';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import InvokeOptionsPanel from 'features/tabs/components/InvokeOptionsPanel';
import { activeTabNameSelector } from 'features/options/store/optionsSelectors';
import { useTranslation } from 'react-i18next';
export default function ImageToImagePanel() {
const { t } = useTranslation();
const imageToImageAccordions = {
seed: {
header: 'Seed',
header: `${t('options:seed')}`,
feature: Feature.SEED,
content: <SeedOptions />,
},
variations: {
header: 'Variations',
header: `${t('options:variations')}`,
feature: Feature.VARIATIONS,
content: <VariationsOptions />,
additionalHeaderComponents: <GenerateVariationsToggle />,
},
face_restore: {
header: 'Face Restoration',
header: `${t('options:faceRestoration')}`,
feature: Feature.FACE_CORRECTION,
content: <FaceRestoreOptions />,
additionalHeaderComponents: <FaceRestoreToggle />,
},
upscale: {
header: 'Upscaling',
header: `${t('options:upscaling')}`,
feature: Feature.UPSCALE,
content: <UpscaleOptions />,
additionalHeaderComponents: <UpscaleToggle />,
},
other: {
header: 'Other Options',
header: `${t('options:otherOptions')}`,
feature: Feature.OTHER,
content: <ImageToImageOutputOptions />,
},
@@ -68,7 +71,7 @@ export default function ImageToImagePanel() {
<ProcessButtons />
<MainOptions />
<ImageToImageStrength
label="Image To Image Strength"
label={t('options:img2imgStrength')}
styleClass="main-option-block image-to-image-strength-main-option"
/>
<ImageFit />

View File

@@ -3,25 +3,23 @@ import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import ImageUploaderIconButton from 'common/components/ImageUploaderIconButton';
import { clearInitialImage } from 'features/options/store/optionsSlice';
import { useTranslation } from 'react-i18next';
export default function InitImagePreview() {
const initialImage = useAppSelector(
(state: RootState) => state.options.initialImage
);
const { t } = useTranslation();
const dispatch = useAppDispatch();
const toast = useToast();
// const handleClickResetInitialImage = (e: SyntheticEvent) => {
// e.stopPropagation();
// dispatch(clearInitialImage());
// };
const alertMissingInitImage = () => {
toast({
title: 'Problem loading parameters',
description: 'Unable to load init image.',
title: t('toast:parametersFailed'),
description: t('toast:parametersFailedDesc'),
status: 'error',
isClosable: true,
});
@@ -31,14 +29,7 @@ export default function InitImagePreview() {
return (
<>
<div className="init-image-preview-header">
{/* <div className="init-image-preview-header"> */}
<h2>Initial Image</h2>
{/* <IconButton
isDisabled={!initialImage}
aria-label={'Reset Initial Image'}
onClick={handleClickResetInitialImage}
icon={<MdClear />}
/> */}
<h2>{t('options:initialImage')}</h2>
<ImageUploaderIconButton />
</div>
{initialImage && (

View File

@@ -22,6 +22,8 @@ import UnifiedCanvasIcon from 'common/icons/UnifiedCanvasIcon';
import TrainingWIP from 'common/components/WorkInProgress/Training';
import TrainingIcon from 'common/icons/TrainingIcon';
import { InvokeTabName } from 'features/tabs/tabMap';
import i18n from 'i18n';
import useUpdateTranslations from 'common/hooks/useUpdateTranslations';
export interface InvokeTabInfo {
title: ReactElement;
@@ -62,6 +64,15 @@ export const tabDict: Record<InvokeTabName, InvokeTabInfo> = {
},
};
function updateTabTranslations() {
tabDict.txt2img.tooltip = i18n.t('common:text2img');
tabDict.img2img.tooltip = i18n.t('common:img2img');
tabDict.unifiedCanvas.tooltip = i18n.t('common:unifiedCanvas');
tabDict.nodes.tooltip = i18n.t('common:nodes');
tabDict.postprocess.tooltip = i18n.t('common:postProcessing');
tabDict.training.tooltip = i18n.t('common:training');
}
export default function InvokeTabs() {
const activeTab = useAppSelector(
(state: RootState) => state.options.activeTab
@@ -70,6 +81,8 @@ export default function InvokeTabs() {
(state: RootState) => state.options.isLightBoxOpen
);
useUpdateTranslations(updateTabTranslations);
const dispatch = useAppDispatch();
useHotkeys('1', () => {

View File

@@ -12,34 +12,37 @@ import OptionsAccordion from 'features/options/components/OptionsAccordion';
import ProcessButtons from 'features/options/components/ProcessButtons/ProcessButtons';
import PromptInput from 'features/options/components/PromptInput/PromptInput';
import InvokeOptionsPanel from 'features/tabs/components/InvokeOptionsPanel';
import { useTranslation } from 'react-i18next';
export default function TextToImagePanel() {
const { t } = useTranslation();
const textToImageAccordions = {
seed: {
header: 'Seed',
header: `${t('options:seed')}`,
feature: Feature.SEED,
content: <SeedOptions />,
},
variations: {
header: 'Variations',
header: `${t('options:variations')}`,
feature: Feature.VARIATIONS,
content: <VariationsOptions />,
additionalHeaderComponents: <GenerateVariationsToggle />,
},
face_restore: {
header: 'Face Restoration',
header: `${t('options:faceRestoration')}`,
feature: Feature.FACE_CORRECTION,
content: <FaceRestoreOptions />,
additionalHeaderComponents: <FaceRestoreToggle />,
},
upscale: {
header: 'Upscaling',
header: `${t('options:upscaling')}`,
feature: Feature.UPSCALE,
content: <UpscaleOptions />,
additionalHeaderComponents: <UpscaleToggle />,
},
other: {
header: 'Other Options',
header: `${t('options:otherOptions')}`,
feature: Feature.OTHER,
content: <OutputOptions />,
},

View File

@@ -5,6 +5,7 @@ import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
import { setBrushSize } from 'features/canvas/store/canvasSlice';
import React from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
export default function UnifiedCanvasBrushSize() {
const dispatch = useAppDispatch();
@@ -13,6 +14,8 @@ export default function UnifiedCanvasBrushSize() {
(state: RootState) => state.canvas.brushSize
);
const { t } = useTranslation();
const isStaging = useAppSelector(isStagingSelector);
useHotkeys(
@@ -41,13 +44,14 @@ export default function UnifiedCanvasBrushSize() {
return (
<IAISlider
label="Size"
label={t('unifiedcanvas:brushSize')}
value={brushSize}
withInput
onChange={(newSize) => dispatch(setBrushSize(newSize))}
sliderNumberInputProps={{ max: 500 }}
inputReadOnly={false}
width={'100px'}
isCompact
/>
);
}

View File

@@ -3,11 +3,13 @@ import IAIButton from 'common/components/IAIButton';
import { clearMask } from 'features/canvas/store/canvasSlice';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { FaTrash } from 'react-icons/fa';
export default function UnifiedCanvasClearMask() {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleClearMask = () => dispatch(clearMask());
@@ -16,9 +18,9 @@ export default function UnifiedCanvasClearMask() {
size={'sm'}
leftIcon={<FaTrash />}
onClick={handleClearMask}
tooltip="Clear Mask (Shift+C)"
tooltip={`${t('unifiedcanvas:clearMask')} (Shift+C)`}
>
Clear
{t('unifiedcanvas:betaClear')}
</IAIButton>
);
}

View File

@@ -3,6 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAICheckbox from 'common/components/IAICheckbox';
import { setShouldDarkenOutsideBoundingBox } from 'features/canvas/store/canvasSlice';
import React from 'react';
import { useTranslation } from 'react-i18next';
export default function UnifiedCanvasDarkenOutsideSelection() {
const shouldDarkenOutsideBoundingBox = useAppSelector(
@@ -11,9 +12,11 @@ export default function UnifiedCanvasDarkenOutsideSelection() {
const dispatch = useAppDispatch();
const { t } = useTranslation();
return (
<IAICheckbox
label="Darken Outside"
label={t('unifiedcanvas:betaDarkenOutside')}
isChecked={shouldDarkenOutsideBoundingBox}
onChange={(e) =>
dispatch(setShouldDarkenOutsideBoundingBox(e.target.checked))

View File

@@ -3,6 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAICheckbox from 'common/components/IAICheckbox';
import { setIsMaskEnabled } from 'features/canvas/store/canvasSlice';
import React from 'react';
import { useTranslation } from 'react-i18next';
export default function UnifiedCanvasEnableMask() {
const isMaskEnabled = useAppSelector(
@@ -10,13 +11,14 @@ export default function UnifiedCanvasEnableMask() {
);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleToggleEnableMask = () =>
dispatch(setIsMaskEnabled(!isMaskEnabled));
return (
<IAICheckbox
label="Enable Mask (H)"
label={`${t('unifiedcanvas:enableMask')} (H)`}
isChecked={isMaskEnabled}
onChange={handleToggleEnableMask}
/>

View File

@@ -3,6 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAICheckbox from 'common/components/IAICheckbox';
import { setShouldRestrictStrokesToBox } from 'features/canvas/store/canvasSlice';
import React from 'react';
import { useTranslation } from 'react-i18next';
export default function UnifiedCanvasLimitStrokesToBox() {
const dispatch = useAppDispatch();
@@ -11,9 +12,11 @@ export default function UnifiedCanvasLimitStrokesToBox() {
(state: RootState) => state.canvas.shouldRestrictStrokesToBox
);
const { t } = useTranslation();
return (
<IAICheckbox
label="Limit To Box"
label={t('unifiedcanvas:betaLimitToBox')}
isChecked={shouldRestrictStrokesToBox}
onChange={(e) =>
dispatch(setShouldRestrictStrokesToBox(e.target.checked))

View File

@@ -3,9 +3,11 @@ import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAICheckbox from 'common/components/IAICheckbox';
import { setShouldPreserveMaskedArea } from 'features/canvas/store/canvasSlice';
import React from 'react';
import { useTranslation } from 'react-i18next';
export default function UnifiedCanvasPreserveMask() {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const shouldPreserveMaskedArea = useAppSelector(
(state: RootState) => state.canvas.shouldPreserveMaskedArea
@@ -13,7 +15,7 @@ export default function UnifiedCanvasPreserveMask() {
return (
<IAICheckbox
label="Preserve Masked"
label={t('unifiedcanvas:betaPreserveMasked')}
isChecked={shouldPreserveMaskedArea}
onChange={(e) => dispatch(setShouldPreserveMaskedArea(e.target.checked))}
/>

View File

@@ -16,6 +16,7 @@ import { canvasSelector } from 'features/canvas/store/canvasSelectors';
import EmptyTempFolderButtonModal from 'features/system/components/ClearTempFolderButtonModal';
import ClearCanvasHistoryButtonModal from 'features/canvas/components/ClearCanvasHistoryButtonModal';
import { useTranslation } from 'react-i18next';
export const canvasControlsSelector = createSelector(
[canvasSelector],
@@ -43,6 +44,8 @@ export const canvasControlsSelector = createSelector(
const UnifiedCanvasSettings = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const {
shouldAutoSave,
shouldCropToBoundingBoxOnSave,
@@ -55,37 +58,37 @@ const UnifiedCanvasSettings = () => {
trigger="hover"
triggerComponent={
<IAIIconButton
tooltip="Canvas Settings"
tooltip={t('unifiedcanvas:canvasSettings')}
tooltipProps={{
placement: 'bottom',
}}
aria-label="Canvas Settings"
aria-label={t('unifiedcanvas:canvasSettings')}
icon={<FaWrench />}
/>
}
>
<Flex direction={'column'} gap={'0.5rem'}>
<IAICheckbox
label="Show Intermediates"
label={t('unifiedcanvas:showIntermediates')}
isChecked={shouldShowIntermediates}
onChange={(e) =>
dispatch(setShouldShowIntermediates(e.target.checked))
}
/>
<IAICheckbox
label="Auto Save to Gallery"
label={t('unifiedcanvas:autoSaveToGallery')}
isChecked={shouldAutoSave}
onChange={(e) => dispatch(setShouldAutoSave(e.target.checked))}
/>
<IAICheckbox
label="Save Box Region Only"
label={t('unifiedcanvas:saveBoxRegionOnly')}
isChecked={shouldCropToBoundingBoxOnSave}
onChange={(e) =>
dispatch(setShouldCropToBoundingBoxOnSave(e.target.checked))
}
/>
<IAICheckbox
label="Show Canvas Debug Info"
label={t('unifiedcanvas:showCanvasDebugInfo')}
isChecked={shouldShowCanvasDebugInfo}
onChange={(e) =>
dispatch(setShouldShowCanvasDebugInfo(e.target.checked))

View File

@@ -3,6 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAICheckbox from 'common/components/IAICheckbox';
import { setShouldShowGrid } from 'features/canvas/store/canvasSlice';
import React from 'react';
import { useTranslation } from 'react-i18next';
export default function UnifiedCanvasShowGrid() {
const shouldShowGrid = useAppSelector(
@@ -10,10 +11,11 @@ export default function UnifiedCanvasShowGrid() {
);
const dispatch = useAppDispatch();
const { t } = useTranslation();
return (
<IAICheckbox
label="Show Grid"
label={t('unifiedcanvas:showGrid')}
isChecked={shouldShowGrid}
onChange={(e) => dispatch(setShouldShowGrid(e.target.checked))}
/>

View File

@@ -3,6 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAICheckbox from 'common/components/IAICheckbox';
import { setShouldSnapToGrid } from 'features/canvas/store/canvasSlice';
import React, { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
export default function UnifiedCanvasSnapToGrid() {
const shouldSnapToGrid = useAppSelector(
@@ -10,13 +11,14 @@ export default function UnifiedCanvasSnapToGrid() {
);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleChangeShouldSnapToGrid = (e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldSnapToGrid(e.target.checked));
return (
<IAICheckbox
label="Snap to Grid (N)"
label={`${t('unifiedcanvas:snapToGrid')} (N)`}
isChecked={shouldSnapToGrid}
onChange={handleChangeShouldSnapToGrid}
/>

View File

@@ -6,6 +6,7 @@ import { mergeAndUploadCanvas } from 'features/canvas/store/thunks/mergeAndUploa
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
import React from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { FaCopy } from 'react-icons/fa';
export default function UnifiedCanvasCopyToClipboard() {
@@ -21,6 +22,7 @@ export default function UnifiedCanvasCopyToClipboard() {
);
const dispatch = useAppDispatch();
const { t } = useTranslation();
useHotkeys(
['meta+c', 'ctrl+c'],
@@ -46,8 +48,8 @@ export default function UnifiedCanvasCopyToClipboard() {
return (
<IAIIconButton
aria-label="Copy to Clipboard (Cmd/Ctrl+C)"
tooltip="Copy to Clipboard (Cmd/Ctrl+C)"
aria-label={`${t('unifiedcanvas:copyToClipboard')} (Cmd/Ctrl+C)`}
tooltip={`${t('unifiedcanvas:copyToClipboard')} (Cmd/Ctrl+C)`}
icon={<FaCopy />}
onClick={handleCopyImageToClipboard}
isDisabled={isStaging}

View File

@@ -6,10 +6,13 @@ import { mergeAndUploadCanvas } from 'features/canvas/store/thunks/mergeAndUploa
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
import React from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { FaDownload } from 'react-icons/fa';
export default function UnifiedCanvasDownloadImage() {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const canvasBaseLayer = getCanvasBaseLayer();
const isStaging = useAppSelector(isStagingSelector);
@@ -45,8 +48,8 @@ export default function UnifiedCanvasDownloadImage() {
};
return (
<IAIIconButton
aria-label="Download as Image (Shift+D)"
tooltip="Download as Image (Shift+D)"
aria-label={`${t('unifiedcanvas:downloadAsImage')} (Shift+D)`}
tooltip={`${t('unifiedcanvas:downloadAsImage')} (Shift+D)`}
icon={<FaDownload />}
onClick={handleDownloadAsImage}
isDisabled={isStaging}

View File

@@ -3,16 +3,18 @@ import IAIIconButton from 'common/components/IAIIconButton';
import useImageUploader from 'common/hooks/useImageUploader';
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { FaUpload } from 'react-icons/fa';
export default function UnifiedCanvasFileUploader() {
const isStaging = useAppSelector(isStagingSelector);
const { openUploader } = useImageUploader();
const { t } = useTranslation();
return (
<IAIIconButton
aria-label="Upload"
tooltip="Upload"
aria-label={t('common:upload')}
tooltip={t('common:upload')}
icon={<FaUpload />}
onClick={openUploader}
isDisabled={isStaging}

View File

@@ -13,6 +13,7 @@ import {
LAYER_NAMES_DICT,
} from 'features/canvas/store/canvasTypes';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
const selector = createSelector(
[canvasSelector, isStagingSelector],
@@ -29,6 +30,7 @@ const selector = createSelector(
export default function UnifiedCanvasLayerSelect() {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const { layer, isMaskEnabled, isStaging } = useAppSelector(selector);
@@ -57,7 +59,7 @@ export default function UnifiedCanvasLayerSelect() {
};
return (
<IAISelect
tooltip={'Layer (Q)'}
tooltip={`${t('unifiedcanvas:layer')} (Q)`}
tooltipProps={{ hasArrow: true, placement: 'top' }}
value={layer}
validValues={LAYER_NAMES_DICT}

View File

@@ -6,10 +6,12 @@ import { mergeAndUploadCanvas } from 'features/canvas/store/thunks/mergeAndUploa
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
import React from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { FaLayerGroup } from 'react-icons/fa';
export default function UnifiedCanvasMergeVisible() {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const canvasBaseLayer = getCanvasBaseLayer();
const isStaging = useAppSelector(isStagingSelector);
const isProcessing = useAppSelector(
@@ -38,8 +40,8 @@ export default function UnifiedCanvasMergeVisible() {
};
return (
<IAIIconButton
aria-label="Merge Visible (Shift+M)"
tooltip="Merge Visible (Shift+M)"
aria-label={`${t('unifiedcanvas:mergeVisible')} (Shift+M)`}
tooltip={`${t('unifiedcanvas:mergeVisible')} (Shift+M)`}
icon={<FaLayerGroup />}
onClick={handleMergeVisible}
isDisabled={isStaging}

View File

@@ -5,12 +5,14 @@ import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
import { setTool } from 'features/canvas/store/canvasSlice';
import React from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { FaArrowsAlt } from 'react-icons/fa';
export default function UnifiedCanvasMoveTool() {
const tool = useAppSelector((state: RootState) => state.canvas.tool);
const isStaging = useAppSelector(isStagingSelector);
const dispatch = useAppDispatch();
const { t } = useTranslation();
useHotkeys(
['v'],
@@ -28,8 +30,8 @@ export default function UnifiedCanvasMoveTool() {
return (
<IAIIconButton
aria-label="Move Tool (V)"
tooltip="Move Tool (V)"
aria-label={`${t('unifiedcanvas:move')} (V)`}
tooltip={`${t('unifiedcanvas:move')} (V)`}
icon={<FaArrowsAlt />}
data-selected={tool === 'move' || isStaging}
onClick={handleSelectMoveTool}

View File

@@ -7,6 +7,7 @@ import CancelButton from 'features/options/components/ProcessButtons/CancelButto
import InvokeButton from 'features/options/components/ProcessButtons/InvokeButton';
import { setShouldShowOptionsPanel } from 'features/options/store/optionsSlice';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { FaSlidersH } from 'react-icons/fa';
export default function UnifiedCanvasProcessingButtons() {
@@ -15,6 +16,7 @@ export default function UnifiedCanvasProcessingButtons() {
);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleShowOptionsPanel = () => {
dispatch(setShouldShowOptionsPanel(true));
@@ -26,9 +28,9 @@ export default function UnifiedCanvasProcessingButtons() {
return (
<Flex flexDirection={'column'} gap="0.5rem">
<IAIIconButton
tooltip="Show Options Panel (O)"
tooltip={`${t('options:showOptionsPanel')} (O)`}
tooltipProps={{ placement: 'top' }}
aria-label="Show Options Panel"
aria-label={t('options:showOptionsPanel')}
onClick={handleShowOptionsPanel}
>
<FaSlidersH />

View File

@@ -6,10 +6,12 @@ import {
resizeAndScaleCanvas,
} from 'features/canvas/store/canvasSlice';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { FaTrash } from 'react-icons/fa';
export default function UnifiedCanvasResetCanvas() {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const isStaging = useAppSelector(isStagingSelector);
const handleResetCanvas = () => {
@@ -18,8 +20,8 @@ export default function UnifiedCanvasResetCanvas() {
};
return (
<IAIIconButton
aria-label="Clear Canvas"
tooltip="Clear Canvas"
aria-label={t('unifiedcanvas:clearCanvas')}
tooltip={t('unifiedcanvas:clearCanvas')}
icon={<FaTrash />}
onClick={handleResetCanvas}
style={{ backgroundColor: 'var(--btn-delete-image)' }}

View File

@@ -4,11 +4,13 @@ import { resetCanvasView } from 'features/canvas/store/canvasSlice';
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
import React from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { FaCrosshairs } from 'react-icons/fa';
export default function UnifiedCanvasResetView() {
const canvasBaseLayer = getCanvasBaseLayer();
const dispatch = useAppDispatch();
const { t } = useTranslation();
useHotkeys(
['r'],
@@ -36,8 +38,8 @@ export default function UnifiedCanvasResetView() {
};
return (
<IAIIconButton
aria-label="Reset View (R)"
tooltip="Reset View (R)"
aria-label={`${t('unifiedcanvas:resetView')} (R)`}
tooltip={`${t('unifiedcanvas:resetView')} (R)`}
icon={<FaCrosshairs />}
onClick={handleResetCanvasView}
/>

View File

@@ -6,6 +6,7 @@ import { mergeAndUploadCanvas } from 'features/canvas/store/thunks/mergeAndUploa
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
import React from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { FaSave } from 'react-icons/fa';
export default function UnifiedCanvasSaveToGallery() {
@@ -19,6 +20,7 @@ export default function UnifiedCanvasSaveToGallery() {
);
const dispatch = useAppDispatch();
const { t } = useTranslation();
useHotkeys(
['shift+s'],
@@ -43,8 +45,8 @@ export default function UnifiedCanvasSaveToGallery() {
};
return (
<IAIIconButton
aria-label="Save to Gallery (Shift+S)"
tooltip="Save to Gallery (Shift+S)"
aria-label={`${t('unifiedcanvas:saveToGallery')} (Shift+S)`}
tooltip={`${t('unifiedcanvas:saveToGallery')} (Shift+S)`}
icon={<FaSave />}
onClick={handleSaveToGallery}
isDisabled={isStaging}

View File

@@ -21,6 +21,7 @@ import {
} from 'features/canvas/store/canvasSelectors';
import { systemSelector } from 'features/system/store/systemSelectors';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
export const selector = createSelector(
[canvasSelector, isStagingSelector, systemSelector],
@@ -43,6 +44,7 @@ export const selector = createSelector(
const UnifiedCanvasToolSelect = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const { tool, isStaging } = useAppSelector(selector);
useHotkeys(
@@ -113,16 +115,16 @@ const UnifiedCanvasToolSelect = () => {
<Flex flexDirection={'column'} gap={'0.5rem'}>
<ButtonGroup>
<IAIIconButton
aria-label="Brush Tool (B)"
tooltip="Brush Tool (B)"
aria-label={`${t('unifiedcanvas:brush')} (B)`}
tooltip={`${t('unifiedcanvas:brush')} (B)`}
icon={<FaPaintBrush />}
data-selected={tool === 'brush' && !isStaging}
onClick={handleSelectBrushTool}
isDisabled={isStaging}
/>
<IAIIconButton
aria-label="Eraser Tool (E)"
tooltip="Eraser Tool (E)"
aria-label={`${t('unifiedcanvas:eraser')} (E)`}
tooltip={`${t('unifiedcanvas:eraser')} (B)`}
icon={<FaEraser />}
data-selected={tool === 'eraser' && !isStaging}
isDisabled={isStaging}
@@ -131,23 +133,23 @@ const UnifiedCanvasToolSelect = () => {
</ButtonGroup>
<ButtonGroup>
<IAIIconButton
aria-label="Fill Bounding Box (Shift+F)"
tooltip="Fill Bounding Box (Shift+F)"
aria-label={`${t('unifiedcanvas:fillBoundingBox')} (Shift+F)`}
tooltip={`${t('unifiedcanvas:fillBoundingBox')} (Shift+F)`}
icon={<FaFillDrip />}
isDisabled={isStaging}
onClick={handleFillRect}
/>
<IAIIconButton
aria-label="Erase Bounding Box Area (Delete/Backspace)"
tooltip="Erase Bounding Box Area (Delete/Backspace)"
aria-label={`${t('unifiedcanvas:eraseBoundingBox')} (Del/Backspace)`}
tooltip={`${t('unifiedcanvas:eraseBoundingBox')} (Del/Backspace)`}
icon={<FaPlus style={{ transform: 'rotate(45deg)' }} />}
isDisabled={isStaging}
onClick={handleEraseBoundingBox}
/>
</ButtonGroup>
<IAIIconButton
aria-label="Color Picker (C)"
tooltip="Color Picker (C)"
aria-label={`${t('unifiedcanvas:colorPicker')} (C)`}
tooltip={`${t('unifiedcanvas:colorPicker')} (C)`}
icon={<FaEyeDropper />}
data-selected={tool === 'colorPicker' && !isStaging}
isDisabled={isStaging}

View File

@@ -12,31 +12,34 @@ import PromptInput from 'features/options/components/PromptInput/PromptInput';
import InvokeOptionsPanel from 'features/tabs/components/InvokeOptionsPanel';
import BoundingBoxSettings from 'features/options/components/AdvancedOptions/Canvas/BoundingBoxSettings/BoundingBoxSettings';
import InfillAndScalingOptions from 'features/options/components/AdvancedOptions/Canvas/InfillAndScalingOptions';
import { useTranslation } from 'react-i18next';
export default function UnifiedCanvasPanel() {
const { t } = useTranslation();
const unifiedCanvasAccordions = {
boundingBox: {
header: 'Bounding Box',
header: `${t('options:boundingBoxHeader')}`,
feature: Feature.BOUNDING_BOX,
content: <BoundingBoxSettings />,
},
seamCorrection: {
header: 'Seam Correction',
header: `${t('options:seamCorrectionHeader')}`,
feature: Feature.SEAM_CORRECTION,
content: <SeamCorrectionOptions />,
},
infillAndScaling: {
header: 'Infill and Scaling',
header: `${t('options:infillScalingHeader')}`,
feature: Feature.INFILL_AND_SCALING,
content: <InfillAndScalingOptions />,
},
seed: {
header: 'Seed',
header: `${t('options:seed')}`,
feature: Feature.SEED,
content: <SeedOptions />,
},
variations: {
header: 'Variations',
header: `${t('options:variations')}`,
feature: Feature.VARIATIONS,
content: <VariationsOptions />,
additionalHeaderComponents: <GenerateVariationsToggle />,
@@ -49,7 +52,7 @@ export default function UnifiedCanvasPanel() {
<ProcessButtons />
<MainOptions />
<ImageToImageStrength
label="Image To Image Strength"
label={t('options:img2imgStrength')}
styleClass="main-option-block image-to-image-strength-main-option"
/>
<OptionsAccordion accordionInfo={unifiedCanvasAccordions} />

30
frontend/src/i18.d.ts vendored Normal file
View File

@@ -0,0 +1,30 @@
import 'i18next';
// Import All Namespaces (for the default language only)
import common from '../public/locales/common/en.json';
import unifiedcanvas from '../public/locales/unifiedcanvas/en.json';
import options from '../public/locales/options/en.json';
import gallery from '../public/locales/gallery/en.json';
import toast from '../public/locales/toast/en.json';
import hotkeys from '../public/locales/hotkeys/en.json';
import settings from '../public/locales/settings/en.json';
declare module 'i18next' {
// Extend CustomTypeOptions
interface CustomTypeOptions {
// Setting Default Namespace As English
defaultNS: 'en';
// Custom Types For Resources
resources: {
common: typeof common;
unifiedcanvas: typeof unifiedcanvas;
options: typeof options;
gallery: typeof gallery;
toast: typeof toast;
hotkeys: typeof hotkeys;
settings: typeof settings;
};
// Never Return Null
returnNull: false;
}
}

31
frontend/src/i18n.ts Normal file
View File

@@ -0,0 +1,31 @@
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';
import { initReactI18next } from 'react-i18next';
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
debug: false,
ns: [
'common',
'options',
'unifiedcanvas',
'gallery',
'toast',
'hotkeys',
'settings',
],
backend: {
loadPath: '/locales/{{ns}}/{{lng}}.json',
},
interpolation: {
escapeValue: false,
},
returnNull: false,
});
export default i18n;

View File

@@ -19,13 +19,18 @@ export const emotionCache = createCache({
// Custom Styling
import './styles/index.scss';
// Localization
import './i18n';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<Provider store={store}>
<PersistGate loading={<Loading />} persistor={persistor}>
<CacheProvider value={emotionCache}>
<ChakraProvider>
<App />
<React.Suspense fallback={<Loading />}>
<App />
</React.Suspense>
</ChakraProvider>
</CacheProvider>
</PersistGate>

View File

@@ -73,6 +73,7 @@
// Slider
--slider-color: rgb(151, 113, 255);
--slider-mark-color: rgb(151, 113, 255);
// Resizable
--resizeable-handle-border-color: var(--accent-color);

View File

@@ -71,6 +71,7 @@
// Slider
--slider-color: var(--accent-color-bright);
--slider-mark-color: var(--accent-color-bright);
// Resizable
--resizeable-handle-border-color: var(--accent-color);

View File

@@ -67,7 +67,8 @@
--switch-bg-active-color: rgb(235, 185, 5);
// Slider
--slider-color: rgb(0, 0, 0);
--slider-color: var(--accent-color);
--slider-mark-color: rgb(0, 0, 0);
// Resizable
--resizeable-handle-border-color: rgb(160, 162, 164);