feat(ui): migrate theming to chakra ui

This commit is contained in:
psychedelicious
2023-03-06 20:02:40 +11:00
parent a305b6adbf
commit 57144ac0cf
325 changed files with 6616 additions and 6246 deletions

View File

@ -1,20 +1,37 @@
import { Flex, Spinner } from '@chakra-ui/react';
import { Flex, Spinner, Text } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
interface LoaderProps {
showText?: boolean;
text?: string;
}
// This component loads before the theme so we cannot use theme tokens here
const Loading = (props: LoaderProps) => {
const { t } = useTranslation();
const { showText = false, text = t('common.loadingInvokeAI') } = props;
const Loading = () => {
return (
<Flex
width="100vw"
height="100vh"
alignItems="center"
justifyContent="center"
bg="#121212"
flexDirection="column"
rowGap={4}
>
<Spinner
thickness="2px"
speed="1s"
emptyColor="gray.200"
color="gray.400"
size="xl"
/>
<Spinner color="grey" w="5rem" h="5rem" />
{showText && (
<Text
color="grey"
fontWeight="semibold"
fontFamily="'Inter', sans-serif"
>
{text}
</Text>
)}
</Flex>
);
};

View File

@ -1,21 +0,0 @@
@use '../styles/Mixins/' as *;
svg {
fill: var(--svg-color);
}
.App {
display: grid;
width: 100vw;
height: 100vh;
background-color: var(--background-color);
}
.app-content {
display: grid;
row-gap: 1rem;
padding: $app-padding;
grid-auto-rows: min-content auto;
width: $app-width;
height: $app-height;
}

View File

@ -9,6 +9,8 @@ import useToastWatcher from 'features/system/hooks/useToastWatcher';
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons';
import { Box, Grid } from '@chakra-ui/react';
import { APP_HEIGHT, APP_PADDING, APP_WIDTH } from 'theme/util/constants';
keepGUIAlive();
@ -16,20 +18,26 @@ const App = () => {
useToastWatcher();
return (
<div className="App">
<Grid w="100vw" h="100vh">
<ImageUploader>
<ProgressBar />
<div className="app-content">
<Grid
gap={4}
p={APP_PADDING}
gridAutoRows="min-content auto"
w={APP_WIDTH}
h={APP_HEIGHT}
>
<SiteHeader />
<InvokeTabs />
</div>
<div className="app-console">
</Grid>
<Box>
<Console />
</div>
</Box>
</ImageUploader>
<FloatingParametersPanelButtons />
<FloatingGalleryButton />
</div>
</Grid>
);
};

View File

@ -0,0 +1,46 @@
import { ChakraProvider, extendTheme } from '@chakra-ui/react';
import { ReactNode, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { theme as invokeAITheme } from 'theme/theme';
import { RootState } from './store';
import { useAppSelector } from './storeHooks';
import { greenTeaThemeColors } from 'theme/colors/greenTea';
import { invokeAIThemeColors } from 'theme/colors/invokeAI';
import { lightThemeColors } from 'theme/colors/lightTheme';
import { oceanBlueColors } from 'theme/colors/oceanBlue';
type ThemeLocaleProviderProps = {
children: ReactNode;
};
const THEMES = {
dark: invokeAIThemeColors,
light: lightThemeColors,
green: greenTeaThemeColors,
ocean: oceanBlueColors,
};
function ThemeLocaleProvider({ children }: ThemeLocaleProviderProps) {
const { i18n } = useTranslation();
const currentTheme = useAppSelector(
(state: RootState) => state.ui.currentTheme
);
const direction = i18n.dir();
const theme = extendTheme({
...invokeAITheme,
colors: THEMES[currentTheme as keyof typeof THEMES],
direction,
});
useEffect(() => {
document.body.dir = direction;
}, [direction]);
return <ChakraProvider theme={theme}>{children}</ChakraProvider>;
}
export default ThemeLocaleProvider;

View File

@ -392,7 +392,7 @@ const makeSocketIOListeners = (
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `${i18n.t(
'modelmanager:modelAdded'
'modelManager.modelAdded'
)}: ${deleted_model_name}`,
level: 'info',
})
@ -400,7 +400,7 @@ const makeSocketIOListeners = (
dispatch(
addToast({
title: `${i18n.t(
'modelmanager:modelEntryDeleted'
'modelManager.modelEntryDeleted'
)}: ${deleted_model_name}`,
status: 'success',
duration: 2500,
@ -424,7 +424,7 @@ const makeSocketIOListeners = (
dispatch(
addToast({
title: `${i18n.t(
'modelmanager:modelConverted'
'modelManager.modelConverted'
)}: ${converted_model_name}`,
status: 'success',
duration: 2500,

View File

@ -1,52 +0,0 @@
import { extendTheme } from '@chakra-ui/react';
import type { StyleFunctionProps } from '@chakra-ui/styled-system';
export const theme = extendTheme({
config: {
initialColorMode: 'dark',
useSystemColorMode: false,
},
components: {
Tooltip: {
baseStyle: (props: StyleFunctionProps) => ({
textColor: props.colorMode === 'dark' ? 'gray.800' : 'gray.100',
}),
},
Accordion: {
baseStyle: (props: StyleFunctionProps) => ({
button: {
fontWeight: 'bold',
_hover: {
bgColor:
props.colorMode === 'dark'
? 'rgba(255,255,255,0.05)'
: 'rgba(0,0,0,0.05)',
},
},
panel: {
paddingBottom: 2,
},
}),
},
FormLabel: {
baseStyle: {
fontWeight: 'light',
},
},
Button: {
variants: {
imageHoverIconButton: (props: StyleFunctionProps) => ({
bg: props.colorMode === 'dark' ? 'blackAlpha.700' : 'whiteAlpha.800',
color:
props.colorMode === 'dark' ? 'whiteAlpha.700' : 'blackAlpha.700',
_hover: {
bg:
props.colorMode === 'dark' ? 'blackAlpha.800' : 'whiteAlpha.800',
color:
props.colorMode === 'dark' ? 'whiteAlpha.900' : 'blackAlpha.900',
},
}),
},
},
},
});

View File

@ -1,20 +0,0 @@
.guide-popover-arrow {
background-color: var(--tab-panel-bg);
box-shadow: none;
}
.guide-popover-content {
background-color: var(--background-color-secondary);
border: none;
}
.guide-popover-guide-content {
background: var(--tab-panel-bg);
border: 2px solid var(--tab-hover-color);
border-radius: 0.4rem;
padding: 0.75rem 1rem 0.75rem 1rem;
display: grid;
grid-template-rows: repeat(auto-fill, 1fr);
grid-row-gap: 0.5rem;
justify-content: space-between;
}

View File

@ -2,6 +2,7 @@ import {
Box,
Popover,
PopoverArrow,
PopoverBody,
PopoverContent,
PopoverTrigger,
} from '@chakra-ui/react';
@ -34,13 +35,12 @@ const GuidePopover = ({ children, feature }: GuideProps) => {
<Box>{children}</Box>
</PopoverTrigger>
<PopoverContent
className="guide-popover-content"
maxWidth="400px"
onClick={(e) => e.preventDefault()}
cursor="initial"
>
<PopoverArrow className="guide-popover-arrow" />
<div className="guide-popover-guide-content">{text}</div>
<PopoverArrow />
<PopoverBody>{text}</PopoverBody>
</PopoverContent>
</Popover>
);

View File

@ -5,11 +5,11 @@ import {
AlertDialogFooter,
AlertDialogHeader,
AlertDialogOverlay,
Button,
forwardRef,
useDisclosure,
} from '@chakra-ui/react';
import { cloneElement, ReactElement, ReactNode, useRef } from 'react';
import IAIButton from './IAIButton';
type Props = {
acceptButtonText?: string;
@ -58,7 +58,7 @@ const IAIAlertDialog = forwardRef((props: Props, ref) => {
onClose={onClose}
>
<AlertDialogOverlay>
<AlertDialogContent className="modal">
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
{title}
</AlertDialogHeader>
@ -66,16 +66,12 @@ const IAIAlertDialog = forwardRef((props: Props, ref) => {
<AlertDialogBody>{children}</AlertDialogBody>
<AlertDialogFooter>
<Button
ref={cancelRef}
onClick={handleCancel}
className="modal-close-btn"
>
<IAIButton ref={cancelRef} onClick={handleCancel}>
{cancelButtonText}
</Button>
<Button colorScheme="red" onClick={handleAccept} ml={3}>
</IAIButton>
<IAIButton colorScheme="error" onClick={handleAccept} ml={3}>
{acceptButtonText}
</Button>
</IAIButton>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>

View File

@ -1,8 +0,0 @@
.invokeai__button {
background-color: var(--btn-base-color);
place-content: center;
&:hover {
background-color: var(--btn-base-color-hover);
}
}

View File

@ -10,19 +10,15 @@ import { ReactNode } from 'react';
export interface IAIButtonProps extends ButtonProps {
tooltip?: string;
tooltipProps?: Omit<TooltipProps, 'children'>;
styleClass?: string;
isChecked?: boolean;
children: ReactNode;
}
const IAIButton = forwardRef((props: IAIButtonProps, forwardedRef) => {
const { children, tooltip = '', tooltipProps, styleClass, ...rest } = props;
const { children, tooltip = '', tooltipProps, isChecked, ...rest } = props;
return (
<Tooltip label={tooltip} {...tooltipProps}>
<Button
ref={forwardedRef}
className={['invokeai__button', styleClass].join(' ')}
{...rest}
>
<Button ref={forwardedRef} aria-checked={isChecked} {...rest}>
{children}
</Button>
</Tooltip>

View File

@ -1,26 +0,0 @@
.invokeai__checkbox {
.chakra-checkbox__label {
margin-top: 1px;
color: var(--text-color-secondary);
font-size: 0.9rem;
}
.chakra-checkbox__control {
width: 1rem;
height: 1rem;
border: none;
border-radius: 0.2rem;
background-color: var(--input-checkbox-bg);
svg {
width: 0.6rem;
height: 0.6rem;
stroke-width: 3px;
}
&[data-checked] {
color: var(--text-color);
background-color: var(--input-checkbox-checked-bg);
}
}
}

View File

@ -3,13 +3,12 @@ import type { ReactNode } from 'react';
type IAICheckboxProps = CheckboxProps & {
label: string | ReactNode;
styleClass?: string;
};
const IAICheckbox = (props: IAICheckboxProps) => {
const { label, styleClass, ...rest } = props;
const { label, ...rest } = props;
return (
<Checkbox className={`invokeai__checkbox ${styleClass}`} {...rest}>
<Checkbox colorScheme="accent" {...rest}>
{label}
</Checkbox>
);

View File

@ -1,8 +0,0 @@
.invokeai__color-picker {
.react-colorful__hue-pointer,
.react-colorful__saturation-pointer {
width: 1.5rem;
height: 1.5rem;
border-color: var(--white);
}
}

View File

@ -1,16 +1,35 @@
import { chakra, ChakraProps } from '@chakra-ui/react';
import { RgbaColorPicker } from 'react-colorful';
import { ColorPickerBaseProps, RgbaColor } from 'react-colorful/dist/types';
type IAIColorPickerProps = ColorPickerBaseProps<RgbaColor> & {
styleClass?: string;
type IAIColorPickerProps = Omit<ColorPickerBaseProps<RgbaColor>, 'color'> &
ChakraProps & {
pickerColor: RgbaColor;
styleClass?: string;
};
const ChakraRgbaColorPicker = chakra(RgbaColorPicker, {
baseStyle: { paddingInline: 4 },
shouldForwardProp: (prop) => !['pickerColor'].includes(prop),
});
const colorPickerStyles: NonNullable<ChakraProps['sx']> = {
width: 6,
height: 6,
borderColor: 'base.100',
};
const IAIColorPicker = (props: IAIColorPickerProps) => {
const { styleClass, ...rest } = props;
const { styleClass = '', ...rest } = props;
return (
<RgbaColorPicker
className={`invokeai__color-picker ${styleClass}`}
<ChakraRgbaColorPicker
sx={{
'.react-colorful__hue-pointer': colorPickerStyles,
'.react-colorful__saturation-pointer': colorPickerStyles,
'.react-colorful__alpha-pointer': colorPickerStyles,
}}
className={styleClass}
{...rest}
/>
);

View File

@ -1,82 +0,0 @@
@use '../../styles/Mixins/' as *;
.invokeai__icon-button {
background: var(--btn-base-color);
cursor: pointer;
&:hover {
background-color: var(--btn-base-color-hover);
}
&[data-selected='true'] {
background-color: var(--accent-color);
&:hover {
background-color: var(--accent-color-hover);
}
}
&[disabled] {
cursor: not-allowed;
}
&[data-variant='link'] {
background: none;
&:hover {
background: none;
}
}
// Check Box Style
&[data-as-checkbox='true'] {
background-color: var(--btn-base-color);
border: 3px solid var(--btn-base-color);
svg {
fill: var(--text-color);
}
&:hover {
background-color: var(--btn-base-color);
border-color: var(--btn-checkbox-border-hover);
svg {
fill: var(--text-color);
}
}
&[data-selected='true'] {
border-color: var(--accent-color);
svg {
fill: var(--accent-color-hover);
}
&:hover {
svg {
fill: var(--accent-color-hover);
}
}
}
}
&[data-alert='true'] {
animation-name: pulseColor;
animation-duration: 1s;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
&:hover {
animation: none;
background-color: var(--accent-color-hover);
}
}
}
@keyframes pulseColor {
0% {
background-color: var(--accent-color);
}
50% {
background-color: var(--accent-color-dim);
}
100% {
background-color: var(--accent-color);
}
}

View File

@ -7,22 +7,13 @@ import {
} from '@chakra-ui/react';
export type IAIIconButtonProps = IconButtonProps & {
styleClass?: string;
tooltip?: string;
tooltipProps?: Omit<TooltipProps, 'children'>;
asCheckbox?: boolean;
isChecked?: boolean;
};
const IAIIconButton = forwardRef((props: IAIIconButtonProps, forwardedRef) => {
const {
tooltip = '',
styleClass,
tooltipProps,
asCheckbox,
isChecked,
...rest
} = props;
const { tooltip = '', tooltipProps, isChecked, ...rest } = props;
return (
<Tooltip
@ -35,13 +26,7 @@ const IAIIconButton = forwardRef((props: IAIIconButtonProps, forwardedRef) => {
>
<IconButton
ref={forwardedRef}
className={
styleClass
? `invokeai__icon-button ${styleClass}`
: `invokeai__icon-button`
}
data-as-checkbox={asCheckbox}
data-selected={isChecked !== undefined ? isChecked : undefined}
aria-checked={isChecked !== undefined ? isChecked : undefined}
{...rest}
/>
</Tooltip>

View File

@ -1,33 +0,0 @@
.input {
display: grid;
grid-template-columns: max-content auto;
column-gap: 1rem;
align-items: center;
.input-label {
color: var(--text-color-secondary);
}
.input-entry {
background-color: var(--background-color-secondary);
border: 2px solid var(--border-color);
border-radius: 0.2rem;
font-weight: bold;
&:focus {
outline: none;
border: 2px solid var(--input-border-color);
box-shadow: 0 0 10px 0 var(--input-box-shadow-color);
}
&:disabled {
opacity: 0.2;
}
&[aria-invalid='true'] {
outline: none;
border: 2px solid var(--border-color-invalid);
box-shadow: 0 0 10px 0 var(--box-shadow-color-invalid);
}
}
}

View File

@ -1,47 +1,37 @@
import { FormControl, FormLabel, Input, InputProps } from '@chakra-ui/react';
import {
FormControl,
FormControlProps,
FormLabel,
Input,
InputProps,
} from '@chakra-ui/react';
import { ChangeEvent } from 'react';
interface IAIInputProps extends InputProps {
styleClass?: string;
label?: string;
width?: string | number;
value?: string;
size?: string;
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
formControlProps?: Omit<FormControlProps, 'isInvalid' | 'isDisabled'>;
}
export default function IAIInput(props: IAIInputProps) {
const {
label = '',
styleClass,
isDisabled = false,
fontSize = 'sm',
width,
size = 'sm',
isInvalid,
formControlProps,
...rest
} = props;
return (
<FormControl
className={`input ${styleClass}`}
isInvalid={isInvalid}
isDisabled={isDisabled}
{...formControlProps}
>
{label !== '' && (
<FormLabel
fontSize={fontSize}
fontWeight="bold"
alignItems="center"
whiteSpace="nowrap"
marginBottom={0}
marginRight={0}
className="input-label"
>
{label}
</FormLabel>
)}
<Input {...rest} className="input-entry" size={size} width={width} />
{label !== '' && <FormLabel>{label}</FormLabel>}
<Input {...rest} />
</FormControl>
);
}

View File

@ -1,66 +0,0 @@
.invokeai__number-input-form-control {
display: flex;
align-items: center;
column-gap: 1rem;
.invokeai__number-input-form-label {
color: var(--text-color-secondary);
&[data-focus] + .invokeai__number-input-root {
outline: none;
border: 2px solid var(--input-border-color);
box-shadow: 0 0 10px 0 var(--input-box-shadow-color);
}
&[aria-invalid='true'] + .invokeai__number-input-root {
outline: none;
border: 2px solid var(--border-color-invalid);
box-shadow: 0 0 10px 0 var(--box-shadow-color-invalid);
}
}
.invokeai__number-input-root {
height: 2rem;
display: grid;
grid-template-columns: auto max-content;
column-gap: 0.5rem;
align-items: center;
background-color: var(--background-color-secondary);
border: 2px solid var(--border-color);
border-radius: 0.3rem;
}
.invokeai__number-input-field {
border: none;
font-weight: bold;
width: 100%;
height: auto;
font-size: 0.9rem;
padding: 0 0.5rem;
&:focus {
outline: none;
box-shadow: none;
}
&:disabled {
opacity: 0.2;
}
}
.invokeai__number-input-stepper {
display: grid;
padding-right: 0.5rem;
.invokeai__number-input-stepper-button {
border: none;
// expand arrow hitbox
padding: 0 0.5rem;
margin: 0 -0.5rem;
svg {
width: 10px;
height: 10px;
}
}
}
}

View File

@ -9,6 +9,7 @@ import {
NumberInputField,
NumberInputFieldProps,
NumberInputProps,
NumberInputStepper,
NumberInputStepperProps,
Tooltip,
TooltipProps,
@ -20,10 +21,7 @@ import { FocusEvent, useEffect, useState } from 'react';
const numberStringRegex = /^-?(0\.)?\.?$/;
interface Props extends Omit<NumberInputProps, 'onChange'> {
styleClass?: string;
label?: string;
labelFontSize?: string | number;
width?: string | number;
showStepper?: boolean;
value?: number;
onChange: (v: number) => void;
@ -45,12 +43,8 @@ interface Props extends Omit<NumberInputProps, 'onChange'> {
const IAINumberInput = (props: Props) => {
const {
label,
labelFontSize = 'sm',
styleClass,
isDisabled = false,
showStepper = true,
width,
textAlign,
isInvalid,
value,
onChange,
@ -119,29 +113,10 @@ const IAINumberInput = (props: Props) => {
<FormControl
isDisabled={isDisabled}
isInvalid={isInvalid}
className={
styleClass
? `invokeai__number-input-form-control ${styleClass}`
: `invokeai__number-input-form-control`
}
{...formControlProps}
>
{label && (
<FormLabel
className="invokeai__number-input-form-label"
style={{ display: label ? 'block' : 'none' }}
fontSize={labelFontSize}
fontWeight="bold"
marginRight={0}
marginBottom={0}
whiteSpace="nowrap"
{...formLabelProps}
>
{label}
</FormLabel>
)}
{label && <FormLabel {...formLabelProps}>{label}</FormLabel>}
<NumberInput
className="invokeai__number-input-root"
value={valueAsString}
min={min}
max={max}
@ -149,25 +124,14 @@ const IAINumberInput = (props: Props) => {
clampValueOnBlur={false}
onChange={handleOnChange}
onBlur={handleBlur}
width={width}
{...rest}
>
<NumberInputField
className="invokeai__number-input-field"
textAlign={textAlign}
{...numberInputFieldProps}
/>
<NumberInputField {...numberInputFieldProps} />
{showStepper && (
<div className="invokeai__number-input-stepper">
<NumberIncrementStepper
{...numberInputStepperProps}
className="invokeai__number-input-stepper-button"
/>
<NumberDecrementStepper
{...numberInputStepperProps}
className="invokeai__number-input-stepper-button"
/>
</div>
<NumberInputStepper>
<NumberIncrementStepper {...numberInputStepperProps} />
<NumberDecrementStepper {...numberInputStepperProps} />
</NumberInputStepper>
)}
</NumberInput>
</FormControl>

View File

@ -1,12 +0,0 @@
.invokeai__popover-content {
min-width: unset;
width: unset;
padding: 1rem;
border-radius: 0.5rem;
background-color: var(--background-color);
border: 2px solid var(--border-color);
.invokeai__popover-arrow {
background-color: var(--background-color) !important;
}
}

View File

@ -12,7 +12,6 @@ type IAIPopoverProps = PopoverProps & {
triggerComponent: ReactNode;
triggerContainerProps?: BoxProps;
children: ReactNode;
styleClass?: string;
hasArrow?: boolean;
};
@ -20,16 +19,16 @@ const IAIPopover = (props: IAIPopoverProps) => {
const {
triggerComponent,
children,
styleClass,
hasArrow = true,
isLazy = true,
...rest
} = props;
return (
<Popover {...rest}>
<Popover isLazy={isLazy} {...rest}>
<PopoverTrigger>{triggerComponent}</PopoverTrigger>
<PopoverContent className={`invokeai__popover-content ${styleClass}`}>
{hasArrow && <PopoverArrow className="invokeai__popover-arrow" />}
<PopoverContent>
{hasArrow && <PopoverArrow />}
{children}
</PopoverContent>
</Popover>

View File

@ -1,31 +0,0 @@
@use '../../styles/Mixins/' as *;
.invokeai__select {
display: flex;
column-gap: 1rem;
align-items: center;
.invokeai__select-label {
color: var(--text-color-secondary);
}
.invokeai__select-picker {
border: 2px solid var(--border-color);
background-color: var(--background-color-secondary);
font-weight: bold;
font-size: 0.9rem;
height: 2rem;
border-radius: 0.2rem;
&:focus {
outline: none;
border: 2px solid var(--input-border-color);
box-shadow: 0 0 10px 0 var(--input-box-shadow-color);
}
}
.invokeai__select-option {
background-color: var(--background-color-secondary);
color: var(--text-color-secondary);
}
}

View File

@ -10,7 +10,6 @@ import { MouseEvent } from 'react';
type IAISelectProps = SelectProps & {
label?: string;
styleClass?: string;
tooltip?: string;
tooltipProps?: Omit<TooltipProps, 'children'>;
validValues:
@ -21,21 +20,11 @@ type IAISelectProps = SelectProps & {
* Customized Chakra FormControl + Select multi-part component.
*/
const IAISelect = (props: IAISelectProps) => {
const {
label,
isDisabled,
validValues,
tooltip,
tooltipProps,
size = 'sm',
fontSize = 'sm',
styleClass,
...rest
} = props;
const { label, isDisabled, validValues, tooltip, tooltipProps, ...rest } =
props;
return (
<FormControl
isDisabled={isDisabled}
className={`invokeai__select ${styleClass}`}
onClick={(e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
@ -43,36 +32,16 @@ const IAISelect = (props: IAISelectProps) => {
e.nativeEvent.cancelBubble = true;
}}
>
{label && (
<FormLabel
className="invokeai__select-label"
fontSize={fontSize}
fontWeight="bold"
marginRight={0}
marginBottom={0}
whiteSpace="nowrap"
>
{label}
</FormLabel>
)}
{label && <FormLabel>{label}</FormLabel>}
<Tooltip label={tooltip} {...tooltipProps}>
<Select
className="invokeai__select-picker"
fontSize={fontSize}
size={size}
{...rest}
>
<Select {...rest}>
{validValues.map((opt) => {
return typeof opt === 'string' || typeof opt === 'number' ? (
<option key={opt} value={opt} className="invokeai__select-option">
<option key={opt} value={opt}>
{opt}
</option>
) : (
<option
key={opt.value}
value={opt.value}
className="invokeai__select-option"
>
<option key={opt.value} value={opt.value}>
{opt.key}
</option>
);

View File

@ -4,14 +4,15 @@ import {
MenuItem,
MenuList,
MenuProps,
MenuButtonProps,
MenuListProps,
MenuItemProps,
IconButton,
Button,
IconButtonProps,
ButtonProps,
} from '@chakra-ui/react';
import { MouseEventHandler, ReactNode } from 'react';
import { MdArrowDropDown, MdArrowDropUp } from 'react-icons/md';
import IAIButton from './IAIButton';
import IAIIconButton from './IAIIconButton';
interface IAIMenuItem {
item: ReactNode | string;
@ -22,9 +23,10 @@ interface IAIMenuProps {
menuType?: 'icon' | 'regular';
buttonText?: string;
iconTooltip?: string;
isLazy?: boolean;
menuItems: IAIMenuItem[];
menuProps?: MenuProps;
menuButtonProps?: MenuButtonProps;
menuButtonProps?: IconButtonProps | ButtonProps;
menuListProps?: MenuListProps;
menuItemProps?: MenuItemProps;
}
@ -34,6 +36,7 @@ export default function IAISimpleMenu(props: IAIMenuProps) {
menuType = 'icon',
iconTooltip,
buttonText,
isLazy = true,
menuItems,
menuProps,
menuButtonProps,
@ -48,13 +51,7 @@ export default function IAISimpleMenu(props: IAIMenuProps) {
<MenuItem
key={index}
onClick={menuItem.onClick}
fontSize="0.9rem"
color="var(--text-color-secondary)"
backgroundColor="var(--background-color-secondary)"
_focus={{
color: 'var(--text-color)',
backgroundColor: 'var(--border-color)',
}}
fontSize="sm"
{...menuItemProps}
>
{menuItem.item}
@ -65,34 +62,20 @@ export default function IAISimpleMenu(props: IAIMenuProps) {
};
return (
<Menu {...menuProps}>
<Menu {...menuProps} isLazy={isLazy}>
{({ isOpen }) => (
<>
<MenuButton
as={menuType === 'icon' ? IAIIconButton : IAIButton}
as={menuType === 'icon' ? IconButton : Button}
tooltip={iconTooltip}
icon={isOpen ? <MdArrowDropUp /> : <MdArrowDropDown />}
padding={menuType === 'regular' ? '0 0.5rem' : 0}
backgroundColor="var(--btn-base-color)"
_hover={{
backgroundColor: 'var(--btn-base-color-hover)',
}}
minWidth="1rem"
minHeight="1rem"
fontSize="1.5rem"
paddingX={0}
paddingY={menuType === 'regular' ? 2 : 0}
{...menuButtonProps}
>
{menuType === 'regular' && buttonText}
</MenuButton>
<MenuList
zIndex={15}
padding={0}
borderRadius="0.5rem"
backgroundColor="var(--background-color-secondary)"
color="var(--text-color-secondary)"
borderColor="var(--border-color)"
{...menuListProps}
>
<MenuList zIndex={15} padding={0} {...menuListProps}>
{renderMenuItems()}
</MenuList>
</>

View File

@ -1,60 +0,0 @@
.invokeai__slider-component {
padding-bottom: 0.5rem;
border-radius: 0.5rem;
.invokeai__slider-component-label {
min-width: max-content;
margin: 0;
font-weight: bold;
color: var(--text-color-secondary);
}
.invokeai__slider_track {
background-color: var(--tab-color);
}
.invokeai__slider_track-filled {
background-color: var(--slider-color);
}
.invokeai__slider-thumb {
width: 4px;
}
.invokeai__slider-mark {
font-size: 0.75rem;
font-weight: bold;
color: var(--slider-mark-color);
margin-top: 0.3rem;
}
.invokeai__slider-number-input {
border: none;
font-size: 0.9rem;
font-weight: bold;
height: 2rem;
background-color: var(--background-color-secondary);
border: 2px solid var(--border-color);
&:focus {
outline: none;
box-shadow: none;
border: 2px solid var(--input-border-color);
box-shadow: 0 0 10px 0 var(--input-box-shadow-color);
}
&:disabled {
opacity: 0.2;
}
}
.invokeai__slider-number-stepper {
border: none;
}
&[data-markers='true'] {
.invokeai__slider_container {
margin-top: -1rem;
}
}
}

View File

@ -37,11 +37,8 @@ export type IAIFullSliderProps = {
step?: number;
onChange: (v: number) => void;
withSliderMarks?: boolean;
sliderMarkLeftOffset?: number;
sliderMarkRightOffset?: number;
withInput?: boolean;
isInteger?: boolean;
width?: string | number;
inputWidth?: string | number;
inputReadOnly?: boolean;
withReset?: boolean;
@ -52,7 +49,6 @@ export type IAIFullSliderProps = {
tooltipSuffix?: string;
hideTooltip?: boolean;
isCompact?: boolean;
styleClass?: string;
sliderFormControlProps?: FormControlProps;
sliderFormLabelProps?: FormLabelProps;
sliderMarkProps?: Omit<SliderMarkProps, 'value'>;
@ -74,14 +70,11 @@ export default function IAISlider(props: IAIFullSliderProps) {
max = 100,
step = 1,
onChange,
width = '100%',
tooltipSuffix = '',
withSliderMarks = false,
sliderMarkLeftOffset = 0,
sliderMarkRightOffset = -1,
withInput = false,
isInteger = false,
inputWidth = '5.5rem',
inputWidth = 16,
inputReadOnly = false,
withReset = false,
hideTooltip = false,
@ -90,7 +83,6 @@ export default function IAISlider(props: IAIFullSliderProps) {
isResetDisabled,
isSliderDisabled,
isInputDisabled,
styleClass,
sliderFormControlProps,
sliderFormLabelProps,
sliderMarkProps,
@ -142,19 +134,13 @@ export default function IAISlider(props: IAIFullSliderProps) {
return (
<FormControl
className={
styleClass
? `invokeai__slider-component ${styleClass}`
: `invokeai__slider-component`
}
data-markers={withSliderMarks}
style={
sx={
isCompact
? {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
columnGap: '1rem',
columnGap: 4,
margin: 0,
padding: 0,
}
@ -162,11 +148,7 @@ export default function IAISlider(props: IAIFullSliderProps) {
}
{...sliderFormControlProps}
>
<FormLabel
className="invokeai__slider-component-label"
fontSize="sm"
{...sliderFormLabelProps}
>
<FormLabel {...sliderFormLabelProps} mb={-1}>
{label}
</FormLabel>
@ -182,23 +164,23 @@ export default function IAISlider(props: IAIFullSliderProps) {
onMouseLeave={() => setShowTooltip(false)}
focusThumbOnChange={false}
isDisabled={isSliderDisabled}
width={width}
// width={width}
{...rest}
>
{withSliderMarks && (
<>
<SliderMark
value={min}
className="invokeai__slider-mark invokeai__slider-mark-start"
ml={sliderMarkLeftOffset}
insetInlineStart={0}
sx={{ insetInlineStart: 'unset !important' }}
{...sliderMarkProps}
>
{min}
</SliderMark>
<SliderMark
value={max}
className="invokeai__slider-mark invokeai__slider-mark-end"
ml={sliderMarkRightOffset}
insetInlineEnd={0}
sx={{ insetInlineStart: 'unset !important' }}
{...sliderMarkProps}
>
{max}
@ -206,23 +188,19 @@ export default function IAISlider(props: IAIFullSliderProps) {
</>
)}
<SliderTrack className="invokeai__slider_track" {...sliderTrackProps}>
<SliderFilledTrack className="invokeai__slider_track-filled" />
<SliderTrack {...sliderTrackProps}>
<SliderFilledTrack />
</SliderTrack>
<Tooltip
hasArrow
className="invokeai__slider-component-tooltip"
placement="top"
isOpen={showTooltip}
label={`${value}${tooltipSuffix}`}
hidden={hideTooltip}
{...sliderTooltipProps}
>
<SliderThumb
className="invokeai__slider-thumb"
{...sliderThumbProps}
/>
<SliderThumb {...sliderThumbProps} />
</Tooltip>
</Slider>
@ -234,13 +212,10 @@ export default function IAISlider(props: IAIFullSliderProps) {
value={localInputValue}
onChange={handleInputChange}
onBlur={handleInputBlur}
className="invokeai__slider-number-field"
isDisabled={isInputDisabled}
{...sliderNumberInputProps}
>
<NumberInputField
className="invokeai__slider-number-input"
width={inputWidth}
readOnly={inputReadOnly}
minWidth={inputWidth}
{...sliderNumberInputFieldProps}
@ -248,11 +223,9 @@ export default function IAISlider(props: IAIFullSliderProps) {
<NumberInputStepper {...sliderNumberInputStepperProps}>
<NumberIncrementStepper
onClick={() => onChange(Number(localInputValue))}
className="invokeai__slider-number-stepper"
/>
<NumberDecrementStepper
onClick={() => onChange(Number(localInputValue))}
className="invokeai__slider-number-stepper"
/>
</NumberInputStepper>
</NumberInput>

View File

@ -1,24 +0,0 @@
.invokeai__switch-form-control {
.invokeai__switch-form-label {
color: var(--text-color-secondary);
}
.invokeai__switch-root {
span {
background-color: var(--switch-bg-color);
span {
background-color: var(--white);
}
}
&[data-checked] {
span {
background: var(--switch-bg-active-color);
span {
background-color: var(--white);
}
}
}
}
}

View File

@ -10,7 +10,6 @@ import {
interface Props extends SwitchProps {
label?: string;
width?: string | number;
styleClass?: string;
formControlProps?: FormControlProps;
formLabelProps?: FormLabelProps;
}
@ -25,34 +24,22 @@ const IAISwitch = (props: Props) => {
width = 'auto',
formControlProps,
formLabelProps,
styleClass,
...rest
} = props;
return (
<FormControl
isDisabled={isDisabled}
width={width}
className={`invokeai__switch-form-control ${styleClass}`}
display="flex"
columnGap="1rem"
gap={4}
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}
>
<FormLabel my={1} {...formLabelProps}>
{label}
</FormLabel>
<Switch className="invokeai__switch-root" {...rest} />
<Switch {...rest} />
</FormControl>
);
};

View File

@ -1,4 +1,4 @@
import { Heading } from '@chakra-ui/react';
import { Box, Flex, Heading } from '@chakra-ui/react';
import { useHotkeys } from 'react-hotkeys-hook';
type ImageUploadOverlayProps = {
@ -11,7 +11,7 @@ type ImageUploadOverlayProps = {
const ImageUploadOverlay = (props: ImageUploadOverlayProps) => {
const {
isDragAccept,
isDragReject,
isDragReject: _isDragAccept,
overlaySecondaryText,
setIsHandlingUpload,
} = props;
@ -21,19 +21,42 @@ const ImageUploadOverlay = (props: ImageUploadOverlayProps) => {
});
return (
<div className="dropzone-container">
{isDragAccept && (
<div className="dropzone-overlay is-drag-accept">
<Box
sx={{
position: 'absolute',
top: 0,
insetInlineStart: 0,
width: '100vw',
height: '100vh',
zIndex: 999,
backdropFilter: 'blur(20px)',
}}
>
<Flex
sx={{
opacity: 0.4,
width: '100%',
height: '100%',
flexDirection: 'column',
rowGap: 4,
alignItems: 'center',
justifyContent: 'center',
bg: 'base.900',
boxShadow: `inset 0 0 20rem 1rem var(--invokeai-colors-${
isDragAccept ? 'accent' : 'error'
}-500)`,
}}
>
{isDragAccept ? (
<Heading size="lg">Upload Image{overlaySecondaryText}</Heading>
</div>
)}
{isDragReject && (
<div className="dropzone-overlay is-drag-reject">
<Heading size="lg">Invalid Upload</Heading>
<Heading size="md">Must be single JPEG or PNG image</Heading>
</div>
)}
</div>
) : (
<>
<Heading size="lg">Invalid Upload</Heading>
<Heading size="md">Must be single JPEG or PNG image</Heading>
</>
)}
</Flex>
</Box>
);
};
export default ImageUploadOverlay;

View File

@ -1,74 +0,0 @@
.dropzone-container {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 999;
backdrop-filter: blur(20px);
.dropzone-overlay {
opacity: 0.5;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
row-gap: 1rem;
align-items: center;
justify-content: center;
background-color: var(--background-color);
&.is-drag-accept {
box-shadow: inset 0 0 20rem 1rem var(--accent-color);
}
&.is-drag-reject {
box-shadow: inset 0 0 20rem 1rem var(--status-bad-color);
}
&.is-handling-upload {
box-shadow: inset 0 0 20rem 1rem var(--status-working-color);
}
}
}
.image-uploader-button-outer {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 0.5rem;
color: var(--tab-list-text-inactive);
background-color: var(--background-color);
&:hover {
background-color: var(--background-color-light);
}
}
.image-upload-button-inner {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.image-upload-button {
display: flex;
flex-direction: column;
row-gap: 2rem;
align-items: center;
justify-content: center;
text-align: center;
svg {
width: 4rem;
height: 4rem;
}
h2 {
font-size: 1.2rem;
}
}

View File

@ -1,4 +1,4 @@
import { useToast } from '@chakra-ui/react';
import { Box, useToast } from '@chakra-ui/react';
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import useImageUploader from 'common/hooks/useImageUploader';
@ -139,7 +139,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
return (
<ImageUploaderTriggerContext.Provider value={open}>
<div
<Box
{...getRootProps({ style: {} })}
onKeyDown={(e: KeyboardEvent) => {
// Bail out if user hits spacebar - do not open the uploader
@ -156,7 +156,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
setIsHandlingUpload={setIsHandlingUpload}
/>
)}
</div>
</Box>
</ImageUploaderTriggerContext.Provider>
);
};

View File

@ -1,4 +1,4 @@
import { Heading } from '@chakra-ui/react';
import { Flex, Heading, Icon } from '@chakra-ui/react';
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
import { useContext } from 'react';
import { FaUpload } from 'react-icons/fa';
@ -16,15 +16,38 @@ const ImageUploaderButton = (props: ImageUploaderButtonProps) => {
};
return (
<div
className={`image-uploader-button-outer ${styleClass}`}
onClick={handleClickUpload}
<Flex
sx={{
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center',
}}
className={styleClass}
>
<div className="image-upload-button">
<FaUpload />
<Heading size="lg">Click or Drag and Drop</Heading>
</div>
</div>
<Flex
onClick={handleClickUpload}
sx={{
display: 'flex',
flexDirection: 'column',
rowGap: 8,
p: 8,
borderRadius: 'base',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
cursor: 'pointer',
color: 'base.600',
bg: 'base.800',
_hover: {
bg: 'base.700',
},
}}
>
<Icon as={FaUpload} boxSize={24} />
<Heading size="md">Click or Drag and Drop</Heading>
</Flex>
</Flex>
);
};

View File

@ -1,55 +0,0 @@
import { Box } from '@chakra-ui/react';
interface SubItemHookProps {
active?: boolean;
width?: string | number;
height?: string | number;
side?: 'left' | 'right';
}
export default function SubItemHook(props: SubItemHookProps) {
const {
active = true,
width = '1rem',
height = '1.3rem',
side = 'right',
} = props;
return (
<>
{side === 'right' && (
<Box
width={width}
height={height}
margin="-0.5rem 0.5rem 0 0.5rem"
borderLeft={
active
? '3px solid var(--subhook-color)'
: '3px solid var(--tab-hover-color)'
}
borderBottom={
active
? '3px solid var(--subhook-color)'
: '3px solid var(--tab-hover-color)'
}
/>
)}
{side === 'left' && (
<Box
width={width}
height={height}
margin="-0.5rem 0.5rem 0 0.5rem"
borderRight={
active
? '3px solid var(--subhook-color)'
: '3px solid var(--tab-hover-color)'
}
borderBottom={
active
? '3px solid var(--subhook-color)'
: '3px solid var(--tab-hover-color)'
}
/>
)}
</>
);
}

View File

@ -1,11 +1,27 @@
import { Flex, Heading, Text, VStack } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import WorkInProgress from './WorkInProgress';
export default function NodesWIP() {
const { t } = useTranslation();
return (
<div className="work-in-progress nodes-work-in-progress">
<h1>{t('common.nodes')}</h1>
<p>{t('common.nodesDesc')}</p>
</div>
<WorkInProgress>
<Flex
sx={{
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
w: '100%',
h: '100%',
gap: 4,
textAlign: 'center',
}}
>
<Heading>{t('common.nodes')}</Heading>
<VStack maxW="50rem" gap={4}>
<Text>{t('common.nodesDesc')}</Text>
</VStack>
</Flex>
</WorkInProgress>
);
}

View File

@ -1,13 +1,29 @@
import { Flex, Heading, Text, VStack } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import WorkInProgress from './WorkInProgress';
export const PostProcessingWIP = () => {
const { t } = useTranslation();
return (
<div className="work-in-progress post-processing-work-in-progress">
<h1>{t('common.postProcessing')}</h1>
<p>{t('common.postProcessDesc1')}</p>
<p>{t('common.postProcessDesc2')}</p>
<p>{t('common.postProcessDesc3')}</p>
</div>
<WorkInProgress>
<Flex
sx={{
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
w: '100%',
h: '100%',
gap: 4,
textAlign: 'center',
}}
>
<Heading>{t('common.postProcessing')}</Heading>
<VStack maxW="50rem" gap={4}>
<Text>{t('common.postProcessDesc1')}</Text>
<Text>{t('common.postProcessDesc2')}</Text>
<Text>{t('common.postProcessDesc3')}</Text>
</VStack>
</Flex>
</WorkInProgress>
);
};

View File

@ -1,16 +1,28 @@
import { Flex, Heading, Text, VStack } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import WorkInProgress from './WorkInProgress';
export default function TrainingWIP() {
const { t } = useTranslation();
return (
<div className="work-in-progress nodes-work-in-progress">
<h1>{t('common.training')}</h1>
<p>
{t('common.trainingDesc1')}
<br />
<br />
{t('common.trainingDesc2')}
</p>
</div>
<WorkInProgress>
<Flex
sx={{
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
w: '100%',
h: '100%',
gap: 4,
textAlign: 'center',
}}
>
<Heading>{t('common.training')}</Heading>
<VStack maxW="50rem" gap={4}>
<Text>{t('common.trainingDesc1')}</Text>
<Text>{t('common.trainingDesc2')}</Text>
</VStack>
</Flex>
</WorkInProgress>
);
}

View File

@ -1,24 +0,0 @@
@use '../../../styles/Mixins/' as *;
.work-in-progress {
display: grid;
width: 100%;
height: $app-content-height;
grid-auto-rows: max-content;
background-color: var(--background-color-secondary);
border-radius: 0.4rem;
place-content: center;
place-items: center;
row-gap: 1rem;
h1 {
font-size: 2rem;
font-weight: bold;
}
p {
text-align: center;
max-width: 50rem;
color: var(--subtext-color-bright);
}
}

View File

@ -0,0 +1,24 @@
import { Flex } from '@chakra-ui/react';
import { ReactNode } from 'react';
type WorkInProgressProps = {
children: ReactNode;
};
const WorkInProgress = (props: WorkInProgressProps) => {
const { children } = props;
return (
<Flex
sx={{
width: '100%',
height: '100%',
bg: 'base.850',
}}
>
{children}
</Flex>
);
};
export default WorkInProgress;

View File

@ -1,62 +0,0 @@
.invokeai__slider-root {
position: relative;
display: flex;
align-items: center;
user-select: none;
touch-action: none;
width: 200px;
&[data-orientation='horizontal'] {
height: 20px;
}
&[data-orientation='vertical'] {
width: 20px;
height: 200px;
}
.invokeai__slider-track {
background-color: black;
position: relative;
flex-grow: 1;
border-radius: 9999px;
&[data-orientation='horizontal'] {
height: 0.25rem;
}
&[data-orientation='vertical'] {
width: 0.25rem;
}
.invokeai__slider-range {
position: absolute;
background-color: white;
border-radius: 9999px;
height: 100%;
}
}
.invokeai__slider-thumb {
display: flex;
align-items: center;
.invokeai__slider-thumb-div {
all: unset;
display: block;
width: 1rem;
height: 1rem;
background-color: white;
box-shadow: 0 2px 10px rgba(0, 2, 10, 0.3);
border-radius: 100%;
&:hover {
background-color: violet;
}
&:focus {
box-shadow: 0 0 0 5px rgba(0, 2, 10, 0.3);
}
}
}
}

View File

@ -1,44 +0,0 @@
import { Tooltip } from '@chakra-ui/react';
import * as Slider from '@radix-ui/react-slider';
type IAISliderProps = Slider.SliderProps & {
value: number[];
tooltipLabel?: string;
orientation?: 'horizontal' | 'vertial';
trackProps?: Slider.SliderTrackProps;
rangeProps?: Slider.SliderRangeProps;
thumbProps?: Slider.SliderThumbProps;
};
const _IAISlider = (props: IAISliderProps) => {
const {
value,
tooltipLabel,
orientation,
trackProps,
rangeProps,
thumbProps,
...rest
} = props;
return (
<Slider.Root
className="invokeai__slider-root"
{...rest}
data-orientation={orientation || 'horizontal'}
>
<Slider.Track {...trackProps} className="invokeai__slider-track">
<Slider.Range {...rangeProps} className="invokeai__slider-range" />
</Slider.Track>
<Tooltip label={tooltipLabel ?? value[0]} placement="top">
<Slider.Thumb {...thumbProps} className="invokeai__slider-thumb">
<div className="invokeai__slider-thumb-div" />
{/*<IAITooltip trigger={<div className="invokeai__slider-thumb-div" />}>
{value && value[0]}
</IAITooltip>*/}
</Slider.Thumb>
</Tooltip>
</Slider.Root>
);
};
export default _IAISlider;

View File

@ -1,8 +0,0 @@
.invokeai__tooltip-content {
padding: 0.5rem;
background-color: grey;
border-radius: 0.25rem;
.invokeai__tooltip-arrow {
background-color: grey;
}
}

View File

@ -1,40 +0,0 @@
import * as Tooltip from '@radix-ui/react-tooltip';
import { ReactNode } from 'react';
type IAITooltipProps = Tooltip.TooltipProps & {
trigger: ReactNode;
children: ReactNode;
triggerProps?: Tooltip.TooltipTriggerProps;
contentProps?: Tooltip.TooltipContentProps;
arrowProps?: Tooltip.TooltipArrowProps;
};
const IAITooltip = (props: IAITooltipProps) => {
const { trigger, children, triggerProps, contentProps, arrowProps, ...rest } =
props;
return (
<Tooltip.Provider>
<Tooltip.Root {...rest} delayDuration={0}>
<Tooltip.Trigger {...triggerProps}>{trigger}</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content
{...contentProps}
onPointerDownOutside={(e) => {
e.preventDefault();
}}
className="invokeai__tooltip-content"
>
<Tooltip.Arrow
{...arrowProps}
className="invokeai__tooltip-arrow"
/>
{children}
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
);
};
export default IAITooltip;

View File

@ -13,5 +13,8 @@ const ImageToImageIcon = createIcon({
/>
</g>
),
defaultProps: {
boxSize: '24px',
},
});
export default ImageToImageIcon;

View File

@ -11,6 +11,9 @@ const NodesIcon = createIcon({
d="M3543.31,770.787C3543.31,515.578 3336.11,308.38 3080.9,308.38L462.407,308.38C207.197,308.38 0,515.578 0,770.787L0,2766.03C0,3021.24 207.197,3228.44 462.407,3228.44L3080.9,3228.44C3336.11,3228.44 3543.31,3021.24 3543.31,2766.03C3543.31,2766.03 3543.31,770.787 3543.31,770.787ZM3427.88,770.787L3427.88,2766.03C3427.88,2957.53 3272.4,3113.01 3080.9,3113.01C3080.9,3113.01 462.407,3113.01 462.407,3113.01C270.906,3113.01 115.431,2957.53 115.431,2766.03L115.431,770.787C115.431,579.286 270.906,423.812 462.407,423.812L3080.9,423.812C3272.4,423.812 3427.88,579.286 3427.88,770.787ZM1214.23,1130.69L1321.47,1130.69C1324.01,1130.69 1326.54,1130.53 1329.05,1130.2C1329.05,1130.2 1367.3,1125.33 1397.94,1149.8C1421.63,1168.72 1437.33,1204.3 1437.33,1265.48L1437.33,2078.74L1220.99,2078.74C1146.83,2078.74 1086.61,2138.95 1086.61,2213.12L1086.61,2762.46C1086.61,2836.63 1146.83,2896.84 1220.99,2896.84L1770.34,2896.84C1844.5,2896.84 1904.71,2836.63 1904.71,2762.46L1904.71,2213.12C1904.71,2138.95 1844.5,2078.74 1770.34,2078.74L1554,2078.74L1554,1604.84C1625.84,1658.19 1703.39,1658.1 1703.39,1658.1C1703.54,1658.1 1703.69,1658.11 1703.84,1658.11L2362.2,1658.11L2362.2,1874.44C2362.2,1948.61 2422.42,2008.82 2496.58,2008.82L3045.93,2008.82C3120.09,2008.82 3180.3,1948.61 3180.3,1874.44L3180.3,1325.1C3180.3,1250.93 3120.09,1190.72 3045.93,1190.72L2496.58,1190.72C2422.42,1190.72 2362.2,1250.93 2362.2,1325.1L2362.2,1558.97L2362.2,1541.44L1704.23,1541.44C1702.2,1541.37 1650.96,1539.37 1609.51,1499.26C1577.72,1468.49 1554,1416.47 1554,1331.69L1554,1265.48C1554,1153.86 1513.98,1093.17 1470.76,1058.64C1411.24,1011.1 1338.98,1012.58 1319.15,1014.03L1214.23,1014.03L1214.23,796.992C1214.23,722.828 1154.02,662.617 1079.85,662.617L530.507,662.617C456.343,662.617 396.131,722.828 396.131,796.992L396.131,1346.34C396.131,1420.5 456.343,1480.71 530.507,1480.71L1079.85,1480.71C1154.02,1480.71 1214.23,1420.5 1214.23,1346.34L1214.23,1130.69Z"
/>
),
defaultProps: {
boxSize: '24px',
},
});
export default NodesIcon;

View File

@ -11,6 +11,9 @@ const PostprocessingIcon = createIcon({
d="M709.477,1596.53L992.591,1275.66L2239.09,2646.81L2891.95,1888.03L3427.88,2460.51L3427.88,994.78C3427.88,954.66 3421.05,916.122 3408.5,880.254L3521.9,855.419C3535.8,899.386 3543.31,946.214 3543.31,994.78L3543.31,2990.02C3543.31,3245.23 3336.11,3452.43 3080.9,3452.43C3080.9,3452.43 462.407,3452.43 462.407,3452.43C207.197,3452.43 -0,3245.23 -0,2990.02L-0,994.78C-0,739.571 207.197,532.373 462.407,532.373L505.419,532.373L504.644,532.546L807.104,600.085C820.223,601.729 832.422,607.722 841.77,617.116C850.131,625.517 855.784,636.21 858.055,647.804L462.407,647.804C270.906,647.804 115.431,803.279 115.431,994.78L115.431,2075.73L-0,2101.5L115.431,2127.28L115.431,2269.78L220.47,2150.73L482.345,2209.21C503.267,2211.83 522.722,2221.39 537.63,2236.37C552.538,2251.35 562.049,2270.9 564.657,2291.93L671.84,2776.17L779.022,2291.93C781.631,2270.9 791.141,2251.35 806.05,2236.37C820.958,2221.39 840.413,2211.83 861.334,2209.21L1353.15,2101.5L861.334,1993.8C840.413,1991.18 820.958,1981.62 806.05,1966.64C791.141,1951.66 781.631,1932.11 779.022,1911.08L709.477,1596.53ZM671.84,1573.09L725.556,2006.07C726.863,2016.61 731.63,2026.4 739.101,2033.91C746.573,2041.42 756.323,2046.21 766.808,2047.53L1197.68,2101.5L766.808,2155.48C756.323,2156.8 746.573,2161.59 739.101,2169.09C731.63,2176.6 726.863,2186.4 725.556,2196.94L671.84,2629.92L618.124,2196.94C616.817,2186.4 612.05,2176.6 604.579,2169.09C597.107,2161.59 587.357,2156.8 576.872,2155.48L146.001,2101.5L576.872,2047.53C587.357,2046.21 597.107,2041.42 604.579,2033.91C612.05,2026.4 616.817,2016.61 618.124,2006.07L671.84,1573.09ZM609.035,1710.36L564.657,1911.08C562.049,1932.11 552.538,1951.66 537.63,1966.64C522.722,1981.62 503.267,1991.18 482.345,1993.8L328.665,2028.11L609.035,1710.36ZM2297.12,938.615L2451.12,973.003C2480.59,976.695 2507.99,990.158 2528.99,1011.26C2549.99,1032.37 2563.39,1059.9 2567.07,1089.52L2672.73,1566.9C2634.5,1580.11 2593.44,1587.29 2550.72,1587.29C2344.33,1587.29 2176.77,1419.73 2176.77,1213.34C2176.77,1104.78 2223.13,1006.96 2297.12,938.615ZM2718.05,76.925L2793.72,686.847C2795.56,701.69 2802.27,715.491 2812.8,726.068C2823.32,736.644 2837.06,743.391 2851.83,745.242L3458.78,821.28L2851.83,897.318C2837.06,899.168 2823.32,905.916 2812.8,916.492C2802.27,927.068 2795.56,940.87 2793.72,955.712L2718.05,1565.63L2642.38,955.712C2640.54,940.87 2633.83,927.068 2623.3,916.492C2612.78,905.916 2599.04,899.168 2584.27,897.318L1977.32,821.28L2584.27,745.242C2599.04,743.391 2612.78,736.644 2623.3,726.068C2633.83,715.491 2640.54,701.69 2642.38,686.847L2718.05,76.925ZM2883.68,1043.06C2909.88,1094.13 2924.67,1152.02 2924.67,1213.34C2924.67,1335.4 2866.06,1443.88 2775.49,1512.14L2869.03,1089.52C2871.07,1073.15 2876.07,1057.42 2883.68,1043.06ZM925.928,201.2L959.611,472.704C960.431,479.311 963.42,485.455 968.105,490.163C972.79,494.871 978.904,497.875 985.479,498.698L1255.66,532.546L985.479,566.395C978.904,567.218 972.79,570.222 968.105,574.93C963.42,579.638 960.431,585.781 959.611,592.388L925.928,863.893L892.245,592.388C891.425,585.781 888.436,579.638 883.751,574.93C879.066,570.222 872.952,567.218 866.378,566.395L596.195,532.546L866.378,498.698C872.952,497.875 879.066,494.871 883.751,490.163C888.436,485.455 891.425,479.311 892.245,472.704L925.928,201.2ZM2864.47,532.373L3080.9,532.373C3258.7,532.373 3413.2,632.945 3490.58,780.281L3319.31,742.773C3257.14,683.925 3173.2,647.804 3080.9,647.804L2927.07,647.804C2919.95,642.994 2913.25,637.473 2907.11,631.298C2886.11,610.194 2872.71,582.655 2869.03,553.04L2864.47,532.373ZM1352.36,532.373L2571.64,532.373L2567.07,553.04C2563.39,582.655 2549.99,610.194 2528.99,631.298C2522.85,637.473 2516.16,642.994 2509.03,647.804L993.801,647.804C996.072,636.21 1001.73,625.517 1010.09,617.116C1019.43,607.722 1031.63,601.729 1044.75,600.085L1353.15,532.546L1352.36,532.373Z"
/>
),
defaultProps: {
boxSize: '24px',
},
});
export default PostprocessingIcon;

File diff suppressed because one or more lines are too long

View File

@ -11,6 +11,9 @@ const TrainingIcon = createIcon({
d="M0,768.593L0,2774.71C0,2930.6 78.519,3068.3 198.135,3150.37C273.059,3202.68 364.177,3233.38 462.407,3233.38C462.407,3233.38 3080.9,3233.38 3080.9,3233.38C3179.13,3233.38 3270.25,3202.68 3345.17,3150.37C3464.79,3068.3 3543.31,2930.6 3543.31,2774.71L3543.31,768.593C3543.31,517.323 3339.31,313.324 3088.04,313.324L455.269,313.324C203.999,313.324 0,517.323 0,768.593ZM3427.88,775.73L3427.88,2770.97C3427.88,2962.47 3272.4,3117.95 3080.9,3117.95L462.407,3117.95C270.906,3117.95 115.431,2962.47 115.431,2770.97C115.431,2770.97 115.431,775.73 115.431,775.73C115.431,584.229 270.906,428.755 462.407,428.755C462.407,428.755 3080.9,428.755 3080.9,428.755C3272.4,428.755 3427.88,584.229 3427.88,775.73ZM796.24,1322.76L796.24,1250.45C796.24,1199.03 836.16,1157.27 885.331,1157.27C885.331,1157.27 946.847,1157.27 946.847,1157.27C996.017,1157.27 1035.94,1199.03 1035.94,1250.45L1035.94,1644.81L2507.37,1644.81L2507.37,1250.45C2507.37,1199.03 2547.29,1157.27 2596.46,1157.27C2596.46,1157.27 2657.98,1157.27 2657.98,1157.27C2707.15,1157.27 2747.07,1199.03 2747.07,1250.45L2747.07,1322.76C2756.66,1319.22 2767.02,1317.29 2777.83,1317.29C2777.83,1317.29 2839.34,1317.29 2839.34,1317.29C2888.51,1317.29 2928.43,1357.21 2928.43,1406.38L2928.43,1527.32C2933.51,1526.26 2938.77,1525.71 2944.16,1525.71L2995.3,1525.71C3036.18,1525.71 3069.37,1557.59 3069.37,1596.86C3069.37,1596.86 3069.37,1946.44 3069.37,1946.44C3069.37,1985.72 3036.18,2017.6 2995.3,2017.6C2995.3,2017.6 2944.16,2017.6 2944.16,2017.6C2938.77,2017.6 2933.51,2017.04 2928.43,2015.99L2928.43,2136.92C2928.43,2186.09 2888.51,2226.01 2839.34,2226.01L2777.83,2226.01C2767.02,2226.01 2756.66,2224.08 2747.07,2220.55L2747.07,2292.85C2747.07,2344.28 2707.15,2386.03 2657.98,2386.03C2657.98,2386.03 2596.46,2386.03 2596.46,2386.03C2547.29,2386.03 2507.37,2344.28 2507.37,2292.85L2507.37,1898.5L1035.94,1898.5L1035.94,2292.85C1035.94,2344.28 996.017,2386.03 946.847,2386.03C946.847,2386.03 885.331,2386.03 885.331,2386.03C836.16,2386.03 796.24,2344.28 796.24,2292.85L796.24,2220.55C786.651,2224.08 776.29,2226.01 765.482,2226.01L703.967,2226.01C654.796,2226.01 614.876,2186.09 614.876,2136.92L614.876,2015.99C609.801,2017.04 604.539,2017.6 599.144,2017.6C599.144,2017.6 548.003,2017.6 548.003,2017.6C507.125,2017.6 473.937,1985.72 473.937,1946.44C473.937,1946.44 473.937,1596.86 473.937,1596.86C473.937,1557.59 507.125,1525.71 548.003,1525.71L599.144,1525.71C604.539,1525.71 609.801,1526.26 614.876,1527.32L614.876,1406.38C614.876,1357.21 654.796,1317.29 703.967,1317.29C703.967,1317.29 765.482,1317.29 765.482,1317.29C776.29,1317.29 786.651,1319.22 796.24,1322.76ZM977.604,1250.45C977.604,1232.7 963.822,1218.29 946.847,1218.29L885.331,1218.29C868.355,1218.29 854.573,1232.7 854.573,1250.45L854.573,2292.85C854.573,2310.61 868.355,2325.02 885.331,2325.02L946.847,2325.02C963.822,2325.02 977.604,2310.61 977.604,2292.85L977.604,1250.45ZM2565.7,1250.45C2565.7,1232.7 2579.49,1218.29 2596.46,1218.29L2657.98,1218.29C2674.95,1218.29 2688.73,1232.7 2688.73,1250.45L2688.73,2292.85C2688.73,2310.61 2674.95,2325.02 2657.98,2325.02L2596.46,2325.02C2579.49,2325.02 2565.7,2310.61 2565.7,2292.85L2565.7,1250.45ZM673.209,1406.38L673.209,2136.92C673.209,2153.9 686.991,2167.68 703.967,2167.68L765.482,2167.68C782.458,2167.68 796.24,2153.9 796.24,2136.92L796.24,1406.38C796.24,1389.41 782.458,1375.63 765.482,1375.63L703.967,1375.63C686.991,1375.63 673.209,1389.41 673.209,1406.38ZM2870.1,1406.38L2870.1,2136.92C2870.1,2153.9 2856.32,2167.68 2839.34,2167.68L2777.83,2167.68C2760.85,2167.68 2747.07,2153.9 2747.07,2136.92L2747.07,1406.38C2747.07,1389.41 2760.85,1375.63 2777.83,1375.63L2839.34,1375.63C2856.32,1375.63 2870.1,1389.41 2870.1,1406.38ZM614.876,1577.5C610.535,1574.24 605.074,1572.3 599.144,1572.3L548.003,1572.3C533.89,1572.3 522.433,1583.3 522.433,1596.86L522.433,1946.44C522.433,1960 533.89,1971.01 548.003,1971.01L599.144,1971.01C605.074,1971.01 610.535,1969.07 614.876,1965.81L614.876,1577.5ZM2928.43,1965.81L2928.43,1577.5C2932.77,1574.24 2938.23,1572.3 2944.16,1572.3L2995.3,1572.3C3009.42,1572.3 3020.87,1583.3 3020.87,1596.86L3020.87,1946.44C3020.87,1960 3009.42,1971.01 2995.3,1971.01L2944.16,1971.01C2938.23,1971.01 2932.77,1969.07 2928.43,1965.81ZM2507.37,1703.14L1035.94,1703.14L1035.94,1840.16L2507.37,1840.16L2507.37,1898.38L2507.37,1659.46L2507.37,1703.14Z"
/>
),
defaultProps: {
boxSize: '24px',
},
});
export default TrainingIcon;

File diff suppressed because one or more lines are too long

View File

@ -144,8 +144,8 @@ export const frontendToBackendParameters = (
variationAmount,
width,
shouldUseSymmetry,
horizontalSymmetryTimePercentage,
verticalSymmetryTimePercentage,
horizontalSymmetrySteps,
verticalSymmetrySteps,
} = generationState;
const {
@ -185,17 +185,17 @@ export const frontendToBackendParameters = (
// Symmetry Settings
if (shouldUseSymmetry) {
if (horizontalSymmetryTimePercentage > 0) {
if (horizontalSymmetrySteps > 0) {
generationParameters.h_symmetry_time_pct = Math.max(
0,
Math.min(1, horizontalSymmetryTimePercentage / steps)
Math.min(1, horizontalSymmetrySteps / steps)
);
}
if (horizontalSymmetryTimePercentage > 0) {
if (verticalSymmetrySteps > 0) {
generationParameters.v_symmetry_time_pct = Math.max(
0,
Math.min(1, verticalSymmetryTimePercentage / steps)
Math.min(1, verticalSymmetrySteps / steps)
);
}
}

View File

@ -1,3 +1,4 @@
import { Box, chakra, Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/storeHooks';
import {
@ -88,6 +89,10 @@ const selector = createSelector(
}
);
const ChakraStage = chakra(Stage, {
shouldForwardProp: (prop) => !['sx'].includes(prop),
});
const IAICanvas = () => {
const {
isMaskEnabled,
@ -135,14 +140,26 @@ const IAICanvas = () => {
useCanvasDragMove();
return (
<div className="inpainting-canvas-container">
<div className="inpainting-canvas-wrapper">
<Stage
<Flex
sx={{
position: 'relative',
height: '100%',
width: '100%',
borderRadius: 'base',
}}
>
<Box sx={{ position: 'relative' }}>
<ChakraStage
tabIndex={-1}
ref={canvasStageRefCallback}
className="inpainting-canvas-stage"
style={{
...(stageCursor ? { cursor: stageCursor } : {}),
sx={{
outline: 'none',
// boxShadow: '0px 0px 0px 1px var(--border-color-light)',
overflow: 'hidden',
cursor: stageCursor ? stageCursor : undefined,
canvas: {
outline: 'none',
},
}}
x={stageCoordinates.x}
y={stageCoordinates.y}
@ -197,11 +214,11 @@ const IAICanvas = () => {
visible={shouldShowBoundingBox && !isStaging}
/>
</Layer>
</Stage>
</ChakraStage>
<IAICanvasStatusText />
<IAICanvasStagingAreaToolbar />
</div>
</div>
</Box>
</Flex>
);
};

View File

@ -1,7 +1,7 @@
// Grid drawing adapted from https://longviewcoder.com/2021/12/08/konva-a-better-grid/
import { useColorMode } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store';
import { useAppSelector } from 'app/storeHooks';
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
import { isEqual, range } from 'lodash';
@ -26,10 +26,13 @@ const gridLinesColor = {
dark: 'rgba(255, 255, 255, 0.2)',
green: 'rgba(255, 255, 255, 0.2)',
light: 'rgba(0, 0, 0, 0.2)',
ocean: 'rgba(136, 148, 184, 0.2)',
};
const IAICanvasGrid = () => {
const { colorMode } = useColorMode();
const currentTheme = useAppSelector(
(state: RootState) => state.ui.currentTheme
);
const { stageScale, stageCoordinates, stageDimensions } =
useAppSelector(selector);
const [gridLines, setGridLines] = useState<ReactNode[]>([]);
@ -42,7 +45,8 @@ const IAICanvasGrid = () => {
);
useLayoutEffect(() => {
const gridLineColor = gridLinesColor[colorMode];
const gridLineColor =
gridLinesColor[currentTheme as keyof typeof gridLinesColor];
const { width, height } = stageDimensions;
const { x, y } = stageCoordinates;
@ -108,7 +112,7 @@ const IAICanvasGrid = () => {
));
setGridLines(xLines.concat(yLines));
}, [stageScale, stageCoordinates, stageDimensions, colorMode, unscale]);
}, [stageScale, stageCoordinates, stageDimensions, currentTheme, unscale]);
return <Group>{gridLines}</Group>;
};

View File

@ -1,4 +1,4 @@
import { Spinner } from '@chakra-ui/react';
import { Flex, Spinner } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import {
@ -70,9 +70,19 @@ const IAICanvasResizer = () => {
]);
return (
<div ref={ref} className="inpainting-canvas-area">
<Flex
ref={ref}
sx={{
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
width: '100%',
height: '100%',
}}
>
<Spinner thickness="2px" speed="1s" size="xl" />
</div>
</Flex>
);
};

View File

@ -115,7 +115,7 @@ const IAICanvasStagingAreaToolbar = () => {
return (
<Flex
pos="absolute"
bottom="1rem"
bottom={4}
w="100%"
align="center"
justify="center"
@ -129,7 +129,7 @@ const IAICanvasStagingAreaToolbar = () => {
aria-label={`${t('unifiedCanvas.previous')} (Left)`}
icon={<FaArrowLeft />}
onClick={handlePrevImage}
data-selected={true}
colorScheme="accent"
isDisabled={isOnFirstImage}
/>
<IAIIconButton
@ -137,7 +137,7 @@ const IAICanvasStagingAreaToolbar = () => {
aria-label={`${t('unifiedCanvas.next')} (Right)`}
icon={<FaArrowRight />}
onClick={handleNextImage}
data-selected={true}
colorScheme="accent"
isDisabled={isOnLastImage}
/>
<IAIIconButton
@ -145,7 +145,7 @@ const IAICanvasStagingAreaToolbar = () => {
aria-label={`${t('unifiedCanvas.accept')} (Enter)`}
icon={<FaCheck />}
onClick={handleAccept}
data-selected={true}
colorScheme="accent"
/>
<IAIIconButton
tooltip={t('unifiedCanvas.showHide')}
@ -155,7 +155,7 @@ const IAICanvasStagingAreaToolbar = () => {
onClick={() =>
dispatch(setShouldShowStagingImage(!shouldShowStagingImage))
}
data-selected={true}
colorScheme="accent"
/>
<IAIIconButton
tooltip={t('unifiedCanvas.saveToGallery')}
@ -166,15 +166,14 @@ const IAICanvasStagingAreaToolbar = () => {
saveStagingAreaImageToGallery(currentStagingAreaImage.image.url)
)
}
data-selected={true}
colorScheme="accent"
/>
<IAIIconButton
tooltip={t('unifiedCanvas.discardAll')}
aria-label={t('unifiedCanvas.discardAll')}
icon={<FaPlus style={{ transform: 'rotate(45deg)' }} />}
onClick={() => dispatch(discardStagedImages())}
data-selected={true}
style={{ backgroundColor: 'var(--btn-delete-image)' }}
colorScheme="error"
fontSize={20}
/>
</ButtonGroup>

View File

@ -1,3 +1,4 @@
import { Box, Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/storeHooks';
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
@ -7,6 +8,8 @@ import { useTranslation } from 'react-i18next';
import roundToHundreth from '../util/roundToHundreth';
import IAICanvasStatusTextCursorPos from './IAICanvasStatusText/IAICanvasStatusTextCursorPos';
const warningColor = 'var(--invokeai-colors-warning-500)';
const selector = createSelector(
[canvasSelector],
(canvas) => {
@ -34,11 +37,10 @@ const selector = createSelector(
(boundingBoxScaleMethod === 'manual' &&
scaledBoxWidth * scaledBoxHeight < 512 * 512)
) {
boundingBoxColor = 'var(--status-working-color)';
boundingBoxColor = warningColor;
}
const activeLayerColor =
layer === 'mask' ? 'var(--status-working-color)' : 'inherit';
const activeLayerColor = layer === 'mask' ? warningColor : 'inherit';
return {
activeLayerColor,
@ -87,55 +89,72 @@ const IAICanvasStatusText = () => {
const { t } = useTranslation();
return (
<div className="canvas-status-text">
<div
<Flex
sx={{
flexDirection: 'column',
position: 'absolute',
top: 0,
insetInlineStart: 0,
opacity: 0.65,
display: 'flex',
fontSize: 'sm',
padding: 1,
px: 2,
minWidth: 48,
margin: 1,
borderRadius: 'base',
pointerEvents: 'none',
bg: 'blackAlpha.500',
}}
>
<Box
style={{
color: activeLayerColor,
}}
>{`${t('unifiedCanvas.activeLayer')}: ${activeLayerString}`}</div>
<div>{`${t('unifiedCanvas.canvasScale')}: ${canvasScaleString}%`}</div>
>{`${t('unifiedCanvas.activeLayer')}: ${activeLayerString}`}</Box>
<Box>{`${t('unifiedCanvas.canvasScale')}: ${canvasScaleString}%`}</Box>
{shouldPreserveMaskedArea && (
<div
<Box
style={{
color: 'var(--status-working-color)',
color: warningColor,
}}
>
Preserve Masked Area: On
</div>
</Box>
)}
{shouldShowBoundingBox && (
<div
<Box
style={{
color: boundingBoxColor,
}}
>{`${t(
'unifiedcanvas:boundingBox'
)}: ${boundingBoxDimensionsString}`}</div>
'unifiedCanvas.boundingBox'
)}: ${boundingBoxDimensionsString}`}</Box>
)}
{shouldShowScaledBoundingBox && (
<div
<Box
style={{
color: boundingBoxColor,
}}
>{`${t(
'unifiedcanvas:scaledBoundingBox'
)}: ${scaledBoundingBoxDimensionsString}`}</div>
'unifiedCanvas.scaledBoundingBox'
)}: ${scaledBoundingBoxDimensionsString}`}</Box>
)}
{shouldShowCanvasDebugInfo && (
<>
<div>{`${t(
'unifiedcanvas:boundingBoxPosition'
)}: ${boundingBoxCoordinatesString}`}</div>
<div>{`${t(
'unifiedcanvas:canvasDimensions'
)}: ${canvasDimensionsString}`}</div>
<div>{`${t(
'unifiedcanvas:canvasPosition'
)}: ${canvasCoordinatesString}`}</div>
<Box>{`${t(
'unifiedCanvas.boundingBoxPosition'
)}: ${boundingBoxCoordinatesString}`}</Box>
<Box>{`${t(
'unifiedCanvas.canvasDimensions'
)}: ${canvasDimensionsString}`}</Box>
<Box>{`${t(
'unifiedCanvas.canvasPosition'
)}: ${canvasCoordinatesString}`}</Box>
<IAICanvasStatusTextCursorPos />
</>
)}
</div>
</Flex>
);
};

View File

@ -1,3 +1,4 @@
import { Box } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/storeHooks';
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
@ -33,8 +34,8 @@ export default function IAICanvasStatusTextCursorPos() {
const { t } = useTranslation();
return (
<div>{`${t(
'unifiedcanvas:cursorPosition'
)}: ${cursorCoordinatesString}`}</div>
<Box>{`${t(
'unifiedCanvas.cursorPosition'
)}: ${cursorCoordinatesString}`}</Box>
);
}

View File

@ -111,17 +111,13 @@ const IAICanvasMaskOptions = () => {
aria-label={t('unifiedCanvas.maskingOptions')}
tooltip={t('unifiedCanvas.maskingOptions')}
icon={<FaMask />}
style={
layer === 'mask'
? { backgroundColor: 'var(--accent-color)' }
: { backgroundColor: 'var(--btn-base-color)' }
}
isChecked={layer === 'mask'}
isDisabled={isStaging}
/>
</ButtonGroup>
}
>
<Flex direction="column" gap="0.5rem">
<Flex direction="column" gap={2}>
<IAICheckbox
label={`${t('unifiedCanvas.enableMask')} (H)`}
isChecked={isMaskEnabled}
@ -135,8 +131,8 @@ const IAICanvasMaskOptions = () => {
}
/>
<IAIColorPicker
style={{ paddingTop: '0.5rem', paddingBottom: '0.5rem' }}
color={maskColor}
sx={{ paddingTop: 2, paddingBottom: 2 }}
pickerColor={maskColor}
onChange={(newColor) => dispatch(setMaskColor(newColor))}
/>
<IAIButton size="sm" leftIcon={<FaTrash />} onClick={handleClearMask}>

View File

@ -97,7 +97,7 @@ const IAICanvasSettingsButtonPopover = () => {
/>
}
>
<Flex direction="column" gap="0.5rem">
<Flex direction="column" gap={2}>
<IAICheckbox
label={t('unifiedCanvas.showIntermediates')}
isChecked={shouldShowIntermediates}

View File

@ -184,7 +184,7 @@ const IAICanvasToolChooserOptions = () => {
aria-label={`${t('unifiedCanvas.brush')} (B)`}
tooltip={`${t('unifiedCanvas.brush')} (B)`}
icon={<FaPaintBrush />}
data-selected={tool === 'brush' && !isStaging}
isChecked={tool === 'brush' && !isStaging}
onClick={handleSelectBrushTool}
isDisabled={isStaging}
/>
@ -192,7 +192,7 @@ const IAICanvasToolChooserOptions = () => {
aria-label={`${t('unifiedCanvas.eraser')} (E)`}
tooltip={`${t('unifiedCanvas.eraser')} (E)`}
icon={<FaEraser />}
data-selected={tool === 'eraser' && !isStaging}
isChecked={tool === 'eraser' && !isStaging}
isDisabled={isStaging}
onClick={handleSelectEraserTool}
/>
@ -214,7 +214,7 @@ const IAICanvasToolChooserOptions = () => {
aria-label={`${t('unifiedCanvas.colorPicker')} (C)`}
tooltip={`${t('unifiedCanvas.colorPicker')} (C)`}
icon={<FaEyeDropper />}
data-selected={tool === 'colorPicker' && !isStaging}
isChecked={tool === 'colorPicker' && !isStaging}
isDisabled={isStaging}
onClick={handleSelectColorPickerTool}
/>
@ -228,8 +228,8 @@ const IAICanvasToolChooserOptions = () => {
/>
}
>
<Flex minWidth="15rem" direction="column" gap="1rem" width="100%">
<Flex gap="1rem" justifyContent="space-between">
<Flex minWidth={60} direction="column" gap={4} width="100%">
<Flex gap={4} justifyContent="space-between">
<IAISlider
label={t('unifiedCanvas.brushSize')}
value={brushSize}
@ -240,12 +240,12 @@ const IAICanvasToolChooserOptions = () => {
/>
</Flex>
<IAIColorPicker
style={{
sx={{
width: '100%',
paddingTop: '0.5rem',
paddingBottom: '0.5rem',
paddingTop: 2,
paddingBottom: 2,
}}
color={brushColor}
pickerColor={brushColor}
onChange={(newColor) => dispatch(setBrushColor(newColor))}
/>
</Flex>

View File

@ -1,4 +1,4 @@
import { ButtonGroup } from '@chakra-ui/react';
import { ButtonGroup, Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
@ -68,7 +68,7 @@ export const selector = createSelector(
}
);
const IAICanvasOutpaintingControls = () => {
const IAICanvasToolbar = () => {
const dispatch = useAppDispatch();
const {
isProcessing,
@ -230,7 +230,12 @@ const IAICanvasOutpaintingControls = () => {
};
return (
<div className="inpainting-settings">
<Flex
sx={{
alignItems: 'center',
gap: 2,
}}
>
<IAISelect
tooltip={`${t('unifiedCanvas.layer')} (Q)`}
tooltipProps={{ hasArrow: true, placement: 'top' }}
@ -248,7 +253,7 @@ const IAICanvasOutpaintingControls = () => {
aria-label={`${t('unifiedCanvas.move')} (V)`}
tooltip={`${t('unifiedCanvas.move')} (V)`}
icon={<FaArrowsAlt />}
data-selected={tool === 'move' || isStaging}
isChecked={tool === 'move' || isStaging}
onClick={handleSelectMoveTool}
/>
<IAIIconButton
@ -307,15 +312,15 @@ const IAICanvasOutpaintingControls = () => {
tooltip={`${t('unifiedCanvas.clearCanvas')}`}
icon={<FaTrash />}
onClick={handleResetCanvas}
style={{ backgroundColor: 'var(--btn-delete-image)' }}
colorScheme="error"
isDisabled={isStaging}
/>
</ButtonGroup>
<ButtonGroup isAttached>
<IAICanvasSettingsButtonPopover />
</ButtonGroup>
</div>
</Flex>
);
};
export default IAICanvasOutpaintingControls;
export default IAICanvasToolbar;

View File

@ -1,32 +0,0 @@
.current-image-options {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
column-gap: 0.5em;
.current-image-send-to-popover,
.current-image-postprocessing-popover {
display: flex;
flex-direction: column;
row-gap: 0.5rem;
max-width: 25rem;
}
.current-image-send-to-popover {
.invokeai__button {
place-content: start;
}
}
.chakra-popover__popper {
z-index: 11;
}
.delete-image-btn {
background-color: var(--btn-base-color);
svg {
fill: var(--btn-delete-image);
}
}
}

View File

@ -1,7 +1,7 @@
import { createSelector } from '@reduxjs/toolkit';
import { isEqual } from 'lodash';
import { ButtonGroup, Link, useToast } from '@chakra-ui/react';
import { ButtonGroup, Flex, FlexProps, Link, useToast } from '@chakra-ui/react';
import { runESRGAN, runFacetool } from 'app/socketio/actions';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAIButton from 'common/components/IAIButton';
@ -102,11 +102,13 @@ const currentImageButtonsSelector = createSelector(
}
);
type CurrentImageButtonsProps = FlexProps;
/**
* Row of buttons for common actions:
* Use as init image, use all params, use seed, upscale, fix faces, details, delete.
*/
const CurrentImageButtons = () => {
const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
const dispatch = useAppDispatch();
const {
isProcessing,
@ -395,7 +397,14 @@ const CurrentImageButtons = () => {
};
return (
<div className="current-image-options">
<Flex
sx={{
justifyContent: 'center',
alignItems: 'center',
columnGap: '0.5em',
}}
{...props}
>
<ButtonGroup isAttached={true}>
<IAIPopover
trigger="hover"
@ -406,7 +415,13 @@ const CurrentImageButtons = () => {
/>
}
>
<div className="current-image-send-to-popover">
<Flex
sx={{
flexDirection: 'column',
rowGap: 2,
w: 52,
}}
>
<IAIButton
size="sm"
onClick={handleClickUseAsInitialImage}
@ -442,7 +457,7 @@ const CurrentImageButtons = () => {
{t('parameters.downloadImage')}
</IAIButton>
</Link>
</div>
</Flex>
</IAIPopover>
<IAIIconButton
icon={<FaExpand />}
@ -456,7 +471,7 @@ const CurrentImageButtons = () => {
? `${t('parameters.openInViewer')} (Z)`
: `${t('parameters.closeViewer')} (Z)`
}
data-selected={isLightboxOpen}
isChecked={isLightboxOpen}
onClick={handleLightBox}
/>
</ButtonGroup>
@ -501,7 +516,12 @@ const CurrentImageButtons = () => {
/>
}
>
<div className="current-image-postprocessing-popover">
<Flex
sx={{
flexDirection: 'column',
rowGap: 4,
}}
>
<FaceRestoreSettings />
<IAIButton
isDisabled={
@ -514,7 +534,7 @@ const CurrentImageButtons = () => {
>
{t('parameters.restoreFaces')}
</IAIButton>
</div>
</Flex>
</IAIPopover>
<IAIPopover
@ -526,7 +546,12 @@ const CurrentImageButtons = () => {
/>
}
>
<div className="current-image-postprocessing-popover">
<Flex
sx={{
flexDirection: 'column',
gap: 4,
}}
>
<UpscaleSettings />
<IAIButton
isDisabled={
@ -539,7 +564,7 @@ const CurrentImageButtons = () => {
>
{t('parameters.upscaleImage')}
</IAIButton>
</div>
</Flex>
</IAIPopover>
</ButtonGroup>
@ -548,7 +573,7 @@ const CurrentImageButtons = () => {
icon={<FaCode />}
tooltip={`${t('parameters.info')} (I)`}
aria-label={`${t('parameters.info')} (I)`}
data-selected={shouldShowImageDetails}
isChecked={shouldShowImageDetails}
onClick={handleClickShowImageDetails}
/>
</ButtonGroup>
@ -559,10 +584,10 @@ const CurrentImageButtons = () => {
tooltip={`${t('parameters.deleteImage')} (Del)`}
aria-label={`${t('parameters.deleteImage')} (Del)`}
isDisabled={!currentImage || !isConnected || isProcessing}
style={{ backgroundColor: 'var(--btn-delete-image)' }}
colorScheme="error"
/>
</DeleteImageModal>
</div>
</Flex>
);
};

View File

@ -1,83 +0,0 @@
@use '../../../styles/Mixins/' as *;
.current-image-area {
display: flex;
flex-direction: column;
height: 100%;
row-gap: 1rem;
background-color: var(--background-color-secondary);
border-radius: 0.5rem;
}
.current-image-preview {
position: relative;
justify-content: center;
align-items: center;
display: flex;
width: 100%;
height: 100%;
img {
border-radius: 0.5rem;
object-fit: contain;
max-width: 100%;
max-height: 100%;
height: auto;
position: absolute;
}
}
.current-image-metadata {
grid-area: current-image-preview;
}
.current-image-next-prev-buttons {
grid-area: current-image-content;
display: flex;
justify-content: space-between;
z-index: 1;
height: 100%;
width: 100%;
pointer-events: none;
}
.next-prev-button-trigger-area {
width: 7rem;
height: 100%;
width: 15%;
display: grid;
align-items: center;
pointer-events: auto;
&.prev-button-trigger-area {
justify-content: flex-start;
}
&.next-button-trigger-area {
justify-content: flex-end;
}
}
.next-prev-button {
font-size: 4rem;
fill: var(--white);
filter: drop-shadow(0 0 1rem var(--text-color-secondary));
opacity: 70%;
}
.current-image-display-placeholder {
background-color: var(--background-color-secondary);
display: grid;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
border-radius: 0.5rem;
svg {
width: 10rem;
height: 10rem;
color: var(--svg-color);
}
}

View File

@ -1,10 +1,6 @@
import { Flex, Icon } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/storeHooks';
import { GalleryState } from 'features/gallery/store/gallerySlice';
import {
activeTabNameSelector,
uiSelector,
} from 'features/ui/store/uiSelectors';
import { isEqual } from 'lodash';
import { MdPhoto } from 'react-icons/md';
@ -13,14 +9,11 @@ import CurrentImageButtons from './CurrentImageButtons';
import CurrentImagePreview from './CurrentImagePreview';
export const currentImageDisplaySelector = createSelector(
[gallerySelector, uiSelector, activeTabNameSelector],
(gallery: GalleryState, ui, activeTabName) => {
[gallerySelector],
(gallery) => {
const { currentImage, intermediateImage } = gallery;
const { shouldShowImageDetails } = ui;
return {
activeTabName,
shouldShowImageDetails,
hasAnImageToDisplay: currentImage || intermediateImage,
};
},
@ -35,23 +28,42 @@ export const currentImageDisplaySelector = createSelector(
* Displays the current image if there is one, plus associated actions.
*/
const CurrentImageDisplay = () => {
const { hasAnImageToDisplay, activeTabName } = useAppSelector(
currentImageDisplaySelector
);
const { hasAnImageToDisplay } = useAppSelector(currentImageDisplaySelector);
return (
<div className="current-image-area" data-tab-name={activeTabName}>
<Flex
sx={{
flexDirection: 'column',
height: '100%',
width: '100%',
rowGap: 4,
borderRadius: 'base',
}}
>
{hasAnImageToDisplay ? (
<>
<CurrentImageButtons />
<CurrentImagePreview />
</>
) : (
<div className="current-image-display-placeholder">
<MdPhoto />
</div>
<Flex
sx={{
alignItems: 'center',
justifyContent: 'center',
width: '100%',
height: '100%',
}}
>
<Icon
as={MdPhoto}
sx={{
boxSize: 24,
color: 'base.500',
}}
/>
</Flex>
)}
</div>
</Flex>
);
};

View File

@ -1,47 +1,24 @@
import { IconButton, Image } from '@chakra-ui/react';
import { Flex, Image } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import {
GalleryCategory,
GalleryState,
selectNextImage,
selectPrevImage,
} from 'features/gallery/store/gallerySlice';
import { useAppSelector } from 'app/storeHooks';
import { GalleryState } from 'features/gallery/store/gallerySlice';
import { uiSelector } from 'features/ui/store/uiSelectors';
import { isEqual } from 'lodash';
import { useState } from 'react';
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
import { gallerySelector } from '../store/gallerySelectors';
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
import NextPrevImageButtons from './NextPrevImageButtons';
export const imagesSelector = createSelector(
[gallerySelector, uiSelector],
(gallery: GalleryState, ui) => {
const { currentCategory, currentImage, intermediateImage } = gallery;
const { currentImage, intermediateImage } = gallery;
const { shouldShowImageDetails } = ui;
const tempImages =
gallery.categories[
currentImage ? (currentImage.category as GalleryCategory) : 'result'
].images;
const currentImageIndex = tempImages.findIndex(
(i) => i.uuid === gallery?.currentImage?.uuid
);
const imagesLength = tempImages.length;
return {
imageToDisplay: intermediateImage ? intermediateImage : currentImage,
isIntermediate: Boolean(intermediateImage),
viewerImageToDisplay: currentImage,
currentCategory,
isOnFirstImage: currentImageIndex === 0,
isOnLastImage:
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
shouldShowImageDetails,
shouldShowPrevImageButton: currentImageIndex === 0,
shouldShowNextImageButton:
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
};
},
{
@ -52,85 +29,44 @@ export const imagesSelector = createSelector(
);
export default function CurrentImagePreview() {
const dispatch = useAppDispatch();
const {
isOnFirstImage,
isOnLastImage,
shouldShowImageDetails,
imageToDisplay,
isIntermediate,
} = useAppSelector(imagesSelector);
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] =
useState<boolean>(false);
const handleCurrentImagePreviewMouseOver = () => {
setShouldShowNextPrevButtons(true);
};
const handleCurrentImagePreviewMouseOut = () => {
setShouldShowNextPrevButtons(false);
};
const handleClickPrevButton = () => {
dispatch(selectPrevImage());
};
const handleClickNextButton = () => {
dispatch(selectNextImage());
};
const { shouldShowImageDetails, imageToDisplay, isIntermediate } =
useAppSelector(imagesSelector);
return (
<div className="current-image-preview">
<Flex
sx={{
position: 'relative',
justifyContent: 'center',
alignItems: 'center',
width: '100%',
height: '100%',
}}
>
{imageToDisplay && (
<Image
src={imageToDisplay.url}
width={imageToDisplay.width}
height={imageToDisplay.height}
style={{
sx={{
objectFit: 'contain',
maxWidth: '100%',
maxHeight: '100%',
height: 'auto',
position: 'absolute',
imageRendering: isIntermediate ? 'pixelated' : 'initial',
borderRadius: 'base',
}}
{...(isIntermediate && {
width: imageToDisplay.width,
height: imageToDisplay.height,
})}
/>
)}
{!shouldShowImageDetails && (
<div className="current-image-next-prev-buttons">
<div
className="next-prev-button-trigger-area prev-button-trigger-area"
onMouseOver={handleCurrentImagePreviewMouseOver}
onMouseOut={handleCurrentImagePreviewMouseOut}
>
{shouldShowNextPrevButtons && !isOnFirstImage && (
<IconButton
aria-label="Previous image"
icon={<FaAngleLeft className="next-prev-button" />}
variant="unstyled"
onClick={handleClickPrevButton}
/>
)}
</div>
<div
className="next-prev-button-trigger-area next-button-trigger-area"
onMouseOver={handleCurrentImagePreviewMouseOver}
onMouseOut={handleCurrentImagePreviewMouseOut}
>
{shouldShowNextPrevButtons && !isOnLastImage && (
<IconButton
aria-label="Next image"
icon={<FaAngleRight className="next-prev-button" />}
variant="unstyled"
onClick={handleClickNextButton}
/>
)}
</div>
</div>
)}
{!shouldShowImageDetails && <NextPrevImageButtons />}
{shouldShowImageDetails && imageToDisplay && (
<ImageMetadataViewer
image={imageToDisplay}
styleClass="current-image-metadata"
/>
)}
</div>
</Flex>
);
}

View File

@ -5,11 +5,8 @@ import {
AlertDialogFooter,
AlertDialogHeader,
AlertDialogOverlay,
Button,
forwardRef,
Flex,
FormControl,
FormLabel,
Switch,
Text,
useDisclosure,
} from '@chakra-ui/react';
@ -17,6 +14,8 @@ import { createSelector } from '@reduxjs/toolkit';
import * as InvokeAI from 'app/invokeai';
import { deleteImage } from 'app/socketio/actions';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAIButton from 'common/components/IAIButton';
import IAISwitch from 'common/components/IAISwitch';
import { systemSelector } from 'features/system/store/systemSelectors';
import {
setShouldConfirmOnDelete,
@ -27,7 +26,6 @@ import { isEqual } from 'lodash';
import {
ChangeEvent,
cloneElement,
forwardRef,
ReactElement,
SyntheticEvent,
useRef,
@ -110,7 +108,7 @@ const DeleteImageModal = forwardRef(
onClose={onClose}
>
<AlertDialogOverlay>
<AlertDialogContent className="modal">
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
Delete image
</AlertDialogHeader>
@ -121,28 +119,20 @@ const DeleteImageModal = forwardRef(
Are you sure? Deleted images will be sent to the Bin. You
can restore from there if you wish to.
</Text>
<FormControl>
<Flex alignItems="center">
<FormLabel mb={0}>Don&apos;t ask me again</FormLabel>
<Switch
checked={!shouldConfirmOnDelete}
onChange={handleChangeShouldConfirmOnDelete}
/>
</Flex>
</FormControl>
<IAISwitch
label="Don't ask me again"
isChecked={!shouldConfirmOnDelete}
onChange={handleChangeShouldConfirmOnDelete}
/>
</Flex>
</AlertDialogBody>
<AlertDialogFooter>
<Button
ref={cancelRef}
onClick={onClose}
className="modal-close-btn"
>
<IAIButton ref={cancelRef} onClick={onClose}>
Cancel
</Button>
<Button colorScheme="red" onClick={handleDelete} ml={3}>
</IAIButton>
<IAIButton colorScheme="error" onClick={handleDelete} ml={3}>
Delete
</Button>
</IAIButton>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>

View File

@ -1,100 +0,0 @@
.hoverable-image {
display: flex;
justify-content: center;
transition: transform 0.2s ease-out;
&:hover {
cursor: pointer;
border-radius: 0.5rem;
z-index: 2;
}
.hoverable-image-image {
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
}
.hoverable-image-delete-button {
position: absolute;
top: 0.25rem;
right: 0.25rem;
}
.hoverable-image-content {
display: flex;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
.hoverable-image-check {
fill: var(--status-good-color);
}
}
.hoverable-image-icons {
position: absolute;
bottom: -2rem;
display: grid;
width: min-content;
grid-template-columns: repeat(2, max-content);
border-radius: 0.4rem;
background-color: var(--background-color-secondary);
padding: 0.2rem;
gap: 0.2rem;
grid-auto-rows: max-content;
button {
width: 12px;
height: 12px;
border-radius: 0.2rem;
padding: 10px 0;
flex-shrink: 2;
svg {
width: 12px;
height: 12px;
}
}
}
}
.hoverable-image-context-menu {
z-index: 15;
padding: 0.4rem;
border-radius: 0.25rem;
background-color: var(--context-menu-bg-color);
box-shadow: var(--context-menu-box-shadow);
[role='menuitem'] {
font-size: 0.8rem;
line-height: 1rem;
border-radius: 3px;
display: flex;
align-items: center;
height: 1.75rem;
padding: 0 0.5rem;
position: relative;
user-select: none;
cursor: pointer;
outline: none;
&[data-disabled] {
color: grey;
pointer-events: none;
cursor: not-allowed;
}
&[data-warning] {
color: var(--status-bad-color);
}
&[data-highlighted] {
background-color: var(--context-menu-bg-color-hover);
}
}
}

View File

@ -1,9 +1,15 @@
import { Box, Icon, IconButton, Image, useToast } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import {
setCurrentImage,
setShouldHoldGalleryOpen,
} from 'features/gallery/store/gallerySlice';
Box,
Flex,
Icon,
Image,
MenuItem,
MenuList,
useTheme,
useToast,
} from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import { setCurrentImage } from 'features/gallery/store/gallerySlice';
import {
setAllImageToImageParameters,
setAllParameters,
@ -13,8 +19,7 @@ import {
import { DragEvent, memo, useState } from 'react';
import { FaCheck, FaTrashAlt } from 'react-icons/fa';
import DeleteImageModal from './DeleteImageModal';
import * as ContextMenu from '@radix-ui/react-context-menu';
import { ContextMenu } from 'chakra-ui-contextmenu';
import * as InvokeAI from 'app/invokeai';
import {
resizeAndScaleCanvas,
@ -24,6 +29,8 @@ import { hoverableImageSelector } from 'features/gallery/store/gallerySelectors'
import { setActiveTab } from 'features/ui/store/uiSlice';
import { useTranslation } from 'react-i18next';
import useSetBothPrompts from 'features/parameters/hooks/usePrompt';
import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice';
import IAIIconButton from 'common/components/IAIIconButton';
interface HoverableImageProps {
image: InvokeAI.Image;
@ -53,6 +60,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const [isHovered, setIsHovered] = useState<boolean>(false);
const toast = useToast();
const { direction } = useTheme();
const { t } = useTranslation();
const setBothPrompts = useSetBothPrompts();
@ -156,110 +164,146 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const handleLightBox = () => {
dispatch(setCurrentImage(image));
dispatch(setIsLightboxOpen(true));
};
return (
<ContextMenu.Root
onOpenChange={(open: boolean) => {
dispatch(setShouldHoldGalleryOpen(open));
}}
<ContextMenu<HTMLDivElement>
menuProps={{ size: 'sm', isLazy: true }}
renderMenu={() => (
<MenuList>
<MenuItem onClickCapture={handleLightBox}>
{t('parameters.openInViewer')}
</MenuItem>
<MenuItem
onClickCapture={handleUsePrompt}
isDisabled={image?.metadata?.image?.prompt === undefined}
>
{t('parameters.usePrompt')}
</MenuItem>
<MenuItem
onClickCapture={handleUseSeed}
isDisabled={image?.metadata?.image?.seed === undefined}
>
{t('parameters.useSeed')}
</MenuItem>
<MenuItem
onClickCapture={handleUseAllParameters}
isDisabled={
!['txt2img', 'img2img'].includes(image?.metadata?.image?.type)
}
>
{t('parameters.useAll')}
</MenuItem>
<MenuItem
onClickCapture={handleUseInitialImage}
isDisabled={image?.metadata?.image?.type !== 'img2img'}
>
{t('parameters.useInitImg')}
</MenuItem>
<MenuItem onClickCapture={handleSendToImageToImage}>
{t('parameters.sendToImg2Img')}
</MenuItem>
<MenuItem onClickCapture={handleSendToCanvas}>
{t('parameters.sendToUnifiedCanvas')}
</MenuItem>
<MenuItem data-warning>
<DeleteImageModal image={image}>
<p>{t('parameters.deleteImage')}</p>
</DeleteImageModal>
</MenuItem>
</MenuList>
)}
>
<ContextMenu.Trigger>
{(ref) => (
<Box
position="relative"
key={uuid}
className="hoverable-image"
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
userSelect="none"
draggable={true}
onDragStart={handleDragStart}
ref={ref}
sx={{
padding: 2,
display: 'flex',
justifyContent: 'center',
transition: 'transform 0.2s ease-out',
_hover: {
cursor: 'pointer',
zIndex: 2,
},
_before: { content: '""', display: 'block', paddingBottom: '100%' },
}}
>
<Image
className="hoverable-image-image"
objectFit={
shouldUseSingleGalleryColumn ? 'contain' : galleryImageObjectFit
}
rounded="md"
src={thumbnail || url}
loading="lazy"
sx={{
position: 'absolute',
width: '100%',
height: '100%',
maxWidth: '100%',
maxHeight: '100%',
top: '50%',
transform: 'translate(-50%,-50%)',
...(direction === 'rtl'
? { insetInlineEnd: '50%' }
: { insetInlineStart: '50%' }),
}}
/>
<div className="hoverable-image-content" onClick={handleSelectImage}>
<Flex
onClick={handleSelectImage}
sx={{
position: 'absolute',
top: '0',
insetInlineStart: '0',
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center',
}}
>
{isSelected && (
<Icon
width="50%"
height="50%"
as={FaCheck}
className="hoverable-image-check"
sx={{
width: '50%',
height: '50%',
fill: 'ok.500',
}}
/>
)}
</div>
</Flex>
{isHovered && galleryImageMinimumWidth >= 64 && (
<div className="hoverable-image-delete-button">
<Box
sx={{
position: 'absolute',
top: 1,
insetInlineEnd: 1,
}}
>
<DeleteImageModal image={image}>
<IconButton
<IAIIconButton
aria-label={t('parameters.deleteImage')}
icon={<FaTrashAlt />}
size="xs"
variant="imageHoverIconButton"
fontSize={14}
isDisabled={!mayDeleteImage}
/>
</DeleteImageModal>
</div>
</Box>
)}
</Box>
</ContextMenu.Trigger>
<ContextMenu.Content
className="hoverable-image-context-menu"
sticky="always"
onInteractOutside={(e) => {
e.detail.originalEvent.preventDefault();
}}
>
<ContextMenu.Item onClickCapture={handleLightBox}>
{t('parameters.openInViewer')}
</ContextMenu.Item>
<ContextMenu.Item
onClickCapture={handleUsePrompt}
disabled={image?.metadata?.image?.prompt === undefined}
>
{t('parameters.usePrompt')}
</ContextMenu.Item>
<ContextMenu.Item
onClickCapture={handleUseSeed}
disabled={image?.metadata?.image?.seed === undefined}
>
{t('parameters.useSeed')}
</ContextMenu.Item>
<ContextMenu.Item
onClickCapture={handleUseAllParameters}
disabled={
!['txt2img', 'img2img'].includes(image?.metadata?.image?.type)
}
>
{t('parameters.useAll')}
</ContextMenu.Item>
<ContextMenu.Item
onClickCapture={handleUseInitialImage}
disabled={image?.metadata?.image?.type !== 'img2img'}
>
{t('parameters.useInitImg')}
</ContextMenu.Item>
<ContextMenu.Item onClickCapture={handleSendToImageToImage}>
{t('parameters.sendToImg2Img')}
</ContextMenu.Item>
<ContextMenu.Item onClickCapture={handleSendToCanvas}>
{t('parameters.sendToUnifiedCanvas')}
</ContextMenu.Item>
<ContextMenu.Item data-warning>
<DeleteImageModal image={image}>
<p>{t('parameters.deleteImage')}</p>
</DeleteImageModal>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Root>
)}
</ContextMenu>
);
}, memoEqualityCheck);

View File

@ -0,0 +1,35 @@
.ltr-image-gallery-css-transition-enter {
transform: translateX(150%);
}
.ltr-image-gallery-css-transition-enter-active {
transform: translateX(0);
transition: all 120ms ease-out;
}
.ltr-image-gallery-css-transition-exit {
transform: translateX(0);
}
.ltr-image-gallery-css-transition-exit-active {
transform: translateX(150%);
transition: all 120ms ease-out;
}
.rtl-image-gallery-css-transition-enter {
transform: translateX(-150%);
}
.rtl-image-gallery-css-transition-enter-active {
transform: translateX(0);
transition: all 120ms ease-out;
}
.rtl-image-gallery-css-transition-exit {
transform: translateX(0);
}
.rtl-image-gallery-css-transition-exit-active {
transform: translateX(-150%);
transition: all 120ms ease-out;
}

View File

@ -1,187 +0,0 @@
@use '../../../styles/Mixins/' as *;
.image-gallery-wrapper-enter {
transform: translateX(150%);
}
.image-gallery-wrapper-enter-active {
transform: translateX(0);
transition: all 120ms ease-out;
}
.image-gallery-wrapper-exit {
transform: translateX(0);
}
.image-gallery-wrapper-exit-active {
transform: translateX(150%);
transition: all 120ms ease-out;
}
.image-gallery-wrapper {
&[data-pinned='false'] {
position: fixed;
height: 100vh;
top: 0;
right: 0;
.image-gallery-popup {
border-radius: 0;
box-shadow: 0 0 1rem var(--text-color-a3);
.image-gallery-container {
max-height: calc($app-height + 5rem);
}
}
}
.image-gallery-popup {
background-color: var(--background-color-secondary);
padding: 1rem;
display: flex;
flex-direction: column;
row-gap: 1rem;
border-radius: 0.5rem;
border-left-width: 0.3rem;
border-color: var(--tab-list-text-inactive);
&[data-resize-alert='true'] {
border-color: var(--status-bad-color);
}
.image-gallery-header {
display: flex;
align-items: center;
column-gap: 0.5rem;
justify-content: space-between;
.image-gallery-header-right-icons {
display: flex;
flex-direction: row;
column-gap: 0.5rem;
}
.image-gallery-icon-btn {
background-color: var(--btn-load-more);
&:hover {
background-color: var(--btn-load-more-hover);
}
}
.image-gallery-settings-popover {
display: flex;
flex-direction: column;
row-gap: 0.5rem;
div {
display: flex;
column-gap: 0.5rem;
align-items: center;
justify-content: space-between;
}
}
h1 {
font-weight: bold;
}
}
.image-gallery-container {
display: flex;
flex-direction: column;
max-height: $app-gallery-popover-height;
overflow-y: scroll;
@include HideScrollbar;
.image-gallery-container-placeholder {
display: flex;
flex-direction: column;
row-gap: 0.5rem;
background-color: var(--background-color);
border-radius: 0.5rem;
place-items: center;
padding: 2rem;
text-align: center;
p {
color: var(--subtext-color-bright);
font-family: Inter;
}
svg {
width: 4rem;
height: 4rem;
color: var(--svg-color);
}
}
.image-gallery-load-more-btn {
background-color: var(--btn-load-more);
font-size: 0.85rem;
padding: 0.5rem;
margin-top: 1rem;
&:disabled {
&:hover {
background-color: var(--btn-load-more);
}
}
&:hover {
background-color: var(--btn-load-more-hover);
}
}
}
}
}
.image-gallery-category-btn-group {
width: max-content;
column-gap: 0;
justify-content: stretch;
button {
background-color: var(--btn-base-color);
&:hover {
background-color: var(--btn-base-color-hover);
}
flex-grow: 1;
&[data-selected='true'] {
background-color: var(--accent-color);
&:hover {
background-color: var(--accent-color-hover);
}
}
}
}
// from https://css-tricks.com/a-grid-of-logos-in-squares/
.image-gallery {
display: grid;
grid-gap: 0.5rem;
.hoverable-image {
padding: 0.5rem;
position: relative;
&::before {
// for apsect ratio
content: '';
display: block;
padding-bottom: 100%;
}
.hoverable-image-image {
position: absolute;
max-width: 100%;
// Alternate Version
// top: 0;
// bottom: 0;
// right: 0;
// left: 0;
// margin: auto;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
}

View File

@ -1,7 +1,14 @@
import { Button } from '@chakra-ui/button';
import { NumberSize, Resizable } from 're-resizable';
import { ButtonGroup } from '@chakra-ui/react';
import {
Box,
ButtonGroup,
Flex,
Grid,
Icon,
chakra,
useTheme,
} from '@chakra-ui/react';
import { requestImages } from 'app/socketio/actions';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAIButton from 'common/components/IAIButton';
@ -17,7 +24,6 @@ import {
setCurrentCategory,
setGalleryImageMinimumWidth,
setGalleryImageObjectFit,
setGalleryScrollPosition,
setGalleryWidth,
setShouldAutoSwitchToNewImages,
setShouldHoldGalleryOpen,
@ -38,12 +44,19 @@ import React, {
} from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { BiReset } from 'react-icons/bi';
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
import { FaImage, FaUser, FaWrench } from 'react-icons/fa';
import { MdPhotoLibrary } from 'react-icons/md';
import { CSSTransition } from 'react-transition-group';
import HoverableImage from './HoverableImage';
import { APP_GALLERY_HEIGHT_PINNED } from 'theme/util/constants';
import './ImageGallery.css';
import { no_scrollbar } from 'theme/components/scrollbar';
const ChakraResizeable = chakra(Resizable, {
shouldForwardProp: (prop) => !['sx'].includes(prop),
});
const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 320;
const GALLERY_IMAGE_WIDTH_OFFSET = 40;
@ -64,6 +77,7 @@ const LIGHTBOX_GALLERY_WIDTH = 400;
export default function ImageGallery() {
const dispatch = useAppDispatch();
const { direction } = useTheme();
const { t } = useTranslation();
@ -73,7 +87,6 @@ export default function ImageGallery() {
currentImageUuid,
shouldPinGallery,
shouldShowGallery,
galleryScrollPosition,
galleryImageMinimumWidth,
galleryGridTemplateColumns,
activeTabName,
@ -107,12 +120,11 @@ export default function ImageGallery() {
const timeoutIdRef = useRef<number | null>(null);
useEffect(() => {
if (galleryWidth >= GALLERY_SHOW_BUTTONS_MIN_WIDTH) {
setShouldShowButtons(false);
}
setShouldShowButtons(galleryWidth >= GALLERY_SHOW_BUTTONS_MIN_WIDTH);
}, [galleryWidth]);
const handleSetShouldPinGallery = () => {
!shouldPinGallery && dispatch(setShouldShowGallery(true));
dispatch(setShouldPinGallery(!shouldPinGallery));
dispatch(setDoesCanvasNeedScaling(true));
};
@ -129,11 +141,6 @@ export default function ImageGallery() {
const handleCloseGallery = useCallback(() => {
dispatch(setShouldShowGallery(false));
dispatch(setShouldHoldGalleryOpen(false));
dispatch(
setGalleryScrollPosition(
galleryContainerRef.current ? galleryContainerRef.current.scrollTop : 0
)
);
setTimeout(
() => shouldPinGallery && dispatch(setDoesCanvasNeedScaling(true)),
400
@ -239,12 +246,6 @@ export default function ImageGallery() {
[galleryImageMinimumWidth]
);
// set gallery scroll position
useEffect(() => {
if (!galleryContainerRef.current) return;
galleryContainerRef.current.scrollTop = galleryScrollPosition;
}, [galleryScrollPosition, shouldShowGallery]);
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (
@ -267,29 +268,63 @@ export default function ImageGallery() {
in={shouldShowGallery || shouldHoldGalleryOpen}
unmountOnExit
timeout={200}
classNames="image-gallery-wrapper"
classNames={`${direction}-image-gallery-css-transition`}
>
<div
className="image-gallery-wrapper"
style={{ zIndex: shouldPinGallery ? 1 : 100 }}
data-pinned={shouldPinGallery}
<Box
className={`${direction}-image-gallery-css-transition`}
sx={
shouldPinGallery
? { zIndex: 1, insetInlineEnd: 0 }
: {
zIndex: 100,
position: 'fixed',
height: '100vh',
top: 0,
insetInlineEnd: 0,
}
}
ref={galleryRef}
onMouseLeave={!shouldPinGallery ? setCloseGalleryTimer : undefined}
onMouseEnter={!shouldPinGallery ? cancelCloseGalleryTimer : undefined}
onMouseOver={!shouldPinGallery ? cancelCloseGalleryTimer : undefined}
>
<Resizable
<ChakraResizeable
sx={{
padding: 4,
display: 'flex',
flexDirection: 'column',
rowGap: 4,
borderRadius: shouldPinGallery ? 'base' : 0,
borderInlineStartWidth: 5,
// boxShadow: '0 0 1rem blackAlpha.700',
bg: 'base.850',
borderColor: 'base.700',
}}
minWidth={galleryMinWidth}
maxWidth={shouldPinGallery ? galleryMaxWidth : window.innerWidth}
className="image-gallery-popup"
handleStyles={{
left: {
width: '15px',
},
}}
enable={{
left: shouldEnableResize,
}}
data-pinned={shouldPinGallery}
handleStyles={
direction === 'rtl'
? {
right: {
width: '15px',
},
}
: {
left: {
width: '15px',
},
}
}
enable={
direction === 'rtl'
? {
right: shouldEnableResize,
}
: {
left: shouldEnableResize,
}
}
size={{
width: galleryWidth,
height: shouldPinGallery ? '100%' : '100vh',
@ -305,7 +340,7 @@ export default function ImageGallery() {
elementRef.style.height = `${elementRef.clientHeight}px`;
if (shouldPinGallery) {
elementRef.style.position = 'fixed';
elementRef.style.right = '1rem';
elementRef.style.insetInlineEnd = '1rem';
setIsResizing(true);
}
}}
@ -327,8 +362,9 @@ export default function ImageGallery() {
elementRef.removeAttribute('data-resize-alert');
if (shouldPinGallery) {
console.log('unpin');
elementRef.style.position = 'relative';
elementRef.style.removeProperty('right');
elementRef.style.removeProperty('inset-inline-end');
elementRef.style.setProperty(
'height',
shouldPinGallery ? '100%' : '100vh'
@ -385,26 +421,28 @@ export default function ImageGallery() {
elementRef.style.height = `${galleryResizeHeight}px`;
}}
>
<div className="image-gallery-header">
<Flex alignItems="center" gap={2} justifyContent="space-between">
<ButtonGroup
size="sm"
isAttached
variant="solid"
className="image-gallery-category-btn-group"
w="max-content"
justifyContent="stretch"
>
{shouldShowButtons ? (
<>
<IAIButton
size="sm"
data-selected={currentCategory === 'result'}
isChecked={currentCategory === 'result'}
onClick={() => dispatch(setCurrentCategory('result'))}
flexGrow={1}
>
{t('gallery.generations')}
</IAIButton>
<IAIButton
size="sm"
data-selected={currentCategory === 'user'}
isChecked={currentCategory === 'user'}
onClick={() => dispatch(setCurrentCategory('user'))}
flexGrow={1}
>
{t('gallery.uploads')}
</IAIButton>
@ -414,14 +452,14 @@ export default function ImageGallery() {
<IAIIconButton
aria-label={t('gallery.showGenerations')}
tooltip={t('gallery.showGenerations')}
data-selected={currentCategory === 'result'}
isChecked={currentCategory === 'result'}
icon={<FaImage />}
onClick={() => dispatch(setCurrentCategory('result'))}
/>
<IAIIconButton
aria-label={t('gallery.showUploads')}
tooltip={t('gallery.showUploads')}
data-selected={currentCategory === 'user'}
isChecked={currentCategory === 'user'}
icon={<FaUser />}
onClick={() => dispatch(setCurrentCategory('user'))}
/>
@ -429,96 +467,85 @@ export default function ImageGallery() {
)}
</ButtonGroup>
<div className="image-gallery-header-right-icons">
<Flex gap={2}>
<IAIPopover
isLazy
trigger="hover"
placement="left"
triggerComponent={
<IAIIconButton
size="sm"
aria-label={t('gallery.gallerySettings')}
icon={<FaWrench />}
className="image-gallery-icon-btn"
cursor="pointer"
/>
}
>
<div className="image-gallery-settings-popover">
<div>
<IAISlider
value={galleryImageMinimumWidth}
onChange={handleChangeGalleryImageMinimumWidth}
min={32}
max={256}
hideTooltip={true}
label={t('gallery.galleryImageSize')}
/>
<IAIIconButton
size="sm"
aria-label={t('gallery.galleryImageResetSize')}
tooltip={t('gallery.galleryImageResetSize')}
onClick={() => dispatch(setGalleryImageMinimumWidth(64))}
icon={<BiReset />}
data-selected={shouldPinGallery}
styleClass="image-gallery-icon-btn"
/>
</div>
<div>
<IAICheckbox
label={t('gallery.maintainAspectRatio')}
isChecked={galleryImageObjectFit === 'contain'}
onChange={() =>
dispatch(
setGalleryImageObjectFit(
galleryImageObjectFit === 'contain'
? 'cover'
: 'contain'
)
<Flex direction="column" gap={2}>
<IAISlider
value={galleryImageMinimumWidth}
onChange={handleChangeGalleryImageMinimumWidth}
min={32}
max={256}
hideTooltip={true}
label={t('gallery.galleryImageSize')}
withReset
handleReset={() =>
dispatch(setGalleryImageMinimumWidth(64))
}
/>
<IAICheckbox
label={t('gallery.maintainAspectRatio')}
isChecked={galleryImageObjectFit === 'contain'}
onChange={() =>
dispatch(
setGalleryImageObjectFit(
galleryImageObjectFit === 'contain'
? 'cover'
: 'contain'
)
}
/>
</div>
<div>
<IAICheckbox
label={t('gallery.autoSwitchNewImages')}
isChecked={shouldAutoSwitchToNewImages}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
dispatch(
setShouldAutoSwitchToNewImages(e.target.checked)
)
}
/>
</div>
<div>
<IAICheckbox
label={t('gallery.singleColumnLayout')}
isChecked={shouldUseSingleGalleryColumn}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
dispatch(
setShouldUseSingleGalleryColumn(e.target.checked)
)
}
/>
</div>
</div>
)
}
/>
<IAICheckbox
label={t('gallery.autoSwitchNewImages')}
isChecked={shouldAutoSwitchToNewImages}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldAutoSwitchToNewImages(e.target.checked))
}
/>
<IAICheckbox
label={t('gallery.singleColumnLayout')}
isChecked={shouldUseSingleGalleryColumn}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
dispatch(
setShouldUseSingleGalleryColumn(e.target.checked)
)
}
/>
</Flex>
</IAIPopover>
<IAIIconButton
size="sm"
className="image-gallery-icon-btn"
aria-label={t('gallery.pinGallery')}
tooltip={`${t('gallery.pinGallery')} (Shift+G)`}
onClick={handleSetShouldPinGallery}
icon={shouldPinGallery ? <BsPinAngleFill /> : <BsPinAngle />}
/>
</div>
</div>
<div className="image-gallery-container" ref={galleryContainerRef}>
</Flex>
</Flex>
<Flex
direction="column"
gap={2}
h={shouldPinGallery ? APP_GALLERY_HEIGHT_PINNED : '100vh'}
maxH={shouldPinGallery ? APP_GALLERY_HEIGHT_PINNED : '100vh'}
overflowY="scroll"
ref={galleryContainerRef}
sx={{
...no_scrollbar,
}}
>
{images.length || areMoreImagesAvailable ? (
<>
<div
className="image-gallery"
<Grid
gap={2}
style={{ gridTemplateColumns: galleryGridTemplateColumns }}
>
{images.map((image) => {
@ -532,34 +559,51 @@ export default function ImageGallery() {
/>
);
})}
</div>
<Button
</Grid>
<IAIButton
onClick={handleClickLoadMore}
isDisabled={!areMoreImagesAvailable}
className="image-gallery-load-more-btn"
flexShrink={0}
>
{areMoreImagesAvailable
? t('gallery.loadMore')
: t('gallery.allImagesLoaded')}
</Button>
</IAIButton>
</>
) : (
<div className="image-gallery-container-placeholder">
<MdPhotoLibrary />
<Flex
sx={{
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 2,
padding: 8,
h: '100%',
w: '100%',
color: 'base.500',
}}
>
<Icon
as={MdPhotoLibrary}
sx={{
w: 16,
h: 16,
}}
/>
<p>{t('gallery.noImagesInGallery')}</p>
</div>
</Flex>
)}
</div>
</Resizable>
</Flex>
</ChakraResizeable>
{isResizing && (
<div
<Box
style={{
width: `${galleryWidth}px`,
height: '100%',
}}
/>
)}
</div>
</Box>
</CSSTransition>
);
}

View File

@ -1,23 +0,0 @@
@use '../../../../styles/Mixins/' as *;
.image-metadata-viewer {
position: absolute;
top: 0;
width: 100%;
border-radius: 0.5rem;
padding: 1rem;
background-color: var(--metadata-bg-color);
overflow: scroll;
max-height: $app-metadata-height;
height: 100%;
z-index: 10;
}
.image-json-viewer {
border-radius: 0.5rem;
margin: 0 0.5rem 1rem 0.5rem;
padding: 1rem;
overflow-x: scroll;
word-break: break-all;
background-color: var(--metadata-json-bg-color);
}

View File

@ -1,5 +1,6 @@
import { ExternalLinkIcon } from '@chakra-ui/icons';
import {
Box,
Center,
Flex,
Heading,
@ -43,6 +44,7 @@ import { memo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { FaCopy } from 'react-icons/fa';
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
import { APP_METADATA_HEIGHT } from 'theme/util/constants';
type MetadataItemProps = {
isLink?: boolean;
@ -163,7 +165,22 @@ const ImageMetadataViewer = memo(
const metadataJSON = JSON.stringify(image.metadata, null, 2);
return (
<div className={`image-metadata-viewer ${styleClass}`}>
<Box
className={styleClass}
sx={{
position: 'absolute',
top: '0',
width: '100%',
borderRadius: 'base',
padding: 4,
overflow: 'scroll',
maxHeight: APP_METADATA_HEIGHT,
height: '100%',
zIndex: '10',
backdropFilter: 'blur(10px)',
bg: 'blackAlpha.600',
}}
>
<Flex gap={1} direction="column" width="100%">
<Flex gap={2}>
<Text fontWeight="semibold">File:</Text>
@ -316,7 +333,7 @@ const ImageMetadataViewer = memo(
if (postprocess.type === 'esrgan') {
const { scale, strength, denoise_str } = postprocess;
return (
<Flex key={i} pl="2rem" gap={1} direction="column">
<Flex key={i} pl={8} gap={1} direction="column">
<Text size="md">{`${
i + 1
}: Upscale (ESRGAN)`}</Text>
@ -346,7 +363,7 @@ const ImageMetadataViewer = memo(
} else if (postprocess.type === 'gfpgan') {
const { strength } = postprocess;
return (
<Flex key={i} pl="2rem" gap={1} direction="column">
<Flex key={i} pl={8} gap={1} direction="column">
<Text size="md">{`${
i + 1
}: Face restoration (GFPGAN)`}</Text>
@ -364,7 +381,7 @@ const ImageMetadataViewer = memo(
} else if (postprocess.type === 'codeformer') {
const { strength, fidelity } = postprocess;
return (
<Flex key={i} pl="2rem" gap={1} direction="column">
<Flex key={i} pl={8} gap={1} direction="column">
<Text size="md">{`${
i + 1
}: Face restoration (Codeformer)`}</Text>
@ -417,9 +434,21 @@ const ImageMetadataViewer = memo(
</Tooltip>
<Text fontWeight="semibold">Metadata JSON:</Text>
</Flex>
<div className="image-json-viewer">
<Box
sx={{
mt: 0,
mr: 2,
mb: 4,
ml: 2,
padding: 4,
borderRadius: 'base',
overflowX: 'scroll',
wordBreak: 'break-all',
bg: 'whiteAlpha.100',
}}
>
<pre>{metadataJSON}</pre>
</div>
</Box>
</Flex>
</>
) : (
@ -430,7 +459,7 @@ const ImageMetadataViewer = memo(
</Center>
)}
</Flex>
</div>
</Box>
);
},
memoEqualityCheck

View File

@ -0,0 +1,130 @@
import { ChakraProps, Flex, Grid, IconButton } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import { isEqual } from 'lodash';
import { useState } from 'react';
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
import { gallerySelector } from '../store/gallerySelectors';
import {
GalleryCategory,
selectNextImage,
selectPrevImage,
} from '../store/gallerySlice';
const nextPrevButtonTriggerAreaStyles: ChakraProps['sx'] = {
height: '100%',
width: '15%',
alignItems: 'center',
pointerEvents: 'auto',
};
const nextPrevButtonStyles: ChakraProps['sx'] = {
color: 'base.100',
};
export const nextPrevImageButtonsSelector = createSelector(
gallerySelector,
(gallery) => {
const { currentImage } = gallery;
const tempImages =
gallery.categories[
currentImage ? (currentImage.category as GalleryCategory) : 'result'
].images;
const currentImageIndex = tempImages.findIndex(
(i) => i.uuid === gallery?.currentImage?.uuid
);
const imagesLength = tempImages.length;
return {
isOnFirstImage: currentImageIndex === 0,
isOnLastImage:
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
};
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
const NextPrevImageButtons = () => {
const dispatch = useAppDispatch();
const { isOnFirstImage, isOnLastImage } = useAppSelector(
nextPrevImageButtonsSelector
);
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] =
useState<boolean>(false);
const handleCurrentImagePreviewMouseOver = () => {
setShouldShowNextPrevButtons(true);
};
const handleCurrentImagePreviewMouseOut = () => {
setShouldShowNextPrevButtons(false);
};
const handleClickPrevButton = () => {
dispatch(selectPrevImage());
};
const handleClickNextButton = () => {
dispatch(selectNextImage());
};
return (
<Flex
sx={{
justifyContent: 'space-between',
zIndex: 1,
height: '100%',
width: '100%',
pointerEvents: 'none',
}}
>
<Grid
sx={{
...nextPrevButtonTriggerAreaStyles,
justifyContent: 'flex-start',
}}
onMouseOver={handleCurrentImagePreviewMouseOver}
onMouseOut={handleCurrentImagePreviewMouseOut}
>
{shouldShowNextPrevButtons && !isOnFirstImage && (
<IconButton
aria-label="Previous image"
icon={<FaAngleLeft size={64} />}
variant="unstyled"
onClick={handleClickPrevButton}
boxSize={16}
sx={nextPrevButtonStyles}
/>
)}
</Grid>
<Grid
sx={{
...nextPrevButtonTriggerAreaStyles,
justifyContent: 'flex-end',
}}
onMouseOver={handleCurrentImagePreviewMouseOver}
onMouseOut={handleCurrentImagePreviewMouseOut}
>
{shouldShowNextPrevButtons && !isOnLastImage && (
<IconButton
aria-label="Next image"
icon={<FaAngleRight size={64} />}
variant="unstyled"
onClick={handleClickNextButton}
boxSize={16}
sx={nextPrevButtonStyles}
/>
)}
</Grid>
</Flex>
);
};
export default NextPrevImageButtons;

View File

@ -19,7 +19,6 @@ export const imageGallerySelector = createSelector(
currentImageUuid,
shouldPinGallery,
shouldShowGallery,
galleryScrollPosition,
galleryImageMinimumWidth,
galleryImageObjectFit,
shouldHoldGalleryOpen,
@ -34,7 +33,6 @@ export const imageGallerySelector = createSelector(
currentImageUuid,
shouldPinGallery,
shouldShowGallery,
galleryScrollPosition,
galleryImageMinimumWidth,
galleryImageObjectFit,
galleryGridTemplateColumns: shouldUseSingleGalleryColumn

View File

@ -31,7 +31,6 @@ export interface GalleryState {
};
shouldPinGallery: boolean;
shouldShowGallery: boolean;
galleryScrollPosition: number;
galleryImageMinimumWidth: number;
galleryImageObjectFit: GalleryImageObjectFitType;
shouldHoldGalleryOpen: boolean;
@ -49,7 +48,6 @@ const initialState: GalleryState = {
currentImageUuid: '',
shouldPinGallery: true,
shouldShowGallery: true,
galleryScrollPosition: 0,
galleryImageMinimumWidth: 64,
galleryImageObjectFit: 'cover',
shouldHoldGalleryOpen: false,
@ -242,9 +240,6 @@ export const gallerySlice = createSlice({
state.shouldShowGallery = action.payload;
},
setGalleryScrollPosition: (state, action: PayloadAction<number>) => {
state.galleryScrollPosition = action.payload;
},
setGalleryImageMinimumWidth: (state, action: PayloadAction<number>) => {
state.galleryImageMinimumWidth = action.payload;
},
@ -286,7 +281,6 @@ export const {
selectPrevImage,
setShouldPinGallery,
setShouldShowGallery,
setGalleryScrollPosition,
setGalleryImageMinimumWidth,
setGalleryImageObjectFit,
setShouldHoldGalleryOpen,

View File

@ -1,89 +0,0 @@
@use '../../../styles/Mixins/' as *;
.lightbox-container {
width: 100%;
height: 100%;
color: var(--text-color);
overflow: hidden;
position: absolute;
left: 0;
top: 0;
background-color: var(--background-color-secondary);
z-index: 30;
animation: popIn 0.3s ease-in;
.image-gallery-wrapper {
max-height: 100% !important;
.image-gallery-container {
max-height: calc(100vh - 5rem);
}
}
.current-image-options {
z-index: 2;
position: absolute;
top: 1rem;
}
.image-metadata-viewer {
left: 0;
max-height: 100%;
}
}
.lightbox-close-btn {
z-index: 3;
position: absolute;
left: 1rem;
top: 1rem;
@include BaseButton;
}
.lightbox-display-container {
display: flex;
flex-direction: row;
}
.lightbox-preview-wrapper {
overflow: hidden;
background-color: var(--background-color-secondary);
display: grid;
grid-template-columns: auto max-content;
place-items: center;
width: 100vw;
height: 100vh;
.current-image-next-prev-buttons {
position: absolute;
}
.lightbox-image {
grid-area: lightbox-content;
border-radius: 0.5rem;
}
.lightbox-image-options {
position: absolute;
z-index: 2;
left: 1rem;
top: 4.5rem;
user-select: none;
border-radius: 0.5rem;
display: flex;
flex-direction: column;
row-gap: 0.5rem;
}
}
@keyframes popIn {
from {
opacity: 0;
filter: blur(100);
}
to {
opacity: 1;
filter: blur(0);
}
}

View File

@ -1,21 +1,40 @@
import { IconButton } from '@chakra-ui/react';
import { Box, Flex, Grid } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import CurrentImageButtons from 'features/gallery/components/CurrentImageButtons';
import { imagesSelector } from 'features/gallery/components/CurrentImagePreview';
import ImageGallery from 'features/gallery/components/ImageGallery';
import ImageMetadataViewer from 'features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer';
import {
selectNextImage,
selectPrevImage,
} from 'features/gallery/store/gallerySlice';
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
import { gallerySelector } from 'features/gallery/store/gallerySelectors';
import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice';
import { useState } from 'react';
import { uiSelector } from 'features/ui/store/uiSelectors';
import { isEqual } from 'lodash';
import { useHotkeys } from 'react-hotkeys-hook';
import { BiExit } from 'react-icons/bi';
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
import ReactPanZoom from './ReactPanZoom';
import { TransformWrapper } from 'react-zoom-pan-pinch';
import useImageTransform from '../hooks/useImageTransform';
import ReactPanZoomButtons from './ReactPanZoomButtons';
import ReactPanZoomImage from './ReactPanZoomImage';
export const lightboxSelector = createSelector(
[gallerySelector, uiSelector],
(gallery, ui) => {
const { currentImage } = gallery;
const { shouldShowImageDetails } = ui;
return {
viewerImageToDisplay: currentImage,
shouldShowImageDetails,
};
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
export default function Lightbox() {
const dispatch = useAppDispatch();
@ -24,30 +43,18 @@ export default function Lightbox() {
);
const {
viewerImageToDisplay,
shouldShowImageDetails,
isOnFirstImage,
isOnLastImage,
} = useAppSelector(imagesSelector);
rotation,
scaleX,
scaleY,
flipHorizontally,
flipVertically,
rotateCounterClockwise,
rotateClockwise,
reset,
} = useImageTransform();
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] =
useState<boolean>(false);
const handleCurrentImagePreviewMouseOver = () => {
setShouldShowNextPrevButtons(true);
};
const handleCurrentImagePreviewMouseOut = () => {
setShouldShowNextPrevButtons(false);
};
const handleClickPrevButton = () => {
dispatch(selectPrevImage());
};
const handleClickNextButton = () => {
dispatch(selectNextImage());
};
const { viewerImageToDisplay, shouldShowImageDetails } =
useAppSelector(lightboxSelector);
useHotkeys(
'Esc',
@ -58,66 +65,106 @@ export default function Lightbox() {
);
return (
<div className="lightbox-container">
<IAIIconButton
icon={<BiExit />}
aria-label="Exit Viewer"
className="lightbox-close-btn"
onClick={() => {
dispatch(setIsLightboxOpen(false));
<TransformWrapper
centerOnInit
minScale={0.1}
initialPositionX={50}
initialPositionY={50}
>
<Box
sx={{
width: '100%',
height: '100%',
overflow: 'hidden',
position: 'absolute',
insetInlineStart: 0,
top: 0,
zIndex: 30,
animation: 'popIn 0.3s ease-in',
bg: 'base.800',
}}
fontSize={20}
/>
>
<Flex
sx={{
flexDir: 'column',
position: 'absolute',
top: 4,
insetInlineStart: 4,
gap: 4,
zIndex: 3,
}}
>
<IAIIconButton
icon={<BiExit />}
aria-label="Exit Viewer"
onClick={() => {
dispatch(setIsLightboxOpen(false));
}}
fontSize={20}
/>
<ReactPanZoomButtons
flipHorizontally={flipHorizontally}
flipVertically={flipVertically}
rotateCounterClockwise={rotateCounterClockwise}
rotateClockwise={rotateClockwise}
reset={reset}
/>
</Flex>
<div className="lightbox-display-container">
<div className="lightbox-preview-wrapper">
<CurrentImageButtons />
{!shouldShowImageDetails && (
<div className="current-image-next-prev-buttons">
<div
className="next-prev-button-trigger-area prev-button-trigger-area"
onMouseOver={handleCurrentImagePreviewMouseOver}
onMouseOut={handleCurrentImagePreviewMouseOut}
>
{shouldShowNextPrevButtons && !isOnFirstImage && (
<IconButton
aria-label="Previous image"
icon={<FaAngleLeft className="next-prev-button" />}
variant="unstyled"
onClick={handleClickPrevButton}
/>
<Flex>
<Grid
sx={{
overflow: 'hidden',
gridTemplateColumns: 'auto max-content',
placeItems: 'center',
width: '100vw',
height: '100vh',
bg: 'base.850',
}}
>
{viewerImageToDisplay && (
<>
<ReactPanZoomImage
rotation={rotation}
scaleX={scaleX}
scaleY={scaleY}
image={viewerImageToDisplay.url}
styleClass="lightbox-image"
/>
{shouldShowImageDetails && (
<ImageMetadataViewer image={viewerImageToDisplay} />
)}
</div>
<div
className="next-prev-button-trigger-area next-button-trigger-area"
onMouseOver={handleCurrentImagePreviewMouseOver}
onMouseOut={handleCurrentImagePreviewMouseOut}
</>
)}
{!shouldShowImageDetails && (
<Box
sx={{
position: 'absolute',
top: 0,
insetInlineStart: 0,
w: `calc(100vw - ${8 * 2 * 4}px)`,
h: '100vh',
mx: 8,
pointerEvents: 'none',
}}
>
{shouldShowNextPrevButtons && !isOnLastImage && (
<IconButton
aria-label="Next image"
icon={<FaAngleRight className="next-prev-button" />}
variant="unstyled"
onClick={handleClickNextButton}
/>
)}
</div>
</div>
)}
{viewerImageToDisplay && (
<>
<ReactPanZoom
image={viewerImageToDisplay.url}
styleClass="lightbox-image"
/>
{shouldShowImageDetails && (
<ImageMetadataViewer image={viewerImageToDisplay} />
)}
</>
)}
</div>
<ImageGallery />
</div>
</div>
<NextPrevImageButtons />
</Box>
)}
<Box
sx={{
position: 'absolute',
top: 4,
}}
>
<CurrentImageButtons />
</Box>
</Grid>
<ImageGallery />
</Flex>
</Box>
</TransformWrapper>
);
}

View File

@ -1,135 +0,0 @@
import IAIIconButton from 'common/components/IAIIconButton';
import * as React from 'react';
import {
BiReset,
BiRotateLeft,
BiRotateRight,
BiZoomIn,
BiZoomOut,
} from 'react-icons/bi';
import { MdFlip } from 'react-icons/md';
import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch';
type ReactPanZoomProps = {
image: string;
styleClass?: string;
alt?: string;
ref?: React.Ref<HTMLImageElement>;
};
export default function ReactPanZoom({
image,
alt,
ref,
styleClass,
}: ReactPanZoomProps) {
const [rotation, setRotation] = React.useState(0);
const [flip, setFlip] = React.useState(false);
const rotateLeft = () => {
if (rotation === -3) {
setRotation(0);
} else {
setRotation(rotation - 1);
}
};
const rotateRight = () => {
if (rotation === 3) {
setRotation(0);
} else {
setRotation(rotation + 1);
}
};
const flipImage = () => {
setFlip(!flip);
};
return (
<TransformWrapper
centerOnInit
minScale={0.1}
initialPositionX={50}
initialPositionY={50}
>
{({ zoomIn, zoomOut, resetTransform, centerView }) => (
<>
<div className="lightbox-image-options">
<IAIIconButton
icon={<BiZoomIn />}
aria-label="Zoom In"
tooltip="Zoom In"
onClick={() => zoomIn()}
fontSize={20}
/>
<IAIIconButton
icon={<BiZoomOut />}
aria-label="Zoom Out"
tooltip="Zoom Out"
onClick={() => zoomOut()}
fontSize={20}
/>
<IAIIconButton
icon={<BiRotateLeft />}
aria-label="Rotate Left"
tooltip="Rotate Left"
onClick={rotateLeft}
fontSize={20}
/>
<IAIIconButton
icon={<BiRotateRight />}
aria-label="Rotate Right"
tooltip="Rotate Right"
onClick={rotateRight}
fontSize={20}
/>
<IAIIconButton
icon={<MdFlip />}
aria-label="Flip Image"
tooltip="Flip Image"
onClick={flipImage}
fontSize={20}
/>
<IAIIconButton
icon={<BiReset />}
aria-label="Reset"
tooltip="Reset"
onClick={() => {
resetTransform();
setRotation(0);
setFlip(false);
}}
fontSize={20}
/>
</div>
<TransformComponent
wrapperStyle={{
width: '100%',
height: '100%',
}}
>
<img
style={{
transform: `rotate(${rotation * 90}deg) scaleX(${
flip ? -1 : 1
})`,
width: '100%',
}}
src={image}
alt={alt}
ref={ref}
className={styleClass ? styleClass : ''}
onLoad={() => centerView(1, 0, 'easeOut')}
/>
</TransformComponent>
</>
)}
</TransformWrapper>
);
}

View File

@ -0,0 +1,94 @@
import { ButtonGroup } from '@chakra-ui/react';
import IAIIconButton from 'common/components/IAIIconButton';
import {
BiReset,
BiRotateLeft,
BiRotateRight,
BiZoomIn,
BiZoomOut,
} from 'react-icons/bi';
import { MdFlip } from 'react-icons/md';
import { useTransformContext } from 'react-zoom-pan-pinch';
type ReactPanZoomButtonsProps = {
flipHorizontally: () => void;
flipVertically: () => void;
rotateCounterClockwise: () => void;
rotateClockwise: () => void;
reset: () => void;
};
const ReactPanZoomButtons = ({
flipHorizontally,
flipVertically,
rotateCounterClockwise,
rotateClockwise,
reset,
}: ReactPanZoomButtonsProps) => {
const { zoomIn, zoomOut, resetTransform } = useTransformContext();
return (
<ButtonGroup isAttached orientation="vertical">
<IAIIconButton
icon={<BiZoomIn />}
aria-label="Zoom In"
tooltip="Zoom In"
onClick={() => zoomIn()}
fontSize={20}
/>
<IAIIconButton
icon={<BiZoomOut />}
aria-label="Zoom Out"
tooltip="Zoom Out"
onClick={() => zoomOut()}
fontSize={20}
/>
<IAIIconButton
icon={<BiRotateLeft />}
aria-label="Rotate Counter-Clockwise"
tooltip="Rotate Counter-Clockwise"
onClick={rotateCounterClockwise}
fontSize={20}
/>
<IAIIconButton
icon={<BiRotateRight />}
aria-label="Rotate Clockwise"
tooltip="Rotate Clockwise"
onClick={rotateClockwise}
fontSize={20}
/>
<IAIIconButton
icon={<MdFlip />}
aria-label="Flip Horizontally"
tooltip="Flip Horizontally"
onClick={flipHorizontally}
fontSize={20}
/>
<IAIIconButton
icon={<MdFlip style={{ transform: 'rotate(90deg)' }} />}
aria-label="Flip Vertically"
tooltip="Flip Vertically"
onClick={flipVertically}
fontSize={20}
/>
<IAIIconButton
icon={<BiReset />}
aria-label="Reset"
tooltip="Reset"
onClick={() => {
resetTransform();
reset();
}}
fontSize={20}
/>
</ButtonGroup>
);
};
export default ReactPanZoomButtons;

View File

@ -0,0 +1,45 @@
import * as React from 'react';
import { TransformComponent, useTransformContext } from 'react-zoom-pan-pinch';
type ReactPanZoomProps = {
image: string;
styleClass?: string;
alt?: string;
ref?: React.Ref<HTMLImageElement>;
rotation: number;
scaleX: number;
scaleY: number;
};
export default function ReactPanZoomImage({
image,
alt,
ref,
styleClass,
rotation,
scaleX,
scaleY,
}: ReactPanZoomProps) {
const { centerView } = useTransformContext();
return (
<TransformComponent
wrapperStyle={{
width: '100%',
height: '100%',
}}
>
<img
style={{
transform: `rotate(${rotation}deg) scaleX(${scaleX}) scaleY(${scaleY})`,
width: '100%',
}}
src={image}
alt={alt}
ref={ref}
className={styleClass ? styleClass : ''}
onLoad={() => centerView(1, 0, 'easeOut')}
/>
</TransformComponent>
);
}

View File

@ -0,0 +1,50 @@
import { useState } from 'react';
const useImageTransform = () => {
const [rotation, setRotation] = useState(0);
const [scaleX, setScaleX] = useState(1);
const [scaleY, setScaleY] = useState(1);
const rotateCounterClockwise = () => {
if (rotation === -270) {
setRotation(0);
} else {
setRotation(rotation - 90);
}
};
const rotateClockwise = () => {
if (rotation === 270) {
setRotation(0);
} else {
setRotation(rotation + 90);
}
};
const flipHorizontally = () => {
setScaleX(scaleX * -1);
};
const flipVertically = () => {
setScaleY(scaleY * -1);
};
const reset = () => {
setRotation(0);
setScaleX(1);
setScaleY(1);
};
return {
rotation,
scaleX,
scaleY,
flipHorizontally,
flipVertically,
rotateCounterClockwise,
rotateClockwise,
reset,
};
};
export default useImageTransform;

View File

@ -1,54 +0,0 @@
@use '../../../../styles/Mixins/' as *;
.advanced-parameters {
padding-top: 0.5rem;
display: grid;
row-gap: 0.5rem;
}
.advanced-parameters-item {
display: grid;
max-width: $options-bar-max-width;
border: none;
border-top: 0px;
border-radius: 0.4rem;
background-color: var(--tab-panel-bg);
&[aria-expanded='true'] {
background-color: var(--tab-hover-color);
border-radius: 0 0 0.4rem 0.4rem;
}
}
.advanced-parameters-panel {
background-color: var(--tab-panel-bg);
border-radius: 0 0 0.4rem 0.4rem;
padding: 1rem;
button {
background-color: var(--btn-base-color);
&:hover {
background-color: var(--btn-base-color-hover);
}
&:disabled {
&:hover {
background-color: var(--btn-base-color);
}
}
}
}
.advanced-parameters-header {
border-radius: 0.4rem;
font-weight: bold;
&[aria-expanded='true'] {
background-color: var(--tab-hover-color);
border-radius: 0.4rem 0.4rem 0 0;
}
&:hover {
background-color: var(--tab-hover-color);
}
}

View File

@ -21,10 +21,10 @@ export default function InvokeAccordionItem(props: InvokeAccordionItemProps) {
const { header, feature, content, additionalHeaderComponents } = props;
return (
<AccordionItem className="advanced-parameters-item">
<AccordionButton className="advanced-parameters-header">
<Flex width="100%" gap="0.5rem" align="center">
<Box flexGrow={1} textAlign="left">
<AccordionItem>
<AccordionButton>
<Flex width="100%" gap={2} align="center">
<Box flexGrow={1} textAlign="start">
{header}
</Box>
{additionalHeaderComponents}
@ -32,9 +32,7 @@ export default function InvokeAccordionItem(props: InvokeAccordionItemProps) {
<AccordionIcon />
</Flex>
</AccordionButton>
<AccordionPanel className="advanced-parameters-panel">
{content}
</AccordionPanel>
<AccordionPanel>{content}</AccordionPanel>
</AccordionItem>
);
}

View File

@ -1,53 +0,0 @@
.inpainting-bounding-box-settings {
display: flex;
flex-direction: column;
border-radius: 0.4rem;
border: 2px solid var(--tab-color);
}
.inpainting-bounding-box-header {
background-color: var(--tab-color);
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 0.5rem 1rem;
border-radius: 0.3rem 0.3rem 0 0;
align-items: center;
button {
width: 0.5rem;
height: 1.2rem;
background: none;
&:hover {
background: none;
}
}
p {
// font-weight: bold;
}
}
.inpainting-bounding-box-settings-items {
padding: 1rem;
display: flex;
flex-direction: column;
row-gap: 1rem;
.inpainting-bounding-box-reset-icon-btn {
background-color: var(--btn-base-color);
&:hover {
background-color: var(--btn-base-color-hover);
}
}
}
.inpainting-bounding-box-dimensions-slider-numberinput {
display: grid;
grid-template-columns: repeat(3, auto);
column-gap: 1rem;
}
.inpainting-bounding-box-darken {
width: max-content;
}

View File

@ -1,4 +1,4 @@
import { Box, Flex } from '@chakra-ui/react';
import { Box, VStack } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISlider from 'common/components/IAISlider';
@ -68,7 +68,7 @@ const BoundingBoxSettings = () => {
};
return (
<Flex direction="column" gap={2}>
<VStack gap={2} alignItems="stretch">
<IAISlider
label={t('parameters.width')}
min={64}
@ -82,7 +82,6 @@ const BoundingBoxSettings = () => {
inputReadOnly
withReset
handleReset={handleResetWidth}
sliderMarkRightOffset={-7}
/>
<IAISlider
label={t('parameters.height')}
@ -97,9 +96,8 @@ const BoundingBoxSettings = () => {
inputReadOnly
withReset
handleReset={handleResetHeight}
sliderMarkRightOffset={-7}
/>
</Flex>
</VStack>
);
};
@ -108,7 +106,7 @@ export default BoundingBoxSettings;
export const BoundingBoxSettingsHeader = () => {
const { t } = useTranslation();
return (
<Box flex="1" textAlign="left">
<Box flex="1" textAlign="start">
{t('parameters.boundingBoxHeader')}
</Box>
);

View File

@ -1,4 +1,4 @@
import { Flex } from '@chakra-ui/react';
import { VStack } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISelect from 'common/components/IAISelect';
@ -107,7 +107,7 @@ const InfillAndScalingSettings = () => {
};
return (
<Flex direction="column" gap={4}>
<VStack gap={2} alignItems="stretch">
<IAISelect
label={t('parameters.scaleBeforeProcessing')}
validValues={BOUNDING_BOX_SCALES_DICT}
@ -130,7 +130,6 @@ const InfillAndScalingSettings = () => {
inputReadOnly
withReset
handleReset={handleResetScaledWidth}
sliderMarkRightOffset={-7}
/>
<IAISlider
isInputDisabled={!isManual}
@ -148,7 +147,6 @@ const InfillAndScalingSettings = () => {
inputReadOnly
withReset
handleReset={handleResetScaledHeight}
sliderMarkRightOffset={-7}
/>
<IAISelect
label={t('parameters.infillMethod')}
@ -160,7 +158,6 @@ const InfillAndScalingSettings = () => {
isInputDisabled={infillMethod !== 'tile'}
isResetDisabled={infillMethod !== 'tile'}
isSliderDisabled={infillMethod !== 'tile'}
sliderMarkRightOffset={-4}
label={t('parameters.tileSize')}
min={16}
max={64}
@ -176,7 +173,7 @@ const InfillAndScalingSettings = () => {
dispatch(setTileSize(32));
}}
/>
</Flex>
</VStack>
);
};

View File

@ -13,7 +13,6 @@ export default function SeamBlur() {
return (
<IAISlider
sliderMarkRightOffset={-4}
label={t('parameters.seamBlur')}
min={0}
max={64}

View File

@ -1,4 +1,4 @@
import { Flex } from '@chakra-ui/react';
import { VStack } from '@chakra-ui/react';
import SeamBlur from './SeamBlur';
import SeamSize from './SeamSize';
import SeamSteps from './SeamSteps';
@ -6,12 +6,12 @@ import SeamStrength from './SeamStrength';
const SeamCorrectionSettings = () => {
return (
<Flex direction="column" gap={2}>
<VStack gap={2} alignItems="stretch">
<SeamSize />
<SeamBlur />
<SeamStrength />
<SeamSteps />
</Flex>
</VStack>
);
};

View File

@ -14,7 +14,6 @@ export default function SeamSize() {
return (
<IAISlider
sliderMarkRightOffset={-6}
label={t('parameters.seamSize')}
min={1}
max={256}

View File

@ -13,7 +13,6 @@ export default function SeamSteps() {
return (
<IAISlider
sliderMarkRightOffset={-4}
label={t('parameters.seamSteps')}
min={1}
max={100}

View File

@ -13,7 +13,6 @@ export default function SeamStrength() {
return (
<IAISlider
sliderMarkRightOffset={-7}
label={t('parameters.seamStrength')}
min={0.01}
max={0.99}

View File

@ -1,4 +1,4 @@
import { Flex } from '@chakra-ui/react';
import { VStack } from '@chakra-ui/react';
import { useAppSelector } from 'app/storeHooks';
import type { RootState } from 'app/store';
import FaceRestoreType from './FaceRestoreType';
@ -14,11 +14,11 @@ const FaceRestoreSettings = () => {
);
return (
<Flex direction="column" gap={2} minWidth="20rem">
<VStack gap={2} alignItems="stretch">
<FaceRestoreType />
<FaceRestoreStrength />
{facetoolType === 'codeformer' && <CodeformerFidelity />}
</Flex>
</VStack>
);
};

View File

@ -6,12 +6,11 @@ import { useTranslation } from 'react-i18next';
interface ImageToImageStrengthProps {
label?: string;
styleClass?: string;
}
export default function ImageToImageStrength(props: ImageToImageStrengthProps) {
const { t } = useTranslation();
const { label = `${t('parameters.strength')}`, styleClass } = props;
const { label = `${t('parameters.strength')}` } = props;
const img2imgStrength = useAppSelector(
(state: RootState) => state.generation.img2imgStrength
);
@ -33,10 +32,9 @@ export default function ImageToImageStrength(props: ImageToImageStrengthProps) {
onChange={handleChangeStrength}
value={img2imgStrength}
isInteger={false}
styleClass={styleClass}
withInput
withSliderMarks
inputWidth="5.5rem"
inputWidth={22}
withReset
handleReset={handleImg2ImgStrengthReset}
/>

View File

@ -1,10 +1,8 @@
import { Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISlider from 'common/components/IAISlider';
import IAISwitch from 'common/components/IAISwitch';
import SubItemHook from 'common/components/SubItemHook';
import { postprocessingSelector } from 'features/parameters/store/postprocessingSelectors';
import {
setHiresFix,
@ -24,7 +22,7 @@ const hiresStrengthSelector = createSelector(
}
);
const HiresStrength = () => {
export const HiresStrength = () => {
const { hiresFix, hiresStrength } = useAppSelector(hiresStrengthSelector);
const dispatch = useAppDispatch();
@ -40,34 +38,30 @@ const HiresStrength = () => {
};
return (
<Flex>
<SubItemHook active={hiresFix} />
<IAISlider
label={t('parameters.hiresStrength')}
step={0.01}
min={0.01}
max={0.99}
onChange={handleHiresStrength}
value={hiresStrength}
isInteger={false}
withInput
withSliderMarks
inputWidth={'5.5rem'}
withReset
handleReset={handleHiResStrengthReset}
isSliderDisabled={!hiresFix}
isInputDisabled={!hiresFix}
isResetDisabled={!hiresFix}
sliderMarkRightOffset={-7}
/>
</Flex>
<IAISlider
label={t('parameters.hiresStrength')}
step={0.01}
min={0.01}
max={0.99}
onChange={handleHiresStrength}
value={hiresStrength}
isInteger={false}
withInput
withSliderMarks
// inputWidth={22}
withReset
handleReset={handleHiResStrengthReset}
isSliderDisabled={!hiresFix}
isInputDisabled={!hiresFix}
isResetDisabled={!hiresFix}
/>
);
};
/**
* Hires Fix Toggle
*/
const HiresSettings = () => {
export const HiresToggle = () => {
const dispatch = useAppDispatch();
const hiresFix = useAppSelector(
@ -80,16 +74,11 @@ const HiresSettings = () => {
dispatch(setHiresFix(e.target.checked));
return (
<Flex rowGap="0.8rem" direction={'column'}>
<IAISwitch
label={t('parameters.hiresOptim')}
fontSize="md"
isChecked={hiresFix}
onChange={handleChangeHiresFix}
/>
<HiresStrength />
</Flex>
<IAISwitch
label={t('parameters.hiresOptim')}
fontSize="md"
isChecked={hiresFix}
onChange={handleChangeHiresFix}
/>
);
};
export default HiresSettings;

View File

@ -1,11 +1,11 @@
import { Flex } from '@chakra-ui/react';
import { VStack } from '@chakra-ui/react';
import SeamlessSettings from './SeamlessSettings';
const ImageToImageOutputSettings = () => {
return (
<Flex gap={2} direction="column">
<VStack gap={2} alignItems="stretch">
<SeamlessSettings />
</Flex>
</VStack>
);
};

View File

@ -1,13 +1,14 @@
import { Flex } from '@chakra-ui/react';
import HiresSettings from './HiresSettings';
import { VStack } from '@chakra-ui/react';
import { HiresStrength, HiresToggle } from './HiresSettings';
import SeamlessSettings from './SeamlessSettings';
const OutputSettings = () => {
return (
<Flex gap={2} direction="column">
<VStack gap={2} alignItems="stretch">
<SeamlessSettings />
<HiresSettings />
</Flex>
<HiresToggle />
<HiresStrength />
</VStack>
);
};

Some files were not shown because too many files have changed in this diff Show More