feat(ui): wip img2img ui

This commit is contained in:
psychedelicious 2023-04-24 20:34:24 +10:00
parent 568f0aad71
commit d40d5276dd
30 changed files with 453 additions and 100 deletions

View File

@ -98,7 +98,8 @@
"pinOptionsPanel": "Pin Options Panel", "pinOptionsPanel": "Pin Options Panel",
"loading": "Loading", "loading": "Loading",
"loadingInvokeAI": "Loading Invoke AI", "loadingInvokeAI": "Loading Invoke AI",
"random": "Random" "random": "Random",
"generate": "Generate"
}, },
"gallery": { "gallery": {
"generations": "Generations", "generations": "Generations",

View File

@ -26,6 +26,7 @@ import {
} from 'features/system/store/systemSlice'; } from 'features/system/store/systemSlice';
import { useIsApplicationReady } from 'features/system/hooks/useIsApplicationReady'; import { useIsApplicationReady } from 'features/system/hooks/useIsApplicationReady';
import { ApplicationFeature } from './invokeai'; import { ApplicationFeature } from './invokeai';
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
keepGUIAlive(); keepGUIAlive();
@ -40,6 +41,7 @@ interface Props extends PropsWithChildren {
const App = (props: Props) => { const App = (props: Props) => {
useToastWatcher(); useToastWatcher();
useGlobalHotkeys();
const currentTheme = useAppSelector((state) => state.ui.currentTheme); const currentTheme = useAppSelector((state) => state.ui.currentTheme);
const disabledFeatures = useAppSelector( const disabledFeatures = useAppSelector(

View File

@ -14,6 +14,7 @@ import generationReducer from 'features/parameters/store/generationSlice';
import postprocessingReducer from 'features/parameters/store/postprocessingSlice'; import postprocessingReducer from 'features/parameters/store/postprocessingSlice';
import systemReducer from 'features/system/store/systemSlice'; import systemReducer from 'features/system/store/systemSlice';
import uiReducer from 'features/ui/store/uiSlice'; import uiReducer from 'features/ui/store/uiSlice';
import hotkeysReducer from 'features/ui/store/hotkeysSlice';
import modelsReducer from 'features/system/store/modelSlice'; import modelsReducer from 'features/system/store/modelSlice';
import nodesReducer from 'features/nodes/store/nodesSlice'; import nodesReducer from 'features/nodes/store/nodesSlice';
@ -55,6 +56,7 @@ const rootReducer = combineReducers({
system: systemReducer, system: systemReducer,
ui: uiReducer, ui: uiReducer,
uploads: uploadsReducer, uploads: uploadsReducer,
hotkeys: hotkeysReducer,
}); });
const rootPersistConfig = getPersistConfig({ const rootPersistConfig = getPersistConfig({
@ -75,6 +77,7 @@ const rootPersistConfig = getPersistConfig({
...uiBlacklist, ...uiBlacklist,
// ...uploadsBlacklist, // ...uploadsBlacklist,
'uploads', 'uploads',
'hotkeys',
], ],
debounce: 300, debounce: 300,
}); });

View File

@ -29,6 +29,7 @@ import { useTranslation } from 'react-i18next';
import { FocusEvent, memo, useEffect, useMemo, useState } from 'react'; import { FocusEvent, memo, useEffect, useMemo, useState } from 'react';
import { BiReset } from 'react-icons/bi'; import { BiReset } from 'react-icons/bi';
import IAIIconButton, { IAIIconButtonProps } from './IAIIconButton'; import IAIIconButton, { IAIIconButtonProps } from './IAIIconButton';
import { roundDownToMultiple } from 'common/util/roundDownToMultiple';
export type IAIFullSliderProps = { export type IAIFullSliderProps = {
label: string; label: string;
@ -119,7 +120,9 @@ const IAISlider = (props: IAIFullSliderProps) => {
min, min,
numberInputMax numberInputMax
); );
onChange(clamped); const quantized = roundDownToMultiple(clamped, step);
onChange(quantized);
setLocalInputValue(quantized);
}; };
const handleInputChange = (v: number | string) => { const handleInputChange = (v: number | string) => {

View File

@ -0,0 +1,37 @@
import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import { shiftKeyPressed } from 'features/ui/store/hotkeysSlice';
import { isEqual } from 'lodash';
import { isHotkeyPressed, useHotkeys } from 'react-hotkeys-hook';
const globalHotkeysSelector = createSelector(
(state: RootState) => state.hotkeys,
(hotkeys) => {
const { shift } = hotkeys;
return { shift };
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
export const useGlobalHotkeys = () => {
const dispatch = useAppDispatch();
const { shift } = useAppSelector(globalHotkeysSelector);
useHotkeys(
'*',
() => {
if (isHotkeyPressed('shift')) {
!shift && dispatch(shiftKeyPressed(true));
} else {
shift && dispatch(shiftKeyPressed(false));
}
},
{ keyup: true, keydown: true },
[shift]
);
};

View File

@ -59,9 +59,6 @@ const CurrentImageDisplay = () => {
justifyContent: 'center', justifyContent: 'center',
}} }}
> >
<Box sx={{ position: 'absolute', top: 0 }}>
<CurrentImageButtons />
</Box>
<Flex <Flex
sx={{ sx={{
w: 'full', w: 'full',
@ -83,6 +80,9 @@ const CurrentImageDisplay = () => {
/> />
)} )}
</Flex> </Flex>
<Box sx={{ position: 'absolute', top: 0 }}>
<CurrentImageButtons />
</Box>
</Flex> </Flex>
); );
}; };

View File

@ -35,7 +35,7 @@ const GALLERY_TAB_WIDTHS: Record<
> = { > = {
// txt2img: { galleryMinWidth: 200, galleryMaxWidth: 500 }, // txt2img: { galleryMinWidth: 200, galleryMaxWidth: 500 },
// img2img: { galleryMinWidth: 200, galleryMaxWidth: 500 }, // img2img: { galleryMinWidth: 200, galleryMaxWidth: 500 },
linear: { galleryMinWidth: 200, galleryMaxWidth: 500 }, generate: { galleryMinWidth: 200, galleryMaxWidth: 500 },
unifiedCanvas: { galleryMinWidth: 200, galleryMaxWidth: 200 }, unifiedCanvas: { galleryMinWidth: 200, galleryMaxWidth: 200 },
nodes: { galleryMinWidth: 200, galleryMaxWidth: 500 }, nodes: { galleryMinWidth: 200, galleryMaxWidth: 500 },
// postprocessing: { galleryMinWidth: 200, galleryMaxWidth: 500 }, // postprocessing: { galleryMinWidth: 200, galleryMaxWidth: 500 },

View File

@ -1,4 +1,4 @@
import { ChangeEvent } from 'react'; import { ChangeEvent, memo } from 'react';
import { RootState } from 'app/store'; import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/storeHooks';
@ -7,7 +7,27 @@ import { setShouldRandomizeSeed } from 'features/parameters/store/generationSlic
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Switch } from '@chakra-ui/react'; import { Switch } from '@chakra-ui/react';
export default function RandomizeSeed() { // export default function RandomizeSeed() {
// const dispatch = useAppDispatch();
// const { t } = useTranslation();
// const shouldRandomizeSeed = useAppSelector(
// (state: RootState) => state.generation.shouldRandomizeSeed
// );
// const handleChangeShouldRandomizeSeed = (e: ChangeEvent<HTMLInputElement>) =>
// dispatch(setShouldRandomizeSeed(e.target.checked));
// return (
// <Switch
// aria-label={t('parameters.randomizeSeed')}
// isChecked={shouldRandomizeSeed}
// onChange={handleChangeShouldRandomizeSeed}
// />
// );
// }
const SeedToggle = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
@ -16,13 +36,15 @@ export default function RandomizeSeed() {
); );
const handleChangeShouldRandomizeSeed = (e: ChangeEvent<HTMLInputElement>) => const handleChangeShouldRandomizeSeed = (e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldRandomizeSeed(e.target.checked)); dispatch(setShouldRandomizeSeed(!e.target.checked));
return ( return (
<Switch <Switch
aria-label={t('parameters.randomizeSeed')} aria-label={t('parameters.randomizeSeed')}
isChecked={shouldRandomizeSeed} isChecked={!shouldRandomizeSeed}
onChange={handleChangeShouldRandomizeSeed} onChange={handleChangeShouldRandomizeSeed}
/> />
); );
} };
export default memo(SeedToggle);

