mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-12 22:35:27 -05:00
tidy(ui): component organization
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { ContextMenu, Flex, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
|
||||
import { CanvasAlertsInvocationProgress } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress';
|
||||
import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask';
|
||||
import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus';
|
||||
import { CanvasContextMenuGlobalMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuGlobalMenuItems';
|
||||
import { CanvasContextMenuSelectedEntityMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems';
|
||||
import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea';
|
||||
import { Filter } from 'features/controlLayers/components/Filters/Filter';
|
||||
import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD';
|
||||
import { InvokeCanvasComponent } from 'features/controlLayers/components/InvokeCanvasComponent';
|
||||
import { SelectObject } from 'features/controlLayers/components/SelectObject/SelectObject';
|
||||
import { StagingAreaIsStagingGate } from 'features/controlLayers/components/StagingArea/StagingAreaIsStagingGate';
|
||||
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
|
||||
import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
|
||||
import { Transform } from 'features/controlLayers/components/Transform/Transform';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import type { AdvancedSessionIdentifier } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
|
||||
|
||||
const FOCUS_REGION_STYLES: SystemStyleObject = {
|
||||
width: 'full',
|
||||
height: 'full',
|
||||
};
|
||||
|
||||
const MenuContent = memo(() => {
|
||||
return (
|
||||
<CanvasManagerProviderGate>
|
||||
<MenuList>
|
||||
<CanvasContextMenuSelectedEntityMenuItems />
|
||||
<CanvasContextMenuGlobalMenuItems />
|
||||
</MenuList>
|
||||
</CanvasManagerProviderGate>
|
||||
);
|
||||
});
|
||||
MenuContent.displayName = 'MenuContent';
|
||||
|
||||
const canvasBgSx = {
|
||||
position: 'relative',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
borderRadius: 'base',
|
||||
overflow: 'hidden',
|
||||
bg: 'base.900',
|
||||
'&[data-dynamic-grid="true"]': {
|
||||
bg: 'base.850',
|
||||
},
|
||||
};
|
||||
|
||||
export const AdvancedSession = memo((_props: { session: AdvancedSessionIdentifier }) => {
|
||||
const dynamicGrid = useAppSelector(selectDynamicGrid);
|
||||
const showHUD = useAppSelector(selectShowHUD);
|
||||
|
||||
const renderMenu = useCallback(() => {
|
||||
return <MenuContent />;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FocusRegionWrapper region="canvas" sx={FOCUS_REGION_STYLES}>
|
||||
<Flex
|
||||
tabIndex={-1}
|
||||
borderRadius="base"
|
||||
position="relative"
|
||||
flexDirection="column"
|
||||
height="full"
|
||||
width="full"
|
||||
gap={2}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
overflow="hidden"
|
||||
>
|
||||
<CanvasManagerProviderGate>
|
||||
<CanvasToolbar />
|
||||
</CanvasManagerProviderGate>
|
||||
<ContextMenu<HTMLDivElement> renderMenu={renderMenu} withLongPress={false}>
|
||||
{(ref) => (
|
||||
<Flex ref={ref} sx={canvasBgSx} data-dynamic-grid={dynamicGrid}>
|
||||
<InvokeCanvasComponent />
|
||||
<CanvasManagerProviderGate>
|
||||
<Flex
|
||||
position="absolute"
|
||||
flexDir="column"
|
||||
top={1}
|
||||
insetInlineStart={1}
|
||||
pointerEvents="none"
|
||||
gap={2}
|
||||
alignItems="flex-start"
|
||||
>
|
||||
{showHUD && <CanvasHUD />}
|
||||
<CanvasAlertsSelectedEntityStatus />
|
||||
<CanvasAlertsPreserveMask />
|
||||
<CanvasAlertsInvocationProgress />
|
||||
</Flex>
|
||||
<Flex position="absolute" top={1} insetInlineEnd={1}>
|
||||
<Menu>
|
||||
<MenuButton as={IconButton} icon={<PiDotsThreeOutlineVerticalFill />} colorScheme="base" />
|
||||
<MenuContent />
|
||||
</Menu>
|
||||
</Flex>
|
||||
</CanvasManagerProviderGate>
|
||||
</Flex>
|
||||
)}
|
||||
</ContextMenu>
|
||||
<Flex position="absolute" bottom={4} gap={2} align="center" justify="center">
|
||||
<CanvasManagerProviderGate>
|
||||
<StagingAreaIsStagingGate>
|
||||
<StagingAreaToolbar />
|
||||
</StagingAreaIsStagingGate>
|
||||
</CanvasManagerProviderGate>
|
||||
</Flex>
|
||||
<Flex position="absolute" bottom={4}>
|
||||
<CanvasManagerProviderGate>
|
||||
<Filter />
|
||||
<Transform />
|
||||
<SelectObject />
|
||||
</CanvasManagerProviderGate>
|
||||
</Flex>
|
||||
<CanvasManagerProviderGate>
|
||||
<CanvasDropArea />
|
||||
</CanvasManagerProviderGate>
|
||||
</Flex>
|
||||
</FocusRegionWrapper>
|
||||
);
|
||||
});
|
||||
AdvancedSession.displayName = 'AdvancedSession';
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
|
||||
import { Button, Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { newCanvasFromImage } from 'features/imageActions/actions';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { PiUploadBold } from 'react-icons/pi';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import type { Param0 } from 'tsafe';
|
||||
|
||||
const generateWithControlImageDndTargetData = newCanvasFromImageDndTarget.getData({
|
||||
type: 'control_layer',
|
||||
withResize: true,
|
||||
});
|
||||
|
||||
export const GenerateWithControlImage = memo(() => {
|
||||
const { getState, dispatch } = useAppStore();
|
||||
const useImageUploadButtonOptions = useMemo<Param0<typeof useImageUploadButton>>(
|
||||
() => ({
|
||||
onUpload: (imageDTO: ImageDTO) => {
|
||||
newCanvasFromImage({ imageDTO, type: 'control_layer', withResize: true, getState, dispatch });
|
||||
},
|
||||
allowMultiple: false,
|
||||
}),
|
||||
[dispatch, getState]
|
||||
);
|
||||
const uploadApi = useImageUploadButton(useImageUploadButtonOptions);
|
||||
const components = useMemo(
|
||||
() => ({
|
||||
UploadButton: (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="link"
|
||||
color="base.300"
|
||||
{...uploadApi.getUploadButtonProps()}
|
||||
rightIcon={<PiUploadBold />}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
[uploadApi]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex position="relative" flexDir="column">
|
||||
<Text fontSize="lg" fontWeight="semibold">
|
||||
Generate with a Control Image
|
||||
</Text>
|
||||
<Text color="base.300">
|
||||
Generate a new image using the control image to guide the structure and composition (Text to Image with
|
||||
Control).
|
||||
</Text>
|
||||
<Text color="base.300">
|
||||
<Trans i18nKey="controlLayers.uploadOrDragAnImage" components={components} />
|
||||
<input {...uploadApi.getUploadInputProps()} />
|
||||
</Text>
|
||||
<DndDropTarget
|
||||
dndTarget={newCanvasFromImageDndTarget}
|
||||
dndTargetData={generateWithControlImageDndTargetData}
|
||||
label="Drop"
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
GenerateWithControlImage.displayName = 'GenerateWithControlImage';
|
||||
@@ -0,0 +1,65 @@
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
|
||||
import { Button, Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { newCanvasFromImage } from 'features/imageActions/actions';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { PiUploadBold } from 'react-icons/pi';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import type { Param0 } from 'tsafe';
|
||||
|
||||
const generateWithStartingImageDndTargetData = newCanvasFromImageDndTarget.getData({
|
||||
type: 'raster_layer',
|
||||
withResize: true,
|
||||
});
|
||||
|
||||
export const GenerateWithStartingImage = memo(() => {
|
||||
const { getState, dispatch } = useAppStore();
|
||||
const useImageUploadButtonOptions = useMemo<Param0<typeof useImageUploadButton>>(
|
||||
() => ({
|
||||
onUpload: (imageDTO: ImageDTO) => {
|
||||
newCanvasFromImage({ imageDTO, type: 'raster_layer', withResize: true, getState, dispatch });
|
||||
},
|
||||
allowMultiple: false,
|
||||
}),
|
||||
[dispatch, getState]
|
||||
);
|
||||
const uploadApi = useImageUploadButton(useImageUploadButtonOptions);
|
||||
const components = useMemo(
|
||||
() => ({
|
||||
UploadButton: (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="link"
|
||||
color="base.300"
|
||||
{...uploadApi.getUploadButtonProps()}
|
||||
rightIcon={<PiUploadBold />}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
[uploadApi]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex position="relative" flexDir="column">
|
||||
<Text fontSize="lg" fontWeight="semibold">
|
||||
Generate with a Starting Image
|
||||
</Text>
|
||||
<Text color="base.300">Regenerate the starting image using the model (Image to Image).</Text>
|
||||
<Text color="base.300">
|
||||
<Trans i18nKey="controlLayers.uploadOrDragAnImage" components={components} />
|
||||
<input {...uploadApi.getUploadInputProps()} />
|
||||
</Text>
|
||||
<DndDropTarget
|
||||
dndTarget={newCanvasFromImageDndTarget}
|
||||
dndTargetData={generateWithStartingImageDndTargetData}
|
||||
label="Drop"
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
GenerateWithStartingImage.displayName = 'GenerateWithStartingImage';
|
||||
@@ -0,0 +1,65 @@
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
|
||||
import { Button, Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { newCanvasFromImage } from 'features/imageActions/actions';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { PiUploadBold } from 'react-icons/pi';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import type { Param0 } from 'tsafe';
|
||||
|
||||
const generateWithStartingImageAndInpaintMaskDndTargetData = newCanvasFromImageDndTarget.getData({
|
||||
type: 'raster_layer',
|
||||
withInpaintMask: true,
|
||||
});
|
||||
|
||||
export const GenerateWithStartingImageAndInpaintMask = memo(() => {
|
||||
const { getState, dispatch } = useAppStore();
|
||||
const useImageUploadButtonOptions = useMemo<Param0<typeof useImageUploadButton>>(
|
||||
() => ({
|
||||
onUpload: (imageDTO: ImageDTO) => {
|
||||
newCanvasFromImage({ imageDTO, type: 'raster_layer', withInpaintMask: true, getState, dispatch });
|
||||
},
|
||||
allowMultiple: false,
|
||||
}),
|
||||
[dispatch, getState]
|
||||
);
|
||||
const uploadApi = useImageUploadButton(useImageUploadButtonOptions);
|
||||
const components = useMemo(
|
||||
() => ({
|
||||
UploadButton: (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="link"
|
||||
color="base.300"
|
||||
{...uploadApi.getUploadButtonProps()}
|
||||
rightIcon={<PiUploadBold />}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
[uploadApi]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex position="relative" flexDir="column">
|
||||
<Text fontSize="lg" fontWeight="semibold">
|
||||
Edit Image
|
||||
</Text>
|
||||
<Text color="base.300">Edit the image by regenerating parts of it (Inpaint).</Text>
|
||||
<Text color="base.300">
|
||||
<Trans i18nKey="controlLayers.uploadOrDragAnImage" components={components} />
|
||||
<input {...uploadApi.getUploadInputProps()} />
|
||||
</Text>
|
||||
<DndDropTarget
|
||||
dndTarget={newCanvasFromImageDndTarget}
|
||||
dndTargetData={generateWithStartingImageAndInpaintMaskDndTargetData}
|
||||
label="Drop"
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
GenerateWithStartingImageAndInpaintMask.displayName = 'GenerateWithStartingImageAndInpaintMask';
|
||||
@@ -0,0 +1,32 @@
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
|
||||
import { Button, Flex, Heading, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { GenerateWithControlImage } from 'features/controlLayers/components/NoSession/GenerateWithControlImage';
|
||||
import { GenerateWithStartingImage } from 'features/controlLayers/components/NoSession/GenerateWithStartingImage';
|
||||
import { GenerateWithStartingImageAndInpaintMask } from 'features/controlLayers/components/NoSession/GenerateWithStartingImageAndInpaintMask';
|
||||
import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
export const NoSession = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const newSesh = useCallback(() => {
|
||||
dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" w="full" h="full" alignItems="center" justifyContent="center">
|
||||
<Heading>Get Started with Invoke</Heading>
|
||||
<Button variant="ghost" onClick={newSesh}>
|
||||
Start a new Canvas Session
|
||||
</Button>
|
||||
<Text>or</Text>
|
||||
<Flex flexDir="column" maxW={512}>
|
||||
<GenerateWithStartingImage />
|
||||
<GenerateWithControlImage />
|
||||
<GenerateWithStartingImageAndInpaintMask />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
NoSession.displayName = 'NoSession';
|
||||
@@ -0,0 +1,55 @@
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
import type { ButtonGroupProps } from '@invoke-ai/ui-library';
|
||||
import { Button, ButtonGroup } from '@invoke-ai/ui-library';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { newCanvasFromImage } from 'features/imageActions/actions';
|
||||
import { memo, useCallback } from 'react';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
export const ImageActions = memo(({ imageDTO, ...rest }: { imageDTO: ImageDTO } & ButtonGroupProps) => {
|
||||
const { getState, dispatch } = useAppStore();
|
||||
|
||||
const vary = useCallback(() => {
|
||||
newCanvasFromImage({
|
||||
imageDTO,
|
||||
type: 'raster_layer',
|
||||
withResize: true,
|
||||
getState,
|
||||
dispatch,
|
||||
});
|
||||
}, [dispatch, getState, imageDTO]);
|
||||
|
||||
const useAsControl = useCallback(() => {
|
||||
newCanvasFromImage({
|
||||
imageDTO,
|
||||
type: 'control_layer',
|
||||
withResize: true,
|
||||
getState,
|
||||
dispatch,
|
||||
});
|
||||
}, [dispatch, getState, imageDTO]);
|
||||
|
||||
const edit = useCallback(() => {
|
||||
newCanvasFromImage({
|
||||
imageDTO,
|
||||
type: 'raster_layer',
|
||||
withInpaintMask: true,
|
||||
getState,
|
||||
dispatch,
|
||||
});
|
||||
}, [dispatch, getState, imageDTO]);
|
||||
return (
|
||||
<ButtonGroup isAttached={false} size="sm" {...rest}>
|
||||
<Button onClick={vary} tooltip="Vary the image using Image to Image">
|
||||
Vary
|
||||
</Button>
|
||||
<Button onClick={useAsControl} tooltip="Use this image to control a new Text to Image generation">
|
||||
Use as Control
|
||||
</Button>
|
||||
<Button onClick={edit} tooltip="Edit parts of this image with Inpainting">
|
||||
Edit
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
);
|
||||
});
|
||||
ImageActions.displayName = 'ImageActions';
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { CircularProgressProps, SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { CircularProgress, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useCanvasSessionContext,useProgressData } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import { getProgressMessage } from 'features/controlLayers/components/SimpleSession/shared';
|
||||
import { memo } from 'react';
|
||||
import type { S } from 'services/api/types';
|
||||
|
||||
const circleStyles: SystemStyleObject = {
|
||||
circle: {
|
||||
transitionProperty: 'none',
|
||||
transitionDuration: '0s',
|
||||
},
|
||||
position: 'absolute',
|
||||
top: 2,
|
||||
right: 2,
|
||||
};
|
||||
|
||||
export const QueueItemCircularProgress = memo(
|
||||
({
|
||||
session_id,
|
||||
status,
|
||||
...rest
|
||||
}: { session_id: string; status: S['SessionQueueItem']['status'] } & CircularProgressProps) => {
|
||||
const { $progressData } = useCanvasSessionContext();
|
||||
const { progressEvent } = useProgressData($progressData, session_id);
|
||||
|
||||
if (status !== 'in_progress') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip label={getProgressMessage(progressEvent)}>
|
||||
<CircularProgress
|
||||
size="14px"
|
||||
color="invokeBlue.500"
|
||||
thickness={14}
|
||||
isIndeterminate={!progressEvent || progressEvent.percentage === null}
|
||||
value={progressEvent?.percentage ? progressEvent.percentage * 100 : undefined}
|
||||
sx={circleStyles}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
);
|
||||
QueueItemCircularProgress.displayName = 'QueueItemCircularProgress';
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { TextProps } from '@invoke-ai/ui-library';
|
||||
import { Text } from '@invoke-ai/ui-library';
|
||||
import { DROP_SHADOW } from 'features/controlLayers/components/SimpleSession/shared';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const QueueItemNumber = memo(({ number, ...rest }: { number: number } & TextProps) => {
|
||||
return <Text pointerEvents="none" userSelect="none" filter={DROP_SHADOW} {...rest}>{`#${number}`}</Text>;
|
||||
});
|
||||
QueueItemNumber.displayName = 'QueueItemNumber';
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { ImageActions } from 'features/controlLayers/components/SimpleSession/ImageActions';
|
||||
import { QueueItemCircularProgress } from 'features/controlLayers/components/SimpleSession/QueueItemCircularProgress';
|
||||
import { QueueItemNumber } from 'features/controlLayers/components/SimpleSession/QueueItemNumber';
|
||||
import { QueueItemProgressImage } from 'features/controlLayers/components/SimpleSession/QueueItemProgressImage';
|
||||
import { QueueItemProgressMessage } from 'features/controlLayers/components/SimpleSession/QueueItemProgressMessage';
|
||||
import { QueueItemStatusLabel } from 'features/controlLayers/components/SimpleSession/QueueItemStatusLabel';
|
||||
import { getQueueItemElementId, useOutputImageDTO } from 'features/controlLayers/components/SimpleSession/shared';
|
||||
import { DndImage } from 'features/dnd/DndImage';
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
import type { S } from 'services/api/types';
|
||||
|
||||
type Props = {
|
||||
item: S['SessionQueueItem'];
|
||||
number: number;
|
||||
};
|
||||
|
||||
const sx = {
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
pos: 'relative',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
h: 'full',
|
||||
w: 'full',
|
||||
} satisfies SystemStyleObject;
|
||||
|
||||
export const QueueItemPreviewFull = memo(({ item, number }: Props) => {
|
||||
const imageDTO = useOutputImageDTO(item);
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
|
||||
const onLoad = useCallback(() => {
|
||||
setImageLoaded(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex id={getQueueItemElementId(item.item_id)} sx={sx}>
|
||||
<QueueItemStatusLabel status={item.status} position="absolute" margin="auto" />
|
||||
{imageDTO && <DndImage imageDTO={imageDTO} onLoad={onLoad} />}
|
||||
{!imageLoaded && <QueueItemProgressImage session_id={item.session_id} position="absolute" />}
|
||||
{imageDTO && imageLoaded && <ImageActions imageDTO={imageDTO} position="absolute" top={1} right={2} />}
|
||||
<QueueItemNumber number={number} position="absolute" top={1} left={2} />
|
||||
<QueueItemProgressMessage
|
||||
session_id={item.session_id}
|
||||
status={item.status}
|
||||
position="absolute"
|
||||
bottom={1}
|
||||
left={2}
|
||||
/>
|
||||
<QueueItemCircularProgress
|
||||
session_id={item.session_id}
|
||||
status={item.status}
|
||||
position="absolute"
|
||||
top={1}
|
||||
right={2}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
QueueItemPreviewFull.displayName = 'QueueItemPreviewFull';
|
||||
@@ -0,0 +1,79 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { QueueItemCircularProgress } from 'features/controlLayers/components/SimpleSession/QueueItemCircularProgress';
|
||||
import { QueueItemNumber } from 'features/controlLayers/components/SimpleSession/QueueItemNumber';
|
||||
import { QueueItemProgressImage } from 'features/controlLayers/components/SimpleSession/QueueItemProgressImage';
|
||||
import { QueueItemStatusLabel } from 'features/controlLayers/components/SimpleSession/QueueItemStatusLabel';
|
||||
import { getQueueItemElementId, useOutputImageDTO } from 'features/controlLayers/components/SimpleSession/shared';
|
||||
import { DndImage } from 'features/dnd/DndImage';
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
import type { S } from 'services/api/types';
|
||||
|
||||
const sx = {
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
pos: 'relative',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
h: 'full',
|
||||
maxH: 'full',
|
||||
maxW: 'full',
|
||||
minW: 0,
|
||||
minH: 0,
|
||||
borderWidth: 1,
|
||||
borderRadius: 'base',
|
||||
'&[data-selected="true"]': {
|
||||
borderColor: 'invokeBlue.300',
|
||||
},
|
||||
aspectRatio: '1/1',
|
||||
flexShrink: 0,
|
||||
} satisfies SystemStyleObject;
|
||||
|
||||
type Props = {
|
||||
item: S['SessionQueueItem'];
|
||||
number: number;
|
||||
isSelected: boolean;
|
||||
onSelectItemId: (item_id: number) => void;
|
||||
onChangeAutoSwitch: (autoSwitch: boolean) => void;
|
||||
};
|
||||
|
||||
export const QueueItemPreviewMini = memo(({ item, isSelected, number, onSelectItemId, onChangeAutoSwitch }: Props) => {
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
const imageDTO = useOutputImageDTO(item);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
onSelectItemId(item.item_id);
|
||||
}, [item.item_id, onSelectItemId]);
|
||||
|
||||
const onDoubleClick = useCallback(() => {
|
||||
onChangeAutoSwitch(item.status === 'in_progress');
|
||||
}, [item.status, onChangeAutoSwitch]);
|
||||
|
||||
const onLoad = useCallback(() => {
|
||||
setImageLoaded(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
id={getQueueItemElementId(item.item_id)}
|
||||
sx={sx}
|
||||
data-selected={isSelected}
|
||||
onClick={onClick}
|
||||
onDoubleClick={onDoubleClick}
|
||||
>
|
||||
<QueueItemStatusLabel status={item.status} position="absolute" margin="auto" />
|
||||
{imageDTO && <DndImage imageDTO={imageDTO} asThumbnail onLoad={onLoad} />}
|
||||
{!imageLoaded && <QueueItemProgressImage session_id={item.session_id} position="absolute" />}
|
||||
<QueueItemNumber number={number} position="absolute" top={0} left={1} />
|
||||
<QueueItemCircularProgress
|
||||
session_id={item.session_id}
|
||||
status={item.status}
|
||||
position="absolute"
|
||||
top={1}
|
||||
right={2}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
QueueItemPreviewMini.displayName = 'QueueItemPreviewMini';
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { ImageProps } from '@invoke-ai/ui-library';
|
||||
import { Image } from '@invoke-ai/ui-library';
|
||||
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import { memo } from 'react';
|
||||
import { useProgressData } from 'services/events/stores';
|
||||
|
||||
export const QueueItemProgressImage = memo(({ session_id, ...rest }: { session_id: string } & ImageProps) => {
|
||||
const { $progressData } = useCanvasSessionContext();
|
||||
const { progressImage } = useProgressData($progressData, session_id);
|
||||
|
||||
if (!progressImage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
objectFit="contain"
|
||||
maxH="full"
|
||||
maxW="full"
|
||||
draggable={false}
|
||||
src={progressImage.dataURL}
|
||||
width={progressImage.width}
|
||||
height={progressImage.height}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
});
|
||||
QueueItemProgressImage.displayName = 'QueueItemProgressImage';
|
||||
@@ -0,0 +1,34 @@
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
import type { TextProps } from '@invoke-ai/ui-library';
|
||||
import { Text } from '@invoke-ai/ui-library';
|
||||
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import { DROP_SHADOW, getProgressMessage } from 'features/controlLayers/components/SimpleSession/shared';
|
||||
import { memo } from 'react';
|
||||
import type { S } from 'services/api/types';
|
||||
import { useProgressData } from 'services/events/stores';
|
||||
|
||||
export const QueueItemProgressMessage = memo(
|
||||
({ session_id, status, ...rest }: { session_id: string; status: S['SessionQueueItem']['status'] } & TextProps) => {
|
||||
const { $progressData } = useCanvasSessionContext();
|
||||
const { progressEvent } = useProgressData($progressData, session_id);
|
||||
|
||||
if (status === 'completed' || status === 'failed' || status === 'canceled') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (status === 'pending') {
|
||||
return (
|
||||
<Text pointerEvents="none" userSelect="none" filter={DROP_SHADOW} {...rest}>
|
||||
Waiting to start...
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text pointerEvents="none" userSelect="none" filter={DROP_SHADOW} {...rest}>
|
||||
{getProgressMessage(progressEvent)}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
);
|
||||
QueueItemProgressMessage.displayName = 'QueueItemProgressMessage';
|
||||
@@ -0,0 +1,42 @@
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
import type { TextProps } from '@invoke-ai/ui-library';
|
||||
import { Text } from '@invoke-ai/ui-library';
|
||||
import { memo } from 'react';
|
||||
import type { S } from 'services/api/types';
|
||||
|
||||
export const QueueItemStatusLabel = memo(
|
||||
({ status, ...rest }: { status: S['SessionQueueItem']['status'] } & TextProps) => {
|
||||
if (status === 'pending') {
|
||||
return (
|
||||
<Text pointerEvents="none" userSelect="none" fontWeight="semibold" color="base.300" {...rest}>
|
||||
Pending
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
if (status === 'canceled') {
|
||||
return (
|
||||
<Text pointerEvents="none" userSelect="none" fontWeight="semibold" color="warning.300" {...rest}>
|
||||
Canceled
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
if (status === 'failed') {
|
||||
return (
|
||||
<Text pointerEvents="none" userSelect="none" fontWeight="semibold" color="error.300" {...rest}>
|
||||
Failed
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'in_progress') {
|
||||
return (
|
||||
<Text pointerEvents="none" userSelect="none" fontWeight="semibold" color="invokeBlue.300" {...rest}>
|
||||
In Progress
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
);
|
||||
QueueItemStatusLabel.displayName = 'QueueItemStatusLabel';
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { CanvasSessionContextValue } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import {
|
||||
buildProgressDataAtom,
|
||||
CanvasSessionContextProvider,
|
||||
} from 'features/controlLayers/components/SimpleSession/context';
|
||||
import { StagingArea } from 'features/controlLayers/components/SimpleSession/StagingArea';
|
||||
import type { SimpleSessionIdentifier } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { memo, useMemo } from 'react';
|
||||
|
||||
export const SimpleSession = memo(({ session }: { session: SimpleSessionIdentifier }) => {
|
||||
const ctx = useMemo(
|
||||
() => ({ session, $progressData: buildProgressDataAtom() }) satisfies CanvasSessionContextValue,
|
||||
[session]
|
||||
);
|
||||
|
||||
return (
|
||||
<CanvasSessionContextProvider value={ctx}>
|
||||
<StagingArea />
|
||||
</CanvasSessionContextProvider>
|
||||
);
|
||||
});
|
||||
SimpleSession.displayName = 'SimpleSession';
|
||||
@@ -0,0 +1,130 @@
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
|
||||
import { Divider, Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { EMPTY_ARRAY } from 'app/store/constants';
|
||||
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import { getQueueItemElementId } from 'features/controlLayers/components/SimpleSession/shared';
|
||||
import { StagingAreaContent } from 'features/controlLayers/components/SimpleSession/StagingAreaContent';
|
||||
import { StagingAreaHeader } from 'features/controlLayers/components/SimpleSession/StagingAreaHeader';
|
||||
import { useStagingAreaKeyboardNav } from 'features/controlLayers/components/SimpleSession/use-staging-keyboard-nav';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useListAllQueueItemsQuery } from 'services/api/endpoints/queue';
|
||||
import type { S } from 'services/api/types';
|
||||
import { $socket, setProgress } from 'services/events/stores';
|
||||
|
||||
const LIST_ALL_OPTIONS = {
|
||||
selectFromResult: ({ data }) => {
|
||||
if (!data) {
|
||||
return { items: EMPTY_ARRAY };
|
||||
}
|
||||
return { items: data.filter(({ status }) => status !== 'canceled') };
|
||||
},
|
||||
} satisfies Parameters<typeof useListAllQueueItemsQuery>[1];
|
||||
|
||||
export const StagingArea = memo(() => {
|
||||
const ctx = useCanvasSessionContext();
|
||||
const [selectedItemId, setSelectedItemId] = useState<number | null>(null);
|
||||
const [autoSwitch, setAutoSwitch] = useState(true);
|
||||
const { items } = useListAllQueueItemsQuery({ destination: ctx.session.id }, LIST_ALL_OPTIONS);
|
||||
const selectedItem = useMemo(() => {
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (selectedItemId === null) {
|
||||
return null;
|
||||
}
|
||||
return items.find(({ item_id }) => item_id === selectedItemId) ?? null;
|
||||
}, [items, selectedItemId]);
|
||||
const selectedItemIndex = useMemo(() => {
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (selectedItemId === null) {
|
||||
return null;
|
||||
}
|
||||
return items.findIndex(({ item_id }) => item_id === selectedItemId) ?? null;
|
||||
}, [items, selectedItemId]);
|
||||
|
||||
const onSelectItemId = useCallback((item_id: number | null) => {
|
||||
setSelectedItemId(item_id);
|
||||
if (item_id !== null) {
|
||||
document.getElementById(getQueueItemElementId(item_id))?.scrollIntoView();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useStagingAreaKeyboardNav(items, selectedItemId, onSelectItemId);
|
||||
|
||||
useEffect(() => {
|
||||
if (items.length === 0) {
|
||||
onSelectItemId(null);
|
||||
return;
|
||||
}
|
||||
if (selectedItemId === null && items.length > 0) {
|
||||
onSelectItemId(items[0]?.item_id ?? null);
|
||||
return;
|
||||
}
|
||||
}, [items, onSelectItemId, selectedItem, selectedItemId]);
|
||||
|
||||
const socket = useStore($socket);
|
||||
useEffect(() => {
|
||||
if (!socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onQueueItemStatusChanged = (data: S['QueueItemStatusChangedEvent']) => {
|
||||
if (data.destination !== ctx.session.id) {
|
||||
return;
|
||||
}
|
||||
if (data.status === 'in_progress' && autoSwitch) {
|
||||
onSelectItemId(data.item_id);
|
||||
}
|
||||
};
|
||||
|
||||
socket.on('queue_item_status_changed', onQueueItemStatusChanged);
|
||||
|
||||
return () => {
|
||||
socket.off('queue_item_status_changed', onQueueItemStatusChanged);
|
||||
};
|
||||
}, [autoSwitch, ctx.$progressData, ctx.session.id, onSelectItemId, socket]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket) {
|
||||
return;
|
||||
}
|
||||
const onProgress = (data: S['InvocationProgressEvent']) => {
|
||||
if (data.destination !== ctx.session.id) {
|
||||
return;
|
||||
}
|
||||
setProgress(ctx.$progressData, data);
|
||||
};
|
||||
socket.on('invocation_progress', onProgress);
|
||||
|
||||
return () => {
|
||||
socket.off('invocation_progress', onProgress);
|
||||
};
|
||||
}, [ctx.$progressData, ctx.session.id, socket]);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" gap={2} w="full" h="full" minW={0} minH={0}>
|
||||
<StagingAreaHeader autoSwitch={autoSwitch} setAutoSwitch={setAutoSwitch} />
|
||||
<Divider />
|
||||
{items.length > 0 && (
|
||||
<StagingAreaContent
|
||||
items={items}
|
||||
selectedItem={selectedItem}
|
||||
selectedItemId={selectedItemId}
|
||||
selectedItemIndex={selectedItemIndex}
|
||||
onChangeAutoSwitch={setAutoSwitch}
|
||||
onSelectItemId={onSelectItemId}
|
||||
/>
|
||||
)}
|
||||
{items.length === 0 && (
|
||||
<Flex w="full" h="full" alignItems="center" justifyContent="center">
|
||||
<Text>No generations</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
StagingArea.displayName = 'StagingArea';
|
||||
@@ -0,0 +1,58 @@
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
import { Divider, Flex, Text } from '@invoke-ai/ui-library';
|
||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||
import { QueueItemPreviewFull } from 'features/controlLayers/components/SimpleSession/QueueItemPreviewFull';
|
||||
import { QueueItemPreviewMini } from 'features/controlLayers/components/SimpleSession/QueueItemPreviewMini';
|
||||
import { memo } from 'react';
|
||||
import type { S } from 'services/api/types';
|
||||
|
||||
export const StagingAreaContent = memo(
|
||||
({
|
||||
items,
|
||||
selectedItem,
|
||||
selectedItemId,
|
||||
selectedItemIndex,
|
||||
onChangeAutoSwitch,
|
||||
onSelectItemId,
|
||||
}: {
|
||||
items: S['SessionQueueItem'][];
|
||||
selectedItem: S['SessionQueueItem'] | null;
|
||||
selectedItemId: number | null;
|
||||
selectedItemIndex: number | null;
|
||||
onChangeAutoSwitch: (autoSwitch: boolean) => void;
|
||||
onSelectItemId: (itemId: number) => void;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<Flex position="relative" w="full" h="full" maxH="full" alignItems="center" justifyContent="center" minH={0}>
|
||||
{selectedItem && selectedItemIndex !== null && (
|
||||
<QueueItemPreviewFull
|
||||
key={`${selectedItem.item_id}-full`}
|
||||
item={selectedItem}
|
||||
number={selectedItemIndex + 1}
|
||||
/>
|
||||
)}
|
||||
{!selectedItem && <Text>No generation selected</Text>}
|
||||
</Flex>
|
||||
<Divider />
|
||||
<Flex position="relative" maxW="full" w="full" h={108}>
|
||||
<ScrollableContent overflowX="scroll" overflowY="hidden">
|
||||
<Flex gap={2} w="full" h="full">
|
||||
{items.map((item, i) => (
|
||||
<QueueItemPreviewMini
|
||||
key={`${item.item_id}-mini`}
|
||||
item={item}
|
||||
number={i + 1}
|
||||
isSelected={selectedItemId === item.item_id}
|
||||
onSelectItemId={onSelectItemId}
|
||||
onChangeAutoSwitch={onChangeAutoSwitch}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
</ScrollableContent>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
StagingAreaContent.displayName = 'StagingAreaContent';
|
||||
@@ -0,0 +1,40 @@
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
import { Button, Flex, FormControl, FormLabel, Spacer, Switch, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
export const StagingAreaHeader = memo(
|
||||
({ autoSwitch, setAutoSwitch }: { autoSwitch: boolean; setAutoSwitch: (autoSwitch: boolean) => void }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const startOver = useCallback(() => {
|
||||
dispatch(canvasSessionStarted({ sessionType: 'simple' }));
|
||||
}, [dispatch]);
|
||||
|
||||
const onChangeAutoSwitch = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
setAutoSwitch(e.target.checked);
|
||||
},
|
||||
[setAutoSwitch]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex gap={2} w="full" alignItems="center">
|
||||
<Text fontSize="lg" fontWeight="bold">
|
||||
Generations
|
||||
</Text>
|
||||
<Spacer />
|
||||
<FormControl w="min-content">
|
||||
<FormLabel m={0}>Auto-switch</FormLabel>
|
||||
<Switch size="sm" isChecked={autoSwitch} onChange={onChangeAutoSwitch} />
|
||||
</FormControl>
|
||||
<Button size="sm" variant="ghost" onClick={startOver}>
|
||||
Start Over
|
||||
</Button>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
);
|
||||
StagingAreaHeader.displayName = 'StagingAreaHeader';
|
||||
@@ -0,0 +1,135 @@
|
||||
import type {
|
||||
AdvancedSessionIdentifier,
|
||||
SimpleSessionIdentifier,
|
||||
} from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import type { ProgressImage } from 'features/nodes/types/common';
|
||||
import { atom, type WritableAtom } from 'nanostores';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { createContext, memo, useContext, useEffect, useState } from 'react';
|
||||
import type { S } from 'services/api/types';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
export type ProgressData = {
|
||||
sessionId: string;
|
||||
progressEvent: S['InvocationProgressEvent'] | null;
|
||||
progressImage: ProgressImage | null;
|
||||
};
|
||||
|
||||
export const buildProgressDataAtom = () => atom<Record<string, ProgressData>>({});
|
||||
|
||||
export const useProgressData = (
|
||||
$progressData: WritableAtom<Record<string, ProgressData>>,
|
||||
sessionId: string
|
||||
): ProgressData => {
|
||||
const [value, setValue] = useState<ProgressData>(() => {
|
||||
return $progressData.get()[sessionId] ?? { sessionId, progressEvent: null, progressImage: null };
|
||||
});
|
||||
useEffect(() => {
|
||||
const unsub = $progressData.subscribe((data) => {
|
||||
const progressData = data[sessionId];
|
||||
if (!progressData) {
|
||||
return;
|
||||
}
|
||||
setValue(progressData);
|
||||
});
|
||||
return () => {
|
||||
unsub();
|
||||
};
|
||||
}, [$progressData, sessionId]);
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
export const useHasProgressImage = (
|
||||
$progressData: WritableAtom<Record<string, ProgressData>>,
|
||||
sessionId: string
|
||||
): boolean => {
|
||||
const [value, setValue] = useState(false);
|
||||
useEffect(() => {
|
||||
const unsub = $progressData.subscribe((data) => {
|
||||
const progressData = data[sessionId];
|
||||
setValue(Boolean(progressData?.progressImage));
|
||||
});
|
||||
return () => {
|
||||
unsub();
|
||||
};
|
||||
}, [$progressData, sessionId]);
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
export const setProgress = (
|
||||
$progressData: WritableAtom<Record<string, ProgressData>>,
|
||||
data: S['InvocationProgressEvent']
|
||||
) => {
|
||||
const progressData = $progressData.get();
|
||||
const current = progressData[data.session_id];
|
||||
if (current) {
|
||||
const next = { ...current };
|
||||
next.progressEvent = data;
|
||||
if (data.image) {
|
||||
next.progressImage = data.image;
|
||||
}
|
||||
$progressData.set({
|
||||
...progressData,
|
||||
[data.session_id]: next,
|
||||
});
|
||||
} else {
|
||||
$progressData.set({
|
||||
...progressData,
|
||||
[data.session_id]: {
|
||||
sessionId: data.session_id,
|
||||
progressEvent: data,
|
||||
progressImage: data.image ?? null,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const clearProgressEvent = ($progressData: WritableAtom<Record<string, ProgressData>>, sessionId: string) => {
|
||||
const progressData = $progressData.get();
|
||||
const current = progressData[sessionId];
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
const next = { ...current };
|
||||
next.progressEvent = null;
|
||||
$progressData.set({
|
||||
...progressData,
|
||||
[sessionId]: next,
|
||||
});
|
||||
};
|
||||
|
||||
export const clearProgressImage = ($progressData: WritableAtom<Record<string, ProgressData>>, sessionId: string) => {
|
||||
const progressData = $progressData.get();
|
||||
const current = progressData[sessionId];
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
const next = { ...current };
|
||||
next.progressImage = null;
|
||||
$progressData.set({
|
||||
...progressData,
|
||||
[sessionId]: next,
|
||||
});
|
||||
};
|
||||
|
||||
export type CanvasSessionContextValue = {
|
||||
session: SimpleSessionIdentifier | AdvancedSessionIdentifier;
|
||||
$progressData: WritableAtom<Record<string, ProgressData>>;
|
||||
};
|
||||
|
||||
const CanvasSessionContext = createContext<CanvasSessionContextValue | null>(null);
|
||||
|
||||
export const CanvasSessionContextProvider = memo(
|
||||
({ value, children }: PropsWithChildren<{ value: CanvasSessionContextValue }>) => (
|
||||
<CanvasSessionContext.Provider value={value}>{children}</CanvasSessionContext.Provider>
|
||||
)
|
||||
);
|
||||
CanvasSessionContextProvider.displayName = 'CanvasSessionContextProvider';
|
||||
|
||||
export const useCanvasSessionContext = () => {
|
||||
const ctx = useContext(CanvasSessionContext);
|
||||
assert(ctx !== null, "'useCanvasSessionContext' must be used within a CanvasSessionContextProvider");
|
||||
return ctx;
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { isImageField } from 'features/nodes/types/common';
|
||||
import { isCanvasOutputNodeId } from 'features/nodes/util/graph/graphBuilderUtils';
|
||||
import { round } from 'lodash-es';
|
||||
import { useMemo } from 'react';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import type { S } from 'services/api/types';
|
||||
import { objectEntries } from 'tsafe';
|
||||
|
||||
export const getProgressMessage = (data?: S['InvocationProgressEvent'] | null) => {
|
||||
if (!data) {
|
||||
return 'Generating';
|
||||
}
|
||||
|
||||
let message = data.message;
|
||||
if (data.percentage) {
|
||||
message += ` (${round(data.percentage * 100)}%)`;
|
||||
}
|
||||
return message;
|
||||
};
|
||||
|
||||
export const DROP_SHADOW = 'drop-shadow(0px 0px 4px rgb(0, 0, 0)) drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.3))';
|
||||
|
||||
export const getQueueItemElementId = (item_id: number) => `queue-item-status-card-${item_id}`;
|
||||
|
||||
const getOutputImageName = (item: S['SessionQueueItem']) => {
|
||||
const nodeId = Object.entries(item.session.source_prepared_mapping).find(([nodeId]) =>
|
||||
isCanvasOutputNodeId(nodeId)
|
||||
)?.[1][0];
|
||||
const output = nodeId ? item.session.results[nodeId] : undefined;
|
||||
|
||||
if (!output) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const [_name, value] of objectEntries(output)) {
|
||||
if (isImageField(value)) {
|
||||
return value.image_name;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const useOutputImageDTO = (item: S['SessionQueueItem']) => {
|
||||
const outputImageName = useMemo(() => getOutputImageName(item), [item]);
|
||||
|
||||
const { currentData: imageDTO } = useGetImageDTOQuery(outputImageName ?? skipToken);
|
||||
|
||||
return imageDTO;
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import type { S } from 'services/api/types';
|
||||
|
||||
export const useStagingAreaKeyboardNav = (
|
||||
items: S['SessionQueueItem'][],
|
||||
selectedItemId: number | null,
|
||||
onSelectItemId: (item_id: number) => void
|
||||
) => {
|
||||
const onNext = useCallback(() => {
|
||||
if (selectedItemId === null) {
|
||||
return;
|
||||
}
|
||||
const currentIndex = items.findIndex((item) => item.item_id === selectedItemId);
|
||||
const nextIndex = (currentIndex + 1) % items.length;
|
||||
const nextItem = items[nextIndex];
|
||||
if (!nextItem) {
|
||||
return;
|
||||
}
|
||||
onSelectItemId(nextItem.item_id);
|
||||
}, [items, onSelectItemId, selectedItemId]);
|
||||
const onPrev = useCallback(() => {
|
||||
if (selectedItemId === null) {
|
||||
return;
|
||||
}
|
||||
const currentIndex = items.findIndex((item) => item.item_id === selectedItemId);
|
||||
const prevIndex = (currentIndex - 1 + items.length) % items.length;
|
||||
const prevItem = items[prevIndex];
|
||||
if (!prevItem) {
|
||||
return;
|
||||
}
|
||||
onSelectItemId(prevItem.item_id);
|
||||
}, [items, onSelectItemId, selectedItemId]);
|
||||
|
||||
const onFirst = useCallback(() => {
|
||||
const first = items.at(0);
|
||||
if (!first) {
|
||||
return;
|
||||
}
|
||||
onSelectItemId(first.item_id);
|
||||
}, [items, onSelectItemId]);
|
||||
const onLast = useCallback(() => {
|
||||
const last = items.at(-1);
|
||||
if (!last) {
|
||||
return;
|
||||
}
|
||||
onSelectItemId(last.item_id);
|
||||
}, [items, onSelectItemId]);
|
||||
|
||||
useHotkeys('left', onPrev, { preventDefault: true });
|
||||
useHotkeys('right', onNext, { preventDefault: true });
|
||||
useHotkeys('meta+left', onFirst, { preventDefault: true });
|
||||
useHotkeys('meta+right', onLast, { preventDefault: true });
|
||||
};
|
||||
@@ -6,8 +6,18 @@ import { canvasReset } from 'features/controlLayers/store/actions';
|
||||
import type { StagingAreaImage, StagingAreaProgressImage } from 'features/controlLayers/store/types';
|
||||
import { selectCanvasQueueCounts } from 'services/api/endpoints/queue';
|
||||
|
||||
export type SimpleSessionIdentifier = {
|
||||
type: 'simple';
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type AdvancedSessionIdentifier = {
|
||||
type: 'advanced';
|
||||
id: string;
|
||||
};
|
||||
|
||||
type CanvasStagingAreaState = {
|
||||
session: { type: 'simple'; id: string } | { type: 'advanced'; id: string } | null;
|
||||
session: SimpleSessionIdentifier | AdvancedSessionIdentifier | null;
|
||||
sessionType: 'simple' | 'advanced' | null;
|
||||
images: (StagingAreaImage | StagingAreaProgressImage)[];
|
||||
selectedImageIndex: number;
|
||||
|
||||
Reference in New Issue
Block a user