diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index ff512cb48e..694a006778 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -164,10 +164,10 @@ "alpha": "Alpha", "selected": "Selected", "tab": "Tab", - "viewing": "Viewing", - "viewingDesc": "Review images in a large gallery view", - "editing": "Editing", - "editingDesc": "Edit on the Control Layers canvas", + "view": "View", + "viewDesc": "Review images in a large gallery view", + "edit": "Edit", + "editDesc": "Edit on the Canvas", "comparing": "Comparing", "comparingDesc": "Comparing two images", "enabled": "Enabled", @@ -328,9 +328,13 @@ "completedIn": "Completed in", "batch": "Batch", "origin": "Origin", - "originCanvas": "Canvas", - "originWorkflows": "Workflows", - "originOther": "Other", + "destination": "Destination", + "upscaling": "Upscaling", + "canvas": "Canvas", + "generation": "Generation", + "workflows": "Workflows", + "other": "Other", + "gallery": "Gallery", "batchFieldValues": "Batch Field Values", "item": "Item", "session": "Session", @@ -1695,6 +1699,10 @@ "inpaintMask": "Inpaint Mask", "regionalGuidance": "Regional Guidance", "ipAdapter": "IP Adapter", + "sendToGallery": "Send To Gallery", + "sendToGalleryDesc": "Generations will be sent to the gallery.", + "sendToCanvas": "Send To Canvas", + "sendToCanvasDesc": "Generations will be staged onto the canvas.", "rasterLayer_withCount_one": "$t(controlLayers.rasterLayer)", "controlLayer_withCount_one": "$t(controlLayers.controlLayer)", "inpaintMask_withCount_one": "$t(controlLayers.inpaintMask)", diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index 89777f5081..afd9489bc5 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -31,7 +31,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) let didStartStaging = false; - if (!state.canvasSession.isStaging && state.canvasSession.mode === 'compose') { + if (!state.canvasSession.isStaging && state.canvasSession.sendToCanvas) { dispatch(sessionStartedStaging()); didStartStaging = true; } @@ -70,7 +70,11 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) const { g, noise, posCond } = buildGraphResult.value; - const prepareBatchResult = withResult(() => prepareLinearUIBatch(state, g, prepend, noise, posCond)); + const destination = state.canvasSession.sendToCanvas ? 'canvas' : 'gallery'; + + const prepareBatchResult = withResult(() => + prepareLinearUIBatch(state, g, prepend, noise, posCond, 'generation', destination) + ); if (isErr(prepareBatchResult)) { log.error({ error: serializeError(prepareBatchResult.error) }, 'Failed to prepare batch'); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts index 847c64f3c3..42cd591e0c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts @@ -32,6 +32,7 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) = workflow: builtWorkflow, runs: state.params.iterations, origin: 'workflows', + destination: 'gallery', }, prepend: action.payload.prepend, }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedUpscale.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedUpscale.ts index 959a1bf5ae..cbfaac6227 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedUpscale.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedUpscale.ts @@ -16,7 +16,7 @@ export const addEnqueueRequestedUpscale = (startAppListening: AppStartListening) const { g, noise, posCond } = await buildMultidiffusionUpscaleGraph(state); - const batchConfig = prepareLinearUIBatch(state, g, prepend, noise, posCond); + const batchConfig = prepareLinearUIBatch(state, g, prepend, noise, posCond, 'upscaling', 'gallery'); const req = dispatch( queueApi.endpoints.enqueueBatch.initiate(batchConfig, { diff --git a/invokeai/frontend/web/src/common/components/IconSwitch.tsx b/invokeai/frontend/web/src/common/components/IconSwitch.tsx new file mode 100644 index 0000000000..43cf6d2f36 --- /dev/null +++ b/invokeai/frontend/web/src/common/components/IconSwitch.tsx @@ -0,0 +1,104 @@ +import type { SystemStyleObject } from '@invoke-ai/ui-library'; +import { Box, Flex, IconButton, Tooltip, useToken } from '@invoke-ai/ui-library'; +import type { ReactElement, ReactNode } from 'react'; +import { memo, useCallback, useMemo } from 'react'; + +type IconSwitchProps = { + isChecked: boolean; + onChange: (checked: boolean) => void; + iconChecked: ReactElement; + tooltipChecked?: ReactNode; + iconUnchecked: ReactElement; + tooltipUnchecked?: ReactNode; + ariaLabel: string; +}; + +const getSx = (padding: string | number): SystemStyleObject => ({ + transition: 'left 0.1s ease-in-out, transform 0.1s ease-in-out', + '&[data-checked="true"]': { + left: `calc(100% - ${padding})`, + transform: 'translateX(-100%)', + }, + '&[data-checked="false"]': { + left: padding, + transform: 'translateX(0)', + }, +}); + +export const IconSwitch = memo( + ({ + isChecked, + onChange, + iconChecked, + tooltipChecked, + iconUnchecked, + tooltipUnchecked, + ariaLabel, + }: IconSwitchProps) => { + const onUncheck = useCallback(() => { + onChange(false); + }, [onChange]); + const onCheck = useCallback(() => { + onChange(true); + }, [onChange]); + + const gap = useToken('space', 1.5); + const sx = useMemo(() => getSx(gap), [gap]); + + return ( + + + + + + + + + + ); + } +); + +IconSwitch.displayName = 'IconSwitch'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasModeSwitcher.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasModeSwitcher.tsx deleted file mode 100644 index e052c1a214..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasModeSwitcher.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Button, ButtonGroup } from '@invoke-ai/ui-library'; -import { createSelector } from '@reduxjs/toolkit'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSessionSlice, sessionModeChanged } from 'features/controlLayers/store/canvasSessionSlice'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -const selectCanvasMode = createSelector(selectCanvasSessionSlice, (canvasSession) => canvasSession.mode); - -export const CanvasModeSwitcher = memo(() => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const mode = useAppSelector(selectCanvasMode); - const onClickGenerate = useCallback(() => dispatch(sessionModeChanged({ mode: 'generate' })), [dispatch]); - const onClickCompose = useCallback(() => dispatch(sessionModeChanged({ mode: 'compose' })), [dispatch]); - - return ( - - - - - ); -}); - -CanvasModeSwitcher.displayName = 'CanvasModeSwitcher'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasSendToToggle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasSendToToggle.tsx new file mode 100644 index 0000000000..4d572beccb --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasSendToToggle.tsx @@ -0,0 +1,59 @@ +import { Flex, Text } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { IconSwitch } from 'common/components/IconSwitch'; +import { selectIsComposing, sessionSendToCanvasChanged } from 'features/controlLayers/store/canvasSessionSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiImageBold, PiPaintBrushBold } from 'react-icons/pi'; + +const TooltipSendToGallery = memo(() => { + const { t } = useTranslation(); + + return ( + + {t('controlLayers.sendToGallery')} + {t('controlLayers.sendToGalleryDesc')} + + ); +}); + +TooltipSendToGallery.displayName = 'TooltipSendToGallery'; + +const TooltipSendToCanvas = memo(() => { + const { t } = useTranslation(); + + return ( + + {t('controlLayers.sendToCanvas')} + {t('controlLayers.sendToCanvasDesc')} + + ); +}); + +TooltipSendToCanvas.displayName = 'TooltipSendToCanvas'; + +export const CanvasSendToToggle = memo(() => { + const dispatch = useAppDispatch(); + const isComposing = useAppSelector(selectIsComposing); + + const onChange = useCallback( + (isChecked: boolean) => { + dispatch(sessionSendToCanvasChanged(isChecked)); + }, + [dispatch] + ); + + return ( + } + tooltipUnchecked={} + iconChecked={} + tooltipChecked={} + ariaLabel="Toggle canvas mode" + /> + ); +}); + +CanvasSendToToggle.displayName = 'CanvasSendToToggle'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index 56133a5749..19ffcc25d7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -1,6 +1,5 @@ /* eslint-disable i18next/no-literal-string */ import { Flex, Spacer } from '@invoke-ai/ui-library'; -import { CanvasModeSwitcher } from 'features/controlLayers/components/CanvasModeSwitcher'; import { CanvasResetViewButton } from 'features/controlLayers/components/CanvasResetViewButton'; import { CanvasScale } from 'features/controlLayers/components/CanvasScale'; import { CanvasSettingsPopover } from 'features/controlLayers/components/Settings/CanvasSettingsPopover'; @@ -28,7 +27,6 @@ export const ControlLayersToolbar = memo(() => { - diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSessionSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSessionSlice.ts index 6947ffd2df..e115b6800b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSessionSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSessionSlice.ts @@ -1,17 +1,17 @@ import { createAction, createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { canvasSlice } from 'features/controlLayers/store/canvasSlice'; -import type { SessionMode, StagingAreaImage } from 'features/controlLayers/store/types'; +import type { StagingAreaImage } from 'features/controlLayers/store/types'; export type CanvasSessionState = { - mode: SessionMode; + sendToCanvas: boolean; isStaging: boolean; stagedImages: StagingAreaImage[]; selectedStagedImageIndex: number; }; const initialState: CanvasSessionState = { - mode: 'generate', + sendToCanvas: false, isStaging: false, stagedImages: [], selectedStagedImageIndex: 0, @@ -27,6 +27,7 @@ export const canvasSessionSlice = createSlice({ }, sessionImageStaged: (state, action: PayloadAction<{ stagingAreaImage: StagingAreaImage }>) => { const { stagingAreaImage } = action.payload; + state.isStaging = true; state.stagedImages.push(stagingAreaImage); state.selectedStagedImageIndex = state.stagedImages.length - 1; }, @@ -50,9 +51,8 @@ export const canvasSessionSlice = createSlice({ state.stagedImages = []; state.selectedStagedImageIndex = 0; }, - sessionModeChanged: (state, action: PayloadAction<{ mode: SessionMode }>) => { - const { mode } = action.payload; - state.mode = mode; + sessionSendToCanvasChanged: (state, action: PayloadAction) => { + state.sendToCanvas = action.payload; }, }, }); @@ -64,7 +64,7 @@ export const { sessionStagingAreaReset, sessionNextStagedImageSelected, sessionPrevStagedImageSelected, - sessionModeChanged, + sessionSendToCanvasChanged, } = canvasSessionSlice.actions; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ @@ -85,3 +85,7 @@ export const sessionStagingAreaImageAccepted = createAction<{ index: number }>( export const selectCanvasSessionSlice = (s: RootState) => s.canvasSession; export const selectIsStaging = createSelector(selectCanvasSessionSlice, (canvasSession) => canvasSession.isStaging); +export const selectIsComposing = createSelector( + selectCanvasSessionSlice, + (canvasSession) => canvasSession.sendToCanvas +); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx index c37a460b12..71b94db445 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx @@ -1,57 +1,60 @@ -import { ButtonGroup, Flex, IconButton, Text, Tooltip } from '@invoke-ai/ui-library'; +import { Flex, Text } from '@invoke-ai/ui-library'; +import { IconSwitch } from 'common/components/IconSwitch'; import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer'; -import { memo } from 'react'; +import { memo, useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiEyeBold, PiPencilBold } from 'react-icons/pi'; -export const ViewerToggle = memo(() => { +const TooltipEdit = memo(() => { const { t } = useTranslation(); + + return ( + + {t('common.edit')} + {t('common.editDesc')} + + ); +}); +TooltipEdit.displayName = 'TooltipEdit'; + +const TooltipView = memo(() => { + const { t } = useTranslation(); + + return ( + + {t('common.view')} + {t('common.viewDesc')} + + ); +}); +TooltipView.displayName = 'TooltipView'; + +export const ViewerToggle = memo(() => { const imageViewer = useImageViewer(); useHotkeys('z', imageViewer.onToggle, [imageViewer]); useHotkeys('esc', imageViewer.onClose, [imageViewer]); + const onChange = useCallback( + (isChecked: boolean) => { + if (isChecked) { + imageViewer.onClose(); + } else { + imageViewer.onOpen(); + } + }, + [imageViewer] + ); return ( - - - - {t('common.viewing')} - {t('common.viewingDesc')} - - } - > - } - onClick={imageViewer.onOpen} - variant={imageViewer.isOpen ? 'solid' : 'outline'} - colorScheme={imageViewer.isOpen ? 'invokeBlue' : 'base'} - aria-label={t('common.viewing')} - w={12} - /> - - - {t('common.editing')} - {t('common.editingDesc')} - - } - > - } - onClick={imageViewer.onClose} - variant={!imageViewer.isOpen ? 'solid' : 'outline'} - colorScheme={!imageViewer.isOpen ? 'invokeBlue' : 'base'} - aria-label={t('common.editing')} - w={12} - /> - - - + } + tooltipUnchecked={} + iconChecked={} + tooltipChecked={} + ariaLabel="Toggle viewer" + /> ); }); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/NodeEditorPanelGroup.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/NodeEditorPanelGroup.tsx index 0fd1bb6ea7..6b7f019f89 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/NodeEditorPanelGroup.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/NodeEditorPanelGroup.tsx @@ -3,7 +3,6 @@ import 'reactflow/dist/style.css'; import { Flex } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { selectWorkflowMode } from 'features/nodes/store/workflowSlice'; -import QueueControls from 'features/queue/components/QueueControls'; import ResizeHandle from 'features/ui/components/tabs/ResizeHandle'; import { usePanelStorage } from 'features/ui/hooks/usePanelStorage'; import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton'; @@ -34,7 +33,6 @@ const NodeEditorPanelGroup = () => { return ( - diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts index 4262298951..6253dc30dc 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts @@ -10,7 +10,9 @@ export const prepareLinearUIBatch = ( g: Graph, prepend: boolean, noise: Invocation<'noise'>, - posCond: Invocation<'compel' | 'sdxl_compel_prompt'> + posCond: Invocation<'compel' | 'sdxl_compel_prompt'>, + origin: 'generation' | 'workflows' | 'upscaling', + destination: 'canvas' | 'gallery' ): BatchConfig => { const { iterations, model, shouldRandomizeSeed, seed, shouldConcatPrompts } = state.params; const { prompts, seedBehaviour } = state.dynamicPrompts; @@ -103,7 +105,8 @@ export const prepareLinearUIBatch = ( graph: g.getGraph(), runs: 1, data, - origin: 'canvas', + origin, + destination, }, }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index f9fa00c5d6..c69820bd49 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -29,7 +29,7 @@ export const addInpaint = async ( const canvas = selectCanvasSlice(state); const { bbox } = canvas; - const { mode } = canvasSession; + const { sendToCanvas: isComposing } = canvasSession; const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect); const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect); @@ -99,7 +99,7 @@ export const addInpaint = async ( g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'generated_image'); g.addEdge(resizeMaskToOriginalSize, 'image', canvasPasteBack, 'mask'); - if (mode === 'generate') { + if (!isComposing) { canvasPasteBack.source_image = { image_name: initialImage.image_name }; } @@ -143,7 +143,7 @@ export const addInpaint = async ( g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image'); - if (mode === 'generate') { + if (!isComposing) { canvasPasteBack.source_image = { image_name: initialImage.image_name }; } diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index ecbc09f916..80cdf5d53d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -30,7 +30,7 @@ export const addOutpaint = async ( const canvas = selectCanvasSlice(state); const { bbox } = canvas; - const { mode } = canvasSession; + const { sendToCanvas: isComposing } = canvasSession; const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect); const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect); @@ -123,7 +123,7 @@ export const addOutpaint = async ( g.addEdge(resizeOutputImageToOriginalSize, 'image', canvasPasteBack, 'generated_image'); g.addEdge(resizeOutputMaskToOriginalSize, 'image', canvasPasteBack, 'mask'); - if (mode === 'generate') { + if (!isComposing) { canvasPasteBack.source_image = { image_name: initialImage.image_name }; } @@ -173,7 +173,7 @@ export const addOutpaint = async ( g.addEdge(createGradientMask, 'expanded_mask_area', canvasPasteBack, 'mask'); g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image'); - if (mode === 'generate') { + if (!isComposing) { canvasPasteBack.source_image = { image_name: initialImage.image_name }; } diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index 112fb20a60..693b194e52 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -282,7 +282,7 @@ export const buildSD1Graph = async ( canvasOutput = addWatermarker(g, canvasOutput); } - const shouldSaveToGallery = canvasSession.mode === 'generate' || canvasSettings.autoSave; + const shouldSaveToGallery = !canvasSession.sendToCanvas || canvasSettings.autoSave; g.updateNode(canvasOutput, { id: getPrefixedId('canvas_output'), diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index ddf8da0dcd..3fbf2f2f56 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -285,7 +285,7 @@ export const buildSDXLGraph = async ( canvasOutput = addWatermarker(g, canvasOutput); } - const shouldSaveToGallery = canvasSession.mode === 'generate' || canvasSettings.autoSave; + const shouldSaveToGallery = !canvasSession.sendToCanvas || canvasSettings.autoSave; g.updateNode(canvasOutput, { id: getPrefixedId('canvas_output'), diff --git a/invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx b/invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx index 39a84c0216..66d445e7c3 100644 --- a/invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx +++ b/invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx @@ -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; - -export const ClearAllQueueIconButton = memo((props: ClearQueueButtonProps) => { +export const ClearQueueIconButton = memo((_) => { + const ref = useRef(null); const { t } = useTranslation(); - const dialogState = useClearQueueConfirmationAlertDialog(); - const { isLoading, isDisabled } = useClearQueue(); + const clearQueue = useClearQueue(); + const cancelCurrentQueueItem = useCancelCurrentQueueItem(); - return ( - } - 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 ( - } - 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 ; - } - - return ; + return ( + <> + : } + 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 */} + + + ); }); ClearQueueIconButton.displayName = 'ClearQueueIconButton'; diff --git a/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx b/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx index 08950671a6..68356c2837 100644 --- a/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx +++ b/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx @@ -15,7 +15,7 @@ export const InvokeQueueBackButton = memo(() => { const isLoadingDynamicPrompts = useAppSelector(selectDynamicPromptsIsLoading); return ( - +