mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-19 09:54:24 -05:00
feat(ui): rework queue controls
- Remove queue front button. Hold shift while clicking `Invoke` button to queue front. - Restore queue menu actions w/ the reclaimed space. - Simplify queue interaction hooks.
This commit is contained in:
committed by
Kent Keirsey
parent
7b9d8df1a7
commit
ccbe1b233d
@@ -22,7 +22,6 @@ export type AppFeature =
|
||||
| 'multiselect'
|
||||
| 'pauseQueue'
|
||||
| 'resumeQueue'
|
||||
| 'prependQueue'
|
||||
| 'invocationCache'
|
||||
| 'bulkDownload'
|
||||
| 'starterModels'
|
||||
|
||||
@@ -2,8 +2,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { addScope, removeScope, setScopes } from 'common/hooks/interactionScopes';
|
||||
import { useClearQueue } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
|
||||
import { useQueueBack } from 'features/queue/hooks/useQueueBack';
|
||||
import { useQueueFront } from 'features/queue/hooks/useQueueFront';
|
||||
import { useInvoke } from 'features/queue/hooks/useInvoke';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
@@ -11,30 +10,28 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
||||
export const useGlobalHotkeys = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const isModelManagerEnabled = useFeatureStatus('modelManager');
|
||||
const { queueBack, isDisabled: isDisabledQueueBack, isLoading: isLoadingQueueBack } = useQueueBack();
|
||||
const queue = useInvoke();
|
||||
|
||||
useHotkeys(
|
||||
['ctrl+enter', 'meta+enter'],
|
||||
queueBack,
|
||||
queue.queueBack,
|
||||
{
|
||||
enabled: !isDisabledQueueBack && !isLoadingQueueBack,
|
||||
enabled: !queue.isDisabled && !queue.isLoading,
|
||||
preventDefault: true,
|
||||
enableOnFormTags: ['input', 'textarea', 'select'],
|
||||
},
|
||||
[queueBack, isDisabledQueueBack, isLoadingQueueBack]
|
||||
[queue]
|
||||
);
|
||||
|
||||
const { queueFront, isDisabled: isDisabledQueueFront, isLoading: isLoadingQueueFront } = useQueueFront();
|
||||
|
||||
useHotkeys(
|
||||
['ctrl+shift+enter', 'meta+shift+enter'],
|
||||
queueFront,
|
||||
queue.queueFront,
|
||||
{
|
||||
enabled: !isDisabledQueueFront && !isLoadingQueueFront,
|
||||
enabled: !queue.isDisabled && !queue.isLoading,
|
||||
preventDefault: true,
|
||||
enableOnFormTags: ['input', 'textarea', 'select'],
|
||||
},
|
||||
[queueFront, isDisabledQueueFront, isLoadingQueueFront]
|
||||
[queue]
|
||||
);
|
||||
|
||||
const {
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { IconButton, useShiftModifier } from '@invoke-ai/ui-library';
|
||||
import { QueueCountBadge } from 'features/queue/components/QueueCountBadge';
|
||||
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
|
||||
import { memo, useRef } from 'react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiTrashSimpleBold, PiXBold } from 'react-icons/pi';
|
||||
|
||||
import { useClearQueue } from './ClearQueueConfirmationAlertDialog';
|
||||
|
||||
export const ClearQueueIconButton = memo((_) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation();
|
||||
const clearQueue = useClearQueue();
|
||||
const cancelCurrentQueueItem = useCancelCurrentQueueItem();
|
||||
@@ -18,22 +16,17 @@ export const ClearQueueIconButton = memo((_) => {
|
||||
const shift = useShiftModifier();
|
||||
|
||||
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} />
|
||||
</>
|
||||
<IconButton
|
||||
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')}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
import { Button, Flex, Spacer } from '@invoke-ai/ui-library';
|
||||
import { Button, Flex, Spacer, useShiftModifier } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectDynamicPromptsIsLoading } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
||||
import { QueueIterationsNumberInput } from 'features/queue/components/QueueIterationsNumberInput';
|
||||
import { useQueueBack } from 'features/queue/hooks/useQueueBack';
|
||||
import { useInvoke } from 'features/queue/hooks/useInvoke';
|
||||
import { memo } from 'react';
|
||||
import { PiSparkleFill } from 'react-icons/pi';
|
||||
import { PiLightningFill, PiSparkleFill } from 'react-icons/pi';
|
||||
|
||||
import { QueueButtonTooltip } from './QueueButtonTooltip';
|
||||
|
||||
const invoke = 'Invoke';
|
||||
|
||||
export const InvokeQueueBackButton = memo(() => {
|
||||
const { queueBack, isLoading, isDisabled } = useQueueBack();
|
||||
export const InvokeButton = memo(() => {
|
||||
const queue = useInvoke();
|
||||
const shift = useShiftModifier();
|
||||
const isLoadingDynamicPrompts = useAppSelector(selectDynamicPromptsIsLoading);
|
||||
|
||||
return (
|
||||
<Flex pos="relative" w="200px">
|
||||
<QueueIterationsNumberInput />
|
||||
<QueueButtonTooltip>
|
||||
<QueueButtonTooltip prepend={shift}>
|
||||
<Button
|
||||
onClick={queueBack}
|
||||
isLoading={isLoading || isLoadingDynamicPrompts}
|
||||
onClick={shift ? queue.queueFront : queue.queueBack}
|
||||
isLoading={queue.isLoading || isLoadingDynamicPrompts}
|
||||
loadingText={invoke}
|
||||
isDisabled={isDisabled}
|
||||
rightIcon={<PiSparkleFill />}
|
||||
isDisabled={queue.isDisabled}
|
||||
rightIcon={shift ? <PiLightningFill /> : <PiSparkleFill />}
|
||||
variant="solid"
|
||||
colorScheme="invokeYellow"
|
||||
size="lg"
|
||||
@@ -40,4 +41,4 @@ export const InvokeQueueBackButton = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
InvokeQueueBackButton.displayName = 'InvokeQueueBackButton';
|
||||
InvokeButton.displayName = 'InvokeQueueBackButton';
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useClearQueue } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||
import { QueueCountBadge } from 'features/queue/components/QueueCountBadge';
|
||||
import { usePauseProcessor } from 'features/queue/hooks/usePauseProcessor';
|
||||
import { useResumeProcessor } from 'features/queue/hooks/useResumeProcessor';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiListBold, PiPauseFill, PiPlayFill, PiQueueBold, PiTrashSimpleBold } from 'react-icons/pi';
|
||||
|
||||
export const QueueActionsMenuButton = memo(() => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const isPauseEnabled = useFeatureStatus('pauseQueue');
|
||||
const isResumeEnabled = useFeatureStatus('resumeQueue');
|
||||
const clearQueue = useClearQueue();
|
||||
const {
|
||||
resumeProcessor,
|
||||
isLoading: isLoadingResumeProcessor,
|
||||
isDisabled: isDisabledResumeProcessor,
|
||||
} = useResumeProcessor();
|
||||
const {
|
||||
pauseProcessor,
|
||||
isLoading: isLoadingPauseProcessor,
|
||||
isDisabled: isDisabledPauseProcessor,
|
||||
} = usePauseProcessor();
|
||||
const openQueue = useCallback(() => {
|
||||
dispatch(setActiveTab('queue'));
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu placement="bottom-end">
|
||||
<MenuButton ref={ref} as={IconButton} size="lg" aria-label="Queue Actions Menu" icon={<PiListBold />} />
|
||||
<MenuList>
|
||||
<MenuItem
|
||||
isDestructive
|
||||
icon={<PiTrashSimpleBold />}
|
||||
onClick={clearQueue.openDialog}
|
||||
isLoading={clearQueue.isLoading}
|
||||
isDisabled={clearQueue.isDisabled}
|
||||
>
|
||||
{t('queue.clearTooltip')}
|
||||
</MenuItem>
|
||||
{isResumeEnabled && (
|
||||
<MenuItem
|
||||
icon={<PiPlayFill />}
|
||||
onClick={resumeProcessor}
|
||||
isLoading={isLoadingResumeProcessor}
|
||||
isDisabled={isDisabledResumeProcessor}
|
||||
>
|
||||
{t('queue.resumeTooltip')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{isPauseEnabled && (
|
||||
<MenuItem
|
||||
icon={<PiPauseFill />}
|
||||
onClick={pauseProcessor}
|
||||
isLoading={isLoadingPauseProcessor}
|
||||
isDisabled={isDisabledPauseProcessor}
|
||||
>
|
||||
{t('queue.pauseTooltip')}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuDivider />
|
||||
<MenuItem icon={<PiQueueBold />} onClick={openQueue}>
|
||||
{t('queue.openQueue')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
{/* The badge is dynamically positioned, needs a ref to the target element */}
|
||||
<QueueCountBadge targetRef={ref} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
QueueActionsMenuButton.displayName = 'QueueActionsMenuButton';
|
||||
@@ -1,26 +1,24 @@
|
||||
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { ClearQueueIconButton } from 'features/queue/components/ClearQueueIconButton';
|
||||
import QueueFrontButton from 'features/queue/components/QueueFrontButton';
|
||||
import { QueueActionsMenuButton } from 'features/queue/components/QueueActionsMenuButton';
|
||||
import { SendToToggle } from 'features/queue/components/SendToToggle';
|
||||
import ProgressBar from 'features/system/components/ProgressBar';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { InvokeQueueBackButton } from './InvokeQueueBackButton';
|
||||
import { InvokeButton } from './InvokeQueueBackButton';
|
||||
|
||||
const QueueControls = () => {
|
||||
const isPrependEnabled = useFeatureStatus('prependQueue');
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
|
||||
return (
|
||||
<Flex w="full" position="relative" borderRadius="base" gap={2} flexDir="column">
|
||||
<Flex gap={2}>
|
||||
{isPrependEnabled && <QueueFrontButton />}
|
||||
<InvokeQueueBackButton />
|
||||
<InvokeButton />
|
||||
<Spacer />
|
||||
{tab === 'canvas' && <SendToToggle />}
|
||||
<QueueActionsMenuButton />
|
||||
<ClearQueueIconButton />
|
||||
</Flex>
|
||||
<ProgressBar />
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useQueueFront } from 'features/queue/hooks/useQueueFront';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiLightningFill } from 'react-icons/pi';
|
||||
|
||||
import { QueueButtonTooltip } from './QueueButtonTooltip';
|
||||
|
||||
const QueueFrontButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const { queueFront, isLoading, isDisabled } = useQueueFront();
|
||||
return (
|
||||
<QueueButtonTooltip prepend>
|
||||
<IconButton
|
||||
aria-label={t('queue.queueFront')}
|
||||
isDisabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
onClick={queueFront}
|
||||
icon={<PiLightningFill />}
|
||||
size="lg"
|
||||
/>
|
||||
</QueueButtonTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(QueueFrontButton);
|
||||
@@ -2,23 +2,28 @@ import { enqueueRequested } from 'app/store/actions';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useIsReadyToEnqueue } from 'common/hooks/useIsReadyToEnqueue';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useEnqueueBatchMutation } from 'services/api/endpoints/queue';
|
||||
|
||||
export const useQueueBack = () => {
|
||||
export const useInvoke = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const tabName = useAppSelector(selectActiveTab);
|
||||
const { isReady } = useIsReadyToEnqueue();
|
||||
const [_, { isLoading }] = useEnqueueBatchMutation({
|
||||
fixedCacheKey: 'enqueueBatch',
|
||||
});
|
||||
const isDisabled = useMemo(() => !isReady, [isReady]);
|
||||
const queueBack = useCallback(() => {
|
||||
if (isDisabled) {
|
||||
if (!isReady) {
|
||||
return;
|
||||
}
|
||||
dispatch(enqueueRequested({ tabName, prepend: false }));
|
||||
}, [dispatch, isDisabled, tabName]);
|
||||
}, [dispatch, isReady, tabName]);
|
||||
const queueFront = useCallback(() => {
|
||||
if (!isReady) {
|
||||
return;
|
||||
}
|
||||
dispatch(enqueueRequested({ tabName, prepend: true }));
|
||||
}, [dispatch, isReady, tabName]);
|
||||
|
||||
return { queueBack, isLoading, isDisabled };
|
||||
return { queueBack, queueFront, isLoading, isDisabled: !isReady };
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import { enqueueRequested } from 'app/store/actions';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useIsReadyToEnqueue } from 'common/hooks/useIsReadyToEnqueue';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useEnqueueBatchMutation } from 'services/api/endpoints/queue';
|
||||
|
||||
export const useQueueFront = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const tabName = useAppSelector(selectActiveTab);
|
||||
const { isReady } = useIsReadyToEnqueue();
|
||||
const [_, { isLoading }] = useEnqueueBatchMutation({
|
||||
fixedCacheKey: 'enqueueBatch',
|
||||
});
|
||||
const prependEnabled = useFeatureStatus('prependQueue');
|
||||
|
||||
const isDisabled = useMemo(() => {
|
||||
return !isReady || !prependEnabled;
|
||||
}, [isReady, prependEnabled]);
|
||||
|
||||
const queueFront = useCallback(() => {
|
||||
if (isDisabled) {
|
||||
return;
|
||||
}
|
||||
dispatch(enqueueRequested({ tabName, prepend: true }));
|
||||
}, [dispatch, isDisabled, tabName]);
|
||||
|
||||
return { queueFront, isLoading, isDisabled };
|
||||
};
|
||||
@@ -1,13 +1,19 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { ButtonGroup, Flex, Icon, IconButton, Portal, spinAnimation } from '@invoke-ai/ui-library';
|
||||
import { ButtonGroup, Flex, Icon, IconButton, Portal, spinAnimation, useShiftModifier } from '@invoke-ai/ui-library';
|
||||
import CancelCurrentQueueItemIconButton from 'features/queue/components/CancelCurrentQueueItemIconButton';
|
||||
import { useClearQueue } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||
import { QueueButtonTooltip } from 'features/queue/components/QueueButtonTooltip';
|
||||
import { useQueueBack } from 'features/queue/hooks/useQueueBack';
|
||||
import { useInvoke } from 'features/queue/hooks/useInvoke';
|
||||
import type { UsePanelReturn } from 'features/ui/hooks/usePanel';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCircleNotchBold, PiSlidersHorizontalBold, PiSparkleFill, PiTrashSimpleBold } from 'react-icons/pi';
|
||||
import {
|
||||
PiCircleNotchBold,
|
||||
PiLightningFill,
|
||||
PiSlidersHorizontalBold,
|
||||
PiSparkleFill,
|
||||
PiTrashSimpleBold,
|
||||
} from 'react-icons/pi';
|
||||
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
|
||||
|
||||
const floatingButtonStyles: SystemStyleObject = {
|
||||
@@ -21,17 +27,21 @@ type Props = {
|
||||
|
||||
const FloatingSidePanelButtons = (props: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { queueBack, isLoading, isDisabled } = useQueueBack();
|
||||
const queue = useInvoke();
|
||||
const shift = useShiftModifier();
|
||||
const clearQueue = useClearQueue();
|
||||
const { data: queueStatus } = useGetQueueStatusQuery();
|
||||
|
||||
const queueButtonIcon = useMemo(() => {
|
||||
const isProcessing = (queueStatus?.queue.in_progress ?? 0) > 0;
|
||||
if (!isDisabled && isProcessing) {
|
||||
if (!queue.isDisabled && isProcessing) {
|
||||
return <Icon boxSize={6} as={PiCircleNotchBold} animation={spinAnimation} />;
|
||||
}
|
||||
if (shift) {
|
||||
return <PiLightningFill size="16px" />;
|
||||
}
|
||||
return <PiSparkleFill size="16px" />;
|
||||
}, [isDisabled, queueStatus]);
|
||||
}, [queue.isDisabled, queueStatus?.queue.in_progress, shift]);
|
||||
|
||||
if (!props.panelApi.isCollapsed) {
|
||||
return null;
|
||||
@@ -58,12 +68,12 @@ const FloatingSidePanelButtons = (props: Props) => {
|
||||
sx={floatingButtonStyles}
|
||||
icon={<PiSlidersHorizontalBold size="16px" />}
|
||||
/>
|
||||
<QueueButtonTooltip>
|
||||
<QueueButtonTooltip prepend={shift}>
|
||||
<IconButton
|
||||
aria-label={t('queue.queueBack')}
|
||||
onClick={queueBack}
|
||||
isLoading={isLoading}
|
||||
isDisabled={isDisabled}
|
||||
onClick={shift ? queue.queueFront : queue.queueBack}
|
||||
isLoading={queue.isLoading}
|
||||
isDisabled={queue.isDisabled}
|
||||
icon={queueButtonIcon}
|
||||
colorScheme="invokeYellow"
|
||||
sx={floatingButtonStyles}
|
||||
|
||||
Reference in New Issue
Block a user