feat(ui): restore floating options and gallery buttons

This commit is contained in:
psychedelicious 2024-01-04 19:32:33 +11:00 committed by Kent Keirsey
parent 8db14911d7
commit 2d922a0a65
8 changed files with 187 additions and 143 deletions

View File

@ -1,27 +1,23 @@
import type { ChakraProps } from '@chakra-ui/react'; import type { ChakraProps } from '@chakra-ui/react';
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem'; import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
import { memo } from 'react'; import { memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaTimes } from 'react-icons/fa'; import { FaTimes } from 'react-icons/fa';
import QueueButton from './common/QueueButton';
type Props = { type Props = {
asIconButton?: boolean;
sx?: ChakraProps['sx']; sx?: ChakraProps['sx'];
}; };
const CancelCurrentQueueItemButton = ({ asIconButton, sx }: Props) => { const CancelCurrentQueueItemIconButton = ({ sx }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { cancelQueueItem, isLoading, isDisabled } = const { cancelQueueItem, isLoading, isDisabled } =
useCancelCurrentQueueItem(); useCancelCurrentQueueItem();
return ( return (
<QueueButton <InvIconButton
isDisabled={isDisabled} isDisabled={isDisabled}
isLoading={isLoading} isLoading={isLoading}
asIconButton={asIconButton} aria-label={t('queue.cancel')}
label={t('queue.cancel')}
tooltip={t('queue.cancelTooltip')} tooltip={t('queue.cancelTooltip')}
icon={<FaTimes />} icon={<FaTimes />}
onClick={cancelQueueItem} onClick={cancelQueueItem}
@ -31,4 +27,4 @@ const CancelCurrentQueueItemButton = ({ asIconButton, sx }: Props) => {
); );
}; };
export default memo(CancelCurrentQueueItemButton); export default memo(CancelCurrentQueueItemIconButton);

View File

@ -1,47 +1,34 @@
import { type ChakraProps, useDisclosure } from '@chakra-ui/react'; import { useDisclosure } from '@chakra-ui/react';
import { InvConfirmationAlertDialog } from 'common/components/InvConfirmationAlertDialog/InvConfirmationAlertDialog'; import { InvButton } from 'common/components/InvButton/InvButton';
import { InvText } from 'common/components/InvText/wrapper'; import type { InvButtonProps } from 'common/components/InvButton/types';
import ClearQueueConfirmationAlertDialog from 'features/queue/components/ClearQueueConfirmationAlertDialog';
import { useClearQueue } from 'features/queue/hooks/useClearQueue'; import { useClearQueue } from 'features/queue/hooks/useClearQueue';
import { memo } from 'react'; import { memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaTrash } from 'react-icons/fa'; import { FaTrash } from 'react-icons/fa';
import QueueButton from './common/QueueButton'; type Props = InvButtonProps;
type Props = { const ClearQueueButton = (props: Props) => {
asIconButton?: boolean;
sx?: ChakraProps['sx'];
};
const ClearQueueButton = ({ asIconButton, sx }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { isOpen, onClose, onOpen } = useDisclosure(); const disclosure = useDisclosure();
const { clearQueue, isLoading, isDisabled } = useClearQueue(); const { isLoading, isDisabled } = useClearQueue();
return ( return (
<> <>
<QueueButton <InvButton
isDisabled={isDisabled} isDisabled={isDisabled}
isLoading={isLoading} isLoading={isLoading}
asIconButton={asIconButton}
label={t('queue.clear')}
tooltip={t('queue.clearTooltip')} tooltip={t('queue.clearTooltip')}
icon={<FaTrash />} leftIcon={<FaTrash />}
colorScheme="error" colorScheme="error"
sx={sx} onClick={disclosure.onOpen}
onClick={onOpen} data-testid={t('queue.clear')}
/> {...props}
<InvConfirmationAlertDialog
isOpen={isOpen}
onClose={onClose}
title={t('queue.clearTooltip')}
acceptCallback={clearQueue}
acceptButtonText={t('queue.clear')}
> >
<InvText>{t('queue.clearQueueAlertDialog')}</InvText> {t('queue.clear')}
<br /> </InvButton>
<InvText>{t('queue.clearQueueAlertDialog2')}</InvText> <ClearQueueConfirmationAlertDialog disclosure={disclosure} />
</InvConfirmationAlertDialog>
</> </>
); );
}; };

View File

@ -0,0 +1,32 @@
import type {
UseDisclosureReturn} from '@chakra-ui/react';
import { InvConfirmationAlertDialog } from 'common/components/InvConfirmationAlertDialog/InvConfirmationAlertDialog';
import { InvText } from 'common/components/InvText/wrapper';
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
type Props = {
disclosure: UseDisclosureReturn;
};
const ClearQueueButton = ({ disclosure }: Props) => {
const { t } = useTranslation();
const { clearQueue } = useClearQueue();
return (
<InvConfirmationAlertDialog
isOpen={disclosure.isOpen}
onClose={disclosure.onClose}
title={t('queue.clearTooltip')}
acceptCallback={clearQueue}
acceptButtonText={t('queue.clear')}
>
<InvText>{t('queue.clearQueueAlertDialog')}</InvText>
<br />
<InvText>{t('queue.clearQueueAlertDialog2')}</InvText>
</InvConfirmationAlertDialog>
);
};
export default memo(ClearQueueButton);

View File

@ -0,0 +1,35 @@
import { useDisclosure } from '@chakra-ui/react';
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
import type { InvIconButtonProps } from 'common/components/InvIconButton/types';
import ClearQueueConfirmationAlertDialog from 'features/queue/components/ClearQueueConfirmationAlertDialog';
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { FaTrash } from 'react-icons/fa';
type Props = Omit<InvIconButtonProps, 'aria-label'>;
const ClearQueueIconButton = (props: Props) => {
const { t } = useTranslation();
const disclosure = useDisclosure();
const { isLoading, isDisabled } = useClearQueue();
return (
<>
<InvIconButton
isDisabled={isDisabled}
isLoading={isLoading}
aria-label={t('queue.clear')}
tooltip={t('queue.clearTooltip')}
icon={<FaTrash />}
colorScheme="error"
onClick={disclosure.onOpen}
data-testid={t('queue.clear')}
{...props}
/>
<ClearQueueConfirmationAlertDialog disclosure={disclosure} />
</>
);
};
export default memo(ClearQueueIconButton);

View File

@ -1,6 +1,6 @@
import { Flex, Spacer } from '@chakra-ui/react'; import { Flex, Spacer } from '@chakra-ui/react';
import { InvButtonGroup } from 'common/components/InvButtonGroup/InvButtonGroup'; import { InvButtonGroup } from 'common/components/InvButtonGroup/InvButtonGroup';
import ClearQueueButton from 'features/queue/components/ClearQueueButton'; import ClearQueueIconButton from 'features/queue/components/ClearQueueIconButton';
import QueueFrontButton from 'features/queue/components/QueueFrontButton'; import QueueFrontButton from 'features/queue/components/QueueFrontButton';
import ProgressBar from 'features/system/components/ProgressBar'; import ProgressBar from 'features/system/components/ProgressBar';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
@ -28,7 +28,7 @@ const QueueControls = () => {
{/* <CancelCurrentQueueItemButton asIconButton /> {/* <CancelCurrentQueueItemButton asIconButton />
{isResumeEnabled && <ResumeProcessorButton asIconButton />} {isResumeEnabled && <ResumeProcessorButton asIconButton />}
{isPauseEnabled && <PauseProcessorButton asIconButton />} */} {isPauseEnabled && <PauseProcessorButton asIconButton />} */}
<ClearQueueButton asIconButton /> <ClearQueueIconButton />
</InvButtonGroup> </InvButtonGroup>
<ProgressBar /> <ProgressBar />
</Flex> </Flex>

View File

@ -1,22 +1,20 @@
import { Flex } from '@chakra-ui/layout'; import { Flex } from '@chakra-ui/layout';
import { Portal } from '@chakra-ui/portal'; import { Portal } from '@chakra-ui/portal';
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton'; import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
import { InvTooltip } from 'common/components/InvTooltip/InvTooltip';
import type { UsePanelReturn } from 'features/ui/hooks/usePanel';
import { memo } from 'react'; import { memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { MdPhotoLibrary } from 'react-icons/md'; import { MdPhotoLibrary } from 'react-icons/md';
type Props = { type Props = {
isGalleryCollapsed: boolean; panelApi: UsePanelReturn;
expandGallery: () => void;
}; };
const FloatingGalleryButton = ({ const FloatingGalleryButton = (props: Props) => {
isGalleryCollapsed,
expandGallery,
}: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
if (!isGalleryCollapsed) { if (!props.panelApi.isCollapsed) {
return null; return null;
} }
@ -27,18 +25,21 @@ const FloatingGalleryButton = ({
transform="translate(0, -50%)" transform="translate(0, -50%)"
minW={8} minW={8}
top="50%" top="50%"
insetInlineEnd="1.63rem" insetInlineEnd={0}
> >
<InvIconButton <InvTooltip
tooltip="Show Gallery (G)" label={t('accessibility.showGalleryPanel')}
aria-label={t('accessibility.showGalleryPanel')} placement="start"
onClick={expandGallery} >
icon={<MdPhotoLibrary />} <InvIconButton
p={0} aria-label={t('accessibility.showGalleryPanel')}
px={3} onClick={props.panelApi.expand}
h={48} icon={<MdPhotoLibrary />}
borderEndRadius={0} p={0}
/> h={48}
borderEndRadius={0}
/>
</InvTooltip>
</Flex> </Flex>
</Portal> </Portal>
); );

View File

@ -1,40 +1,45 @@
import type { ChakraProps } from '@chakra-ui/react'; import { SpinnerIcon } from '@chakra-ui/icons';
import type { SystemStyleObject } from '@chakra-ui/react';
import { Flex, Portal } from '@chakra-ui/react'; import { Flex, Portal } from '@chakra-ui/react';
import { InvButtonGroup } from 'common/components/InvButtonGroup/InvButtonGroup'; import { InvButtonGroup } from 'common/components/InvButtonGroup/InvButtonGroup';
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton'; import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
import CancelCurrentQueueItemButton from 'features/queue/components/CancelCurrentQueueItemButton'; import CancelCurrentQueueItemIconButton from 'features/queue/components/CancelCurrentQueueItemIconButton';
import ClearQueueButton from 'features/queue/components/ClearQueueButton'; import ClearQueueIconButton from 'features/queue/components/ClearQueueIconButton';
import { QueueButtonTooltip } from 'features/queue/components/QueueButtonTooltip'; import { QueueButtonTooltip } from 'features/queue/components/QueueButtonTooltip';
import { useQueueBack } from 'features/queue/hooks/useQueueBack'; import { useQueueBack } from 'features/queue/hooks/useQueueBack';
import type { RefObject } from 'react'; import type { UsePanelReturn } from 'features/ui/hooks/usePanel';
import { memo, useCallback } from 'react'; import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaSlidersH } from 'react-icons/fa'; import { FaSlidersH } from 'react-icons/fa';
import { IoSparkles } from 'react-icons/io5'; import { IoSparkles } from 'react-icons/io5';
import type { ImperativePanelHandle } from 'react-resizable-panels'; import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
import { spinAnimationSlow } from 'theme/animations';
const floatingButtonStyles: ChakraProps['sx'] = { const floatingButtonStyles: SystemStyleObject = {
borderStartRadius: 0, borderStartRadius: 0,
flexGrow: 1, flexGrow: 1,
}; };
type Props = { type Props = {
isSidePanelCollapsed: boolean; panelApi: UsePanelReturn;
sidePanelRef: RefObject<ImperativePanelHandle>;
}; };
const FloatingSidePanelButtons = ({ const FloatingSidePanelButtons = (props: Props) => {
isSidePanelCollapsed,
sidePanelRef,
}: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { queueBack, isLoading, isDisabled } = useQueueBack(); const { queueBack, isLoading, isDisabled } = useQueueBack();
const { data: queueStatus } = useGetQueueStatusQuery();
const handleShowSidePanel = useCallback(() => { const queueButtonIcon = useMemo(
sidePanelRef.current?.expand(); () =>
}, [sidePanelRef]); queueStatus?.processor.is_processing ? (
<SpinnerIcon animation={spinAnimationSlow} />
) : (
<IoSparkles />
),
[queueStatus?.processor.is_processing]
);
if (!isSidePanelCollapsed) { if (!props.panelApi.isCollapsed) {
return null; return null;
} }
@ -45,7 +50,7 @@ const FloatingSidePanelButtons = ({
transform="translate(0, -50%)" transform="translate(0, -50%)"
minW={8} minW={8}
top="50%" top="50%"
insetInlineStart="5.13rem" insetInlineStart="54px"
direction="column" direction="column"
gap={2} gap={2}
h={48} h={48}
@ -54,29 +59,23 @@ const FloatingSidePanelButtons = ({
<InvIconButton <InvIconButton
tooltip={t('parameters.showOptionsPanel')} tooltip={t('parameters.showOptionsPanel')}
aria-label={t('parameters.showOptionsPanel')} aria-label={t('parameters.showOptionsPanel')}
onClick={handleShowSidePanel} onClick={props.panelApi.expand}
sx={floatingButtonStyles} sx={floatingButtonStyles}
icon={<FaSlidersH />} icon={<FaSlidersH />}
/> />
<InvIconButton <InvIconButton
aria-label={t('queue.queueBack')} aria-label={t('queue.queueBack')}
pos="absolute"
insetInlineStart={0}
onClick={queueBack} onClick={queueBack}
isLoading={isLoading} isLoading={isLoading}
isDisabled={isDisabled} isDisabled={isDisabled}
icon={<IoSparkles />} icon={queueButtonIcon}
variant="solid" colorScheme="invokeYellow"
colorScheme="yellow"
tooltip={<QueueButtonTooltip />} tooltip={<QueueButtonTooltip />}
sx={floatingButtonStyles} sx={floatingButtonStyles}
/> />
<CancelCurrentQueueItemButton <CancelCurrentQueueItemIconButton sx={floatingButtonStyles} />
asIconButton
sx={floatingButtonStyles}
/>
</InvButtonGroup> </InvButtonGroup>
<ClearQueueButton asIconButton sx={floatingButtonStyles} /> <ClearQueueIconButton sx={floatingButtonStyles} />
</Flex> </Flex>
</Portal> </Portal>
); );

View File

@ -16,6 +16,8 @@ import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditor
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent'; import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
import SettingsMenu from 'features/system/components/SettingsModal/SettingsMenu'; import SettingsMenu from 'features/system/components/SettingsModal/SettingsMenu';
import StatusIndicator from 'features/system/components/StatusIndicator'; import StatusIndicator from 'features/system/components/StatusIndicator';
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons';
import type { UsePanelOptions } from 'features/ui/hooks/usePanel'; import type { UsePanelOptions } from 'features/ui/hooks/usePanel';
import { usePanel } from 'features/ui/hooks/usePanel'; import { usePanel } from 'features/ui/hooks/usePanel';
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage'; import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
@ -99,8 +101,8 @@ const enabledTabsSelector = createMemoizedSelector(
} }
); );
export const NO_GALLERY_TABS: InvokeTabName[] = ['modelManager', 'queue']; export const NO_GALLERY_PANEL_TABS: InvokeTabName[] = ['modelManager', 'queue'];
export const NO_SIDE_PANEL_TABS: InvokeTabName[] = ['modelManager', 'queue']; export const NO_OPTIONS_PANEL_TABS: InvokeTabName[] = ['modelManager', 'queue'];
const panelStyles: CSSProperties = { height: '100%', width: '100%' }; const panelStyles: CSSProperties = { height: '100%', width: '100%' };
const GALLERY_MIN_SIZE_PX = 310; const GALLERY_MIN_SIZE_PX = 310;
const GALLERY_MIN_SIZE_PCT = 20; const GALLERY_MIN_SIZE_PCT = 20;
@ -121,6 +123,14 @@ const InvokeTabs = () => {
e.target.blur(); e.target.blur();
} }
}, []); }, []);
const shouldShowOptionsPanel = useMemo(
() => !NO_OPTIONS_PANEL_TABS.includes(activeTabName),
[activeTabName]
);
const shouldShowGalleryPanel = useMemo(
() => !NO_GALLERY_PANEL_TABS.includes(activeTabName),
[activeTabName]
);
const tabs = useMemo( const tabs = useMemo(
() => () =>
@ -185,60 +195,38 @@ const InvokeTabs = () => {
const panelStorage = usePanelStorage(); const panelStorage = usePanelStorage();
const { const optionsPanel = usePanel(optionsPanelUsePanelOptions);
ref: optionsPanelRef,
minSize: optionsPanelMinSize,
isCollapsed: isOptionsPanelCollapsed,
onCollapse: onCollapseOptionsPanel,
onExpand: onExpandOptionsPanel,
reset: resetOptionsPanel,
expand: expandOptionsPanel,
collapse: collapseOptionsPanel,
toggle: toggleOptionsPanel,
onDoubleClickHandle: onDoubleClickOptionsPanelHandle,
} = usePanel(optionsPanelUsePanelOptions);
const { const galleryPanel = usePanel(galleryPanelUsePanelOptions);
ref: galleryPanelRef,
minSize: galleryPanelMinSize,
isCollapsed: isGalleryPanelCollapsed,
onCollapse: onCollapseGalleryPanel,
onExpand: onExpandGalleryPanel,
reset: resetGalleryPanel,
expand: expandGalleryPanel,
collapse: collapseGalleryPanel,
toggle: toggleGalleryPanel,
onDoubleClickHandle: onDoubleClickGalleryPanelHandle,
} = usePanel(galleryPanelUsePanelOptions);
useHotkeys('g', toggleGalleryPanel, [toggleGalleryPanel]); useHotkeys('g', galleryPanel.toggle, [galleryPanel.toggle]);
useHotkeys(['t', 'o'], toggleOptionsPanel, [toggleOptionsPanel]); useHotkeys(['t', 'o'], optionsPanel.toggle, [optionsPanel.toggle]);
useHotkeys( useHotkeys(
'shift+r', 'shift+r',
() => { () => {
resetOptionsPanel(); optionsPanel.reset();
resetGalleryPanel(); galleryPanel.reset();
}, },
[resetOptionsPanel, resetGalleryPanel] [optionsPanel.reset, galleryPanel.reset]
); );
useHotkeys( useHotkeys(
'f', 'f',
() => { () => {
if (isOptionsPanelCollapsed || isGalleryPanelCollapsed) { if (optionsPanel.isCollapsed || galleryPanel.isCollapsed) {
expandOptionsPanel(); optionsPanel.expand();
expandGalleryPanel(); galleryPanel.expand();
} else { } else {
collapseOptionsPanel(); optionsPanel.collapse();
collapseGalleryPanel(); galleryPanel.collapse();
} }
}, },
[ [
isOptionsPanelCollapsed, optionsPanel.isCollapsed,
isGalleryPanelCollapsed, galleryPanel.isCollapsed,
expandOptionsPanel, optionsPanel.expand,
expandGalleryPanel, galleryPanel.expand,
collapseOptionsPanel, optionsPanel.collapse,
collapseGalleryPanel, galleryPanel.collapse,
] ]
); );
@ -272,16 +260,16 @@ const InvokeTabs = () => {
style={panelStyles} style={panelStyles}
storage={panelStorage} storage={panelStorage}
> >
{!NO_SIDE_PANEL_TABS.includes(activeTabName) && ( {shouldShowOptionsPanel && (
<> <>
<Panel <Panel
id="options-panel" id="options-panel"
ref={optionsPanelRef} ref={optionsPanel.ref}
order={0} order={0}
defaultSize={optionsPanelMinSize} defaultSize={optionsPanel.minSize}
minSize={optionsPanelMinSize} minSize={optionsPanel.minSize}
onCollapse={onCollapseOptionsPanel} onCollapse={optionsPanel.onCollapse}
onExpand={onExpandOptionsPanel} onExpand={optionsPanel.onExpand}
collapsible collapsible
> >
{activeTabName === 'nodes' ? ( {activeTabName === 'nodes' ? (
@ -292,7 +280,7 @@ const InvokeTabs = () => {
</Panel> </Panel>
<ResizeHandle <ResizeHandle
id="options-main-handle" id="options-main-handle"
onDoubleClick={onDoubleClickOptionsPanelHandle} onDoubleClick={optionsPanel.onDoubleClickHandle}
orientation="vertical" orientation="vertical"
/> />
</> </>
@ -302,21 +290,21 @@ const InvokeTabs = () => {
{tabPanels} {tabPanels}
</InvTabPanels> </InvTabPanels>
</Panel> </Panel>
{!NO_GALLERY_TABS.includes(activeTabName) && ( {shouldShowGalleryPanel && (
<> <>
<ResizeHandle <ResizeHandle
id="main-gallery-handle" id="main-gallery-handle"
orientation="vertical" orientation="vertical"
onDoubleClick={onDoubleClickGalleryPanelHandle} onDoubleClick={galleryPanel.onDoubleClickHandle}
/> />
<Panel <Panel
id="gallery-panel" id="gallery-panel"
ref={galleryPanelRef} ref={galleryPanel.ref}
order={2} order={2}
defaultSize={galleryPanelMinSize} defaultSize={galleryPanel.minSize}
minSize={galleryPanelMinSize} minSize={galleryPanel.minSize}
onCollapse={onCollapseGalleryPanel} onCollapse={galleryPanel.onCollapse}
onExpand={onExpandGalleryPanel} onExpand={galleryPanel.onExpand}
collapsible collapsible
> >
<ImageGalleryContent /> <ImageGalleryContent />
@ -324,6 +312,12 @@ const InvokeTabs = () => {
</> </>
)} )}
</PanelGroup> </PanelGroup>
{shouldShowOptionsPanel && (
<FloatingParametersPanelButtons panelApi={optionsPanel} />
)}
{shouldShowGalleryPanel && (
<FloatingGalleryButton panelApi={galleryPanel} />
)}
</InvTabs> </InvTabs>
); );
}; };