View File

@ -0,0 +1,75 @@
import { Flex, Text } from '@chakra-ui/react';
import { memo, useMemo } from 'react';
export const ratioToCSSString = (
ratio: AspectRatio,
orientation: Orientation
) => {
if (orientation === 'portrait') {
return `${ratio[0]}/${ratio[1]}`;
}
return `${ratio[1]}/${ratio[0]}`;
};
export const ratioToDisplayString = (
ratio: AspectRatio,
orientation: Orientation
) => {
if (orientation === 'portrait') {
return `${ratio[0]}:${ratio[1]}`;
}
return `${ratio[1]}:${ratio[0]}`;
};
type AspectRatioPreviewProps = {
ratio: AspectRatio;
orientation: Orientation;
size: string;
};
export type AspectRatio = [number, number];
export type Orientation = 'portrait' | 'landscape';
const AspectRatioPreview = (props: AspectRatioPreviewProps) => {
const { ratio, size, orientation } = props;
const ratioCSSString = useMemo(() => {
if (orientation === 'portrait') {
return `${ratio[0]}/${ratio[1]}`;
}
return `${ratio[1]}/${ratio[0]}`;
}, [ratio, orientation]);
const ratioDisplayString = useMemo(() => `${ratio[0]}:${ratio[1]}`, [ratio]);
return (
<Flex
sx={{
alignItems: 'center',
justifyContent: 'center',
w: size,
h: size,
}}
>
<Flex
sx={{
alignItems: 'center',
justifyContent: 'center',
bg: 'base.700',
color: 'base.400',
borderRadius: 'base',
aspectRatio: ratioCSSString,
objectFit: 'contain',
...(orientation === 'landscape' ? { h: 'full' } : { w: 'full' }),
}}
>
<Text sx={{ size: 'xs', userSelect: 'none' }}>
{ratioDisplayString}
</Text>
</Flex>
</Flex>
);
};
export default memo(AspectRatioPreview);

