mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): better invoke button checks
- Improved/more thorough checking before invoking for control layers - Improved styling for the tooltip
This commit is contained in:
parent
d9ce9c62ac
commit
19f5a9c3a9
@ -935,7 +935,19 @@
|
|||||||
"noPrompts": "No prompts generated",
|
"noPrompts": "No prompts generated",
|
||||||
"noNodesInGraph": "No nodes in graph",
|
"noNodesInGraph": "No nodes in graph",
|
||||||
"systemDisconnected": "System disconnected",
|
"systemDisconnected": "System disconnected",
|
||||||
"t2iAdapterMismatchedDimensions": "T2I Adapters require image dimensions to be multiples of 64"
|
"layer": {
|
||||||
|
"initialImageNoImageSelected": "no initial image selected",
|
||||||
|
"controlAdapterNoModelSelected": "no Control Adapter model selected",
|
||||||
|
"controlAdapterIncompatibleBaseModel": "incompatible Control Adapter base model",
|
||||||
|
"controlAdapterNoImageSelected": "no Control Adapter image selected",
|
||||||
|
"controlAdapterImageNotProcessed": "Control Adapter image not processed",
|
||||||
|
"t2iAdapterIncompatibleDimensions": "T2I Adapter requires image dimension to be multiples of 64",
|
||||||
|
"ipAdapterNoModelSelected": "no IP adapter selected",
|
||||||
|
"ipAdapterIncompatibleBaseModel": "incompatible IP Adapter base model",
|
||||||
|
"ipAdapterNoImageSelected": "no IP Adapter image selected",
|
||||||
|
"rgNoPromptsOrIPAdapters": "no text prompts or IP Adapters",
|
||||||
|
"rgNoRegion": "no region selected"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"maskBlur": "Mask Blur",
|
"maskBlur": "Mask Blur",
|
||||||
"negativePromptPlaceholder": "Negative Prompt",
|
"negativePromptPlaceholder": "Negative Prompt",
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
|
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
|
||||||
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
|
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import type { Layer } from 'features/controlLayers/store/types';
|
||||||
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
||||||
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
|
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
|
||||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
||||||
@ -14,9 +15,16 @@ import { selectGenerationSlice } from 'features/parameters/store/generationSlice
|
|||||||
import { selectSystemSlice } from 'features/system/store/systemSlice';
|
import { selectSystemSlice } from 'features/system/store/systemSlice';
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
import { forEach } from 'lodash-es';
|
import { forEach, upperFirst } from 'lodash-es';
|
||||||
import { getConnectedEdges } from 'reactflow';
|
import { getConnectedEdges } from 'reactflow';
|
||||||
|
|
||||||
|
const LAYER_TYPE_TO_TKEY: Record<Layer['type'], string> = {
|
||||||
|
initial_image_layer: 'controlLayers.globalInitialImage',
|
||||||
|
control_adapter_layer: 'controlLayers.globalControlAdapter',
|
||||||
|
ip_adapter_layer: 'controlLayers.globalIPAdapter',
|
||||||
|
regional_guidance_layer: 'controlLayers.regionalGuidance',
|
||||||
|
};
|
||||||
|
|
||||||
const selector = createMemoizedSelector(
|
const selector = createMemoizedSelector(
|
||||||
[
|
[
|
||||||
selectControlAdaptersSlice,
|
selectControlAdaptersSlice,
|
||||||
@ -34,17 +42,17 @@ const selector = createMemoizedSelector(
|
|||||||
|
|
||||||
const { isConnected } = system;
|
const { isConnected } = system;
|
||||||
|
|
||||||
const reasons: string[] = [];
|
const reasons: { prefix?: string; content: string }[] = [];
|
||||||
|
|
||||||
// Cannot generate if not connected
|
// Cannot generate if not connected
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
reasons.push(i18n.t('parameters.invoke.systemDisconnected'));
|
reasons.push({ content: i18n.t('parameters.invoke.systemDisconnected') });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeTabName === 'workflows') {
|
if (activeTabName === 'workflows') {
|
||||||
if (nodes.shouldValidateGraph) {
|
if (nodes.shouldValidateGraph) {
|
||||||
if (!nodes.nodes.length) {
|
if (!nodes.nodes.length) {
|
||||||
reasons.push(i18n.t('parameters.invoke.noNodesInGraph'));
|
reasons.push({ content: i18n.t('parameters.invoke.noNodesInGraph') });
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes.nodes.forEach((node) => {
|
nodes.nodes.forEach((node) => {
|
||||||
@ -56,7 +64,7 @@ const selector = createMemoizedSelector(
|
|||||||
|
|
||||||
if (!nodeTemplate) {
|
if (!nodeTemplate) {
|
||||||
// Node type not found
|
// Node type not found
|
||||||
reasons.push(i18n.t('parameters.invoke.missingNodeTemplate'));
|
reasons.push({ content: i18n.t('parameters.invoke.missingNodeTemplate') });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,17 +77,17 @@ const selector = createMemoizedSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!fieldTemplate) {
|
if (!fieldTemplate) {
|
||||||
reasons.push(i18n.t('parameters.invoke.missingFieldTemplate'));
|
reasons.push({ content: i18n.t('parameters.invoke.missingFieldTemplate') });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fieldTemplate.required && field.value === undefined && !hasConnection) {
|
if (fieldTemplate.required && field.value === undefined && !hasConnection) {
|
||||||
reasons.push(
|
reasons.push({
|
||||||
i18n.t('parameters.invoke.missingInputForField', {
|
content: i18n.t('parameters.invoke.missingInputForField', {
|
||||||
nodeLabel: node.data.label || nodeTemplate.title,
|
nodeLabel: node.data.label || nodeTemplate.title,
|
||||||
fieldLabel: field.label || fieldTemplate.title,
|
fieldLabel: field.label || fieldTemplate.title,
|
||||||
})
|
}),
|
||||||
);
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -87,65 +95,94 @@ const selector = createMemoizedSelector(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (dynamicPrompts.prompts.length === 0 && getShouldProcessPrompt(positivePrompt)) {
|
if (dynamicPrompts.prompts.length === 0 && getShouldProcessPrompt(positivePrompt)) {
|
||||||
reasons.push(i18n.t('parameters.invoke.noPrompts'));
|
reasons.push({ content: i18n.t('parameters.invoke.noPrompts') });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!model) {
|
if (!model) {
|
||||||
reasons.push(i18n.t('parameters.invoke.noModelSelected'));
|
reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeTabName === 'generation') {
|
if (activeTabName === 'generation') {
|
||||||
// Handling for generation tab
|
// Handling for generation tab
|
||||||
controlLayers.present.layers
|
controlLayers.present.layers
|
||||||
.filter((l) => l.isEnabled)
|
.filter((l) => l.isEnabled)
|
||||||
.flatMap((l) => {
|
.forEach((l, i) => {
|
||||||
|
const layerLiteral = i18n.t('controlLayers.layers_one');
|
||||||
|
const layerNumber = i + 1;
|
||||||
|
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[l.type]);
|
||||||
|
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
|
||||||
|
const problems: string[] = [];
|
||||||
if (l.type === 'control_adapter_layer') {
|
if (l.type === 'control_adapter_layer') {
|
||||||
return l.controlAdapter;
|
// Must have model
|
||||||
} else if (l.type === 'ip_adapter_layer') {
|
if (!l.controlAdapter.model) {
|
||||||
return l.ipAdapter;
|
problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected'));
|
||||||
} else if (l.type === 'regional_guidance_layer') {
|
}
|
||||||
return l.ipAdapters;
|
// Model base must match
|
||||||
|
if (l.controlAdapter.model?.base !== model?.base) {
|
||||||
|
problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel'));
|
||||||
|
}
|
||||||
|
// Must have a control image OR, if it has a processor, it must have a processed image
|
||||||
|
if (!l.controlAdapter.image) {
|
||||||
|
problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoImageSelected'));
|
||||||
|
} else if (l.controlAdapter.processorConfig && !l.controlAdapter.processedImage) {
|
||||||
|
problems.push(i18n.t('parameters.invoke.layer.controlAdapterImageNotProcessed'));
|
||||||
|
}
|
||||||
|
// T2I Adapters require images have dimensions that are multiples of 64
|
||||||
|
if (l.controlAdapter.type === 't2i_adapter' && (size.width % 64 !== 0 || size.height % 64 !== 0)) {
|
||||||
|
problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
|
||||||
})
|
|
||||||
.forEach((ca, i) => {
|
|
||||||
const hasNoModel = !ca.model;
|
|
||||||
const mismatchedModelBase = ca.model?.base !== model?.base;
|
|
||||||
const hasNoImage = !ca.image;
|
|
||||||
const imageNotProcessed =
|
|
||||||
(ca.type === 'controlnet' || ca.type === 't2i_adapter') && !ca.processedImage && ca.processorConfig;
|
|
||||||
|
|
||||||
if (hasNoModel) {
|
if (l.type === 'ip_adapter_layer') {
|
||||||
reasons.push(
|
// Must have model
|
||||||
i18n.t('parameters.invoke.noModelForControlAdapter', {
|
if (!l.ipAdapter.model) {
|
||||||
number: i + 1,
|
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected'));
|
||||||
})
|
}
|
||||||
);
|
// Model base must match
|
||||||
|
if (l.ipAdapter.model?.base !== model?.base) {
|
||||||
|
problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel'));
|
||||||
|
}
|
||||||
|
// Must have an image
|
||||||
|
if (!l.ipAdapter.image) {
|
||||||
|
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (mismatchedModelBase) {
|
|
||||||
// This should never happen, just a sanity check
|
if (l.type === 'initial_image_layer') {
|
||||||
reasons.push(
|
// Must have an image
|
||||||
i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', {
|
if (!l.image) {
|
||||||
number: i + 1,
|
problems.push(i18n.t('parameters.invoke.layer.initialImageNoImageSelected'));
|
||||||
})
|
}
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (hasNoImage) {
|
|
||||||
reasons.push(
|
if (l.type === 'regional_guidance_layer') {
|
||||||
i18n.t('parameters.invoke.noControlImageForControlAdapter', {
|
// Must have a region
|
||||||
number: i + 1,
|
if (l.maskObjects.length === 0) {
|
||||||
})
|
problems.push(i18n.t('parameters.invoke.layer.rgNoRegion'));
|
||||||
);
|
}
|
||||||
|
// Must have at least 1 prompt or IP Adapter
|
||||||
|
if (l.positivePrompt === null && l.negativePrompt === null && l.ipAdapters.length === 0) {
|
||||||
|
problems.push(i18n.t('parameters.invoke.layer.rgNoPromptsOrIPAdapters'));
|
||||||
|
}
|
||||||
|
l.ipAdapters.forEach((ipAdapter) => {
|
||||||
|
// Must have model
|
||||||
|
if (!ipAdapter.model) {
|
||||||
|
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected'));
|
||||||
|
}
|
||||||
|
// Model base must match
|
||||||
|
if (ipAdapter.model?.base !== model?.base) {
|
||||||
|
problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel'));
|
||||||
|
}
|
||||||
|
// Must have an image
|
||||||
|
if (!ipAdapter.image) {
|
||||||
|
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected'));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (imageNotProcessed) {
|
|
||||||
reasons.push(
|
if (problems.length) {
|
||||||
i18n.t('parameters.invoke.imageNotProcessedForControlAdapter', {
|
const content = upperFirst(problems.join(', '));
|
||||||
number: i + 1,
|
reasons.push({ prefix, content });
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (ca.type === 't2i_adapter' && (size.width % 64 !== 0 || size.height % 64 !== 0)) {
|
|
||||||
reasons.push(i18n.t('parameters.invoke.t2iAdapterMismatchedDimensions'));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -158,29 +195,19 @@ const selector = createMemoizedSelector(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!ca.model) {
|
if (!ca.model) {
|
||||||
reasons.push(
|
reasons.push({ content: i18n.t('parameters.invoke.noModelForControlAdapter', { number: i + 1 }) });
|
||||||
i18n.t('parameters.invoke.noModelForControlAdapter', {
|
|
||||||
number: i + 1,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else if (ca.model.base !== model?.base) {
|
} else if (ca.model.base !== model?.base) {
|
||||||
// This should never happen, just a sanity check
|
// This should never happen, just a sanity check
|
||||||
reasons.push(
|
reasons.push({
|
||||||
i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', {
|
content: i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', { number: i + 1 }),
|
||||||
number: i + 1,
|
});
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!ca.controlImage ||
|
!ca.controlImage ||
|
||||||
(isControlNetOrT2IAdapter(ca) && !ca.processedControlImage && ca.processorType !== 'none')
|
(isControlNetOrT2IAdapter(ca) && !ca.processedControlImage && ca.processorType !== 'none')
|
||||||
) {
|
) {
|
||||||
reasons.push(
|
reasons.push({ content: i18n.t('parameters.invoke.noControlImageForControlAdapter', { number: i + 1 }) });
|
||||||
i18n.t('parameters.invoke.noControlImageForControlAdapter', {
|
|
||||||
number: i + 1,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -191,6 +218,6 @@ const selector = createMemoizedSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const useIsReadyToEnqueue = () => {
|
export const useIsReadyToEnqueue = () => {
|
||||||
const { isReady, reasons } = useAppSelector(selector);
|
const value = useAppSelector(selector);
|
||||||
return { isReady, reasons };
|
return value;
|
||||||
};
|
};
|
||||||
|
@ -16,25 +16,26 @@ export const InvokeQueueBackButton = memo(() => {
|
|||||||
return (
|
return (
|
||||||
<Flex pos="relative" flexGrow={1} minW="240px">
|
<Flex pos="relative" flexGrow={1} minW="240px">
|
||||||
<QueueIterationsNumberInput />
|
<QueueIterationsNumberInput />
|
||||||
<Button
|
<QueueButtonTooltip>
|
||||||
onClick={queueBack}
|
<Button
|
||||||
isLoading={isLoading || isLoadingDynamicPrompts}
|
onClick={queueBack}
|
||||||
loadingText={invoke}
|
isLoading={isLoading || isLoadingDynamicPrompts}
|
||||||
isDisabled={isDisabled}
|
loadingText={invoke}
|
||||||
rightIcon={<RiSparkling2Fill />}
|
isDisabled={isDisabled}
|
||||||
tooltip={<QueueButtonTooltip />}
|
rightIcon={<RiSparkling2Fill />}
|
||||||
variant="solid"
|
variant="solid"
|
||||||
zIndex={1}
|
zIndex={1}
|
||||||
colorScheme="invokeYellow"
|
colorScheme="invokeYellow"
|
||||||
size="lg"
|
size="lg"
|
||||||
w="calc(100% - 60px)"
|
w="calc(100% - 60px)"
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
spinnerPlacement="end"
|
spinnerPlacement="end"
|
||||||
>
|
>
|
||||||
<span>{invoke}</span>
|
<span>{invoke}</span>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
</Button>
|
</Button>
|
||||||
|
</QueueButtonTooltip>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { Divider, Flex, ListItem, Text, UnorderedList } from '@invoke-ai/ui-library';
|
import { Divider, Flex, ListItem, Text, Tooltip, UnorderedList } from '@invoke-ai/ui-library';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useIsReadyToEnqueue } from 'common/hooks/useIsReadyToEnqueue';
|
import { useIsReadyToEnqueue } from 'common/hooks/useIsReadyToEnqueue';
|
||||||
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
|
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
||||||
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
|
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
|
||||||
|
import type { PropsWithChildren } from 'react';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useEnqueueBatchMutation } from 'services/api/endpoints/queue';
|
import { useEnqueueBatchMutation } from 'services/api/endpoints/queue';
|
||||||
@ -21,7 +22,15 @@ type Props = {
|
|||||||
prepend?: boolean;
|
prepend?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const QueueButtonTooltip = memo(({ prepend = false }: Props) => {
|
export const QueueButtonTooltip = (props: PropsWithChildren<Props>) => {
|
||||||
|
return (
|
||||||
|
<Tooltip label={<TooltipContent prepend={props.prepend} />} maxW={512}>
|
||||||
|
{props.children}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TooltipContent = memo(({ prepend = false }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isReady, reasons } = useIsReadyToEnqueue();
|
const { isReady, reasons } = useIsReadyToEnqueue();
|
||||||
const isLoadingDynamicPrompts = useAppSelector((s) => s.dynamicPrompts.isLoading);
|
const isLoadingDynamicPrompts = useAppSelector((s) => s.dynamicPrompts.isLoading);
|
||||||
@ -64,8 +73,15 @@ export const QueueButtonTooltip = memo(({ prepend = false }: Props) => {
|
|||||||
<Divider opacity={0.2} borderColor="base.900" />
|
<Divider opacity={0.2} borderColor="base.900" />
|
||||||
<UnorderedList>
|
<UnorderedList>
|
||||||
{reasons.map((reason, i) => (
|
{reasons.map((reason, i) => (
|
||||||
<ListItem key={`${reason}.${i}`}>
|
<ListItem key={`${reason.content}.${i}`}>
|
||||||
<Text>{reason}</Text>
|
<span>
|
||||||
|
{reason.prefix && (
|
||||||
|
<Text as="span" fontWeight="semibold">
|
||||||
|
{reason.prefix}:{' '}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Text as="span">{reason.content}</Text>
|
||||||
|
</span>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
</UnorderedList>
|
</UnorderedList>
|
||||||
@ -82,4 +98,4 @@ export const QueueButtonTooltip = memo(({ prepend = false }: Props) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
QueueButtonTooltip.displayName = 'QueueButtonTooltip';
|
TooltipContent.displayName = 'QueueButtonTooltipContent';
|
||||||
|
@ -10,15 +10,16 @@ const QueueFrontButton = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { queueFront, isLoading, isDisabled } = useQueueFront();
|
const { queueFront, isLoading, isDisabled } = useQueueFront();
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<QueueButtonTooltip prepend>
|
||||||
aria-label={t('queue.queueFront')}
|
<IconButton
|
||||||
isDisabled={isDisabled}
|
aria-label={t('queue.queueFront')}
|
||||||
isLoading={isLoading}
|
isDisabled={isDisabled}
|
||||||
onClick={queueFront}
|
isLoading={isLoading}
|
||||||
tooltip={<QueueButtonTooltip prepend />}
|
onClick={queueFront}
|
||||||
icon={<AiFillThunderbolt />}
|
icon={<AiFillThunderbolt />}
|
||||||
size="lg"
|
size="lg"
|
||||||
/>
|
/>
|
||||||
|
</QueueButtonTooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -63,16 +63,17 @@ const FloatingSidePanelButtons = (props: Props) => {
|
|||||||
sx={floatingButtonStyles}
|
sx={floatingButtonStyles}
|
||||||
icon={<PiSlidersHorizontalBold size="16px" />}
|
icon={<PiSlidersHorizontalBold size="16px" />}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<QueueButtonTooltip>
|
||||||
aria-label={t('queue.queueBack')}
|
<IconButton
|
||||||
onClick={queueBack}
|
aria-label={t('queue.queueBack')}
|
||||||
isLoading={isLoading}
|
onClick={queueBack}
|
||||||
isDisabled={isDisabled}
|
isLoading={isLoading}
|
||||||
icon={queueButtonIcon}
|
isDisabled={isDisabled}
|
||||||
colorScheme="invokeYellow"
|
icon={queueButtonIcon}
|
||||||
tooltip={<QueueButtonTooltip />}
|
colorScheme="invokeYellow"
|
||||||
sx={floatingButtonStyles}
|
sx={floatingButtonStyles}
|
||||||
/>
|
/>
|
||||||
|
</QueueButtonTooltip>
|
||||||
<CancelCurrentQueueItemIconButton sx={floatingButtonStyles} />
|
<CancelCurrentQueueItemIconButton sx={floatingButtonStyles} />
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<ClearAllQueueIconButton sx={floatingButtonStyles} onOpen={disclosure.onOpen} />
|
<ClearAllQueueIconButton sx={floatingButtonStyles} onOpen={disclosure.onOpen} />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user