mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(ui): revise generation mode logic
- Canvas generation mode is replace with a boolean `sendToCanvas` flag. When off, images generated on the canvas go to the gallery. When on, they get added to the staging area. - When an image result is received, if its destination is the canvas, staging is automatically started. - Updated queue list to show the destination column. - Added `IconSwitch` component to represent binary choices, used for the new `sendToCanvas` flag and image viewer toggle. - Remove the queue actions menu in `QueueControls`. Move the queue count badge to the cancel button. - Redo layout of `QueueControls` to prevent duplicate queue count badges. - Fix issue where gallery and options panels could show thru transparent regions of queue tab. - Disable panel hotkeys when on mm/queue tabs.
This commit is contained in:
@@ -1,67 +1,39 @@
|
||||
import type { IconButtonProps } from '@invoke-ai/ui-library';
|
||||
import { IconButton, useShiftModifier } from '@invoke-ai/ui-library';
|
||||
import { useClearQueueConfirmationAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||
import { QueueCountBadge } from 'features/queue/components/QueueCountBadge';
|
||||
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
|
||||
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
|
||||
import { memo } from 'react';
|
||||
import { memo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiTrashSimpleBold, PiXBold } from 'react-icons/pi';
|
||||
|
||||
type ClearQueueButtonProps = Omit<IconButtonProps, 'aria-label'>;
|
||||
|
||||
export const ClearAllQueueIconButton = memo((props: ClearQueueButtonProps) => {
|
||||
export const ClearQueueIconButton = memo((_) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation();
|
||||
const dialogState = useClearQueueConfirmationAlertDialog();
|
||||
const { isLoading, isDisabled } = useClearQueue();
|
||||
const clearQueue = useClearQueue();
|
||||
const cancelCurrentQueueItem = useCancelCurrentQueueItem();
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
isDisabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
aria-label={t('queue.clear')}
|
||||
tooltip={t('queue.clearTooltip')}
|
||||
icon={<PiTrashSimpleBold size="16px" />}
|
||||
colorScheme="error"
|
||||
onClick={dialogState.setTrue}
|
||||
data-testid={t('queue.clear')}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
ClearAllQueueIconButton.displayName = 'ClearAllQueueIconButton';
|
||||
|
||||
const ClearSingleQueueItemIconButton = memo((props: ClearQueueButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { cancelQueueItem, isLoading, isDisabled } = useCancelCurrentQueueItem();
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
isDisabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
aria-label={t('queue.cancel')}
|
||||
tooltip={t('queue.cancelTooltip')}
|
||||
icon={<PiXBold size="16px" />}
|
||||
colorScheme="error"
|
||||
onClick={cancelQueueItem}
|
||||
data-testid={t('queue.cancel')}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
ClearSingleQueueItemIconButton.displayName = 'ClearSingleQueueItemIconButton';
|
||||
|
||||
export const ClearQueueIconButton = memo((props: ClearQueueButtonProps) => {
|
||||
// Show the single item clear button when shift is pressed
|
||||
// Otherwise show the clear queue button
|
||||
const shift = useShiftModifier();
|
||||
|
||||
if (shift) {
|
||||
return <ClearAllQueueIconButton {...props} />;
|
||||
}
|
||||
|
||||
return <ClearSingleQueueItemIconButton {...props} />;
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
ref={ref}
|
||||
size="lg"
|
||||
isDisabled={shift ? clearQueue.isDisabled : cancelCurrentQueueItem.isDisabled}
|
||||
isLoading={shift ? clearQueue.isLoading : cancelCurrentQueueItem.isLoading}
|
||||
aria-label={shift ? t('queue.clear') : t('queue.cancel')}
|
||||
tooltip={shift ? t('queue.clearTooltip') : t('queue.cancelTooltip')}
|
||||
icon={shift ? <PiTrashSimpleBold /> : <PiXBold />}
|
||||
colorScheme="error"
|
||||
onClick={shift ? clearQueue.openDialog : cancelCurrentQueueItem.cancelQueueItem}
|
||||
data-testid={shift ? t('queue.clear') : t('queue.cancel')}
|
||||
/>
|
||||
{/* The badge is dynamically positioned, needs a ref to the target element */}
|
||||
<QueueCountBadge targetRef={ref} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
ClearQueueIconButton.displayName = 'ClearQueueIconButton';
|
||||
|
||||
@@ -15,7 +15,7 @@ export const InvokeQueueBackButton = memo(() => {
|
||||
const isLoadingDynamicPrompts = useAppSelector(selectDynamicPromptsIsLoading);
|
||||
|
||||
return (
|
||||
<Flex pos="relative" flexGrow={1} minW="240px">
|
||||
<Flex pos="relative" w="192px">
|
||||
<QueueIterationsNumberInput />
|
||||
<QueueButtonTooltip>
|
||||
<Button
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import { ButtonGroup, Flex, Spacer } from '@invoke-ai/ui-library';
|
||||
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasSendToToggle } from 'features/controlLayers/components/CanvasSendToToggle';
|
||||
import { ClearQueueIconButton } from 'features/queue/components/ClearQueueIconButton';
|
||||
import QueueFrontButton from 'features/queue/components/QueueFrontButton';
|
||||
import ProgressBar from 'features/system/components/ProgressBar';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { memo, useRef } from 'react';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { InvokeQueueBackButton } from './InvokeQueueBackButton';
|
||||
import { QueueActionsMenuButton } from './QueueActionsMenuButton';
|
||||
|
||||
const QueueControls = () => {
|
||||
const isPrependEnabled = useFeatureStatus('prependQueue');
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
return (
|
||||
<Flex ref={containerRef} w="full" position="relative" borderRadius="base" gap={2} flexDir="column">
|
||||
<ButtonGroup size="lg" isAttached={false}>
|
||||
<Flex w="full" position="relative" borderRadius="base" gap={2} flexDir="column">
|
||||
<Flex gap={2}>
|
||||
{isPrependEnabled && <QueueFrontButton />}
|
||||
<InvokeQueueBackButton />
|
||||
<Spacer />
|
||||
<QueueActionsMenuButton containerRef={containerRef} />
|
||||
{tab === 'generation' && <CanvasSendToToggle />}
|
||||
<ClearQueueIconButton />
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
<ProgressBar />
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Badge, Portal } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $isParametersPanelOpen } from 'features/ui/store/uiSlice';
|
||||
import type { RefObject } from 'react';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
|
||||
|
||||
type Props = {
|
||||
targetRef: RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export const QueueCountBadge = memo(({ targetRef }: Props) => {
|
||||
const [badgePos, setBadgePos] = useState<{ x: string; y: string } | null>(null);
|
||||
const isParametersPanelOpen = useStore($isParametersPanelOpen);
|
||||
const { queueSize } = useGetQueueStatusQuery(undefined, {
|
||||
selectFromResult: (res) => ({
|
||||
queueSize: res.data ? res.data.queue.pending + res.data.queue.in_progress : 0,
|
||||
}),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!targetRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = targetRef.current;
|
||||
const parent = target.parentElement;
|
||||
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cb = () => {
|
||||
if (!$isParametersPanelOpen.get()) {
|
||||
return;
|
||||
}
|
||||
const { x, y } = target.getBoundingClientRect();
|
||||
setBadgePos({ x: `${x - 7}px`, y: `${y - 5}px` });
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(cb);
|
||||
resizeObserver.observe(parent);
|
||||
cb();
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [targetRef]);
|
||||
|
||||
if (queueSize === 0) {
|
||||
return null;
|
||||
}
|
||||
if (!badgePos) {
|
||||
return null;
|
||||
}
|
||||
if (!isParametersPanelOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Badge
|
||||
pos="absolute"
|
||||
insetInlineStart={badgePos.x}
|
||||
insetBlockStart={badgePos.y}
|
||||
colorScheme="invokeYellow"
|
||||
zIndex="docked"
|
||||
shadow="dark-lg"
|
||||
userSelect="none"
|
||||
>
|
||||
{queueSize}
|
||||
</Badge>
|
||||
</Portal>
|
||||
);
|
||||
});
|
||||
|
||||
QueueCountBadge.displayName = 'QueueCountBadge';
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ChakraProps, CollapseProps } from '@invoke-ai/ui-library';
|
||||
import { ButtonGroup, Collapse, Flex, IconButton, Text } from '@invoke-ai/ui-library';
|
||||
import QueueStatusBadge from 'features/queue/components/common/QueueStatusBadge';
|
||||
import { useDestinationText } from 'features/queue/components/QueueList/useDestinationText';
|
||||
import { useOriginText } from 'features/queue/components/QueueList/useOriginText';
|
||||
import { useCancelQueueItem } from 'features/queue/hooks/useCancelQueueItem';
|
||||
import { getSecondsFromTimestamps } from 'features/queue/util/getSecondsFromTimestamps';
|
||||
@@ -52,6 +53,7 @@ const QueueItemComponent = ({ index, item, context }: InnerItemProps) => {
|
||||
|
||||
const isCanceled = useMemo(() => ['canceled', 'completed', 'failed'].includes(item.status), [item.status]);
|
||||
const originText = useOriginText(item.origin);
|
||||
const destinationText = useDestinationText(item.destination);
|
||||
|
||||
const icon = useMemo(() => <PiXBold />, []);
|
||||
return (
|
||||
@@ -76,6 +78,11 @@ const QueueItemComponent = ({ index, item, context }: InnerItemProps) => {
|
||||
{originText}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex w={COLUMN_WIDTHS.destination} flexShrink={0}>
|
||||
<Text overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap" alignItems="center">
|
||||
{destinationText}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex w={COLUMN_WIDTHS.time} alignItems="center" flexShrink={0}>
|
||||
{executionTime || '-'}
|
||||
</Flex>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Button, ButtonGroup, Flex, Heading, Spinner, Text } from '@invoke-ai/ui-library';
|
||||
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
|
||||
import { useDestinationText } from 'features/queue/components/QueueList/useDestinationText';
|
||||
import { useOriginText } from 'features/queue/components/QueueList/useOriginText';
|
||||
import { useCancelBatch } from 'features/queue/hooks/useCancelBatch';
|
||||
import { useCancelQueueItem } from 'features/queue/hooks/useCancelQueueItem';
|
||||
@@ -17,7 +18,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const QueueItemComponent = ({ queueItemDTO }: Props) => {
|
||||
const { session_id, batch_id, item_id, origin } = queueItemDTO;
|
||||
const { session_id, batch_id, item_id, origin, destination } = queueItemDTO;
|
||||
const { t } = useTranslation();
|
||||
const { cancelBatch, isLoading: isLoadingCancelBatch, isCanceled } = useCancelBatch(batch_id);
|
||||
|
||||
@@ -26,6 +27,7 @@ const QueueItemComponent = ({ queueItemDTO }: Props) => {
|
||||
const { data: queueItem } = useGetQueueItemQuery(item_id);
|
||||
|
||||
const originText = useOriginText(origin);
|
||||
const destinationText = useDestinationText(destination);
|
||||
|
||||
const statusAndTiming = useMemo(() => {
|
||||
if (!queueItem) {
|
||||
@@ -54,6 +56,7 @@ const QueueItemComponent = ({ queueItemDTO }: Props) => {
|
||||
>
|
||||
<QueueItemData label={t('queue.status')} data={statusAndTiming} />
|
||||
<QueueItemData label={t('queue.origin')} data={originText} />
|
||||
<QueueItemData label={t('queue.destination')} data={destinationText} />
|
||||
<QueueItemData label={t('queue.item')} data={item_id} />
|
||||
<QueueItemData label={t('queue.batch')} data={batch_id} />
|
||||
<QueueItemData label={t('queue.session')} data={session_id} />
|
||||
|
||||
@@ -25,6 +25,9 @@ const QueueListHeader = () => {
|
||||
<Flex ps={0.5} w={COLUMN_WIDTHS.origin} alignItems="center">
|
||||
<Text variant="subtext">{t('queue.origin')}</Text>
|
||||
</Flex>
|
||||
<Flex ps={0.5} w={COLUMN_WIDTHS.destination} alignItems="center">
|
||||
<Text variant="subtext">{t('queue.destination')}</Text>
|
||||
</Flex>
|
||||
<Flex ps={0.5} w={COLUMN_WIDTHS.time} alignItems="center">
|
||||
<Text variant="subtext">{t('queue.time')}</Text>
|
||||
</Flex>
|
||||
|
||||
@@ -4,7 +4,8 @@ export const COLUMN_WIDTHS = {
|
||||
statusDot: 2,
|
||||
time: '4rem',
|
||||
origin: '5rem',
|
||||
destination: '6rem',
|
||||
batchId: '5rem',
|
||||
fieldValues: 'auto',
|
||||
actions: 'auto',
|
||||
};
|
||||
} as const;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { SessionQueueItemDTO } from 'services/api/types';
|
||||
|
||||
export const useDestinationText = (destination: SessionQueueItemDTO['destination']) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (destination === 'canvas') {
|
||||
return t('queue.canvas');
|
||||
}
|
||||
|
||||
if (destination === 'gallery') {
|
||||
return t('queue.gallery');
|
||||
}
|
||||
|
||||
return t('queue.other');
|
||||
};
|
||||
@@ -4,13 +4,13 @@ import type { SessionQueueItemDTO } from 'services/api/types';
|
||||
export const useOriginText = (origin: SessionQueueItemDTO['origin']) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (origin === 'canvas') {
|
||||
return t('queue.originCanvas');
|
||||
if (origin === 'generation') {
|
||||
return t('queue.generation');
|
||||
}
|
||||
|
||||
if (origin === 'workflows') {
|
||||
return t('queue.originWorkflows');
|
||||
return t('queue.workflows');
|
||||
}
|
||||
|
||||
return t('queue.originOther');
|
||||
return t('queue.other');
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user