tidy(ui): app layout components

This commit is contained in:
psychedelicious
2025-06-04 11:44:37 +10:00
parent 6f4ab4f100
commit c66a16d889
20 changed files with 379 additions and 296 deletions

View File

@@ -9,7 +9,7 @@ import { selectEntityCountActive } from 'features/controlLayers/store/selectors'
import { multipleImageDndSource, singleImageDndSource } from 'features/dnd/dnd';
import { DndDropOverlay } from 'features/dnd/DndDropOverlay';
import type { DndTargetState } from 'features/dnd/types';
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
import RightPanelContent from 'features/gallery/components/GalleryTopBar';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { selectActiveTabCanvasRightPanel } from 'features/ui/store/uiSelectors';
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
@@ -61,7 +61,7 @@ export const CanvasRightPanel = memo(() => {
</CanvasManagerProviderGate>
</TabPanel>
<TabPanel w="full" h="full" p={0} pt={3}>
<GalleryPanelContent />
<RightPanelContent />
</TabPanel>
</TabPanels>
</Tabs>

View File

@@ -9,7 +9,7 @@ import { selectEntityCountActive } from 'features/controlLayers/store/selectors'
import { multipleImageDndSource, singleImageDndSource } from 'features/dnd/dnd';
import { DndDropOverlay } from 'features/dnd/DndDropOverlay';
import type { DndTargetState } from 'features/dnd/types';
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
import RightPanelContent from 'features/gallery/components/GalleryTopBar';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { selectActiveTabCanvasRightPanel } from 'features/ui/store/uiSelectors';
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
@@ -49,7 +49,7 @@ export const CanvasRightPanelStacked = memo(() => {
return (
<PanelGroup direction="vertical">
<Panel>
<GalleryPanelContent />
<RightPanelContent />
</Panel>
<PanelResizeHandle />
<Panel>

View File

@@ -8,7 +8,7 @@ import {
selectSelectedBoardId,
} from 'features/gallery/store/gallerySelectors';
import { selectAllowPrivateBoards } from 'features/system/store/configSelectors';
import { useMemo } from 'react';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCaretDownBold } from 'react-icons/pi';
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
@@ -21,7 +21,7 @@ type Props = {
isPrivate: boolean;
};
export const BoardsList = ({ isPrivate }: Props) => {
export const BoardsList = memo(({ isPrivate }: Props) => {
const { t } = useTranslation();
const selectedBoardId = useAppSelector(selectSelectedBoardId);
const boardSearchText = useAppSelector(selectBoardSearchText);
@@ -118,4 +118,5 @@ export const BoardsList = ({ isPrivate }: Props) => {
</Collapse>
</Flex>
);
};
});
BoardsList.displayName = 'BoardsList';

View File

@@ -17,7 +17,7 @@ const overlayScrollbarsStyles: CSSProperties = {
width: '100%',
};
const BoardsListWrapper = () => {
export const BoardsListWrapper = memo(() => {
const allowPrivateBoards = useAppSelector(selectAllowPrivateBoards);
const [os, osRef] = useState<OverlayScrollbarsComponentRef | null>(null);
useEffect(() => {
@@ -54,5 +54,6 @@ const BoardsListWrapper = () => {
</Box>
</Box>
);
};
export default memo(BoardsListWrapper);
});
BoardsListWrapper.displayName = 'BoardsListWrapper';

View File

@@ -7,7 +7,7 @@ import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
const BoardsSearch = () => {
export const BoardsSearch = memo(() => {
const dispatch = useAppDispatch();
const boardSearchText = useAppSelector(selectBoardSearchText);
const { t } = useTranslation();
@@ -62,6 +62,5 @@ const BoardsSearch = () => {
)}
</InputGroup>
);
};
export default memo(BoardsSearch);
});
BoardsSearch.displayName = 'BoardsSearch';

View File

@@ -3,7 +3,7 @@ import { Combobox, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectBoardsListOrderBy, selectBoardsListOrderDir } from 'features/gallery/store/gallerySelectors';
import { boardsListOrderByChanged, boardsListOrderDirChanged } from 'features/gallery/store/gallerySlice';
import { useCallback, useMemo } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
@@ -15,7 +15,7 @@ const zDirection = z.enum(['ASC', 'DESC']);
type Direction = z.infer<typeof zDirection>;
const isDirection = (v: unknown): v is Direction => zDirection.safeParse(v).success;
export const BoardsListSortControls = () => {
export const BoardsListSortControls = memo(() => {
const { t } = useTranslation();
const orderBy = useAppSelector(selectBoardsListOrderBy);
@@ -83,4 +83,5 @@ export const BoardsListSortControls = () => {
</FormControl>
</Flex>
);
};
});
BoardsListSortControls.displayName = 'BoardsListSortControls';

View File

@@ -17,7 +17,7 @@ import { PiGearSixFill } from 'react-icons/pi';
import { BoardsListSortControls } from './BoardsListSortControls';
const BoardsSettingsPopover = () => {
export const BoardsSettingsPopover = memo(() => {
const { t } = useTranslation();
return (
@@ -48,6 +48,5 @@ const BoardsSettingsPopover = () => {
</PopoverContent>
</Popover>
);
};
export default memo(BoardsSettingsPopover);
});
BoardsSettingsPopover.displayName = 'BoardsSettingsPopover';

View File

@@ -0,0 +1,25 @@
import type { UseDisclosureReturn } from '@invoke-ai/ui-library';
import { Box, Collapse, Divider, Flex } from '@invoke-ai/ui-library';
import { BoardsListWrapper } from 'features/gallery/components/Boards/BoardsList/BoardsListWrapper';
import { BoardsSearch } from 'features/gallery/components/Boards/BoardsList/BoardsSearch';
import type { CSSProperties } from 'react';
import { memo } from 'react';
const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 };
export const BoardsListPanelContent = memo(
({ boardSearchDisclosure }: { boardSearchDisclosure: UseDisclosureReturn }) => {
return (
<Flex flexDir="column" w="full" h="full">
<Collapse in={boardSearchDisclosure.isOpen} style={COLLAPSE_STYLES}>
<Box w="full" pt={2}>
<BoardsSearch />
</Box>
</Collapse>
<Divider pt={2} />
<BoardsListWrapper />
</Flex>
);
}
);
BoardsListPanelContent.displayName = 'BoardsListPanelContent';

View File

@@ -18,12 +18,12 @@ import { useGallerySearchTerm } from 'features/gallery/components/ImageGrid/useG
import { selectSelectedBoardId } from 'features/gallery/store/gallerySelectors';
import { galleryViewChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
import type { CSSProperties } from 'react';
import { useCallback } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiMagnifyingGlassBold } from 'react-icons/pi';
import { useBoardName } from 'services/api/hooks/useBoardName';
import GallerySettingsPopover from './GallerySettingsPopover/GallerySettingsPopover';
import { GallerySettingsPopover } from './GallerySettingsPopover/GallerySettingsPopover';
import { GalleryUploadButton } from './GalleryUploadButton';
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
import { GalleryPagination } from './ImageGrid/GalleryPagination';
@@ -46,7 +46,7 @@ const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0, width: '10
const selectGalleryView = createSelector(selectGallerySlice, (gallery) => gallery.galleryView);
const selectSearchTerm = createSelector(selectGallerySlice, (gallery) => gallery.searchTerm);
export const Gallery = () => {
export const Gallery = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const galleryView = useAppSelector(selectGalleryView);
@@ -116,4 +116,5 @@ export const Gallery = () => {
<GalleryPagination />
</Flex>
);
};
});
Gallery.displayName = 'Gallery';

View File

@@ -1,159 +0,0 @@
import {
Box,
Button,
Collapse,
Divider,
Flex,
IconButton,
type SystemStyleObject,
useDisclosure,
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { GalleryHeader } from 'features/gallery/components/GalleryHeader';
import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors';
import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
import { HorizontalResizeHandle } from 'features/ui/components/tabs/ResizeHandle';
import { usePanel, type UsePanelOptions } from 'features/ui/hooks/usePanel';
import type { CSSProperties } from 'react';
import { memo, useCallback, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCaretDownBold, PiCaretUpBold, PiMagnifyingGlassBold } from 'react-icons/pi';
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
import { Panel, PanelGroup } from 'react-resizable-panels';
import BoardsListWrapper from './Boards/BoardsList/BoardsListWrapper';
import BoardsSearch from './Boards/BoardsList/BoardsSearch';
import BoardsSettingsPopover from './Boards/BoardsSettingsPopover';
import { Gallery } from './Gallery';
const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 };
const FOCUS_REGION_STYLES: SystemStyleObject = {
width: 'full',
height: 'full',
position: 'relative',
flexDirection: 'column',
display: 'flex',
};
const GalleryPanelContent = () => {
const { t } = useTranslation();
const boardSearchText = useAppSelector(selectBoardSearchText);
const dispatch = useAppDispatch();
const boardSearchDisclosure = useDisclosure({ defaultIsOpen: !!boardSearchText.length });
const imperativePanelGroupRef = useRef<ImperativePanelGroupHandle>(null);
const sessionType = useAppSelector(selectCanvasSessionType);
const boardsListPanelOptions = useMemo<UsePanelOptions>(
() => ({
id: 'boards-list-panel',
minSizePx: 128,
defaultSizePx: 256,
imperativePanelGroupRef,
panelGroupDirection: 'vertical',
}),
[]
);
const boardsListPanel = usePanel(boardsListPanelOptions);
const galleryPanelOptions = useMemo<UsePanelOptions>(
() => ({
id: 'gallery-panel',
minSizePx: 128,
defaultSizePx: 256,
imperativePanelGroupRef,
panelGroupDirection: 'vertical',
}),
[]
);
const galleryPanel = usePanel(galleryPanelOptions);
const canvasLayersPanelOptions = useMemo<UsePanelOptions>(
() => ({
id: 'canvas-layers-panel',
minSizePx: 128,
defaultSizePx: 256,
imperativePanelGroupRef,
panelGroupDirection: 'vertical',
}),
[]
);
const canvasLayersPanel = usePanel(canvasLayersPanelOptions);
const handleClickBoardSearch = useCallback(() => {
if (boardSearchText.length) {
dispatch(boardSearchTextChanged(''));
}
boardSearchDisclosure.onToggle();
boardsListPanel.expand();
}, [boardSearchText.length, boardSearchDisclosure, boardsListPanel, dispatch]);
return (
<FocusRegionWrapper region="gallery" sx={FOCUS_REGION_STYLES}>
<Flex alignItems="center" justifyContent="space-between" w="full">
<Flex flexGrow={1} flexBasis={0}>
<Button
size="sm"
variant="ghost"
onClick={boardsListPanel.toggle}
rightIcon={boardsListPanel.isCollapsed ? <PiCaretDownBold /> : <PiCaretUpBold />}
>
{boardsListPanel.isCollapsed ? t('boards.viewBoards') : t('boards.hideBoards')}
</Button>
</Flex>
<Flex>
<GalleryHeader />
</Flex>
<Flex flexGrow={1} flexBasis={0} justifyContent="flex-end">
<BoardsSettingsPopover />
<IconButton
size="sm"
variant="link"
alignSelf="stretch"
onClick={handleClickBoardSearch}
tooltip={
boardSearchDisclosure.isOpen ? `${t('gallery.exitBoardSearch')}` : `${t('gallery.displayBoardSearch')}`
}
aria-label={t('gallery.displayBoardSearch')}
icon={<PiMagnifyingGlassBold />}
colorScheme={boardSearchDisclosure.isOpen ? 'invokeBlue' : 'base'}
/>
</Flex>
</Flex>
<PanelGroup ref={imperativePanelGroupRef} direction="vertical" autoSaveId="boards-list-panel">
<Panel order={0} id="boards-panel" collapsible {...boardsListPanel.panelProps}>
<Flex flexDir="column" w="full" h="full">
<Collapse in={boardSearchDisclosure.isOpen} style={COLLAPSE_STYLES}>
<Box w="full" pt={2}>
<BoardsSearch />
</Box>
</Collapse>
<Divider pt={2} />
<BoardsListWrapper />
</Flex>
</Panel>
<HorizontalResizeHandle id="boards-list-to-gallery-panel-handle" {...boardsListPanel.resizeHandleProps} />
<Panel order={1} id="gallery-wrapper-panel" collapsible {...galleryPanel.panelProps}>
<Gallery />
</Panel>
{sessionType === 'advanced' && (
<>
<HorizontalResizeHandle id="gallery-panel-to-layers-handle" {...galleryPanel.resizeHandleProps} />
<Panel order={2} id="canvas-layers-panel" collapsible {...canvasLayersPanel.panelProps}>
<CanvasManagerProviderGate>
<CanvasLayersPanelContent />
</CanvasManagerProviderGate>
</Panel>
</>
)}
</PanelGroup>
</FocusRegionWrapper>
);
};
export default memo(GalleryPanelContent);

View File

@@ -8,7 +8,7 @@ import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiGearSixFill } from 'react-icons/pi';
const GallerySettingsPopover = () => {
export const GallerySettingsPopover = memo(() => {
const { t } = useTranslation();
return (
@@ -37,6 +37,5 @@ const GallerySettingsPopover = () => {
</PopoverContent>
</Popover>
);
};
export default memo(GallerySettingsPopover);
});
GallerySettingsPopover.displayName = 'GallerySettingsPopover';

View File

@@ -0,0 +1,67 @@
import type { UseDisclosureReturn } from '@invoke-ai/ui-library';
import { Button, Flex, IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { BoardsSettingsPopover } from 'features/gallery/components/Boards/BoardsSettingsPopover';
import { GalleryHeader } from 'features/gallery/components/GalleryHeader';
import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors';
import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
import type { UsePanelReturn } from 'features/ui/hooks/usePanel';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCaretDownBold, PiCaretUpBold, PiMagnifyingGlassBold } from 'react-icons/pi';
export const GalleryTopBar = memo(
({
boardsListPanel,
boardSearchDisclosure,
}: {
boardsListPanel: UsePanelReturn;
boardSearchDisclosure: UseDisclosureReturn;
}) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const boardSearchText = useAppSelector(selectBoardSearchText);
const onClickBoardSearch = useCallback(() => {
if (boardSearchText.length) {
dispatch(boardSearchTextChanged(''));
}
boardSearchDisclosure.onToggle();
boardsListPanel.expand();
}, [boardSearchText.length, boardSearchDisclosure, boardsListPanel, dispatch]);
return (
<Flex alignItems="center" justifyContent="space-between" w="full">
<Flex flexGrow={1} flexBasis={0}>
<Button
size="sm"
variant="ghost"
onClick={boardsListPanel.toggle}
rightIcon={boardsListPanel.isCollapsed ? <PiCaretDownBold /> : <PiCaretUpBold />}
>
{boardsListPanel.isCollapsed ? t('boards.viewBoards') : t('boards.hideBoards')}
</Button>
</Flex>
<Flex>
<GalleryHeader />
</Flex>
<Flex flexGrow={1} flexBasis={0} justifyContent="flex-end">
<BoardsSettingsPopover />
<IconButton
size="sm"
variant="link"
alignSelf="stretch"
onClick={onClickBoardSearch}
tooltip={
boardSearchDisclosure.isOpen ? `${t('gallery.exitBoardSearch')}` : `${t('gallery.displayBoardSearch')}`
}
aria-label={t('gallery.displayBoardSearch')}
icon={<PiMagnifyingGlassBold />}
colorScheme={boardSearchDisclosure.isOpen ? 'invokeBlue' : 'base'}
/>
</Flex>
</Flex>
);
}
);
GalleryTopBar.displayName = 'GalleryTopBar';

View File

@@ -1,10 +1,13 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import { t } from 'i18next';
import { memo } from 'react';
import { PiUploadBold } from 'react-icons/pi';
export const GalleryUploadButton = () => {
const uploadApi = useImageUploadButton({ allowMultiple: true });
const UPLOAD_OPTIONS: Parameters<typeof useImageUploadButton>[0] = { allowMultiple: true };
export const GalleryUploadButton = memo(() => {
const uploadApi = useImageUploadButton(UPLOAD_OPTIONS);
return (
<>
<IconButton
@@ -19,4 +22,5 @@ export const GalleryUploadButton = () => {
<input {...uploadApi.getUploadInputProps()} />
</>
);
};
});
GalleryUploadButton.displayName = 'GalleryUploadButton';

View File

@@ -1,16 +1,15 @@
import { Box, Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { CanvasMainPanelContent } from 'features/controlLayers/components/CanvasMainPanelContent';
import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { useDndMonitor } from 'features/dnd/useDndMonitor';
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
import WorkflowsTabLeftPanel from 'features/nodes/components/sidePanel/WorkflowsTabLeftPanel';
import QueueControls from 'features/queue/components/QueueControls';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons';
import { FloatingLeftPanelButtons } from 'features/ui/components/FloatingLeftPanelButtons';
import { FloatingRightPanelButtons } from 'features/ui/components/FloatingRightPanelButtons';
import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage';
import { RightPanelContent } from 'features/ui/components/RightPanelContent';
import ModelManagerTab from 'features/ui/components/tabs/ModelManagerTab';
import QueueTab from 'features/ui/components/tabs/QueueTab';
import { WorkflowsMainPanel } from 'features/ui/components/tabs/WorkflowsTabContent';
@@ -30,6 +29,8 @@ import type { CSSProperties } from 'react';
import { memo, useMemo, useRef } from 'react';
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
import { Panel, PanelGroup } from 'react-resizable-panels';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
import ParametersPanelUpscale from './ParametersPanels/ParametersPanelUpscale';
import { VerticalResizeHandle } from './tabs/ResizeHandle';
@@ -128,26 +129,21 @@ export const AppContent = memo(() => {
>
{withLeftPanel && (
<>
<Panel order={0} collapsible style={panelStyles} {...leftPanel.panelProps}>
<Flex flexDir="column" w="full" h="full" gap={2}>
<QueueControls />
<Box position="relative" w="full" h="full">
<LeftPanelContent />
</Box>
</Flex>
<Panel id="left-panel" order={0} collapsible style={panelStyles} {...leftPanel.panelProps}>
<LeftPanelContent />
</Panel>
<VerticalResizeHandle id="left-main-handle" {...leftPanel.resizeHandleProps} />
</>
)}
<Panel id="main-panel" order={1} minSize={20} style={panelStyles}>
<MainPanelContent />
{withLeftPanel && <FloatingParametersPanelButtons togglePanel={leftPanel.toggle} />}
{withRightPanel && <FloatingGalleryButton panelApi={rightPanel} />}
{withLeftPanel && <FloatingLeftPanelButtons onToggle={leftPanel.toggle} />}
{withRightPanel && <FloatingRightPanelButtons onToggle={rightPanel.toggle} />}
</Panel>
{withRightPanel && (
<>
<VerticalResizeHandle id="main-right-handle" {...rightPanel.resizeHandleProps} />
<Panel order={2} style={panelStyles} collapsible {...rightPanel.panelProps}>
<Panel id="right-panel" order={2} style={panelStyles} collapsible {...rightPanel.panelProps}>
<RightPanelContent />
</Panel>
</>
@@ -156,35 +152,21 @@ export const AppContent = memo(() => {
</Flex>
);
});
AppContent.displayName = 'AppContent';
const RightPanelContent = memo(() => {
const tab = useAppSelector(selectActiveTab);
const sessionType = useAppSelector(selectCanvasSessionType);
if (tab === 'upscaling' || tab === 'workflows' || tab === 'canvas') {
return <GalleryPanelContent />;
}
return null;
});
RightPanelContent.displayName = 'RightPanelContent';
const LeftPanelContent = memo(() => {
const tab = useAppSelector(selectActiveTab);
if (tab === 'canvas') {
return <ParametersPanelTextToImage />;
}
if (tab === 'upscaling') {
return <ParametersPanelUpscale />;
}
if (tab === 'workflows') {
return <WorkflowsTabLeftPanel />;
}
return null;
return (
<Flex flexDir="column" w="full" h="full" gap={2}>
<QueueControls />
<Box position="relative" w="full" h="full">
{tab === 'canvas' && <ParametersPanelTextToImage />}
{tab === 'upscaling' && <ParametersPanelUpscale />}
{tab === 'workflows' && <WorkflowsTabLeftPanel />}
</Box>
</Flex>
);
});
LeftPanelContent.displayName = 'LeftPanelContent';
@@ -207,6 +189,6 @@ const MainPanelContent = memo(() => {
return <QueueTab />;
}
return null;
assert<Equals<never, typeof tab>>(false);
});
MainPanelContent.displayName = 'MainPanelContent';

View File

@@ -1,28 +0,0 @@
import { Flex, IconButton, Tooltip } from '@invoke-ai/ui-library';
import type { UsePanelReturn } from 'features/ui/hooks/usePanel';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiImagesSquareBold } from 'react-icons/pi';
type Props = {
panelApi: UsePanelReturn;
};
const FloatingGalleryButton = (props: Props) => {
const { t } = useTranslation();
return (
<Flex pos="absolute" transform="translate(0, -50%)" minW={8} top="50%" insetInlineEnd={2}>
<Tooltip label={t('accessibility.toggleRightPanel')} placement="start">
<IconButton
aria-label={t('accessibility.toggleRightPanel')}
onClick={props.panelApi.toggle}
icon={<PiImagesSquareBold />}
h={48}
/>
</Tooltip>
</Flex>
);
};
export default memo(FloatingGalleryButton);

View File

@@ -2,15 +2,14 @@ import { ButtonGroup, Flex, Icon, IconButton, spinAnimation, Tooltip, useShiftMo
import { useAppSelector } from 'app/store/storeHooks';
import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { selectCanvasSession } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
import { useClearQueueDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
import { InvokeButtonTooltip } from 'features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip';
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
import { useInvoke } from 'features/queue/hooks/useInvoke';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo, useMemo } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import {
PiCircleNotchBold,
@@ -23,60 +22,48 @@ import {
} from 'react-icons/pi';
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
type Props = {
togglePanel: () => void;
};
const FloatingSidePanelButtons = ({ togglePanel }: Props) => {
const { t } = useTranslation();
export const FloatingLeftPanelButtons = memo((props: { onToggle: () => void }) => {
const tab = useAppSelector(selectActiveTab);
const isCancelAndClearAllEnabled = useFeatureStatus('cancelAndClearAll');
const sessionType = useAppSelector(selectCanvasSessionType);
const session = useAppSelector(selectCanvasSession);
return (
<Flex pos="absolute" transform="translate(0, -50%)" top="50%" insetInlineStart={2} direction="column" gap={2}>
{tab === 'canvas' && sessionType === 'advanced' && (
{tab === 'canvas' && session?.type === 'advanced' && (
<CanvasManagerProviderGate>
<ToolChooser />
</CanvasManagerProviderGate>
)}
<ButtonGroup orientation="vertical" h={48}>
<Tooltip label={t('accessibility.toggleLeftPanel')} placement="end">
<IconButton
aria-label={t('accessibility.toggleLeftPanel')}
onClick={togglePanel}
icon={<PiSlidersHorizontalBold />}
flexGrow={1}
/>
</Tooltip>
<ToggleLeftPanelButton onToggle={props.onToggle} />
<InvokeIconButton />
<CancelCurrentIconButton />
{/* Show the cancel all except current button instead of cancel and clear all when it is disabled */}
{isCancelAndClearAllEnabled && <CancelAndClearAllIconButton />}
{!isCancelAndClearAllEnabled && <CancelAllExceptCurrentIconButton />}
<CancelAllExceptCurrentIconButton />
</ButtonGroup>
</Flex>
);
};
});
export default memo(FloatingSidePanelButtons);
FloatingLeftPanelButtons.displayName = 'FloatingLeftPanelButtons';
const ToggleLeftPanelButton = memo((props: { onToggle: () => void }) => {
const { t } = useTranslation();
return (
<Tooltip label={t('accessibility.toggleLeftPanel')} placement="end">
<IconButton
aria-label={t('accessibility.toggleLeftPanel')}
onClick={props.onToggle}
icon={<PiSlidersHorizontalBold />}
flexGrow={1}
/>
</Tooltip>
);
});
ToggleLeftPanelButton.displayName = 'ToggleLeftPanelButton';
const InvokeIconButton = memo(() => {
const { t } = useTranslation();
const queue = useInvoke();
const shift = useShiftModifier();
const { data: queueStatus } = useGetQueueStatusQuery();
const queueButtonIcon = useMemo(() => {
const isProcessing = (queueStatus?.queue.in_progress ?? 0) > 0;
if (!queue.isDisabled && isProcessing) {
return <Icon boxSize={6} as={PiCircleNotchBold} animation={spinAnimation} />;
}
if (shift) {
return <PiLightningFill />;
}
return <PiSparkleFill />;
}, [queue.isDisabled, queueStatus?.queue.in_progress, shift]);
return (
<InvokeButtonTooltip prepend={shift} placement="end">
@@ -85,7 +72,7 @@ const InvokeIconButton = memo(() => {
onClick={shift ? queue.enqueueFront : queue.enqueueBack}
isLoading={queue.isLoading}
isDisabled={queue.isDisabled}
icon={queueButtonIcon}
icon={<InvokeIconButtonIcon />}
colorScheme="invokeYellow"
flexGrow={1}
/>
@@ -94,6 +81,30 @@ const InvokeIconButton = memo(() => {
});
InvokeIconButton.displayName = 'InvokeIconButton';
const InvokeIconButtonIcon = memo(() => {
const shift = useShiftModifier();
const queue = useInvoke();
const { isProcessing } = useGetQueueStatusQuery(undefined, {
selectFromResult: ({ data }) => {
if (!data) {
return { isProcessing: false };
}
return { isProcessing: data.queue.in_progress > 0 };
},
});
if (!queue.isDisabled && isProcessing) {
return <Icon boxSize={6} as={PiCircleNotchBold} animation={spinAnimation} />;
}
if (shift) {
return <PiLightningFill />;
}
return <PiSparkleFill />;
});
InvokeIconButtonIcon.displayName = 'InvokeIconButtonIcon';
const CancelCurrentIconButton = memo(() => {
const { t } = useTranslation();
const cancelCurrentQueueItem = useCancelCurrentQueueItem();

View File

@@ -0,0 +1,28 @@
import { Flex, IconButton, Tooltip } from '@invoke-ai/ui-library';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiImagesSquareBold } from 'react-icons/pi';
export const FloatingRightPanelButtons = memo((props: { onToggle: () => void }) => {
return (
<Flex pos="absolute" transform="translate(0, -50%)" minW={8} top="50%" insetInlineEnd={2}>
<ToggleRightPanelButton onToggle={props.onToggle} />
</Flex>
);
});
FloatingRightPanelButtons.displayName = 'FloatingRightPanelButtons';
const ToggleRightPanelButton = memo((props: { onToggle: () => void }) => {
const { t } = useTranslation();
return (
<Tooltip label={t('accessibility.toggleRightPanel')} placement="start">
<IconButton
aria-label={t('accessibility.toggleRightPanel')}
onClick={props.onToggle}
icon={<PiImagesSquareBold />}
h={48}
/>
</Tooltip>
);
});
ToggleRightPanelButton.displayName = 'ToggleRightPanelButton';

View File

@@ -0,0 +1,25 @@
import { Box, Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import WorkflowsTabLeftPanel from 'features/nodes/components/sidePanel/WorkflowsTabLeftPanel';
import QueueControls from 'features/queue/components/QueueControls';
import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo } from 'react';
import ParametersPanelUpscale from './ParametersPanels/ParametersPanelUpscale';
const LeftPanelContent = memo(() => {
const tab = useAppSelector(selectActiveTab);
return (
<Flex flexDir="column" w="full" h="full" gap={2}>
<QueueControls />
<Box position="relative" w="full" h="full">
{tab === 'canvas' && <ParametersPanelTextToImage />}
{tab === 'upscaling' && <ParametersPanelUpscale />}
{tab === 'workflows' && <WorkflowsTabLeftPanel />}
</Box>
</Flex>
);
});
LeftPanelContent.displayName = 'LeftPanelContent';

View File

@@ -0,0 +1,33 @@
import { useAppSelector } from 'app/store/storeHooks';
import { CanvasMainPanelContent } from 'features/controlLayers/components/CanvasMainPanelContent';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
import ModelManagerTab from 'features/ui/components/tabs/ModelManagerTab';
import QueueTab from 'features/ui/components/tabs/QueueTab';
import { WorkflowsMainPanel } from 'features/ui/components/tabs/WorkflowsTabContent';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo } from 'react';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
const MainPanelContent = memo(() => {
const tab = useAppSelector(selectActiveTab);
if (tab === 'canvas') {
return <CanvasMainPanelContent />;
}
if (tab === 'upscaling') {
return <ImageViewer />;
}
if (tab === 'workflows') {
return <WorkflowsMainPanel />;
}
if (tab === 'models') {
return <ModelManagerTab />;
}
if (tab === 'queue') {
return <QueueTab />;
}
assert<Equals<never, typeof tab>>(false);
});
MainPanelContent.displayName = 'MainPanelContent';

View File

@@ -0,0 +1,94 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { useDisclosure } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectCanvasSession } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { BoardsListPanelContent } from 'features/gallery/components/BoardsListPanelContent';
import { Gallery } from 'features/gallery/components/Gallery';
import { GalleryTopBar } from 'features/gallery/components/GalleryTopBar';
import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors';
import { HorizontalResizeHandle } from 'features/ui/components/tabs/ResizeHandle';
import type { UsePanelOptions } from 'features/ui/hooks/usePanel';
import { usePanel } from 'features/ui/hooks/usePanel';
import { memo, useMemo, useRef } from 'react';
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
import { Panel, PanelGroup } from 'react-resizable-panels';
const FOCUS_REGION_STYLES: SystemStyleObject = {
width: 'full',
height: 'full',
position: 'relative',
flexDirection: 'column',
display: 'flex',
};
export const RightPanelContent = memo(() => {
const boardSearchText = useAppSelector(selectBoardSearchText);
const boardSearchDisclosure = useDisclosure({ defaultIsOpen: !!boardSearchText.length });
const imperativePanelGroupRef = useRef<ImperativePanelGroupHandle>(null);
const session = useAppSelector(selectCanvasSession);
const boardsListPanelOptions = useMemo<UsePanelOptions>(
() => ({
id: 'boards-list-panel',
minSizePx: 128,
defaultSizePx: 256,
imperativePanelGroupRef,
panelGroupDirection: 'vertical',
}),
[]
);
const boardsListPanel = usePanel(boardsListPanelOptions);
const galleryPanelOptions = useMemo<UsePanelOptions>(
() => ({
id: 'gallery-panel',
minSizePx: 128,
defaultSizePx: 256,
imperativePanelGroupRef,
panelGroupDirection: 'vertical',
}),
[]
);
const galleryPanel = usePanel(galleryPanelOptions);
const canvasLayersPanelOptions = useMemo<UsePanelOptions>(
() => ({
id: 'canvas-layers-panel',
minSizePx: 128,
defaultSizePx: 256,
imperativePanelGroupRef,
panelGroupDirection: 'vertical',
}),
[]
);
const canvasLayersPanel = usePanel(canvasLayersPanelOptions);
return (
<FocusRegionWrapper region="gallery" sx={FOCUS_REGION_STYLES}>
<GalleryTopBar boardsListPanel={boardsListPanel} boardSearchDisclosure={boardSearchDisclosure} />
<PanelGroup ref={imperativePanelGroupRef} direction="vertical" autoSaveId="boards-list-panel">
<Panel order={0} id="boards-panel" collapsible {...boardsListPanel.panelProps}>
<BoardsListPanelContent boardSearchDisclosure={boardSearchDisclosure} />
</Panel>
<HorizontalResizeHandle id="boards-list-to-gallery-panel-handle" {...boardsListPanel.resizeHandleProps} />
<Panel order={1} id="gallery-wrapper-panel" collapsible {...galleryPanel.panelProps}>
<Gallery />
</Panel>
{session?.type === 'advanced' && (
<>
<HorizontalResizeHandle id="gallery-panel-to-layers-handle" {...galleryPanel.resizeHandleProps} />
<Panel order={2} id="canvas-layers-panel" collapsible {...canvasLayersPanel.panelProps}>
<CanvasManagerProviderGate>
<CanvasLayersPanelContent />
</CanvasManagerProviderGate>
</Panel>
</>
)}
</PanelGroup>
</FocusRegionWrapper>
);
});
RightPanelContent.displayName = 'RightPanelContent';