View File

@ -0,0 +1,76 @@
import { Box, Flex, FormControl, FormLabel, Select } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISlider from 'common/components/IAISlider';
import { setWidth } from 'features/parameters/store/generationSlice';
import { memo, useState } from 'react';
import AspectRatioPreview, {
AspectRatio,
Orientation,
} from './AspectRatioPreview';
const RATIOS: AspectRatio[] = [
[1, 1],
[5, 4],
[3, 2],
[16, 10],
[16, 9],
];
RATIOS.forEach((r) => {
const float = r[0] / r[1];
console.log((512 * float) / 8);
});
const dimensionsSettingsSelector = createSelector(
(state: RootState) => state.generation,
(generation) => {
const { width, height } = generation;
return { width, height };
}
);
const DimensionsSettings = () => {
const { width, height } = useAppSelector(dimensionsSettingsSelector);
const dispatch = useAppDispatch();
const [ratioIndex, setRatioIndex] = useState(4);
const [orientation, setOrientation] = useState<Orientation>('portrait');
return (
<Flex gap={3}>
<Box flexShrink={0}>
<AspectRatioPreview
ratio={RATIOS[ratioIndex]}
orientation={orientation}
size="4rem"
/>
</Box>
<FormControl>
<FormLabel>Aspect Ratio</FormLabel>
<Select
onChange={(e) => {
setRatioIndex(Number(e.target.value));
}}
>
{RATIOS.map((r, i) => (
<option key={r.join()} value={i}>{`${r[0]}:${r[1]}`}</option>
))}
</Select>
</FormControl>
<IAISlider
label="Size"
value={width}
min={64}
max={2048}
step={8}
onChange={(v) => {
dispatch(setWidth(v));
}}
/>
</Flex>
);
};
export default memo(DimensionsSettings);

View File

@ -0,0 +1,38 @@
import { Box, BoxProps } from '@chakra-ui/react';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISlider from 'common/components/IAISlider';
import { setHeight } from 'features/parameters/store/generationSlice';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const HeightSlider = (props: BoxProps) => {
const height = useAppSelector((state: RootState) => state.generation.height);
const shift = useAppSelector((state: RootState) => state.hotkeys.shift);
const activeTabName = useAppSelector(activeTabNameSelector);
const dispatch = useAppDispatch();
const { t } = useTranslation();
return (
<Box {...props}>
<IAISlider
isDisabled={activeTabName === 'unifiedCanvas'}
label={t('parameters.height')}
value={height}
min={64}
step={shift ? 8 : 64}
max={2048}
onChange={(v) => dispatch(setHeight(v))}
handleReset={() => dispatch(setHeight(512))}
withInput
withReset
withSliderMarks
sliderNumberInputProps={{ max: 15360 }}
/>
</Box>
);
};
export default memo(HeightSlider);

View File

