mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): migrate theming to chakra ui
This commit is contained in:
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
46
invokeai/frontend/web/src/app/ThemeLocaleProvider.tsx
Normal file
46
invokeai/frontend/web/src/app/ThemeLocaleProvider.tsx
Normal 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;
|
@ -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,
|
||||
|
@ -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',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
Binary file not shown.
Binary file not shown.
@ -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;
|
||||
}
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -1,8 +0,0 @@
|
||||
.invokeai__button {
|
||||
background-color: var(--btn-base-color);
|
||||
place-content: center;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--btn-base-color-hover);
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
</>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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)'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
@ -1,8 +0,0 @@
|
||||
.invokeai__tooltip-content {
|
||||
padding: 0.5rem;
|
||||
background-color: grey;
|
||||
border-radius: 0.25rem;
|
||||
.invokeai__tooltip-arrow {
|
||||
background-color: grey;
|
||||
}
|
||||
}
|
@ -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;
|
@ -13,5 +13,8 @@ const ImageToImageIcon = createIcon({
|
||||
/>
|
||||
</g>
|
||||
),
|
||||
defaultProps: {
|
||||
boxSize: '24px',
|
||||
},
|
||||
});
|
||||
export default ImageToImageIcon;
|
||||
|
@ -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;
|
||||
|
@ -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
@ -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
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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>;
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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}>
|
||||
|
@ -97,7 +97,7 @@ const IAICanvasSettingsButtonPopover = () => {
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Flex direction="column" gap="0.5rem">
|
||||
<Flex direction="column" gap={2}>
|
||||
<IAICheckbox
|
||||
label={t('unifiedCanvas.showIntermediates')}
|
||||
isChecked={shouldShowIntermediates}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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'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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
}
|
@ -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%);
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -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
|
||||
|
@ -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;
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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;
|
@ -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>
|
||||
);
|
||||
}
|
@ -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;
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -13,7 +13,6 @@ export default function SeamBlur() {
|
||||
|
||||
return (
|
||||
<IAISlider
|
||||
sliderMarkRightOffset={-4}
|
||||
label={t('parameters.seamBlur')}
|
||||
min={0}
|
||||
max={64}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -14,7 +14,6 @@ export default function SeamSize() {
|
||||
|
||||
return (
|
||||
<IAISlider
|
||||
sliderMarkRightOffset={-6}
|
||||
label={t('parameters.seamSize')}
|
||||
min={1}
|
||||
max={256}
|
||||
|
@ -13,7 +13,6 @@ export default function SeamSteps() {
|
||||
|
||||
return (
|
||||
<IAISlider
|
||||
sliderMarkRightOffset={-4}
|
||||
label={t('parameters.seamSteps')}
|
||||
min={1}
|
||||
max={100}
|
||||
|
@ -13,7 +13,6 @@ export default function SeamStrength() {
|
||||
|
||||
return (
|
||||
<IAISlider
|
||||
sliderMarkRightOffset={-7}
|
||||
label={t('parameters.seamStrength')}
|
||||
min={0.01}
|
||||
max={0.99}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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
Reference in New Issue
Block a user