mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-13 00:45:01 -05:00
chore(ui): lint (partial cleanup)
This commit is contained in:
@@ -81,7 +81,7 @@ const deleteRasterLayerImages = (state: RootState, dispatch: AppDispatch, imageD
|
||||
selectCanvasSlice(state).rasterLayers.entities.forEach(({ id, objects }) => {
|
||||
let shouldDelete = false;
|
||||
for (const obj of objects) {
|
||||
if (obj.type === 'image' && obj.image.image_name === imageDTO.image_name) {
|
||||
if (obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === imageDTO.image_name) {
|
||||
shouldDelete = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ type UploadImageButtonProps = {
|
||||
isError?: boolean;
|
||||
} & ButtonProps;
|
||||
|
||||
export const UploadImageButton = memo((props: UploadImageButtonProps) => {
|
||||
const UploadImageButton = memo((props: UploadImageButtonProps) => {
|
||||
const { children, isDisabled = false, onUpload, isError = false, ...rest } = props;
|
||||
const uploadApi = useImageUploadButton({ isDisabled, allowMultiple: false, onUpload });
|
||||
return (
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { EMPTY_ARRAY } from 'app/store/constants';
|
||||
import { useMemo } from 'react';
|
||||
import { useGetRelatedModelIdsBatchQuery } from 'services/api/endpoints/modelRelationships';
|
||||
|
||||
const options: Parameters<typeof useGetRelatedModelIdsBatchQuery>[1] = {
|
||||
selectFromResult: ({ data }) => {
|
||||
if (!data) {
|
||||
return { related: EMPTY_ARRAY };
|
||||
}
|
||||
return data;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches related model keys for a given set of selected model keys.
|
||||
* Returns a Set<string> for fast lookup.
|
||||
*/
|
||||
export const useRelatedModelKeys = (selectedKeys: string[]) => {
|
||||
const { related } = useGetRelatedModelIdsBatchQuery(selectedKeys, options);
|
||||
|
||||
return useMemo(() => new Set(related), [related]);
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
|
||||
/**
|
||||
* Gathers all currently selected model keys from parameters and loras.
|
||||
* This includes the main model, VAE, refiner model, controlnet, and loras.
|
||||
*/
|
||||
export const useSelectedModelKeys = () => {
|
||||
return useAppSelector((state) => {
|
||||
const keys = new Set<string>();
|
||||
const main = state.params.model;
|
||||
const vae = state.params.vae;
|
||||
const refiner = state.params.refinerModel;
|
||||
const controlnet = state.params.controlLora;
|
||||
const loras = state.loras.loras.map((l) => l.model);
|
||||
|
||||
if (main) {
|
||||
keys.add(main.key);
|
||||
}
|
||||
if (vae) {
|
||||
keys.add(vae.key);
|
||||
}
|
||||
if (refiner) {
|
||||
keys.add(refiner.key);
|
||||
}
|
||||
if (controlnet) {
|
||||
keys.add(controlnet.key);
|
||||
}
|
||||
for (const lora of loras) {
|
||||
keys.add(lora.key);
|
||||
}
|
||||
|
||||
return keys;
|
||||
});
|
||||
};
|
||||
@@ -1,146 +0,0 @@
|
||||
import { Alert, AlertDescription, AlertIcon, AlertTitle, Button, Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useBoolean } from 'common/hooks/useBoolean';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useCurrentDestination } from 'features/queue/hooks/useCurrentDestination';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { activeTabCanvasRightPanelChanged, setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import type { PropsWithChildren, ReactNode } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
const ActivateImageViewerButton = (props: PropsWithChildren) => {
|
||||
const imageViewer = useImageViewer();
|
||||
const dispatch = useAppDispatch();
|
||||
const onClick = useCallback(() => {
|
||||
imageViewer.open();
|
||||
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||
}, [imageViewer, dispatch]);
|
||||
return (
|
||||
<Button onClick={onClick} size="sm" variant="link" color="base.50">
|
||||
{props.children}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const CanvasAlertsSendingToGallery = () => {
|
||||
const { t } = useTranslation();
|
||||
const destination = useCurrentDestination();
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
const isVisible = useMemo(() => {
|
||||
// This alert should only be visible when the destination is gallery and the tab is canvas
|
||||
if (tab !== 'canvas') {
|
||||
return false;
|
||||
}
|
||||
if (!destination) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return destination === 'gallery';
|
||||
}, [destination, tab]);
|
||||
|
||||
return (
|
||||
<AlertWrapper
|
||||
title={t('controlLayers.sendingToGallery')}
|
||||
description={
|
||||
<Trans i18nKey="controlLayers.viewProgressInViewer" components={{ Btn: <ActivateImageViewerButton /> }} />
|
||||
}
|
||||
isVisible={isVisible}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ActivateCanvasButton = (props: PropsWithChildren) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const imageViewer = useImageViewer();
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(setActiveTab('canvas'));
|
||||
dispatch(activeTabCanvasRightPanelChanged('layers'));
|
||||
imageViewer.close();
|
||||
}, [dispatch, imageViewer]);
|
||||
return (
|
||||
<Button onClick={onClick} size="sm" variant="link" color="base.50">
|
||||
{props.children}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const CanvasAlertsSendingToCanvas = () => {
|
||||
const { t } = useTranslation();
|
||||
const destination = useCurrentDestination();
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
const isVisible = useMemo(() => {
|
||||
// When we are on a non-canvas tab, and the current generation's destination is not the canvas, we don't show the alert
|
||||
// For example, on the workflows tab, when the destinatin is gallery, we don't show the alert
|
||||
if (tab !== 'canvas' && destination !== 'canvas') {
|
||||
return false;
|
||||
}
|
||||
if (isStaging) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!destination) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return destination === 'canvas';
|
||||
}, [destination, isStaging, tab]);
|
||||
|
||||
return (
|
||||
<AlertWrapper
|
||||
title={t('controlLayers.sendingToCanvas')}
|
||||
description={
|
||||
<Trans i18nKey="controlLayers.viewProgressOnCanvas" components={{ Btn: <ActivateCanvasButton /> }} />
|
||||
}
|
||||
isVisible={isVisible}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const AlertWrapper = ({
|
||||
title,
|
||||
description,
|
||||
isVisible,
|
||||
}: {
|
||||
title: ReactNode;
|
||||
description: ReactNode;
|
||||
isVisible: boolean;
|
||||
}) => {
|
||||
const isHovered = useBoolean(false);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{(isVisible || isHovered.isTrue) && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1, transition: { duration: 0.1, ease: 'easeOut' } }}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
transition: { duration: 0.1, delay: !isHovered.isTrue ? 1 : 0.1, ease: 'easeIn' },
|
||||
}}
|
||||
onMouseEnter={isHovered.setTrue}
|
||||
onMouseLeave={isHovered.setFalse}
|
||||
>
|
||||
<Alert
|
||||
status="warning"
|
||||
flexDir="column"
|
||||
pointerEvents="auto"
|
||||
borderRadius="base"
|
||||
fontSize="sm"
|
||||
shadow="md"
|
||||
w="fit-content"
|
||||
>
|
||||
<Flex w="full" alignItems="center">
|
||||
<AlertIcon />
|
||||
<AlertTitle>{title}</AlertTitle>
|
||||
</Flex>
|
||||
<AlertDescription>{description}</AlertDescription>
|
||||
</Alert>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
@@ -1,261 +0,0 @@
|
||||
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||
import { dropTargetForElements, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { dropTargetForExternal, monitorForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter';
|
||||
import { Box, Button, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectEntityCountActive } from 'features/controlLayers/store/selectors';
|
||||
import { multipleImageDndSource, singleImageDndSource } from 'features/dnd/dnd';
|
||||
import { DndDropOverlay } from 'features/dnd/DndDropOverlay';
|
||||
import type { DndTargetState } from 'features/dnd/types';
|
||||
import RightPanelContent from 'features/gallery/components/GalleryTopBar';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { selectActiveTabCanvasRightPanel } from 'features/ui/store/uiSelectors';
|
||||
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const CanvasRightPanel = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const activeTab = useAppSelector(selectActiveTabCanvasRightPanel);
|
||||
const imageViewer = useImageViewer();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const tabIndex = useMemo(() => {
|
||||
if (activeTab === 'gallery') {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}, [activeTab]);
|
||||
|
||||
const onClickViewerToggleButton = useCallback(() => {
|
||||
imageViewer.open();
|
||||
}, [imageViewer]);
|
||||
|
||||
const onChangeTab = useCallback(
|
||||
(index: number) => {
|
||||
if (index === 0) {
|
||||
dispatch(activeTabCanvasRightPanelChanged('layers'));
|
||||
} else {
|
||||
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<Tabs index={tabIndex} onChange={onChangeTab} w="full" h="full" display="flex" flexDir="column">
|
||||
<TabList alignItems="center">
|
||||
<PanelTabs />
|
||||
<Spacer />
|
||||
<Button size="sm" variant="ghost" onClick={onClickViewerToggleButton}>
|
||||
{t('gallery.openViewer')}
|
||||
</Button>
|
||||
</TabList>
|
||||
<TabPanels w="full" h="full">
|
||||
<TabPanel w="full" h="full" p={0} pt={3}>
|
||||
<CanvasManagerProviderGate>
|
||||
<CanvasLayersPanelContent />
|
||||
</CanvasManagerProviderGate>
|
||||
</TabPanel>
|
||||
<TabPanel w="full" h="full" p={0} pt={3}>
|
||||
<RightPanelContent />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasRightPanel.displayName = 'CanvasRightPanel';
|
||||
|
||||
const PanelTabs = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const store = useAppStore();
|
||||
const activeEntityCount = useAppSelector(selectEntityCountActive);
|
||||
const [layersTabDndState, setLayersTabDndState] = useState<DndTargetState>('idle');
|
||||
const [galleryTabDndState, setGalleryTabDndState] = useState<DndTargetState>('idle');
|
||||
const layersTabRef = useRef<HTMLDivElement>(null);
|
||||
const galleryTabRef = useRef<HTMLDivElement>(null);
|
||||
const timeoutRef = useRef<number | null>(null);
|
||||
|
||||
const layersTabLabel = useMemo(() => {
|
||||
if (activeEntityCount === 0) {
|
||||
return t('controlLayers.layer_other');
|
||||
}
|
||||
return `${t('controlLayers.layer_other')} (${activeEntityCount})`;
|
||||
}, [activeEntityCount, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!layersTabRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const getIsOnLayersTab = () => selectActiveTabCanvasRightPanel(store.getState()) === 'layers';
|
||||
|
||||
const onDragEnter = () => {
|
||||
// If we are already on the layers tab, do nothing
|
||||
if (getIsOnLayersTab()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Else set the state to active and switch to the layers tab after a timeout
|
||||
setLayersTabDndState('over');
|
||||
timeoutRef.current = window.setTimeout(() => {
|
||||
timeoutRef.current = null;
|
||||
store.dispatch(activeTabCanvasRightPanelChanged('layers'));
|
||||
// When we switch tabs, the other tab should be pending
|
||||
setLayersTabDndState('idle');
|
||||
setGalleryTabDndState('potential');
|
||||
}, 300);
|
||||
};
|
||||
const onDragLeave = () => {
|
||||
// Set the state to idle or pending depending on the current tab
|
||||
if (getIsOnLayersTab()) {
|
||||
setLayersTabDndState('idle');
|
||||
} else {
|
||||
setLayersTabDndState('potential');
|
||||
}
|
||||
// Abort the tab switch if it hasn't happened yet
|
||||
if (timeoutRef.current !== null) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
const onDragStart = () => {
|
||||
// Set the state to pending when a drag starts
|
||||
setLayersTabDndState('potential');
|
||||
};
|
||||
return combine(
|
||||
dropTargetForElements({
|
||||
element: layersTabRef.current,
|
||||
onDragEnter,
|
||||
onDragLeave,
|
||||
}),
|
||||
monitorForElements({
|
||||
canMonitor: ({ source }) => {
|
||||
if (!singleImageDndSource.typeGuard(source.data) && !multipleImageDndSource.typeGuard(source.data)) {
|
||||
return false;
|
||||
}
|
||||
// Only monitor if we are not already on the gallery tab
|
||||
return !getIsOnLayersTab();
|
||||
},
|
||||
onDragStart,
|
||||
}),
|
||||
dropTargetForExternal({
|
||||
element: layersTabRef.current,
|
||||
onDragEnter,
|
||||
onDragLeave,
|
||||
}),
|
||||
monitorForExternal({
|
||||
canMonitor: () => !getIsOnLayersTab(),
|
||||
onDragStart,
|
||||
})
|
||||
);
|
||||
}, [store]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!galleryTabRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const getIsOnGalleryTab = () => selectActiveTabCanvasRightPanel(store.getState()) === 'gallery';
|
||||
|
||||
const onDragEnter = () => {
|
||||
// If we are already on the gallery tab, do nothing
|
||||
if (getIsOnGalleryTab()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Else set the state to active and switch to the gallery tab after a timeout
|
||||
setGalleryTabDndState('over');
|
||||
timeoutRef.current = window.setTimeout(() => {
|
||||
timeoutRef.current = null;
|
||||
store.dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||
// When we switch tabs, the other tab should be pending
|
||||
setGalleryTabDndState('idle');
|
||||
setLayersTabDndState('potential');
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const onDragLeave = () => {
|
||||
// Set the state to idle or pending depending on the current tab
|
||||
if (getIsOnGalleryTab()) {
|
||||
setGalleryTabDndState('idle');
|
||||
} else {
|
||||
setGalleryTabDndState('potential');
|
||||
}
|
||||
// Abort the tab switch if it hasn't happened yet
|
||||
if (timeoutRef.current !== null) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
const onDragStart = () => {
|
||||
// Set the state to pending when a drag starts
|
||||
setGalleryTabDndState('potential');
|
||||
};
|
||||
|
||||
return combine(
|
||||
dropTargetForElements({
|
||||
element: galleryTabRef.current,
|
||||
onDragEnter,
|
||||
onDragLeave,
|
||||
}),
|
||||
monitorForElements({
|
||||
canMonitor: ({ source }) => {
|
||||
if (!singleImageDndSource.typeGuard(source.data) && !multipleImageDndSource.typeGuard(source.data)) {
|
||||
return false;
|
||||
}
|
||||
// Only monitor if we are not already on the gallery tab
|
||||
return !getIsOnGalleryTab();
|
||||
},
|
||||
onDragStart,
|
||||
}),
|
||||
dropTargetForExternal({
|
||||
element: galleryTabRef.current,
|
||||
onDragEnter,
|
||||
onDragLeave,
|
||||
}),
|
||||
monitorForExternal({
|
||||
canMonitor: () => !getIsOnGalleryTab(),
|
||||
onDragStart,
|
||||
})
|
||||
);
|
||||
}, [store]);
|
||||
|
||||
useEffect(() => {
|
||||
const onDrop = () => {
|
||||
// Reset the dnd state when a drop happens
|
||||
setGalleryTabDndState('idle');
|
||||
setLayersTabDndState('idle');
|
||||
};
|
||||
const cleanup = combine(monitorForElements({ onDrop }), monitorForExternal({ onDrop }));
|
||||
|
||||
return () => {
|
||||
cleanup();
|
||||
if (timeoutRef.current !== null) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tab ref={layersTabRef} position="relative" w={32}>
|
||||
<Box as="span" w="full">
|
||||
{layersTabLabel}
|
||||
</Box>
|
||||
<DndDropOverlay dndState={layersTabDndState} withBackdrop={false} />
|
||||
</Tab>
|
||||
<Tab ref={galleryTabRef} position="relative" w={32}>
|
||||
<Box as="span" w="full">
|
||||
{t('gallery.gallery')}
|
||||
</Box>
|
||||
<DndDropOverlay dndState={galleryTabDndState} withBackdrop={false} />
|
||||
</Tab>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
PanelTabs.displayName = 'PanelTabs';
|
||||
@@ -1,254 +0,0 @@
|
||||
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||
import { dropTargetForElements, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { dropTargetForExternal, monitorForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter';
|
||||
import { Box, Tab } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectEntityCountActive } from 'features/controlLayers/store/selectors';
|
||||
import { multipleImageDndSource, singleImageDndSource } from 'features/dnd/dnd';
|
||||
import { DndDropOverlay } from 'features/dnd/DndDropOverlay';
|
||||
import type { DndTargetState } from 'features/dnd/types';
|
||||
import RightPanelContent from 'features/gallery/components/GalleryTopBar';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { selectActiveTabCanvasRightPanel } from 'features/ui/store/uiSelectors';
|
||||
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
||||
|
||||
export const CanvasRightPanelStacked = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const activeTab = useAppSelector(selectActiveTabCanvasRightPanel);
|
||||
const imageViewer = useImageViewer();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const tabIndex = useMemo(() => {
|
||||
if (activeTab === 'gallery') {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}, [activeTab]);
|
||||
|
||||
const onClickViewerToggleButton = useCallback(() => {
|
||||
imageViewer.open();
|
||||
}, [imageViewer]);
|
||||
|
||||
const onChangeTab = useCallback(
|
||||
(index: number) => {
|
||||
if (index === 0) {
|
||||
dispatch(activeTabCanvasRightPanelChanged('layers'));
|
||||
} else {
|
||||
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<PanelGroup direction="vertical">
|
||||
<Panel>
|
||||
<RightPanelContent />
|
||||
</Panel>
|
||||
<PanelResizeHandle />
|
||||
<Panel>
|
||||
<CanvasManagerProviderGate>
|
||||
<CanvasLayersPanelContent />
|
||||
</CanvasManagerProviderGate>
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasRightPanelStacked.displayName = 'CanvasRightPanelStacked';
|
||||
|
||||
const PanelTabs = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const store = useAppStore();
|
||||
const activeEntityCount = useAppSelector(selectEntityCountActive);
|
||||
const [layersTabDndState, setLayersTabDndState] = useState<DndTargetState>('idle');
|
||||
const [galleryTabDndState, setGalleryTabDndState] = useState<DndTargetState>('idle');
|
||||
const layersTabRef = useRef<HTMLDivElement>(null);
|
||||
const galleryTabRef = useRef<HTMLDivElement>(null);
|
||||
const timeoutRef = useRef<number | null>(null);
|
||||
|
||||
const layersTabLabel = useMemo(() => {
|
||||
if (activeEntityCount === 0) {
|
||||
return t('controlLayers.layer_other');
|
||||
}
|
||||
return `${t('controlLayers.layer_other')} (${activeEntityCount})`;
|
||||
}, [activeEntityCount, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!layersTabRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const getIsOnLayersTab = () => selectActiveTabCanvasRightPanel(store.getState()) === 'layers';
|
||||
|
||||
const onDragEnter = () => {
|
||||
// If we are already on the layers tab, do nothing
|
||||
if (getIsOnLayersTab()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Else set the state to active and switch to the layers tab after a timeout
|
||||
setLayersTabDndState('over');
|
||||
timeoutRef.current = window.setTimeout(() => {
|
||||
timeoutRef.current = null;
|
||||
store.dispatch(activeTabCanvasRightPanelChanged('layers'));
|
||||
// When we switch tabs, the other tab should be pending
|
||||
setLayersTabDndState('idle');
|
||||
setGalleryTabDndState('potential');
|
||||
}, 300);
|
||||
};
|
||||
const onDragLeave = () => {
|
||||
// Set the state to idle or pending depending on the current tab
|
||||
if (getIsOnLayersTab()) {
|
||||
setLayersTabDndState('idle');
|
||||
} else {
|
||||
setLayersTabDndState('potential');
|
||||
}
|
||||
// Abort the tab switch if it hasn't happened yet
|
||||
if (timeoutRef.current !== null) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
const onDragStart = () => {
|
||||
// Set the state to pending when a drag starts
|
||||
setLayersTabDndState('potential');
|
||||
};
|
||||
return combine(
|
||||
dropTargetForElements({
|
||||
element: layersTabRef.current,
|
||||
onDragEnter,
|
||||
onDragLeave,
|
||||
}),
|
||||
monitorForElements({
|
||||
canMonitor: ({ source }) => {
|
||||
if (!singleImageDndSource.typeGuard(source.data) && !multipleImageDndSource.typeGuard(source.data)) {
|
||||
return false;
|
||||
}
|
||||
// Only monitor if we are not already on the gallery tab
|
||||
return !getIsOnLayersTab();
|
||||
},
|
||||
onDragStart,
|
||||
}),
|
||||
dropTargetForExternal({
|
||||
element: layersTabRef.current,
|
||||
onDragEnter,
|
||||
onDragLeave,
|
||||
}),
|
||||
monitorForExternal({
|
||||
canMonitor: () => !getIsOnLayersTab(),
|
||||
onDragStart,
|
||||
})
|
||||
);
|
||||
}, [store]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!galleryTabRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const getIsOnGalleryTab = () => selectActiveTabCanvasRightPanel(store.getState()) === 'gallery';
|
||||
|
||||
const onDragEnter = () => {
|
||||
// If we are already on the gallery tab, do nothing
|
||||
if (getIsOnGalleryTab()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Else set the state to active and switch to the gallery tab after a timeout
|
||||
setGalleryTabDndState('over');
|
||||
timeoutRef.current = window.setTimeout(() => {
|
||||
timeoutRef.current = null;
|
||||
store.dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||
// When we switch tabs, the other tab should be pending
|
||||
setGalleryTabDndState('idle');
|
||||
setLayersTabDndState('potential');
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const onDragLeave = () => {
|
||||
// Set the state to idle or pending depending on the current tab
|
||||
if (getIsOnGalleryTab()) {
|
||||
setGalleryTabDndState('idle');
|
||||
} else {
|
||||
setGalleryTabDndState('potential');
|
||||
}
|
||||
// Abort the tab switch if it hasn't happened yet
|
||||
if (timeoutRef.current !== null) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
const onDragStart = () => {
|
||||
// Set the state to pending when a drag starts
|
||||
setGalleryTabDndState('potential');
|
||||
};
|
||||
|
||||
return combine(
|
||||
dropTargetForElements({
|
||||
element: galleryTabRef.current,
|
||||
onDragEnter,
|
||||
onDragLeave,
|
||||
}),
|
||||
monitorForElements({
|
||||
canMonitor: ({ source }) => {
|
||||
if (!singleImageDndSource.typeGuard(source.data) && !multipleImageDndSource.typeGuard(source.data)) {
|
||||
return false;
|
||||
}
|
||||
// Only monitor if we are not already on the gallery tab
|
||||
return !getIsOnGalleryTab();
|
||||
},
|
||||
onDragStart,
|
||||
}),
|
||||
dropTargetForExternal({
|
||||
element: galleryTabRef.current,
|
||||
onDragEnter,
|
||||
onDragLeave,
|
||||
}),
|
||||
monitorForExternal({
|
||||
canMonitor: () => !getIsOnGalleryTab(),
|
||||
onDragStart,
|
||||
})
|
||||
);
|
||||
}, [store]);
|
||||
|
||||
useEffect(() => {
|
||||
const onDrop = () => {
|
||||
// Reset the dnd state when a drop happens
|
||||
setGalleryTabDndState('idle');
|
||||
setLayersTabDndState('idle');
|
||||
};
|
||||
const cleanup = combine(monitorForElements({ onDrop }), monitorForExternal({ onDrop }));
|
||||
|
||||
return () => {
|
||||
cleanup();
|
||||
if (timeoutRef.current !== null) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tab ref={layersTabRef} position="relative" w={32}>
|
||||
<Box as="span" w="full">
|
||||
{layersTabLabel}
|
||||
</Box>
|
||||
<DndDropOverlay dndState={layersTabDndState} withBackdrop={false} />
|
||||
</Tab>
|
||||
<Tab ref={galleryTabRef} position="relative" w={32}>
|
||||
<Box as="span" w="full">
|
||||
{t('gallery.gallery')}
|
||||
</Box>
|
||||
<DndDropOverlay dndState={galleryTabDndState} withBackdrop={false} />
|
||||
</Tab>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
PanelTabs.displayName = 'PanelTabs';
|
||||
@@ -17,7 +17,7 @@ import type { S } from 'services/api/types';
|
||||
import { $socket } from 'services/events/stores';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
export type ProgressData = {
|
||||
type ProgressData = {
|
||||
itemId: number;
|
||||
progressEvent: S['InvocationProgressEvent'] | null;
|
||||
progressImage: ProgressImage | null;
|
||||
@@ -46,28 +46,7 @@ export const useProgressData = (
|
||||
return value;
|
||||
};
|
||||
|
||||
export const useHasProgressImage = (
|
||||
$progressData: WritableAtom<Record<number, ProgressData>>,
|
||||
itemId: number
|
||||
): boolean => {
|
||||
const [value, setValue] = useState(false);
|
||||
useEffect(() => {
|
||||
const unsub = $progressData.subscribe((data) => {
|
||||
const progressData = data[itemId];
|
||||
setValue(Boolean(progressData?.progressImage));
|
||||
});
|
||||
return () => {
|
||||
unsub();
|
||||
};
|
||||
}, [$progressData, itemId]);
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
export const setProgress = (
|
||||
$progressData: WritableAtom<Record<number, ProgressData>>,
|
||||
data: S['InvocationProgressEvent']
|
||||
) => {
|
||||
const setProgress = ($progressData: WritableAtom<Record<number, ProgressData>>, data: S['InvocationProgressEvent']) => {
|
||||
const progressData = $progressData.get();
|
||||
const current = progressData[data.item_id];
|
||||
if (current) {
|
||||
@@ -120,7 +99,7 @@ export const clearProgressImage = ($progressData: WritableAtom<Record<number, Pr
|
||||
});
|
||||
};
|
||||
|
||||
export type CanvasSessionContextValue = {
|
||||
type CanvasSessionContextValue = {
|
||||
session: SimpleSessionIdentifier | AdvancedSessionIdentifier;
|
||||
$items: Atom<S['SessionQueueItem'][]>;
|
||||
$itemCount: Atom<number>;
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo } from 'react';
|
||||
import { useGetQueueCountsByDestinationQuery } from 'services/api/endpoints/queue';
|
||||
|
||||
// This hook just serves as a persistent subscriber for the queue count query.
|
||||
const queueCountArg = { destination: 'canvas' };
|
||||
const useCanvasQueueCountWatcher = () => {
|
||||
useGetQueueCountsByDestinationQuery(queueCountArg);
|
||||
};
|
||||
|
||||
export const StagingAreaIsStagingGate = memo((props: PropsWithChildren) => {
|
||||
useCanvasQueueCountWatcher();
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
|
||||
if (!isStaging) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return props.children;
|
||||
});
|
||||
|
||||
StagingAreaIsStagingGate.displayName = 'StagingAreaIsStagingGate';
|
||||
@@ -5,10 +5,7 @@ import { useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
|
||||
import {
|
||||
selectImageCount,
|
||||
stagingAreaReset,
|
||||
} from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { selectImageCount, stagingAreaReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { selectBboxRect, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
|
||||
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
|
||||
import { imageNameToImageObject } from 'features/controlLayers/store/util';
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import {
|
||||
selectImageCount,
|
||||
selectSelectedImage,
|
||||
selectStagedImageIndex,
|
||||
} from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiXBold } from 'react-icons/pi';
|
||||
@@ -14,12 +8,8 @@ import { useDeleteQueueItemMutation } from 'services/api/endpoints/queue';
|
||||
|
||||
export const StagingAreaToolbarDiscardSelectedButton = memo(() => {
|
||||
const ctx = useCanvasSessionContext();
|
||||
const dispatch = useAppDispatch();
|
||||
const [deleteQueueItem] = useDeleteQueueItemMutation();
|
||||
const selectedItemId = useStore(ctx.$selectedItemId);
|
||||
const index = useAppSelector(selectStagedImageIndex);
|
||||
const selectedImage = useAppSelector(selectSelectedImage);
|
||||
const imageCount = useAppSelector(selectImageCount);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -28,11 +18,6 @@ export const StagingAreaToolbarDiscardSelectedButton = memo(() => {
|
||||
return;
|
||||
}
|
||||
deleteQueueItem({ item_id: selectedItemId });
|
||||
// if (imageCount === 1) {
|
||||
// dispatch(stagingAreaReset());
|
||||
// } else {
|
||||
// dispatch(stagingAreaStagedImageDiscarded({ index }));
|
||||
// }
|
||||
}, [selectedItemId, deleteQueueItem]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import { Button } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import { selectImageCount, selectStagedImageIndex } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { memo, useMemo } from 'react';
|
||||
|
||||
export const StagingAreaToolbarImageCountButton = memo(() => {
|
||||
const ctx = useCanvasSessionContext();
|
||||
const selectItemIndex = useStore(ctx.$selectedItemIndex);
|
||||
const itemCount = useStore(ctx.$itemCount);
|
||||
const index = useAppSelector(selectStagedImageIndex);
|
||||
const imageCount = useAppSelector(selectImageCount);
|
||||
|
||||
const counterText = useMemo(() => {
|
||||
if (itemCount > 0 && selectItemIndex !== null) {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectImageCount } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -13,9 +11,7 @@ import { PiArrowRightBold } from 'react-icons/pi';
|
||||
export const StagingAreaToolbarNextButton = memo(() => {
|
||||
const ctx = useCanvasSessionContext();
|
||||
const itemCount = useStore(ctx.$itemCount);
|
||||
const dispatch = useAppDispatch();
|
||||
const canvasManager = useCanvasManager();
|
||||
const imageCount = useAppSelector(selectImageCount);
|
||||
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
|
||||
const isCanvasFocused = useIsRegionFocused('canvas');
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectImageCount } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -13,9 +11,7 @@ import { PiArrowLeftBold } from 'react-icons/pi';
|
||||
export const StagingAreaToolbarPrevButton = memo(() => {
|
||||
const ctx = useCanvasSessionContext();
|
||||
const itemCount = useStore(ctx.$itemCount);
|
||||
const dispatch = useAppDispatch();
|
||||
const canvasManager = useCanvasManager();
|
||||
const imageCount = useAppSelector(selectImageCount);
|
||||
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
|
||||
const isCanvasFocused = useIsRegionFocused('canvas');
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { EMPTY_ARRAY } from 'app/store/constants';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useEntityIsEnabled } from 'features/controlLayers/hooks/useEntityIsEnabled';
|
||||
import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import {
|
||||
@@ -18,7 +19,6 @@ import { upperFirst } from 'lodash-es';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiWarningBold } from 'react-icons/pi';
|
||||
import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
|
||||
import type { Equals } from 'tsafe';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
rgNegativePromptChanged,
|
||||
rgPositivePromptChanged,
|
||||
} from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectBase } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectBase, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors';
|
||||
import type {
|
||||
CanvasEntityIdentifier,
|
||||
@@ -35,11 +35,7 @@ import {
|
||||
} from 'features/controlLayers/store/util';
|
||||
import { zModelIdentifierField } from 'features/nodes/types/common';
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
modelConfigsAdapterSelectors,
|
||||
selectModelConfigsQuery,
|
||||
} from 'services/api/endpoints/models';
|
||||
import { selectMainModelConfig } from '../store/paramsSlice';
|
||||
import { modelConfigsAdapterSelectors, selectModelConfigsQuery } from 'services/api/endpoints/models';
|
||||
import type {
|
||||
ControlLoRAModelConfig,
|
||||
ControlNetModelConfig,
|
||||
|
||||
@@ -14,7 +14,12 @@ import {
|
||||
rgAdded,
|
||||
rgIPAdapterImageChanged,
|
||||
} from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectNegativePrompt, selectPositivePrompt, selectSeed } from 'features/controlLayers/store/paramsSlice';
|
||||
import {
|
||||
selectMainModelConfig,
|
||||
selectNegativePrompt,
|
||||
selectPositivePrompt,
|
||||
selectSeed,
|
||||
} from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectCanvasMetadata } from 'features/controlLayers/store/selectors';
|
||||
import type {
|
||||
CanvasControlLayerState,
|
||||
@@ -33,7 +38,6 @@ import { toast } from 'features/toast/toast';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { serializeError } from 'serialize-error';
|
||||
import { selectMainModelConfig } from '../store/paramsSlice';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import type { JsonObject } from 'type-fest';
|
||||
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
import { Mutex } from 'async-mutex';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { getPrefixedId, loadImage } from 'features/controlLayers/konva/util';
|
||||
import { selectShowProgressOnCanvas } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import Konva from 'konva';
|
||||
import { atom } from 'nanostores';
|
||||
import type { Logger } from 'roarr';
|
||||
import { selectCanvasQueueCounts } from 'services/api/endpoints/queue';
|
||||
import type { S } from 'services/api/types';
|
||||
import type { SetNonNullable } from 'type-fest';
|
||||
|
||||
type ProgressEventWithImage = SetNonNullable<S['InvocationProgressEvent'], 'image'>;
|
||||
const isProgressEventWithImage = (val: S['InvocationProgressEvent']): val is ProgressEventWithImage =>
|
||||
Boolean(val.image);
|
||||
|
||||
export class CanvasProgressImageModule extends CanvasModuleBase {
|
||||
readonly type = 'progress_image';
|
||||
readonly id: string;
|
||||
readonly path: string[];
|
||||
readonly parent: CanvasManager;
|
||||
readonly manager: CanvasManager;
|
||||
readonly log: Logger;
|
||||
|
||||
konva: {
|
||||
group: Konva.Group;
|
||||
image: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately
|
||||
};
|
||||
$isLoading = atom<boolean>(false);
|
||||
$isError = atom<boolean>(false);
|
||||
imageElement: HTMLImageElement | null = null;
|
||||
|
||||
subscriptions = new Set<() => void>();
|
||||
$lastProgressEvent = atom<ProgressEventWithImage | null>(null);
|
||||
$hasActiveGeneration = atom<boolean>(false);
|
||||
mutex: Mutex = new Mutex();
|
||||
|
||||
constructor(manager: CanvasManager) {
|
||||
super();
|
||||
this.id = getPrefixedId(this.type);
|
||||
this.parent = manager;
|
||||
this.manager = manager;
|
||||
this.path = this.manager.buildPath(this);
|
||||
this.log = this.manager.buildLogger(this);
|
||||
|
||||
this.log.debug('Creating progress image module');
|
||||
|
||||
this.konva = {
|
||||
group: new Konva.Group({ name: `${this.type}:group`, listening: false }),
|
||||
image: null,
|
||||
};
|
||||
|
||||
this.subscriptions.add(this.manager.stagingArea.$shouldShowStagedImage.listen(this.render));
|
||||
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectShowProgressOnCanvas, this.render));
|
||||
this.subscriptions.add(this.setSocketEventListeners());
|
||||
this.subscriptions.add(
|
||||
this.manager.stateApi.createStoreSubscription(selectCanvasQueueCounts, ({ data }) => {
|
||||
if (data && (data.in_progress > 0 || data.pending > 0)) {
|
||||
this.$hasActiveGeneration.set(true);
|
||||
} else {
|
||||
this.$hasActiveGeneration.set(false);
|
||||
}
|
||||
})
|
||||
);
|
||||
this.subscriptions.add(this.$lastProgressEvent.listen(this.render));
|
||||
}
|
||||
|
||||
setSocketEventListeners = (): (() => void) => {
|
||||
const progressListener = (data: S['InvocationProgressEvent']) => {
|
||||
if (data.destination !== 'canvas') {
|
||||
return;
|
||||
}
|
||||
if (!isProgressEventWithImage(data)) {
|
||||
return;
|
||||
}
|
||||
if (!this.$hasActiveGeneration.get()) {
|
||||
return;
|
||||
}
|
||||
this.$lastProgressEvent.set(data);
|
||||
};
|
||||
|
||||
// Handle a canceled or failed canvas generation. We should clear the progress image in this case.
|
||||
const queueItemStatusChangedListener = (data: S['QueueItemStatusChangedEvent']) => {
|
||||
if (data.destination !== 'canvas') {
|
||||
return;
|
||||
}
|
||||
|
||||
// The staging area module handles _completed_ events. Only care about failed or canceled here.
|
||||
if (data.status === 'failed' || data.status === 'canceled') {
|
||||
this.$lastProgressEvent.set(null);
|
||||
this.$hasActiveGeneration.set(false);
|
||||
}
|
||||
};
|
||||
|
||||
const clearProgress = () => {
|
||||
this.$lastProgressEvent.set(null);
|
||||
};
|
||||
|
||||
this.manager.socket.on('invocation_progress', progressListener);
|
||||
this.manager.socket.on('queue_item_status_changed', queueItemStatusChangedListener);
|
||||
this.manager.socket.on('connect', clearProgress);
|
||||
this.manager.socket.on('connect_error', clearProgress);
|
||||
this.manager.socket.on('disconnect', clearProgress);
|
||||
|
||||
return () => {
|
||||
this.manager.socket.off('invocation_progress', progressListener);
|
||||
this.manager.socket.off('queue_item_status_changed', queueItemStatusChangedListener);
|
||||
this.manager.socket.off('connect', clearProgress);
|
||||
this.manager.socket.off('connect_error', clearProgress);
|
||||
this.manager.socket.off('disconnect', clearProgress);
|
||||
};
|
||||
};
|
||||
|
||||
getNodes = () => {
|
||||
return [this.konva.group];
|
||||
};
|
||||
|
||||
render = async () => {
|
||||
const release = await this.mutex.acquire();
|
||||
|
||||
const event = this.$lastProgressEvent.get();
|
||||
const showProgressOnCanvas = this.manager.stateApi.runSelector(selectShowProgressOnCanvas);
|
||||
|
||||
if (!event || !showProgressOnCanvas) {
|
||||
this.konva.group.visible(false);
|
||||
this.konva.image?.destroy();
|
||||
this.konva.image = null;
|
||||
this.imageElement = null;
|
||||
this.$isLoading.set(false);
|
||||
this.$isError.set(false);
|
||||
release();
|
||||
return;
|
||||
}
|
||||
|
||||
this.$isLoading.set(true);
|
||||
|
||||
const { x, y, width, height } = this.manager.stateApi.getBbox().rect;
|
||||
try {
|
||||
this.imageElement = await loadImage(event.image.dataURL);
|
||||
if (this.konva.image) {
|
||||
this.konva.image.setAttrs({
|
||||
image: this.imageElement,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
} else {
|
||||
this.konva.image = new Konva.Image({
|
||||
name: `${this.type}:image`,
|
||||
listening: false,
|
||||
image: this.imageElement,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
perfectDrawEnabled: false,
|
||||
});
|
||||
this.konva.group.add(this.konva.image);
|
||||
}
|
||||
// Should not be visible if the user has disabled showing staging images
|
||||
this.konva.group.visible(this.manager.stagingArea.$shouldShowStagedImage.get());
|
||||
} catch {
|
||||
this.$isError.set(true);
|
||||
} finally {
|
||||
this.$isLoading.set(false);
|
||||
release();
|
||||
}
|
||||
};
|
||||
|
||||
destroy = () => {
|
||||
this.log.debug('Destroying module');
|
||||
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
||||
this.subscriptions.clear();
|
||||
this.konva.group.destroy();
|
||||
};
|
||||
|
||||
repr = () => {
|
||||
return {
|
||||
id: this.id,
|
||||
type: this.type,
|
||||
path: this.path,
|
||||
$lastProgressEvent: parseify(this.$lastProgressEvent.get()),
|
||||
$hasActiveGeneration: this.$hasActiveGeneration.get(),
|
||||
$isError: this.$isError.get(),
|
||||
$isLoading: this.$isLoading.get(),
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -64,7 +64,6 @@ const zImageWithDimsDataURL = z.object({
|
||||
width: z.number().int().positive(),
|
||||
height: z.number().int().positive(),
|
||||
});
|
||||
export type ImageWithDimsDataURL = z.infer<typeof zImageWithDimsDataURL>;
|
||||
|
||||
const zBeginEndStepPct = z
|
||||
.tuple([z.number().gte(0).lte(1), z.number().gte(0).lte(1)])
|
||||
@@ -100,7 +99,7 @@ const zRgbColor = z.object({
|
||||
b: z.number().int().min(0).max(255),
|
||||
});
|
||||
export type RgbColor = z.infer<typeof zRgbColor>;
|
||||
export const zRgbaColor = zRgbColor.extend({
|
||||
const zRgbaColor = zRgbColor.extend({
|
||||
a: z.number().min(0).max(1),
|
||||
});
|
||||
export type RgbaColor = z.infer<typeof zRgbaColor>;
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Skeleton } from '@invoke-ai/ui-library';
|
||||
import { memo } from 'react';
|
||||
|
||||
type Props = {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export const SizedSkeletonLoader = memo(({ width, height }: Props) => {
|
||||
return <Skeleton w={`${width}px`} h="auto" objectFit="contain" aspectRatio={`${width}/${height}`} />;
|
||||
});
|
||||
|
||||
SizedSkeletonLoader.displayName = 'SizedSkeletonLoader';
|
||||
@@ -2,7 +2,6 @@ import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasAlertsInvocationProgress } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress';
|
||||
import { CanvasAlertsSendingToCanvas } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSendingTo';
|
||||
import { DndImage } from 'features/dnd/DndImage';
|
||||
import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer';
|
||||
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
|
||||
@@ -52,7 +51,6 @@ const CurrentImagePreview = ({ imageDTO }: { imageDTO?: ImageDTO }) => {
|
||||
pointerEvents="none"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<CanvasAlertsSendingToCanvas />
|
||||
<CanvasAlertsInvocationProgress />
|
||||
</Flex>
|
||||
{shouldShowImageDetails && imageDTO && (
|
||||
|
||||
@@ -7,7 +7,6 @@ import CurrentImagePreview from 'features/gallery/components/ImageViewer/Current
|
||||
import { ImageComparison } from 'features/gallery/components/ImageViewer/ImageComparison';
|
||||
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar';
|
||||
import { selectLastSelectedImageName } from 'features/gallery/store/gallerySelectors';
|
||||
import type { ReactNode } from 'react';
|
||||
import { memo, useRef } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -16,25 +15,25 @@ import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
|
||||
import { useImageViewer } from './useImageViewer';
|
||||
|
||||
type Props = {
|
||||
closeButton?: ReactNode;
|
||||
};
|
||||
// type Props = {
|
||||
// closeButton?: ReactNode;
|
||||
// };
|
||||
|
||||
const useFocusRegionOptions = {
|
||||
focusOnMount: true,
|
||||
};
|
||||
// const useFocusRegionOptions = {
|
||||
// focusOnMount: true,
|
||||
// };
|
||||
|
||||
const FOCUS_REGION_STYLES: SystemStyleObject = {
|
||||
display: 'flex',
|
||||
width: 'full',
|
||||
height: 'full',
|
||||
position: 'absolute',
|
||||
flexDirection: 'column',
|
||||
inset: 0,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
};
|
||||
// const FOCUS_REGION_STYLES: SystemStyleObject = {
|
||||
// display: 'flex',
|
||||
// width: 'full',
|
||||
// height: 'full',
|
||||
// position: 'absolute',
|
||||
// flexDirection: 'column',
|
||||
// inset: 0,
|
||||
// alignItems: 'center',
|
||||
// justifyContent: 'center',
|
||||
// overflow: 'hidden',
|
||||
// };
|
||||
|
||||
export const ImageViewer = memo(() => {
|
||||
const lastSelectedImageName = useAppSelector(selectLastSelectedImageName);
|
||||
|
||||
@@ -93,7 +93,7 @@ export const zMainModelBase = z.enum([
|
||||
'chatgpt-4o',
|
||||
'flux-kontext',
|
||||
]);
|
||||
export type MainModelBase = z.infer<typeof zMainModelBase>;
|
||||
type MainModelBase = z.infer<typeof zMainModelBase>;
|
||||
export const isMainModelBase = (base: unknown): base is MainModelBase => zMainModelBase.safeParse(base).success;
|
||||
const zModelType = z.enum([
|
||||
'main',
|
||||
|
||||
@@ -149,7 +149,7 @@ export const getInfill = (
|
||||
assert(false, 'Unknown infill method');
|
||||
};
|
||||
|
||||
export const CANVAS_OUTPUT_PREFIX = 'canvas_output';
|
||||
const CANVAS_OUTPUT_PREFIX = 'canvas_output';
|
||||
|
||||
export const isMainModelWithoutUnet = (modelLoader: Invocation<MainModelLoaderNodes>) => {
|
||||
return (
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import type { ButtonProps } from '@invoke-ai/ui-library';
|
||||
import { Button } from '@invoke-ai/ui-library';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiTrashSimpleFill } from 'react-icons/pi';
|
||||
|
||||
import { useClearQueueDialog } from './ClearQueueConfirmationAlertDialog';
|
||||
|
||||
type Props = ButtonProps;
|
||||
|
||||
const ClearQueueButton = (props: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const clearQueue = useClearQueueDialog();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
isDisabled={clearQueue.isDisabled}
|
||||
isLoading={clearQueue.isLoading}
|
||||
tooltip={t('queue.clearTooltip')}
|
||||
leftIcon={<PiTrashSimpleFill />}
|
||||
colorScheme="error"
|
||||
onClick={clearQueue.openDialog}
|
||||
data-testid={t('queue.clear')}
|
||||
{...props}
|
||||
>
|
||||
{t('queue.clear')}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ClearQueueButton);
|
||||
@@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
const [useClearQueueConfirmationAlertDialog] = buildUseBoolean(false);
|
||||
|
||||
export const useClearQueueDialog = () => {
|
||||
const useClearQueueDialog = () => {
|
||||
const dialog = useClearQueueConfirmationAlertDialog();
|
||||
const { clearQueue, isLoading, isDisabled, queueStatus } = useClearQueue();
|
||||
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasMainPanelContent } from 'features/controlLayers/components/CanvasMainPanelContent';
|
||||
import { useDndMonitor } from 'features/dnd/useDndMonitor';
|
||||
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
||||
import WorkflowsTabLeftPanel from 'features/nodes/components/sidePanel/WorkflowsTabLeftPanel';
|
||||
import QueueControls from 'features/queue/components/QueueControls';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { FloatingLeftPanelButtons } from 'features/ui/components/FloatingLeftPanelButtons';
|
||||
import { FloatingRightPanelButtons } from 'features/ui/components/FloatingRightPanelButtons';
|
||||
import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage';
|
||||
import { LeftPanelContent } from 'features/ui/components/LeftPanelContent';
|
||||
import { MainPanelContent } from 'features/ui/components/MainPanelContent';
|
||||
import { RightPanelContent } from 'features/ui/components/RightPanelContent';
|
||||
import ModelManagerTab from 'features/ui/components/tabs/ModelManagerTab';
|
||||
import QueueTab from 'features/ui/components/tabs/QueueTab';
|
||||
import { WorkflowsMainPanel } from 'features/ui/components/tabs/WorkflowsTabContent';
|
||||
import { VerticalNavBar } from 'features/ui/components/VerticalNavBar';
|
||||
import type { UsePanelOptions } from 'features/ui/hooks/usePanel';
|
||||
import { usePanel } from 'features/ui/hooks/usePanel';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import {
|
||||
$isLeftPanelOpen,
|
||||
$isRightPanelOpen,
|
||||
@@ -29,10 +22,7 @@ import type { CSSProperties } from 'react';
|
||||
import { memo, useMemo, useRef } from 'react';
|
||||
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
|
||||
import { Panel, PanelGroup } from 'react-resizable-panels';
|
||||
import type { Equals } from 'tsafe';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
import ParametersPanelUpscale from './ParametersPanels/ParametersPanelUpscale';
|
||||
import { VerticalResizeHandle } from './tabs/ResizeHandle';
|
||||
|
||||
const panelStyles: CSSProperties = { position: 'relative', height: '100%', width: '100%', minWidth: 0 };
|
||||
@@ -153,42 +143,3 @@ export const AppContent = memo(() => {
|
||||
);
|
||||
});
|
||||
AppContent.displayName = 'AppContent';
|
||||
|
||||
const LeftPanelContent = memo(() => {
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" w="full" h="full" gap={2}>
|
||||
<QueueControls />
|
||||
<Box position="relative" w="full" h="full">
|
||||
{tab === 'canvas' && <ParametersPanelTextToImage />}
|
||||
{tab === 'upscaling' && <ParametersPanelUpscale />}
|
||||
{tab === 'workflows' && <WorkflowsTabLeftPanel />}
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
LeftPanelContent.displayName = 'LeftPanelContent';
|
||||
|
||||
const MainPanelContent = memo(() => {
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
|
||||
if (tab === 'canvas') {
|
||||
return <CanvasMainPanelContent />;
|
||||
}
|
||||
if (tab === 'upscaling') {
|
||||
return <ImageViewer />;
|
||||
}
|
||||
if (tab === 'workflows') {
|
||||
return <WorkflowsMainPanel />;
|
||||
}
|
||||
if (tab === 'models') {
|
||||
return <ModelManagerTab />;
|
||||
}
|
||||
if (tab === 'queue') {
|
||||
return <QueueTab />;
|
||||
}
|
||||
|
||||
assert<Equals<never, typeof tab>>(false);
|
||||
});
|
||||
MainPanelContent.displayName = 'MainPanelContent';
|
||||
|
||||
@@ -8,7 +8,7 @@ import { memo } from 'react';
|
||||
|
||||
import ParametersPanelUpscale from './ParametersPanels/ParametersPanelUpscale';
|
||||
|
||||
const LeftPanelContent = memo(() => {
|
||||
export const LeftPanelContent = memo(() => {
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,7 +9,7 @@ import { memo } from 'react';
|
||||
import type { Equals } from 'tsafe';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
const MainPanelContent = memo(() => {
|
||||
export const MainPanelContent = memo(() => {
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
|
||||
if (tab === 'canvas') {
|
||||
|
||||
@@ -324,4 +324,3 @@ export const {
|
||||
} = modelsApi;
|
||||
|
||||
export const selectModelConfigsQuery = modelsApi.endpoints.getModelConfigs.select();
|
||||
|
||||
|
||||
@@ -364,11 +364,8 @@ export const {
|
||||
useClearQueueMutation,
|
||||
usePruneQueueMutation,
|
||||
useGetQueueStatusQuery,
|
||||
useGetQueueItemQuery,
|
||||
useListQueueItemsQuery,
|
||||
useListAllQueueItemsQuery,
|
||||
useCancelQueueItemMutation,
|
||||
useCancelByDestinationMutation,
|
||||
useDeleteQueueItemMutation,
|
||||
useDeleteQueueItemsByDestinationMutation,
|
||||
useGetBatchStatusQuery,
|
||||
|
||||
@@ -11,9 +11,7 @@ import { boardsApi } from 'services/api/endpoints/boards';
|
||||
import { getImageDTOSafe, imagesApi } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO, S } from 'services/api/types';
|
||||
import { getCategories, getListImagesUrl } from 'services/api/util';
|
||||
import {
|
||||
$lastProgressEvent,
|
||||
} from 'services/events/stores';
|
||||
import { $lastProgressEvent } from 'services/events/stores';
|
||||
import type { Param0 } from 'tsafe';
|
||||
import { objectEntries } from 'tsafe';
|
||||
import type { JsonObject } from 'type-fest';
|
||||
|
||||
@@ -31,14 +31,11 @@ import type { Socket } from 'socket.io-client';
|
||||
import type { JsonObject } from 'type-fest';
|
||||
|
||||
import {
|
||||
$lastCanvasProgressEvent,
|
||||
$lastCanvasProgressImage,
|
||||
$lastProgressEvent,
|
||||
$lastUpscalingProgressEvent,
|
||||
$lastUpscalingProgressImage,
|
||||
$lastWorkflowsProgressEvent,
|
||||
$lastWorkflowsProgressImage,
|
||||
$progressImages,
|
||||
} from './stores';
|
||||
|
||||
const log = logger('events');
|
||||
@@ -116,22 +113,6 @@ export const setEventListeners = ({ socket, store, setIsConnected }: SetEventLis
|
||||
|
||||
$lastProgressEvent.set(data);
|
||||
|
||||
if (data.image) {
|
||||
const progressData = $progressImages.get()[session_id];
|
||||
if (progressData) {
|
||||
$progressImages.setKey(session_id, { ...progressData, progressImage: data.image });
|
||||
} else {
|
||||
$progressImages.setKey(session_id, { sessionId: session_id, isFinished: false, progressImage: data.image });
|
||||
}
|
||||
}
|
||||
|
||||
if (origin === 'canvas') {
|
||||
$lastCanvasProgressEvent.set(data);
|
||||
if (image) {
|
||||
$lastCanvasProgressImage.set({ sessionId: session_id, image });
|
||||
}
|
||||
}
|
||||
|
||||
if (origin === 'upscaling') {
|
||||
$lastUpscalingProgressEvent.set(data);
|
||||
if (image) {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { EphemeralProgressImage } from 'features/controlLayers/store/types';
|
||||
import type { ProgressImage } from 'features/nodes/types/common';
|
||||
import { round } from 'lodash-es';
|
||||
import { atom, computed, map } from 'nanostores';
|
||||
import type { ImageDTO, S } from 'services/api/types';
|
||||
import type { S } from 'services/api/types';
|
||||
import type { AppSocket } from 'services/events/types';
|
||||
import type { ManagerOptions, SocketOptions } from 'socket.io-client';
|
||||
|
||||
@@ -11,22 +10,6 @@ export const $socketOptions = map<Partial<ManagerOptions & SocketOptions>>({});
|
||||
export const $isConnected = atom<boolean>(false);
|
||||
export const $lastProgressEvent = atom<S['InvocationProgressEvent'] | null>(null);
|
||||
|
||||
export type ProgressAndResult = {
|
||||
sessionId: string;
|
||||
isFinished: boolean;
|
||||
progressImage?: ProgressImage;
|
||||
resultImage?: ImageDTO;
|
||||
};
|
||||
export const $progressImages = map({} as Record<string, ProgressAndResult>);
|
||||
|
||||
export type ProgressData = {
|
||||
sessionId: string;
|
||||
progressEvent: S['InvocationProgressEvent'] | null;
|
||||
progressImage: ProgressImage | null;
|
||||
};
|
||||
|
||||
export const $lastCanvasProgressEvent = atom<S['InvocationProgressEvent'] | null>(null);
|
||||
export const $lastCanvasProgressImage = atom<EphemeralProgressImage | null>(null);
|
||||
export const $lastWorkflowsProgressEvent = atom<S['InvocationProgressEvent'] | null>(null);
|
||||
export const $lastWorkflowsProgressImage = atom<EphemeralProgressImage | null>(null);
|
||||
export const $lastUpscalingProgressEvent = atom<S['InvocationProgressEvent'] | null>(null);
|
||||
|
||||
Reference in New Issue
Block a user