@ -23,7 +23,7 @@ export default function MainHeight() {
label={t('parameters.height')} label={t('parameters.height')}
value={height} value={height}
min={64} min={64}
step={64} step={8}
max={2048} max={2048}
onChange={(v) => dispatch(setHeight(v))} onChange={(v) => dispatch(setHeight(v))}
handleReset={() => dispatch(setHeight(512))} handleReset={() => dispatch(setHeight(512))}

View File

@ -1,3 +1,4 @@
import { Box, BoxProps } from '@chakra-ui/react';
import { DIFFUSERS_SAMPLERS, SAMPLERS } from 'app/constants'; import { DIFFUSERS_SAMPLERS, SAMPLERS } from 'app/constants';
import { RootState } from 'app/store'; import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/storeHooks';
@ -7,7 +8,7 @@ import { activeModelSelector } from 'features/system/store/systemSelectors';
import { ChangeEvent } from 'react'; import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export default function MainSampler() { export default function MainSampler(props: BoxProps) {
const sampler = useAppSelector( const sampler = useAppSelector(
(state: RootState) => state.generation.sampler (state: RootState) => state.generation.sampler
); );
@ -19,14 +20,16 @@ export default function MainSampler() {
dispatch(setSampler(e.target.value)); dispatch(setSampler(e.target.value));
return ( return (
<IAISelect <Box {...props}>
label={t('parameters.sampler')} <IAISelect
value={sampler} label={t('parameters.sampler')}
onChange={handleChangeSampler} value={sampler}
validValues={ onChange={handleChangeSampler}
activeModel.format === 'diffusers' ? DIFFUSERS_SAMPLERS : SAMPLERS validValues={
} activeModel.format === 'diffusers' ? DIFFUSERS_SAMPLERS : SAMPLERS
minWidth={36} }
/> minWidth={36}
/>
</Box>
); );
} }

View File

@ -1,12 +1,15 @@
import { Flex, VStack } from '@chakra-ui/react'; import { Divider, Flex, VStack } from '@chakra-ui/react';
import { RootState } from 'app/store'; import { RootState } from 'app/store';
import { useAppSelector } from 'app/storeHooks'; import { useAppSelector } from 'app/storeHooks';
import { ModelSelect } from 'exports';
import HeightSlider from './HeightSlider';
import MainCFGScale from './MainCFGScale'; import MainCFGScale from './MainCFGScale';
import MainHeight from './MainHeight'; import MainHeight from './MainHeight';
import MainIterations from './MainIterations'; import MainIterations from './MainIterations';
import MainSampler from './MainSampler'; import MainSampler from './MainSampler';
import MainSteps from './MainSteps'; import MainSteps from './MainSteps';
import MainWidth from './MainWidth'; import MainWidth from './MainWidth';
import WidthSlider from './WidthSlider';
export default function MainSettings() { export default function MainSettings() {
const shouldUseSliders = useAppSelector( const shouldUseSliders = useAppSelector(
@ -23,17 +26,18 @@ export default function MainSettings() {
<MainSampler /> <MainSampler />
</VStack> </VStack>
) : ( ) : (
<Flex rowGap={2} flexDirection="column"> <Flex gap={3} flexDirection="column">
<Flex columnGap={1}> <Flex gap={3}>
<MainIterations /> <MainIterations />
<MainSteps /> <MainSteps />
<MainCFGScale /> <MainCFGScale />
</Flex> </Flex>
<Flex columnGap={1}> <Flex gap={3}>
<MainWidth /> <MainSampler flexGrow={2} />
<MainHeight /> <ModelSelect flexGrow={3} />
<MainSampler />
</Flex> </Flex>
<WidthSlider />
<HeightSlider />
</Flex> </Flex>
); );
} }

View File

@ -22,7 +22,7 @@ export default function MainWidth() {
isDisabled={activeTabName === 'unifiedCanvas'} isDisabled={activeTabName === 'unifiedCanvas'}
label={t('parameters.width')} label={t('parameters.width')}
value={width} value={width}
min={64} min={8}
step={64} step={64}
max={2048} max={2048}
onChange={(v) => dispatch(setWidth(v))} onChange={(v) => dispatch(setWidth(v))}

View File

@ -0,0 +1,37 @@
import { Box, BoxProps } from '@chakra-ui/react';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISlider from 'common/components/IAISlider';
import { setWidth } from 'features/parameters/store/generationSlice';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const WidthSlider = (props: BoxProps) => {
const width = useAppSelector((state: RootState) => state.generation.width);
const shift = useAppSelector((state: RootState) => state.hotkeys.shift);
const activeTabName = useAppSelector(activeTabNameSelector);
const { t } = useTranslation();
const dispatch = useAppDispatch();
return (
<Box {...props}>
<IAISlider
isDisabled={activeTabName === 'unifiedCanvas'}
label={t('parameters.width')}
value={width}
min={64}
step={shift ? 8 : 64}
max={2048}
onChange={(v) => dispatch(setWidth(v))}
handleReset={() => dispatch(setWidth(512))}
withInput
withReset
withSliderMarks
sliderNumberInputProps={{ max: 15360 }}
/>
</Box>
);
};
export default memo(WidthSlider);

View File

@ -4,7 +4,10 @@ import { Feature } from 'app/features';
import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import { systemSelector } from 'features/system/store/systemSelectors'; import { systemSelector } from 'features/system/store/systemSelectors';
import { tabMap } from 'features/ui/store/tabMap'; import { tabMap } from 'features/ui/store/tabMap';
import { uiSelector } from 'features/ui/store/uiSelectors'; import {
activeTabNameSelector,
uiSelector,
} from 'features/ui/store/uiSelectors';
import { openAccordionItemsChanged } from 'features/ui/store/uiSlice'; import { openAccordionItemsChanged } from 'features/ui/store/uiSlice';
import { filter, map } from 'lodash'; import { filter, map } from 'lodash';
import { ReactNode, useCallback } from 'react'; import { ReactNode, useCallback } from 'react';
@ -23,7 +26,7 @@ const parametersAccordionSelector = createSelector(
let openAccordions: number[] = []; let openAccordions: number[] = [];
if (tabMap[activeTab] === 'linear') { if (tabMap[activeTab] === 'generate') {
openAccordions = openLinearAccordionItems; openAccordions = openLinearAccordionItems;
} }

View File

@ -11,7 +11,7 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaPlay } from 'react-icons/fa'; import { FaPlay } from 'react-icons/fa';
import { linearGraphBuilt, sessionCreated } from 'services/thunks/session'; import { generateGraphBuilt, sessionCreated } from 'services/thunks/session';
interface InvokeButton interface InvokeButton
extends Omit<IAIButtonProps | IAIIconButtonProps, 'aria-label'> { extends Omit<IAIButtonProps | IAIIconButtonProps, 'aria-label'> {
@ -26,7 +26,7 @@ export default function InvokeButton(props: InvokeButton) {
const handleClickGenerate = () => { const handleClickGenerate = () => {
// dispatch(generateImage(activeTabName)); // dispatch(generateImage(activeTabName));
dispatch(linearGraphBuilt()); dispatch(generateGraphBuilt());
}; };
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -1,4 +1,4 @@
import { Flex } from '@chakra-ui/react'; import { Box, BoxProps, Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { ChangeEvent } from 'react'; import { ChangeEvent } from 'react';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
@ -30,7 +30,7 @@ const selector = createSelector(
} }
); );
const ModelSelect = () => { const ModelSelect = (props: BoxProps) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const { allModelNames, selectedModel } = useAppSelector(selector); const { allModelNames, selectedModel } = useAppSelector(selector);
@ -39,12 +39,9 @@ const ModelSelect = () => {
}; };
return ( return (
<Flex <Box {...props}>
style={{
paddingInlineStart: 1.5,
}}
>
<IAISelect <IAISelect
label={t('modelManager.model')}
style={{ fontSize: 'sm' }} style={{ fontSize: 'sm' }}
aria-label={t('accessibility.modelSelect')} aria-label={t('accessibility.modelSelect')}
tooltip={selectedModel?.description || ''} tooltip={selectedModel?.description || ''}
@ -52,7 +49,7 @@ const ModelSelect = () => {
validValues={allModelNames} validValues={allModelNames}
onChange={handleChangeModel} onChange={handleChangeModel}
/> />
</Flex> </Box>
); );
}; };

View File

@ -34,8 +34,6 @@ const SiteHeader = () => {
> >
<StatusIndicator /> <StatusIndicator />
<ModelSelect />
{resolution === 'desktop' ? ( {resolution === 'desktop' ? (
<SiteHeaderMenu /> <SiteHeaderMenu />
) : ( ) : (

View File

@ -39,7 +39,7 @@ export const floatingParametersPanelButtonSelector = createSelector(
const shouldShowParametersPanelButton = const shouldShowParametersPanelButton =
!canvasBetaLayoutCheck && !canvasBetaLayoutCheck &&
(!shouldPinParametersPanel || !shouldShowParametersPanel) && (!shouldPinParametersPanel || !shouldShowParametersPanel) &&
['linear', 'unifiedCanvas'].includes(activeTabName); ['generate', 'unifiedCanvas'].includes(activeTabName);
return { return {
shouldPinParametersPanel, shouldPinParametersPanel,

View File

@ -23,8 +23,10 @@ import { useTranslation } from 'react-i18next';
import { ResourceKey } from 'i18next'; import { ResourceKey } from 'i18next';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
import NodeEditor from 'features/nodes/components/NodeEditor'; import NodeEditor from 'features/nodes/components/NodeEditor';
import LinearWorkspace from './tabs/Linear/LinearWorkspace'; import GenerateWorkspace from './tabs/Generate/GenerateWorkspace';
import { FaImage } from 'react-icons/fa'; import { FaImage } from 'react-icons/fa';
import { createSelector } from '@reduxjs/toolkit';
import { BsLightningChargeFill, BsLightningFill } from 'react-icons/bs';
export interface InvokeTabInfo { export interface InvokeTabInfo {
id: InvokeTabName; id: InvokeTabName;
@ -36,30 +38,36 @@ const tabIconStyles: ChakraProps['sx'] = {
boxSize: 6, boxSize: 6,
}; };
const buildTabs = (disabledTabs: InvokeTabName[]): InvokeTabInfo[] => { const tabs: InvokeTabInfo[] = [
const tabs: InvokeTabInfo[] = [ {
{ id: 'generate',
id: 'linear', icon: <Icon as={BsLightningChargeFill} sx={{ boxSize: 5 }} />,
icon: <Icon as={FaImage} sx={tabIconStyles} />, workarea: <GenerateWorkspace />,
workarea: <LinearWorkspace />, },
}, {
{ id: 'unifiedCanvas',
id: 'unifiedCanvas', icon: <Icon as={MdGridOn} sx={{ boxSize: 6 }} />,
icon: <Icon as={MdGridOn} sx={tabIconStyles} />, workarea: <UnifiedCanvasWorkarea />,
workarea: <UnifiedCanvasWorkarea />, },
}, {
{ id: 'nodes',
id: 'nodes', icon: <Icon as={MdDeviceHub} sx={{ boxSize: 6 }} />,
icon: <Icon as={MdDeviceHub} sx={tabIconStyles} />, workarea: <NodeEditor />,
workarea: <NodeEditor />, },
}, ];
];
return tabs.filter((tab) => !disabledTabs.includes(tab.id)); const enabledTabsSelector = createSelector(
}; (state: RootState) => state.ui,
(ui) => {
const { disabledTabs } = ui;
return tabs.filter((tab) => !disabledTabs.includes(tab.id));
}
);
export default function InvokeTabs() { export default function InvokeTabs() {
const activeTab = useAppSelector(activeTabIndexSelector); const activeTab = useAppSelector(activeTabIndexSelector);
const enabledTabs = useAppSelector(enabledTabsSelector);
const isLightBoxOpen = useAppSelector( const isLightBoxOpen = useAppSelector(
(state: RootState) => state.lightbox.isLightboxOpen (state: RootState) => state.lightbox.isLightboxOpen
); );
@ -72,22 +80,20 @@ export default function InvokeTabs() {
(state: RootState) => state.system.disabledTabs (state: RootState) => state.system.disabledTabs
); );
const activeTabs = buildTabs(disabledTabs);
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
useHotkeys('1', () => { useHotkeys('1', () => {
dispatch(setActiveTab(0)); dispatch(setActiveTab('generate'));
}); });
useHotkeys('2', () => { useHotkeys('2', () => {
dispatch(setActiveTab(1)); dispatch(setActiveTab('unifiedCanvas'));
}); });
useHotkeys('3', () => { useHotkeys('3', () => {
dispatch(setActiveTab(2)); dispatch(setActiveTab('nodes'));
}); });
// Lightbox Hotkey // Lightbox Hotkey
@ -111,7 +117,7 @@ export default function InvokeTabs() {
const tabs = useMemo( const tabs = useMemo(
() => () =>
activeTabs.map((tab) => ( enabledTabs.map((tab) => (
<Tooltip <Tooltip
key={tab.id} key={tab.id}
hasArrow hasArrow
@ -126,13 +132,15 @@ export default function InvokeTabs() {
</Tab> </Tab>
</Tooltip> </Tooltip>
)), )),
[t, activeTabs] [t, enabledTabs]
); );
const tabPanels = useMemo( const tabPanels = useMemo(
() => () =>
activeTabs.map((tab) => <TabPanel key={tab.id}>{tab.workarea}</TabPanel>), enabledTabs.map((tab) => (
[activeTabs] <TabPanel key={tab.id}>{tab.workarea}</TabPanel>
)),
[enabledTabs]
); );
return ( return (

View File

@ -1,7 +1,7 @@
import { Box, Flex } from '@chakra-ui/react'; import { Box, Flex } from '@chakra-ui/react';
import CurrentImageDisplay from 'features/gallery/components/CurrentImageDisplay'; import CurrentImageDisplay from 'features/gallery/components/CurrentImageDisplay';
const LinearContent = () => { const GenerateContent = () => {
return ( return (
<Box <Box
sx={{ sx={{
@ -18,4 +18,4 @@ const LinearContent = () => {
); );
}; };
export default LinearContent; export default GenerateContent;

View File

@ -1,5 +1,16 @@
import { Flex } from '@chakra-ui/react'; import {
AspectRatio,
Box,
Flex,
Select,
Slider,
SliderFilledTrack,
SliderThumb,
SliderTrack,
Text,
} from '@chakra-ui/react';
import { Feature } from 'app/features'; import { Feature } from 'app/features';
import IAISlider from 'common/components/IAISlider';
import IAISwitch from 'common/components/IAISwitch'; import IAISwitch from 'common/components/IAISwitch';
import ImageToImageSettings from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageSettings'; import ImageToImageSettings from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageSettings';
import ImageToImageToggle from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageToggle'; import ImageToImageToggle from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageToggle';
@ -10,6 +21,7 @@ import RandomizeSeed from 'features/parameters/components/AdvancedParameters/See
import SeedSettings from 'features/parameters/components/AdvancedParameters/Seed/SeedSettings'; import SeedSettings from 'features/parameters/components/AdvancedParameters/Seed/SeedSettings';
import GenerateVariationsToggle from 'features/parameters/components/AdvancedParameters/Variations/GenerateVariations'; import GenerateVariationsToggle from 'features/parameters/components/AdvancedParameters/Variations/GenerateVariations';
import VariationsSettings from 'features/parameters/components/AdvancedParameters/Variations/VariationsSettings'; import VariationsSettings from 'features/parameters/components/AdvancedParameters/Variations/VariationsSettings';
import DimensionsSettings from 'features/parameters/components/ImageDimensions/DimensionsSettings';
import MainSettings from 'features/parameters/components/MainParameters/MainSettings'; import MainSettings from 'features/parameters/components/MainParameters/MainSettings';
import ParametersAccordion, { import ParametersAccordion, {
ParametersAccordionItems, ParametersAccordionItems,
@ -17,14 +29,15 @@ import ParametersAccordion, {
import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons'; import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons';
import NegativePromptInput from 'features/parameters/components/PromptInput/NegativePromptInput'; import NegativePromptInput from 'features/parameters/components/PromptInput/NegativePromptInput';
import PromptInput from 'features/parameters/components/PromptInput/PromptInput'; import PromptInput from 'features/parameters/components/PromptInput/PromptInput';
import { memo, useMemo } from 'react'; import { findIndex } from 'lodash';
import { memo, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants'; import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants';
const LinearParameters = () => { const GenerateParameters = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const linearAccordions: ParametersAccordionItems = useMemo( const generateAccordionItems: ParametersAccordionItems = useMemo(
() => ({ () => ({
// general: { // general: {
// name: 'general', // name: 'general',
@ -80,15 +93,16 @@ const LinearParameters = () => {
gap: 2, gap: 2,
bg: 'base.800', bg: 'base.800',
p: 4, p: 4,
pb: 6,
borderRadius: 'base', borderRadius: 'base',
}} }}
> >
<MainSettings /> <MainSettings />
<ImageToImageToggle />
</Flex> </Flex>
<ParametersAccordion accordionItems={linearAccordions} /> <ImageToImageToggle />
<ParametersAccordion accordionItems={generateAccordionItems} />
</Flex> </Flex>
); );
}; };
export default memo(LinearParameters); export default memo(GenerateParameters);

View File

@ -1,15 +1,15 @@
import { Box, Flex } from '@chakra-ui/react'; import { Box, Flex } from '@chakra-ui/react';
import { useAppSelector } from 'app/storeHooks'; import { useAppSelector } from 'app/storeHooks';
import { memo } from 'react'; import { memo } from 'react';
import LinearContent from './LinearContent'; import GenerateContent from './GenerateContent';
import LinearParameters from './LinearParameters'; import GenerateParameters from './GenerateParameters';
import PinParametersPanelButton from '../../PinParametersPanelButton'; import PinParametersPanelButton from '../../PinParametersPanelButton';
import { RootState } from 'app/store'; import { RootState } from 'app/store';
import Scrollable from '../../common/Scrollable'; import Scrollable from '../../common/Scrollable';
import ParametersSlide from '../../common/ParametersSlide'; import ParametersSlide from '../../common/ParametersSlide';
import AnimatedImageToImagePanel from 'features/parameters/components/AnimatedImageToImagePanel'; import AnimatedImageToImagePanel from 'features/parameters/components/AnimatedImageToImagePanel';
const LinearWorkspace = () => { const GenerateWorkspace = () => {
const shouldPinParametersPanel = useAppSelector( const shouldPinParametersPanel = useAppSelector(
(state: RootState) => state.ui.shouldPinParametersPanel (state: RootState) => state.ui.shouldPinParametersPanel
); );
@ -33,7 +33,7 @@ const LinearWorkspace = () => {
}} }}
> >
<Scrollable> <Scrollable>
<LinearParameters /> <GenerateParameters />
</Scrollable> </Scrollable>
<PinParametersPanelButton <PinParametersPanelButton
sx={{ position: 'absolute', top: 0, insetInlineEnd: 0 }} sx={{ position: 'absolute', top: 0, insetInlineEnd: 0 }}
@ -42,12 +42,12 @@ const LinearWorkspace = () => {
</Flex> </Flex>
) : ( ) : (
<ParametersSlide> <ParametersSlide>
<LinearParameters /> <GenerateParameters />
</ParametersSlide> </ParametersSlide>
)} )}
<LinearContent /> <GenerateContent />
</Flex> </Flex>
); );
}; };
export default memo(LinearWorkspace); export default memo(GenerateWorkspace);

View File

@ -0,0 +1,26 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
type HotkeysState = {
shift: boolean;
};
const initialHotkeysState: HotkeysState = {
shift: false,
};
const initialState: HotkeysState = initialHotkeysState;
export const hotkeysSlice = createSlice({
name: 'hotkeys',
initialState,
reducers: {
shiftKeyPressed: (state, action: PayloadAction<boolean>) => {
state.shift = action.payload;
},
},
});
export const { shiftKeyPressed } = hotkeysSlice.actions;
export default hotkeysSlice.reducer;

View File

@ -1,7 +1,7 @@
export const tabMap = [ export const tabMap = [
// 'txt2img', // 'txt2img',
// 'img2img', // 'img2img',
'linear', 'generate',
'unifiedCanvas', 'unifiedCanvas',
'nodes', 'nodes',
// 'postprocessing', // 'postprocessing',

View File

@ -19,6 +19,9 @@ const initialUIState: UIState = {
shouldShowGallery: true, shouldShowGallery: true,
shouldHidePreview: false, shouldHidePreview: false,
openLinearAccordionItems: [], openLinearAccordionItems: [],
disabledParameterPanels: [],
disabledTabs: [],
openGenerateAccordionItems: [],
openUnifiedCanvasAccordionItems: [], openUnifiedCanvasAccordionItems: [],
}; };
@ -96,8 +99,8 @@ export const uiSlice = createSlice({
} }
}, },
openAccordionItemsChanged: (state, action: PayloadAction<number[]>) => { openAccordionItemsChanged: (state, action: PayloadAction<number[]>) => {
if (tabMap[state.activeTab] === 'linear') { if (tabMap[state.activeTab] === 'generate') {
state.openLinearAccordionItems = action.payload; state.openGenerateAccordionItems = action.payload;
} }
if (tabMap[state.activeTab] === 'unifiedCanvas') { if (tabMap[state.activeTab] === 'unifiedCanvas') {

View File

@ -15,5 +15,8 @@ export interface UIState {
shouldPinGallery: boolean; shouldPinGallery: boolean;
shouldShowGallery: boolean; shouldShowGallery: boolean;
openLinearAccordionItems: number[]; openLinearAccordionItems: number[];
disabledParameterPanels: string[];
disabledTabs: InvokeTabName[];
openGenerateAccordionItems: number[];
openUnifiedCanvasAccordionItems: number[]; openUnifiedCanvasAccordionItems: number[];
} }

View File

@ -1,13 +1,13 @@
import { createAppAsyncThunk } from 'app/storeUtils'; import { createAppAsyncThunk } from 'app/storeUtils';
import { SessionsService } from 'services/api'; import { SessionsService } from 'services/api';
import { buildLinearGraph } from 'features/nodes/util/linearGraphBuilder/buildLinearGraph'; import { buildLinearGraph as buildGenerateGraph } from 'features/nodes/util/linearGraphBuilder/buildLinearGraph';
import { isAnyOf, isFulfilled } from '@reduxjs/toolkit'; import { isAnyOf, isFulfilled } from '@reduxjs/toolkit';
import { buildNodesGraph } from 'features/nodes/util/nodesGraphBuilder/buildNodesGraph'; import { buildNodesGraph } from 'features/nodes/util/nodesGraphBuilder/buildNodesGraph';
export const linearGraphBuilt = createAppAsyncThunk( export const generateGraphBuilt = createAppAsyncThunk(
'api/linearGraphBuilt', 'api/generateGraphBuilt',
async (_, { dispatch, getState }) => { async (_, { dispatch, getState }) => {
const graph = buildLinearGraph(getState()); const graph = buildGenerateGraph(getState());
dispatch(sessionCreated({ graph })); dispatch(sessionCreated({ graph }));
@ -27,7 +27,7 @@ export const nodesGraphBuilt = createAppAsyncThunk(
); );
export const isFulfilledAnyGraphBuilt = isAnyOf( export const isFulfilledAnyGraphBuilt = isAnyOf(
linearGraphBuilt.fulfilled, generateGraphBuilt.fulfilled,
nodesGraphBuilt.fulfilled nodesGraphBuilt.fulfilled
); );