This commit is contained in:
psychedelicious
2025-06-11 23:21:56 +10:00
parent d5c238e7c2
commit abaa33e22c
37 changed files with 1602 additions and 279 deletions

View File

@@ -67,6 +67,7 @@
"chakra-react-select": "^4.9.2",
"cmdk": "^1.1.1",
"compare-versions": "^6.1.1",
"dockview": "^4.3.1",
"filesize": "^10.1.6",
"fracturedjsonjs": "^4.1.0",
"framer-motion": "^11.10.0",

View File

@@ -50,6 +50,9 @@ dependencies:
compare-versions:
specifier: ^6.1.1
version: 6.1.1
dockview:
specifier: ^4.3.1
version: 4.3.1(react@18.3.1)
filesize:
specifier: ^10.1.6
version: 10.1.6
@@ -4492,6 +4495,19 @@ packages:
resolution: {integrity: sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==}
dev: false
/dockview-core@4.3.1:
resolution: {integrity: sha512-cjGIXKc1wtHHkeKisuDLNt3HSHCVzvabxm1K9Auna27A9T3QR7ISOiTJyEUKUPllkcztFYBut0vwnnvwLnPAuQ==}
dev: false
/dockview@4.3.1(react@18.3.1):
resolution: {integrity: sha512-D4SvZPs1GJxGUBPkrehlKNGsWlSDaBiPuSYI+IEXnZ7b2bCUs1/h954sVs7xyykqEW3r6TkPKLWdTR/47Q7/QQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
dependencies:
dockview-core: 4.3.1
react: 18.3.1
dev: false
/doctrine@2.1.0:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}

View File

@@ -17,6 +17,7 @@ const Loading = () => {
right={0}
bottom={0}
left={0}
zIndex={99999}
>
<Image src={InvokeLogoWhite} w="8rem" h="8rem" />
<Spinner

View File

@@ -1,5 +1,18 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { ContextMenu, Divider, Flex, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
import {
ContextMenu,
Divider,
Flex,
IconButton,
Menu,
MenuButton,
MenuList,
Tab,
TabList,
TabPanel,
TabPanels,
Tabs,
} from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
import { CanvasAlertsInvocationProgress } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress';
@@ -13,12 +26,15 @@ import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD';
import { InvokeCanvasComponent } from 'features/controlLayers/components/InvokeCanvasComponent';
import { SelectObject } from 'features/controlLayers/components/SelectObject/SelectObject';
import { CanvasSessionContextProvider } from 'features/controlLayers/components/SimpleSession/context';
import { InitialState } from 'features/controlLayers/components/SimpleSession/InitialState';
import { StagingAreaItemsList } from 'features/controlLayers/components/SimpleSession/StagingAreaItemsList';
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
import { Transform } from 'features/controlLayers/components/Transform/Transform';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar';
import { memo, useCallback } from 'react';
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
@@ -60,87 +76,107 @@ export const AdvancedSession = memo(({ id }: { id: string | null }) => {
}, []);
return (
<FocusRegionWrapper region="canvas" sx={FOCUS_REGION_STYLES}>
<Flex
tabIndex={-1}
borderRadius="base"
position="relative"
flexDirection="column"
height="full"
width="full"
gap={2}
alignItems="center"
justifyContent="center"
overflow="hidden"
>
<CanvasManagerProviderGate>
<CanvasToolbar />
</CanvasManagerProviderGate>
<Divider />
<ContextMenu<HTMLDivElement> renderMenu={renderMenu} withLongPress={false}>
{(ref) => (
<Flex ref={ref} sx={canvasBgSx} data-dynamic-grid={dynamicGrid}>
<InvokeCanvasComponent />
<Tabs w="full" h="full">
<TabList>
<Tab>Welcome</Tab>
<Tab>Workspace</Tab>
<Tab>Viewer</Tab>
</TabList>
<TabPanels w="full" h="full">
<TabPanel w="full" h="full" justifyContent="center">
<InitialState />
</TabPanel>
<TabPanel w="full" h="full">
<FocusRegionWrapper region="canvas" sx={FOCUS_REGION_STYLES}>
<Flex
tabIndex={-1}
borderRadius="base"
position="relative"
flexDirection="column"
height="full"
width="full"
gap={2}
alignItems="center"
justifyContent="center"
overflow="hidden"
>
<CanvasManagerProviderGate>
<Flex
position="absolute"
flexDir="column"
top={1}
insetInlineStart={1}
pointerEvents="none"
gap={2}
alignItems="flex-start"
>
{showHUD && <CanvasHUD />}
<CanvasAlertsSelectedEntityStatus />
<CanvasAlertsPreserveMask />
<CanvasAlertsInvocationProgress />
</Flex>
<Flex position="absolute" top={1} insetInlineEnd={1}>
<Menu>
<MenuButton as={IconButton} icon={<PiDotsThreeOutlineVerticalFill />} colorScheme="base" />
<MenuContent />
</Menu>
</Flex>
<CanvasToolbar />
</CanvasManagerProviderGate>
<Divider />
<ContextMenu<HTMLDivElement> renderMenu={renderMenu} withLongPress={false}>
{(ref) => (
<Flex ref={ref} sx={canvasBgSx} data-dynamic-grid={dynamicGrid}>
<InvokeCanvasComponent />
<CanvasManagerProviderGate>
<Flex
position="absolute"
flexDir="column"
top={1}
insetInlineStart={1}
pointerEvents="none"
gap={2}
alignItems="flex-start"
>
{showHUD && <CanvasHUD />}
<CanvasAlertsSelectedEntityStatus />
<CanvasAlertsPreserveMask />
<CanvasAlertsInvocationProgress />
</Flex>
<Flex position="absolute" top={1} insetInlineEnd={1}>
<Menu>
<MenuButton as={IconButton} icon={<PiDotsThreeOutlineVerticalFill />} colorScheme="base" />
<MenuContent />
</Menu>
</Flex>
</CanvasManagerProviderGate>
</Flex>
)}
</ContextMenu>
{id !== null && (
<CanvasManagerProviderGate>
<CanvasSessionContextProvider type="advanced" id={id}>
<Flex
position="absolute"
flexDir="column"
bottom={4}
gap={2}
align="center"
justify="center"
left={4}
right={4}
>
<Flex position="relative" maxW="full" w="full" h={108}>
<StagingAreaItemsList />
</Flex>
<Flex gap={2}>
<StagingAreaToolbar />
</Flex>
</Flex>
</CanvasSessionContextProvider>
</CanvasManagerProviderGate>
)}
<Flex position="absolute" bottom={4}>
<CanvasManagerProviderGate>
<Filter />
<Transform />
<SelectObject />
</CanvasManagerProviderGate>
</Flex>
<CanvasManagerProviderGate>
<CanvasDropArea />
</CanvasManagerProviderGate>
</Flex>
)}
</ContextMenu>
{id !== null && (
<CanvasManagerProviderGate>
<CanvasSessionContextProvider type="advanced" id={id}>
<Flex
position="absolute"
flexDir="column"
bottom={4}
gap={2}
align="center"
justify="center"
left={4}
right={4}
>
<Flex position="relative" maxW="full" w="full" h={108}>
<StagingAreaItemsList />
</Flex>
<Flex gap={2}>
<StagingAreaToolbar />
</Flex>
</Flex>
</CanvasSessionContextProvider>
</CanvasManagerProviderGate>
)}
<Flex position="absolute" bottom={4}>
<CanvasManagerProviderGate>
<Filter />
<Transform />
<SelectObject />
</CanvasManagerProviderGate>
</Flex>
<CanvasManagerProviderGate>
<CanvasDropArea />
</CanvasManagerProviderGate>
</Flex>
</FocusRegionWrapper>
</FocusRegionWrapper>
</TabPanel>
<TabPanel w="full" h="full">
<Flex flexDir="column" w="full" h="full">
<ViewerToolbar />
<ImageViewer />
</Flex>
</TabPanel>
</TabPanels>
</Tabs>
);
});
AdvancedSession.displayName = 'AdvancedSession';

View File

@@ -1,4 +1,4 @@
import { Alert, Button, Divider, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library';
import { Alert, Button, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { InitialStateAddAStyleReference } from 'features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference';
import { InitialStateGenerateFromText } from 'features/controlLayers/components/SimpleSession/InitialStateGenerateFromText';
@@ -14,11 +14,7 @@ export const InitialState = memo(() => {
return (
<Flex flexDir="column" h="full" w="full" alignItems="center" justifyContent="center" gap={2}>
<Flex px={2} w="full" alignItems="center" minH="24px" justifyContent="flex-start" flexShrink={0}>
<Heading size="sm">Get Started</Heading>
</Flex>
<Divider />
<Flex flexDir="column" w="full" h="full" justifyContent="center" gap={4} mx={16} maxW={768}>
<Flex flexDir="column" w="full" h="full" justifyContent="center" gap={4} px={12} maxW={768}>
<Heading mb={4}>Get started with Invoke.</Heading>
<Flex flexDir="column" gap={8}>
<Grid gridTemplateColumns="1fr 1fr" gap={8}>

View File

@@ -1,16 +1,45 @@
import { CanvasSessionContextProvider } from 'features/controlLayers/components/SimpleSession/context';
import { Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InitialState } from 'features/controlLayers/components/SimpleSession/InitialState';
import { StagingArea } from 'features/controlLayers/components/SimpleSession/StagingArea';
import { memo } from 'react';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer2';
import { ProgressImage } from 'features/gallery/components/ImageViewer/ProgressImage2';
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2';
import { selectShowGenerateTabSplashScreen } from 'features/ui/store/uiSelectors';
import { showGenerateTabSplashScreenChanged } from 'features/ui/store/uiSlice';
import { memo, useCallback } from 'react';
export const SimpleSession = memo(() => {
const showGenerateTabSplashScreen = useAppSelector(selectShowGenerateTabSplashScreen);
const dispatch = useAppDispatch();
const showSplashScreen = useCallback(() => {
dispatch(showGenerateTabSplashScreenChanged(true));
}, [dispatch]);
export const SimpleSession = memo(({ id }: { id: string | null }) => {
if (id === null) {
return <InitialState />;
}
return (
<CanvasSessionContextProvider type="simple" id={id}>
<StagingArea />
</CanvasSessionContextProvider>
<Tabs w="full" h="full" px={2}>
<TabList>
<Tab>Launchpad</Tab>
<Tab>Viewer</Tab>
<Tab>Generation Progress</Tab>
</TabList>
<TabPanels w="full" h="full">
<TabPanel w="full" h="full" justifyContent="center">
<InitialState />
</TabPanel>
<TabPanel w="full" h="full">
<Flex flexDir="column" w="full" h="full">
<ViewerToolbar />
<ImageViewer />
</Flex>
</TabPanel>
<TabPanel w="full" h="full">
<Flex flexDir="column" w="full" h="full" overflow="hidden" p={2}>
<ProgressImage />
</Flex>
</TabPanel>
</TabPanels>
</Tabs>
);
});
SimpleSession.displayName = 'SimpleSession';

View File

@@ -0,0 +1,15 @@
import { Divider, Flex } from '@invoke-ai/ui-library';
import { StagingAreaHeader } from 'features/controlLayers/components/SimpleSession/StagingAreaHeader';
import { StagingAreaNoItems } from 'features/controlLayers/components/SimpleSession/StagingAreaNoItems';
import { memo } from 'react';
export const SimpleSessionNoId = memo(() => {
return (
<Flex flexDir="column" gap={2} w="full" h="full" minW={0} minH={0}>
<StagingAreaHeader />
<Divider />
<StagingAreaNoItems />
</Flex>
);
});
SimpleSessionNoId.displayName = 'StSimpleSessionNoIdagingArea';

View File

@@ -1,5 +1,6 @@
import { IconButton, Input, InputGroup, InputRightElement } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { buildUseDisclosure } from 'common/hooks/useBoolean';
import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors';
import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
import type { ChangeEvent, KeyboardEvent } from 'react';
@@ -7,6 +8,8 @@ import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
export const [useBoardSearchDisclosure, $boardSearchIsOpen] = buildUseDisclosure(false);
export const BoardsSearch = memo(() => {
const dispatch = useAppDispatch();
const boardSearchText = useAppSelector(selectBoardSearchText);

View File

@@ -1,25 +1,26 @@
import type { UseDisclosureReturn } from '@invoke-ai/ui-library';
import { Box, Collapse, Divider, Flex } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { BoardsListWrapper } from 'features/gallery/components/Boards/BoardsList/BoardsListWrapper';
import { BoardsSearch } from 'features/gallery/components/Boards/BoardsList/BoardsSearch';
import { $boardSearchIsOpen, BoardsSearch } from 'features/gallery/components/Boards/BoardsList/BoardsSearch';
import { GalleryTopBar } from 'features/gallery/components/GalleryTopBar';
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>
);
}
);
export const BoardsListPanelContent = memo(() => {
const boardSearchDisclosure = useStore($boardSearchIsOpen);
return (
<Flex flexDir="column" w="full" h="full" p={2}>
<GalleryTopBar />
<Collapse in={boardSearchDisclosure} style={COLLAPSE_STYLES}>
<Box w="full" pt={2}>
<BoardsSearch />
</Box>
</Collapse>
<Divider pt={2} />
<BoardsListWrapper />
</Flex>
);
});
BoardsListPanelContent.displayName = 'BoardsListPanelContent';

View File

@@ -70,7 +70,7 @@ export const Gallery = memo(() => {
const boardName = useBoardName(selectedBoardId);
return (
<Flex flexDirection="column" alignItems="center" justifyContent="space-between" h="full" w="full" pt={1} minH={0}>
<Flex flexDirection="column" alignItems="center" justifyContent="space-between" h="full" w="full" p={2} minH={0}>
<Tabs index={galleryView === 'images' ? 0 : 1} variant="enclosed" display="flex" flexDir="column" w="full">
<TabList gap={2} fontSize="sm" borderColor="base.800" alignItems="center" w="full">
<Text fontSize="sm" fontWeight="semibold" noOfLines={1} px="2" wordBreak="break-all">

View File

@@ -1,67 +1,67 @@
import type { UseDisclosureReturn } from '@invoke-ai/ui-library';
import { Button, Flex, IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useBoardSearchDisclosure } from 'features/gallery/components/Boards/BoardsList/BoardsSearch';
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 { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context';
import { useCollapsibleGridviewPanel } from 'features/ui/layouts/use-collapsible-gridview-panel';
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);
export const GalleryTopBar = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const boardSearchText = useAppSelector(selectBoardSearchText);
const boardSearchDisclosure = useBoardSearchDisclosure();
const api = useAutoLayoutContext();
const boardsPanel = useCollapsibleGridviewPanel(api, 'boards', 'vertical', 256);
const isBoardsPanelCollapsed = useStore(boardsPanel.$isCollapsed);
const onClickBoardSearch = useCallback(() => {
if (boardSearchText.length) {
dispatch(boardSearchTextChanged(''));
}
boardSearchDisclosure.onToggle();
boardsListPanel.expand();
}, [boardSearchText.length, boardSearchDisclosure, boardsListPanel, dispatch]);
const onClickBoardSearch = useCallback(() => {
if (boardSearchText.length) {
dispatch(boardSearchTextChanged(''));
}
if (!boardSearchDisclosure.isOpen && boardsPanel.$isCollapsed.get()) {
boardsPanel.expand();
}
boardSearchDisclosure.toggle();
}, [boardSearchText.length, boardSearchDisclosure, dispatch, boardsPanel]);
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>
return (
<Flex alignItems="center" justifyContent="space-between" w="full">
<Flex flexGrow={1} flexBasis={0}>
<Button
size="sm"
variant="ghost"
onClick={boardsPanel.toggle}
rightIcon={isBoardsPanelCollapsed ? <PiCaretDownBold /> : <PiCaretUpBold />}
>
{isBoardsPanelCollapsed ? 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,5 +1,4 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { selectSearchTerm } from 'features/gallery/store/gallerySelectors';
import { searchTermChanged } from 'features/gallery/store/gallerySlice';
import { debounce } from 'lodash-es';
@@ -7,7 +6,7 @@ import { useCallback, useMemo, useState } from 'react';
export const useGallerySearchTerm = () => {
// Highlander!
useAssertSingleton('gallery-search-state');
// useAssertSingleton('gallery-search-state');
const dispatch = useAppDispatch();
const searchTerm = useAppSelector(selectSearchTerm);

View File

@@ -0,0 +1,110 @@
import { Box, Flex } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
import { CanvasAlertsInvocationProgress } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress';
import { DndImage } from 'features/dnd/DndImage';
import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer';
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
import { selectShouldShowImageDetails, selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
import type { AnimationProps } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import { memo, useCallback, useRef, useState } from 'react';
import type { ImageDTO } from 'services/api/types';
import { $hasLastProgressImage } from 'services/events/stores';
import { NoContentForViewer } from './NoContentForViewer';
export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO?: ImageDTO }) => {
const shouldShowImageDetails = useAppSelector(selectShouldShowImageDetails);
// Show and hide the next/prev buttons on mouse move
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] = useState<boolean>(false);
const timeoutId = useRef(0);
const onMouseOver = useCallback(() => {
setShouldShowNextPrevButtons(true);
window.clearTimeout(timeoutId.current);
}, []);
const onMouseOut = useCallback(() => {
timeoutId.current = window.setTimeout(() => {
setShouldShowNextPrevButtons(false);
}, 500);
}, []);
return (
<Flex
onMouseOver={onMouseOver}
onMouseOut={onMouseOut}
width="full"
height="full"
alignItems="center"
justifyContent="center"
position="relative"
>
<ImageContent imageDTO={imageDTO} />
<Flex
flexDir="column"
gap={2}
position="absolute"
top={0}
insetInlineStart={0}
pointerEvents="none"
alignItems="flex-start"
>
<CanvasAlertsInvocationProgress />
</Flex>
{shouldShowImageDetails && imageDTO && (
<Box position="absolute" opacity={0.8} top={0} width="full" height="full" borderRadius="base">
<ImageMetadataViewer image={imageDTO} />
</Box>
)}
<AnimatePresence>
{shouldShowNextPrevButtons && imageDTO && (
<Box
as={motion.div}
key="nextPrevButtons"
initial={initial}
animate={animateArrows}
exit={exit}
position="absolute"
top={0}
right={0}
bottom={0}
left={0}
pointerEvents="none"
>
<NextPrevImageButtons />
</Box>
)}
</AnimatePresence>
</Flex>
);
});
CurrentImagePreview.displayName = 'CurrentImagePreview';
const ImageContent = memo(({ imageDTO }: { imageDTO?: ImageDTO }) => {
const hasProgressImage = useStore($hasLastProgressImage);
const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer);
if (!imageDTO) {
return <NoContentForViewer />;
}
return (
<Flex w="full" h="full" position="absolute" alignItems="center" justifyContent="center">
<DndImage imageDTO={imageDTO} />
</Flex>
);
});
ImageContent.displayName = 'ImageContent';
const initial: AnimationProps['initial'] = {
opacity: 0,
};
const animateArrows: AnimationProps['animate'] = {
opacity: 1,
transition: { duration: 0.07 },
};
const exit: AnimationProps['exit'] = {
opacity: 0,
transition: { duration: 0.07 },
};

View File

@@ -0,0 +1,125 @@
import { Box, Flex, IconButton, type SystemStyleObject, useOutsideClick } from '@invoke-ai/ui-library';
import { skipToken } from '@reduxjs/toolkit/query';
import { useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { selectImageToCompare } from 'features/gallery/components/ImageViewer/common';
import { CurrentImagePreview } from 'features/gallery/components/ImageViewer/CurrentImagePreview2';
import { ImageComparison } from 'features/gallery/components/ImageViewer/ImageComparison';
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2';
import { selectLastSelectedImageName } from 'features/gallery/store/gallerySelectors';
import { memo, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import { useImageViewer } from './useImageViewer';
// type Props = {
// closeButton?: ReactNode;
// };
// const useFocusRegionOptions = {
// focusOnMount: true,
// };
// const FOCUS_REGION_STYLES: SystemStyleObject = {
// display: 'flex',
// width: 'full',
// height: 'full',
// position: 'absolute',
// flexDirection: 'column',
// inset: 0,
// alignItems: 'center',
// justifyContent: 'center',
// overflow: 'hidden',
// };
export const ImageViewer = memo(() => {
const lastSelectedImageName = useAppSelector(selectLastSelectedImageName);
const { data: lastSelectedImageDTO } = useGetImageDTOQuery(lastSelectedImageName ?? skipToken);
const comparisonImageDTO = useAppSelector(selectImageToCompare);
if (lastSelectedImageDTO && comparisonImageDTO) {
return <ImageComparison firstImage={lastSelectedImageDTO} secondImage={comparisonImageDTO} />;
}
return <CurrentImagePreview imageDTO={lastSelectedImageDTO} />;
});
ImageViewer.displayName = 'ImageViewer';
const imageViewerContainerSx: SystemStyleObject = {
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
transition: 'opacity 0.15s ease',
opacity: 1,
pointerEvents: 'auto',
'&[data-hidden="true"]': {
opacity: 0,
pointerEvents: 'none',
},
backdropFilter: 'blur(10px) brightness(70%)',
};
export const ImageViewerModal = memo(() => {
const ref = useRef<HTMLDivElement>(null);
const imageViewer = useImageViewer();
useOutsideClick({
ref,
handler: imageViewer.close,
});
useHotkeys(
'esc',
imageViewer.close,
{
preventDefault: true,
enabled: imageViewer.isOpen,
},
[imageViewer.isOpen]
);
return (
<Box sx={imageViewerContainerSx} data-hidden={!imageViewer.isOpen}>
<Flex
ref={ref}
flexDir="column"
position="absolute"
bg="base.900"
borderRadius="base"
top={16}
right={16}
bottom={16}
left={16}
>
<ViewerToolbar />
<ImageViewer />
</Flex>
</Box>
);
});
ImageViewerModal.displayName = 'GatedImageViewer';
const ImageViewerCloseButton = memo(() => {
const { t } = useTranslation();
const imageViewer = useImageViewer();
useAssertSingleton('ImageViewerCloseButton');
useHotkeys('esc', imageViewer.close);
return (
<IconButton
tooltip={t('gallery.closeViewer')}
aria-label={t('gallery.closeViewer')}
icon={<PiXBold />}
variant="link"
alignSelf="stretch"
onClick={imageViewer.close}
/>
);
});
ImageViewerCloseButton.displayName = 'ImageViewerCloseButton';

View File

@@ -0,0 +1,56 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex, Image } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { selectSystemSlice } from 'features/system/store/systemSlice';
import { memo, useMemo } from 'react';
import { PiPulseBold } from 'react-icons/pi';
import { $lastProgressImage } from 'services/events/stores';
const selectShouldAntialiasProgressImage = createSelector(
selectSystemSlice,
(system) => system.shouldAntialiasProgressImage
);
export const ProgressImage = memo(() => {
const progressImage = useStore($lastProgressImage);
const shouldAntialiasProgressImage = useAppSelector(selectShouldAntialiasProgressImage);
const sx = useMemo<SystemStyleObject>(
() => ({
imageRendering: shouldAntialiasProgressImage ? 'auto' : 'pixelated',
}),
[shouldAntialiasProgressImage]
);
if (!progressImage) {
return (
<Flex width="full" height="full" alignItems="center" justifyContent="center">
<IAINoContentFallback icon={PiPulseBold} label="No Generation in Progress" />
</Flex>
);
}
return (
<Flex width="full" height="full" alignItems="center" justifyContent="center" minW={0} minH={0}>
<Image
src={progressImage.dataURL}
width={progressImage.width}
height={progressImage.height}
draggable={false}
data-testid="progress-image"
objectFit="contain"
maxWidth="full"
maxHeight="full"
borderRadius="base"
sx={sx}
minH={0}
minW={0}
/>
</Flex>
);
});
ProgressImage.displayName = 'ProgressImage';

View File

@@ -0,0 +1,18 @@
import { ButtonGroup, Flex } from '@invoke-ai/ui-library';
import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton';
import { memo } from 'react';
import CurrentImageButtons from './CurrentImageButtons';
export const ViewerToolbar = memo(() => {
return (
<Flex w="full" justifyContent="center" h="24px">
<ButtonGroup>
<ToggleMetadataViewerButton />
<CurrentImageButtons />
</ButtonGroup>
</Flex>
);
});
ViewerToolbar.displayName = 'ViewerToolbar';

View File

@@ -1,6 +1,5 @@
import { useAppSelector } from 'app/store/storeHooks';
import { useIsRegionFocused } from 'common/hooks/focus';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state';
import { useGalleryNavigation } from 'features/gallery/hooks/useGalleryNavigation';
import { useGalleryPagination } from 'features/gallery/hooks/useGalleryPagination';
@@ -14,7 +13,7 @@ import { useListImagesQuery } from 'services/api/endpoints/images';
* Registers gallery hotkeys. This hook is a singleton.
*/
export const useGalleryHotkeys = () => {
useAssertSingleton('useGalleryHotkeys');
// useAssertSingleton('useGalleryHotkeys');
const { goNext, goPrev, isNextEnabled, isPrevEnabled } = useGalleryPagination();
const selection = useAppSelector((s) => s.gallery.selection);
const queryArgs = useAppSelector(selectListImagesQueryArgs);

View File

@@ -1,6 +1,7 @@
import { useStore } from '@nanostores/react';
import { adHocPostProcessingRequested } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAppStore } from 'app/store/nanostores/store';
import { useAppSelector } from 'app/store/storeHooks';
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state';
import {
@@ -24,11 +25,10 @@ import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
import type { ImageDTO } from 'services/api/types';
export const useImageActions = (imageDTO: ImageDTO) => {
const dispatch = useAppDispatch();
const { dispatch, getState } = useAppStore();
const { t } = useTranslation();
const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId);
const isStaging = useAppSelector(selectIsStaging);
const activeTabName = useAppSelector(selectActiveTab);
const { metadata } = useDebouncedMetadata(imageDTO.image_name);
const [hasMetadata, setHasMetadata] = useState(false);
const [hasSeed, setHasSeed] = useState(false);
@@ -82,18 +82,20 @@ export const useImageActions = (imageDTO: ImageDTO) => {
if (!metadata) {
return;
}
const activeTabName = selectActiveTab(getState());
parseAndRecallAllMetadata(metadata, activeTabName === 'canvas', isStaging ? ['width', 'height'] : []);
clearStylePreset();
}, [metadata, activeTabName, isStaging, clearStylePreset]);
}, [metadata, getState, isStaging, clearStylePreset]);
const remix = useCallback(() => {
if (!metadata) {
return;
}
const activeTabName = selectActiveTab(getState());
// Recalls all metadata parameters except seed
parseAndRecallAllMetadata(metadata, activeTabName === 'canvas', ['seed']);
clearStylePreset();
}, [activeTabName, metadata, clearStylePreset]);
}, [metadata, getState, clearStylePreset]);
const recallSeed = useCallback(() => {
if (!metadata) {

View File

@@ -12,12 +12,10 @@ import ParamClipSkip from 'features/parameters/components/Advanced/ParamClipSkip
import ParamT5EncoderModelSelect from 'features/parameters/components/Advanced/ParamT5EncoderModelSelect';
import ParamSeamlessXAxis from 'features/parameters/components/Seamless/ParamSeamlessXAxis';
import ParamSeamlessYAxis from 'features/parameters/components/Seamless/ParamSeamlessYAxis';
import { ParamSeed } from 'features/parameters/components/Seed/ParamSeed';
import ParamFLUXVAEModelSelect from 'features/parameters/components/VAEModel/ParamFLUXVAEModelSelect';
import ParamVAEModelSelect from 'features/parameters/components/VAEModel/ParamVAEModelSelect';
import ParamVAEPrecision from 'features/parameters/components/VAEModel/ParamVAEPrecision';
import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetModelConfigQuery } from 'services/api/endpoints/models';
@@ -33,7 +31,6 @@ const formLabelProps2: FormLabelProps = {
export const AdvancedSettingsAccordion = memo(() => {
const vaeKey = useAppSelector(selectVAEKey);
const { currentData: vaeConfig } = useGetModelConfigQuery(vaeKey ?? skipToken);
const activeTabName = useAppSelector(selectActiveTab);
const isFLUX = useAppSelector(selectIsFLUX);
const isSD3 = useAppSelector(selectIsSD3);
@@ -68,19 +65,16 @@ export const AdvancedSettingsAccordion = memo(() => {
if (params.seamlessXAxis || params.seamlessYAxis) {
badges.push('seamless');
}
if (activeTabName === 'upscaling' && !params.shouldRandomizeSeed) {
badges.push('Manual Seed');
}
}
return badges;
}),
[vaeConfig, activeTabName]
[vaeConfig]
);
const badges = useAppSelector(selectBadges);
const { t } = useTranslation();
const { isOpen, onToggle } = useStandaloneAccordionToggle({
id: `'advanced-settings-${activeTabName}`,
id: `'advanced-settings-generate`,
defaultIsOpen: false,
});
@@ -91,39 +85,33 @@ export const AdvancedSettingsAccordion = memo(() => {
{isFLUX ? <ParamFLUXVAEModelSelect /> : <ParamVAEModelSelect />}
{!isFLUX && !isSD3 && <ParamVAEPrecision />}
</Flex>
{activeTabName === 'upscaling' ? (
<ParamSeed />
) : (
{!isFLUX && !isSD3 && (
<>
{!isFLUX && !isSD3 && (
<>
<FormControlGroup formLabelProps={formLabelProps}>
<ParamClipSkip />
<ParamCFGRescaleMultiplier />
</FormControlGroup>
<Flex gap={4} w="full">
<FormControlGroup formLabelProps={formLabelProps2}>
<ParamSeamlessXAxis />
<ParamSeamlessYAxis />
</FormControlGroup>
</Flex>
</>
)}
{isFLUX && (
<FormControlGroup>
<ParamT5EncoderModelSelect />
<ParamCLIPEmbedModelSelect />
<FormControlGroup formLabelProps={formLabelProps}>
<ParamClipSkip />
<ParamCFGRescaleMultiplier />
</FormControlGroup>
<Flex gap={4} w="full">
<FormControlGroup formLabelProps={formLabelProps2}>
<ParamSeamlessXAxis />
<ParamSeamlessYAxis />
</FormControlGroup>
)}
{isSD3 && (
<FormControlGroup>
<ParamT5EncoderModelSelect />
<ParamCLIPLEmbedModelSelect />
<ParamCLIPGEmbedModelSelect />
</FormControlGroup>
)}
</Flex>
</>
)}
{isFLUX && (
<FormControlGroup>
<ParamT5EncoderModelSelect />
<ParamCLIPEmbedModelSelect />
</FormControlGroup>
)}
{isSD3 && (
<FormControlGroup>
<ParamT5EncoderModelSelect />
<ParamCLIPLEmbedModelSelect />
<ParamCLIPGEmbedModelSelect />
</FormControlGroup>
)}
</Flex>
</StandaloneAccordion>
);

View File

@@ -0,0 +1,90 @@
import type { FormLabelProps } from '@invoke-ai/ui-library';
import { Flex, StandaloneAccordion } from '@invoke-ai/ui-library';
import { skipToken } from '@reduxjs/toolkit/query';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectIsFLUX, selectIsSD3, selectParamsSlice, selectVAEKey } from 'features/controlLayers/store/paramsSlice';
import { ParamSeed } from 'features/parameters/components/Seed/ParamSeed';
import ParamFLUXVAEModelSelect from 'features/parameters/components/VAEModel/ParamFLUXVAEModelSelect';
import ParamVAEModelSelect from 'features/parameters/components/VAEModel/ParamVAEModelSelect';
import ParamVAEPrecision from 'features/parameters/components/VAEModel/ParamVAEPrecision';
import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetModelConfigQuery } from 'services/api/endpoints/models';
const formLabelProps: FormLabelProps = {
minW: '9.2rem',
};
const formLabelProps2: FormLabelProps = {
flexGrow: 1,
};
export const AdvancedSettingsAccordion = memo(() => {
const vaeKey = useAppSelector(selectVAEKey);
const { currentData: vaeConfig } = useGetModelConfigQuery(vaeKey ?? skipToken);
const isFLUX = useAppSelector(selectIsFLUX);
const isSD3 = useAppSelector(selectIsSD3);
const selectBadges = useMemo(
() =>
createMemoizedSelector([selectParamsSlice, selectIsFLUX], (params, isFLUX) => {
const badges: (string | number)[] = [];
if (isFLUX) {
if (vaeConfig) {
let vaeBadge = vaeConfig.name;
if (params.vaePrecision === 'fp16') {
vaeBadge += ` ${params.vaePrecision}`;
}
badges.push(vaeBadge);
}
} else {
if (vaeConfig) {
let vaeBadge = vaeConfig.name;
if (params.vaePrecision === 'fp16') {
vaeBadge += ` ${params.vaePrecision}`;
}
badges.push(vaeBadge);
} else if (params.vaePrecision === 'fp16') {
badges.push(`VAE ${params.vaePrecision}`);
}
if (params.clipSkip) {
badges.push(`Skip ${params.clipSkip}`);
}
if (params.cfgRescaleMultiplier) {
badges.push(`Rescale ${params.cfgRescaleMultiplier}`);
}
if (params.seamlessXAxis || params.seamlessYAxis) {
badges.push('seamless');
}
if (!params.shouldRandomizeSeed) {
badges.push('Manual Seed');
}
}
return badges;
}),
[vaeConfig]
);
const badges = useAppSelector(selectBadges);
const { t } = useTranslation();
const { isOpen, onToggle } = useStandaloneAccordionToggle({
id: `'advanced-settings-upscaling`,
defaultIsOpen: false,
});
return (
<StandaloneAccordion label={t('accordions.advanced.title')} badges={badges} isOpen={isOpen} onToggle={onToggle}>
<Flex gap={4} alignItems="center" p={4} flexDir="column" data-testid="advanced-settings-accordion">
<Flex gap={4} w="full">
{isFLUX ? <ParamFLUXVAEModelSelect /> : <ParamVAEModelSelect />}
{!isFLUX && !isSD3 && <ParamVAEPrecision />}
</Flex>
<ParamSeed />
</Flex>
</StandaloneAccordion>
);
});
AdvancedSettingsAccordion.displayName = 'AdvancedSettingsAccordion';

View File

@@ -12,14 +12,11 @@ import ParamGuidance from 'features/parameters/components/Core/ParamGuidance';
import ParamScheduler from 'features/parameters/components/Core/ParamScheduler';
import ParamSteps from 'features/parameters/components/Core/ParamSteps';
import { DisabledModelWarning } from 'features/parameters/components/MainModel/DisabledModelWarning';
import ParamUpscaleCFGScale from 'features/parameters/components/Upscale/ParamUpscaleCFGScale';
import ParamUpscaleScheduler from 'features/parameters/components/Upscale/ParamUpscaleScheduler';
import { useIsApiModel } from 'features/parameters/hooks/useIsApiModel';
import { API_BASE_MODELS } from 'features/parameters/types/constants';
import { MainModelPicker } from 'features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker';
import { useExpanderToggle } from 'features/settingsAccordions/hooks/useExpanderToggle';
import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelectedModelConfig } from 'services/api/hooks/useSelectedModelConfig';
@@ -32,16 +29,12 @@ const formLabelProps: FormLabelProps = {
export const GenerationSettingsAccordion = memo(() => {
const { t } = useTranslation();
const modelConfig = useSelectedModelConfig();
const activeTabName = useAppSelector(selectActiveTab);
const isFLUX = useAppSelector(selectIsFLUX);
const isSD3 = useAppSelector(selectIsSD3);
const isCogView4 = useAppSelector(selectIsCogView4);
const isApiModel = useIsApiModel();
const isUpscaling = useMemo(() => {
return activeTabName === 'upscaling';
}, [activeTabName]);
const selectBadges = useMemo(
() =>
createMemoizedSelector(selectLoRAsSlice, (loras) => {
@@ -63,8 +56,8 @@ export const GenerationSettingsAccordion = memo(() => {
defaultIsOpen: false,
});
const { isOpen: isOpenAccordion, onToggle: onToggleAccordion } = useStandaloneAccordionToggle({
id: `generation-settings-${activeTabName}`,
defaultIsOpen: activeTabName !== 'upscaling',
id: `generation-settings-generate`,
defaultIsOpen: true,
});
return (
@@ -85,12 +78,10 @@ export const GenerationSettingsAccordion = memo(() => {
<Expander label={t('accordions.advanced.options')} isOpen={isOpenExpander} onToggle={onToggleExpander}>
<Flex gap={4} flexDir="column" pb={4}>
<FormControlGroup formLabelProps={formLabelProps}>
{!isFLUX && !isSD3 && !isCogView4 && !isUpscaling && <ParamScheduler />}
{isUpscaling && <ParamUpscaleScheduler />}
{!isFLUX && !isSD3 && !isCogView4 && <ParamScheduler />}
<ParamSteps />
{isFLUX && modelConfig && !isFluxFillMainModelModelConfig(modelConfig) && <ParamGuidance />}
{isUpscaling && <ParamUpscaleCFGScale />}
{!isFLUX && !isUpscaling && <ParamCFGScale />}
{!isFLUX && <ParamCFGScale />}
</FormControlGroup>
</Flex>
</Expander>

View File

@@ -0,0 +1,92 @@
import type { FormLabelProps } from '@invoke-ai/ui-library';
import { Box, Expander, Flex, FormControlGroup, StandaloneAccordion } from '@invoke-ai/ui-library';
import { EMPTY_ARRAY } from 'app/store/constants';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectLoRAsSlice } from 'features/controlLayers/store/lorasSlice';
import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
import { LoRAList } from 'features/lora/components/LoRAList';
import LoRASelect from 'features/lora/components/LoRASelect';
import ParamGuidance from 'features/parameters/components/Core/ParamGuidance';
import ParamSteps from 'features/parameters/components/Core/ParamSteps';
import { DisabledModelWarning } from 'features/parameters/components/MainModel/DisabledModelWarning';
import ParamUpscaleCFGScale from 'features/parameters/components/Upscale/ParamUpscaleCFGScale';
import ParamUpscaleScheduler from 'features/parameters/components/Upscale/ParamUpscaleScheduler';
import { useIsApiModel } from 'features/parameters/hooks/useIsApiModel';
import { API_BASE_MODELS } from 'features/parameters/types/constants';
import { MainModelPicker } from 'features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker';
import { useExpanderToggle } from 'features/settingsAccordions/hooks/useExpanderToggle';
import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelectedModelConfig } from 'services/api/hooks/useSelectedModelConfig';
import { isFluxFillMainModelModelConfig } from 'services/api/types';
const formLabelProps: FormLabelProps = {
minW: '4rem',
};
export const UpscaleTabGenerationSettingsAccordion = memo(() => {
const { t } = useTranslation();
const modelConfig = useSelectedModelConfig();
const isFLUX = useAppSelector(selectIsFLUX);
const isApiModel = useIsApiModel();
const selectBadges = useMemo(
() =>
createMemoizedSelector(selectLoRAsSlice, (loras) => {
const enabledLoRAsCount = loras.loras.filter((l) => l.isEnabled).length;
const loraTabBadges = enabledLoRAsCount ? [`${enabledLoRAsCount} ${t('models.concepts')}`] : EMPTY_ARRAY;
const accordionBadges =
modelConfig && API_BASE_MODELS.includes(modelConfig.base)
? [modelConfig.name]
: modelConfig
? [modelConfig.name, modelConfig.base]
: EMPTY_ARRAY;
return { loraTabBadges, accordionBadges };
}),
[modelConfig, t]
);
const { loraTabBadges, accordionBadges } = useAppSelector(selectBadges);
const { isOpen: isOpenExpander, onToggle: onToggleExpander } = useExpanderToggle({
id: 'generation-settings-advanced',
defaultIsOpen: false,
});
const { isOpen: isOpenAccordion, onToggle: onToggleAccordion } = useStandaloneAccordionToggle({
id: `generation-settings-upscaling`,
defaultIsOpen: false,
});
return (
<StandaloneAccordion
label={t('accordions.generation.title')}
badges={[...accordionBadges, ...loraTabBadges]}
isOpen={isOpenAccordion}
onToggle={onToggleAccordion}
>
<Box px={4} pt={4} data-testid="generation-accordion">
<Flex gap={4} flexDir="column" pb={isApiModel ? 4 : 0}>
<DisabledModelWarning />
<MainModelPicker />
{!isApiModel && <LoRASelect />}
{!isApiModel && <LoRAList />}
</Flex>
{!isApiModel && (
<Expander label={t('accordions.advanced.options')} isOpen={isOpenExpander} onToggle={onToggleExpander}>
<Flex gap={4} flexDir="column" pb={4}>
<FormControlGroup formLabelProps={formLabelProps}>
<ParamUpscaleScheduler />
<ParamSteps />
{isFLUX && modelConfig && !isFluxFillMainModelModelConfig(modelConfig) && <ParamGuidance />}
<ParamUpscaleCFGScale />
</FormControlGroup>
</Flex>
</Expander>
)}
</Box>
</StandaloneAccordion>
);
});
UpscaleTabGenerationSettingsAccordion.displayName = 'UpscaleTabGenerationSettingsAccordion';

View File

@@ -1,15 +1,16 @@
import { Flex } from '@invoke-ai/ui-library';
import 'dockview/dist/styles/dockview.css';
import 'features/ui/styles/dockview-theme-invoke.css';
import { TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useDndMonitor } from 'features/dnd/useDndMonitor';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { FloatingLeftPanelButtons } from 'features/ui/components/FloatingLeftPanelButtons';
import { FloatingRightPanelButtons } from 'features/ui/components/FloatingRightPanelButtons';
import { LeftPanelContent } from 'features/ui/components/LeftPanelContent';
import { MainPanelContent } from 'features/ui/components/MainPanelContent';
import { RightPanelContent } from 'features/ui/components/RightPanelContent';
import { VerticalNavBar } from 'features/ui/components/VerticalNavBar';
import type { UsePanelOptions } from 'features/ui/hooks/usePanel';
import { usePanel } from 'features/ui/hooks/usePanel';
import { CanvasTabAutoLayout } from 'features/ui/layouts/canvas-tab-auto-layout';
import { GenerateTabAutoLayout } from 'features/ui/layouts/generate-tab-auto-layout';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import {
$isLeftPanelOpen,
$isRightPanelOpen,
@@ -21,9 +22,6 @@ import {
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 { VerticalResizeHandle } from './tabs/ResizeHandle';
const panelStyles: CSSProperties = { position: 'relative', height: '100%', width: '100%', minWidth: 0 };
@@ -31,6 +29,7 @@ const onLeftPanelCollapse = (isCollapsed: boolean) => $isLeftPanelOpen.set(!isCo
const onRightPanelCollapse = (isCollapsed: boolean) => $isRightPanelOpen.set(!isCollapsed);
export const AppContent = memo(() => {
const tab = useAppSelector(selectActiveTab);
const imperativePanelGroupRef = useRef<ImperativePanelGroupHandle>(null);
useDndMonitor();
@@ -108,38 +107,19 @@ export const AppContent = memo(() => {
});
return (
<Flex id="invoke-app-tabs" w="full" h="full" gap={4} p={4} overflow="hidden">
<VerticalNavBar />
<PanelGroup
ref={imperativePanelGroupRef}
id="app-panel-group"
autoSaveId="app-panel-group"
direction="horizontal"
style={panelStyles}
>
{withLeftPanel && (
<>
<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 && <FloatingLeftPanelButtons onToggle={leftPanel.toggle} />}
{withRightPanel && <FloatingRightPanelButtons onToggle={rightPanel.toggle} />}
</Panel>
{withRightPanel && (
<>
<VerticalResizeHandle id="main-right-handle" {...rightPanel.resizeHandleProps} />
<Panel id="right-panel" order={2} style={panelStyles} collapsible {...rightPanel.panelProps}>
<RightPanelContent />
</Panel>
</>
)}
</PanelGroup>
</Flex>
<Tabs index={tab === 'generate' ? 0 : 1} variant="unstyled" w="full" h="full" display="flex" p={0}>
<TabList>
<VerticalNavBar />
</TabList>
<TabPanels w="full" h="full" p={0}>
<TabPanel w="full" h="full" p={0}>
<GenerateTabAutoLayout />
</TabPanel>
<TabPanel w="full" h="full" p={0}>
<CanvasTabAutoLayout />
</TabPanel>
</TabPanels>
</Tabs>
);
});
AppContent.displayName = 'AppContent';

View File

@@ -12,7 +12,7 @@ export const LeftPanelContent = memo(() => {
const tab = useAppSelector(selectActiveTab);
return (
<Flex flexDir="column" w="full" h="full" gap={2}>
<Flex flexDir="column" w="full" h="full" gap={2} py={2} pe={2}>
<QueueControls />
<Box position="relative" w="full" h="full">
{tab === 'generate' && <ParametersPanelTextToImage />}

View File

@@ -1,27 +1,22 @@
import { useAppSelector } from 'app/store/storeHooks';
import { AdvancedSession } from 'features/controlLayers/components/AdvancedSession/AdvancedSession';
import { SimpleSession } from 'features/controlLayers/components/SimpleSession/SimpleSession';
import { selectCanvasSessionId, selectGenerateSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
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 { atom } from 'nanostores';
import { memo } from 'react';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
export const $simpleId = atom<string | null>(null);
export const $advancedId = atom<string | null>(null);
export const MainPanelContent = memo(() => {
const tab = useAppSelector(selectActiveTab);
const generateId = useAppSelector(selectGenerateSessionId);
const canvasId = useAppSelector(selectCanvasSessionId);
if (tab === 'generate') {
return <SimpleSession id={generateId} />;
return <SimpleSession />;
}
if (tab === 'canvas') {
return <AdvancedSession id={canvasId} />;

View File

@@ -1,5 +1,5 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { IconButton, Tab, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
@@ -25,7 +25,8 @@ export const TabButton = memo(({ tab, icon, label }: { tab: TabName; icon: React
return (
<Tooltip label={label} placement="end">
<IconButton
<Tab
as={IconButton}
p={0}
ref={ref}
onClick={selectTab}

View File

@@ -25,7 +25,7 @@ export const VerticalNavBar = memo(() => {
const customNavComponent = useStore($customNavComponent);
return (
<Flex flexDir="column" alignItems="center" py={2} gap={4} minW={0}>
<Flex flexDir="column" alignItems="center" py={6} px={4} gap={4} minW={0}>
<InvokeAILogoComponent />
<Flex gap={4} pt={6} h="full" flexDir="column">
<TabMountGate tab="generate">

View File

@@ -0,0 +1,45 @@
import { useAppSelector } from 'app/store/storeHooks';
import type { GridviewApi, IGridviewReactProps } from 'dockview';
import { GridviewReact, Orientation } from 'dockview';
import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context';
import { canvasTabComponents, initializeCanvasTabLayout } from 'features/ui/layouts/canvas-tab-auto-layout';
import { generateTabComponents, initializeGenerateTabLayout } from 'features/ui/layouts/generate-tab-auto-layout';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import type { TabName } from 'features/ui/store/uiTypes';
import { memo, useCallback, useEffect, useState } from 'react';
const components: IGridviewReactProps['components'] = {
...generateTabComponents,
...canvasTabComponents,
};
export const AutoLayout = memo(() => {
const tab = useAppSelector(selectActiveTab);
const [api, setApi] = useState<GridviewApi | null>(null);
const syncLayout = useCallback((tab: TabName, api: GridviewApi) => {
if (tab === 'generate') {
initializeGenerateTabLayout(api);
} else if (tab === 'canvas') {
initializeCanvasTabLayout(api);
}
}, []);
const onReady = useCallback<IGridviewReactProps['onReady']>((event) => {
setApi(event.api);
}, []);
useEffect(() => {
if (api) {
syncLayout(tab, api);
}
}, [api, syncLayout, tab]);
return (
<AutoLayoutProvider api={api}>
<GridviewReact
className="dockview-theme-invoke"
components={components}
onReady={onReady}
orientation={Orientation.VERTICAL}
/>
</AutoLayoutProvider>
);
});
AutoLayout.displayName = 'AutoLayout';

View File

@@ -0,0 +1,35 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter';
import type { IDockviewPanelHeaderProps } from 'dockview';
import { useCallback, useEffect, useId, useRef } from 'react';
export const TabWithoutCloseButton = (props: IDockviewPanelHeaderProps) => {
const id = useId();
const ref = useRef<HTMLDivElement>(null);
const setActive = useCallback(() => {
if (!props.api.isActive) {
props.api.setActive();
}
}, [props.api]);
useCallbackOnDragEnter(setActive, ref, 300);
useEffect(() => {
const el = document.querySelector(`[data-id="${id}"]`);
if (!el) {
return;
}
const parentTab = el.closest('.dv-tab');
if (!parentTab) {
return;
}
parentTab.setAttribute('draggable', 'false');
}, [id]);
return (
<Flex ref={ref}>
<Text userSelect="none">{props.api.title ?? props.api.id}</Text>
</Flex>
);
};
TabWithoutCloseButton.displayName = 'TabWithoutCloseButton';

View File

@@ -0,0 +1,14 @@
import type { GridviewApi } from 'dockview';
import type { PropsWithChildren } from 'react';
import { createContext, useContext } from 'react';
const AutoLayoutContext = createContext<GridviewApi | null>(null);
export const AutoLayoutProvider = (props: PropsWithChildren<{ api: GridviewApi | null }>) => {
return <AutoLayoutContext.Provider value={props.api}>{props.children}</AutoLayoutContext.Provider>;
};
export const useAutoLayoutContext = () => {
const api = useContext(AutoLayoutContext);
return api;
};

View File

@@ -0,0 +1,339 @@
import { Box, ContextMenu, Divider, Flex, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import type { GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview';
import { DockviewReact, GridviewReact, Orientation } from 'dockview';
import { CanvasAlertsInvocationProgress } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress';
import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask';
import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus';
import { CanvasContextMenuGlobalMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuGlobalMenuItems';
import { CanvasContextMenuSelectedEntityMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems';
import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea';
import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
import { Filter } from 'features/controlLayers/components/Filters/Filter';
import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD';
import { InvokeCanvasComponent } from 'features/controlLayers/components/InvokeCanvasComponent';
import { SelectObject } from 'features/controlLayers/components/SelectObject/SelectObject';
import { CanvasSessionContextProvider } from 'features/controlLayers/components/SimpleSession/context';
import { InitialState } from 'features/controlLayers/components/SimpleSession/InitialState';
import { StagingAreaItemsList } from 'features/controlLayers/components/SimpleSession/StagingAreaItemsList';
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
import { Transform } from 'features/controlLayers/components/Transform/Transform';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
import { selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { BoardsListPanelContent } from 'features/gallery/components/BoardsListPanelContent';
import { Gallery } from 'features/gallery/components/Gallery';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer2';
import { ProgressImage } from 'features/gallery/components/ImageViewer/ProgressImage2';
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2';
import QueueControls from 'features/queue/components/QueueControls';
import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage';
import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context';
import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton';
import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from 'features/ui/store/uiSlice';
import { dockviewTheme } from 'features/ui/styles/theme';
import { memo, useCallback, useState } from 'react';
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
const MenuContent = memo(() => {
return (
<CanvasManagerProviderGate>
<MenuList>
<CanvasContextMenuSelectedEntityMenuItems />
<CanvasContextMenuGlobalMenuItems />
</MenuList>
</CanvasManagerProviderGate>
);
});
MenuContent.displayName = 'MenuContent';
const canvasBgSx = {
position: 'relative',
w: 'full',
h: 'full',
borderRadius: 'base',
overflow: 'hidden',
bg: 'base.900',
'&[data-dynamic-grid="true"]': {
bg: 'base.850',
},
};
export const CanvasPanel = memo(() => {
const dynamicGrid = useAppSelector(selectDynamicGrid);
const showHUD = useAppSelector(selectShowHUD);
const canvasId = useAppSelector(selectCanvasSessionId);
const renderMenu = useCallback(() => {
return <MenuContent />;
}, []);
return (
<Flex
tabIndex={-1}
borderRadius="base"
position="relative"
flexDirection="column"
height="full"
width="full"
gap={2}
alignItems="center"
justifyContent="center"
overflow="hidden"
>
<CanvasManagerProviderGate>
<CanvasToolbar />
</CanvasManagerProviderGate>
<Divider />
<ContextMenu<HTMLDivElement> renderMenu={renderMenu} withLongPress={false}>
{(ref) => (
<Flex ref={ref} sx={canvasBgSx} data-dynamic-grid={dynamicGrid}>
<InvokeCanvasComponent />
<CanvasManagerProviderGate>
<Flex
position="absolute"
flexDir="column"
top={1}
insetInlineStart={1}
pointerEvents="none"
gap={2}
alignItems="flex-start"
>
{showHUD && <CanvasHUD />}
<CanvasAlertsSelectedEntityStatus />
<CanvasAlertsPreserveMask />
<CanvasAlertsInvocationProgress />
</Flex>
<Flex position="absolute" top={1} insetInlineEnd={1}>
<Menu>
<MenuButton as={IconButton} icon={<PiDotsThreeOutlineVerticalFill />} colorScheme="base" />
<MenuContent />
</Menu>
</Flex>
</CanvasManagerProviderGate>
</Flex>
)}
</ContextMenu>
{canvasId !== null && (
<CanvasManagerProviderGate>
<CanvasSessionContextProvider type="advanced" id={canvasId}>
<Flex
position="absolute"
flexDir="column"
bottom={4}
gap={2}
align="center"
justify="center"
left={4}
right={4}
>
<Flex position="relative" maxW="full" w="full" h={108}>
<StagingAreaItemsList />
</Flex>
<Flex gap={2}>
<StagingAreaToolbar />
</Flex>
</Flex>
</CanvasSessionContextProvider>
</CanvasManagerProviderGate>
)}
<Flex position="absolute" bottom={4}>
<CanvasManagerProviderGate>
<Filter />
<Transform />
<SelectObject />
</CanvasManagerProviderGate>
</Flex>
<CanvasManagerProviderGate>
<CanvasDropArea />
</CanvasManagerProviderGate>
</Flex>
);
});
CanvasPanel.displayName = 'CanvasPanel';
const LayersPanelContent = memo(() => (
<CanvasManagerProviderGate>
<CanvasLayersPanelContent />
</CanvasManagerProviderGate>
));
LayersPanelContent.displayName = 'LayersPanelContent';
const ViewerPanelContent = memo(() => (
<Flex flexDir="column" w="full" h="full" overflow="hidden" p={2} gap={2}>
<ViewerToolbar />
<Divider />
<ImageViewer />
</Flex>
));
ViewerPanelContent.displayName = 'ViewerPanelContent';
const ProgressPanelContent = memo(() => (
<Flex flexDir="column" w="full" h="full" overflow="hidden" p={2}>
<ProgressImage />
</Flex>
));
ProgressPanelContent.displayName = 'ProgressPanelContent';
const mainPanelComponents: IDockviewReactProps['components'] = {
welcome: InitialState,
canvas: CanvasPanel,
viewer: ViewerPanelContent,
progress: ProgressPanelContent,
};
const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => {
const { api } = event;
api.addPanel({
id: 'welcome',
component: 'welcome',
title: 'Launchpad',
});
api.addPanel({
id: 'canvas',
component: 'canvas',
title: 'Canvas',
position: {
direction: 'within',
referencePanel: 'welcome',
},
});
api.addPanel({
id: 'viewer',
component: 'viewer',
title: 'Image Viewer',
position: {
direction: 'within',
referencePanel: 'welcome',
},
});
api.addPanel({
id: 'progress',
component: 'progress',
title: 'Generation Progress',
position: {
direction: 'within',
referencePanel: 'welcome',
},
});
const disposables = [
api.onWillShowOverlay((e) => {
if (e.kind === 'header_space' || e.kind === 'tab') {
return;
}
e.preventDefault();
}),
];
return () => {
disposables.forEach((disposable) => {
disposable.dispose();
});
};
};
const MainPanel = memo(() => {
return (
<Flex w="full" h="full">
<DockviewReact
disableDnd={true}
locked={true}
disableFloatingGroups={true}
dndEdges={false}
defaultTabComponent={TabWithoutCloseButton}
components={mainPanelComponents}
onReady={onReadyMainPanel}
theme={dockviewTheme}
/>
</Flex>
);
});
MainPanel.displayName = 'MainPanel';
const Left = memo(() => {
return (
<Flex flexDir="column" w="full" h="full" gap={2} py={2} pe={2}>
<QueueControls />
<Box position="relative" w="full" h="full">
<ParametersPanelTextToImage />
</Box>
</Flex>
);
});
Left.displayName = 'Left';
export const canvasTabComponents: IGridviewReactProps['components'] = {
left: Left,
main: MainPanel,
boards: BoardsListPanelContent,
gallery: Gallery,
layers: LayersPanelContent,
};
export const initializeCanvasTabLayout = (api: GridviewApi) => {
const main = api.addPanel({
id: 'main',
component: 'main',
minimumWidth: 256,
});
const left = api.addPanel({
id: 'left',
component: 'left',
minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
position: {
direction: 'left',
referencePanel: 'main',
},
});
api.addPanel({
id: 'gallery',
component: 'gallery',
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
minimumHeight: 232,
position: {
direction: 'right',
referencePanel: 'main',
},
});
api.addPanel({
id: 'layers',
component: 'layers',
minimumHeight: 256,
position: {
direction: 'below',
referencePanel: 'gallery',
},
});
const boards = api.addPanel({
id: 'boards',
component: 'boards',
minimumHeight: 36,
position: {
direction: 'above',
referencePanel: 'gallery',
},
});
left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
boards.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX });
};
export const CanvasTabAutoLayout = memo(() => {
const [api, setApi] = useState<GridviewApi | null>(null);
const onReady = useCallback<IGridviewReactProps['onReady']>((event) => {
setApi(event.api);
initializeCanvasTabLayout(event.api);
}, []);
return (
<AutoLayoutProvider api={api}>
<GridviewReact
className="dockview-theme-invoke"
components={canvasTabComponents}
onReady={onReady}
orientation={Orientation.VERTICAL}
/>
</AutoLayoutProvider>
);
});
CanvasTabAutoLayout.displayName = 'CanvasTabAutoLayout';

View File

@@ -0,0 +1,175 @@
import { Box, Divider, Flex } from '@invoke-ai/ui-library';
import type { GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview';
import { DockviewReact, GridviewReact, Orientation } from 'dockview';
import { InitialState } from 'features/controlLayers/components/SimpleSession/InitialState';
import { BoardsListPanelContent } from 'features/gallery/components/BoardsListPanelContent';
import { Gallery } from 'features/gallery/components/Gallery';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer2';
import { ProgressImage } from 'features/gallery/components/ImageViewer/ProgressImage2';
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2';
import QueueControls from 'features/queue/components/QueueControls';
import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage';
import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context';
import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton';
import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from 'features/ui/store/uiSlice';
import { dockviewTheme } from 'features/ui/styles/theme';
import { memo, useCallback, useState } from 'react';
const ViewerPanelContent = memo(() => (
<Flex flexDir="column" w="full" h="full" overflow="hidden" p={2} gap={2}>
<ViewerToolbar />
<Divider />
<ImageViewer />
</Flex>
));
ViewerPanelContent.displayName = 'ViewerPanelContent';
const ProgressPanelContent = memo(() => (
<Flex flexDir="column" w="full" h="full" overflow="hidden" p={2}>
<ProgressImage />
</Flex>
));
ProgressPanelContent.displayName = 'ProgressPanelContent';
const mainPanelComponents: IDockviewReactProps['components'] = {
welcome: InitialState,
viewer: ViewerPanelContent,
progress: ProgressPanelContent,
};
const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => {
const { api } = event;
api.addPanel({
id: 'welcome',
component: 'welcome',
title: 'Launchpad',
});
api.addPanel({
id: 'viewer',
component: 'viewer',
title: 'Image Viewer',
position: {
direction: 'within',
referencePanel: 'welcome',
},
});
api.addPanel({
id: 'progress',
component: 'progress',
title: 'Generation Progress',
position: {
direction: 'within',
referencePanel: 'welcome',
},
});
const disposables = [
api.onWillShowOverlay((e) => {
if (e.kind === 'header_space' || e.kind === 'tab') {
return;
}
e.preventDefault();
}),
];
return () => {
disposables.forEach((disposable) => {
disposable.dispose();
});
};
};
const MainPanel = memo(() => {
return (
<Flex w="full" h="full">
<DockviewReact
disableDnd={true}
locked={true}
disableFloatingGroups={true}
dndEdges={false}
defaultTabComponent={TabWithoutCloseButton}
components={mainPanelComponents}
onReady={onReadyMainPanel}
theme={dockviewTheme}
/>
</Flex>
);
});
MainPanel.displayName = 'MainPanel';
const Left = memo(() => {
return (
<Flex flexDir="column" w="full" h="full" gap={2} py={2} pe={2}>
<QueueControls />
<Box position="relative" w="full" h="full">
<ParametersPanelTextToImage />
</Box>
</Flex>
);
});
Left.displayName = 'Left';
export const generateTabComponents: IGridviewReactProps['components'] = {
left: Left,
main: MainPanel,
boards: BoardsListPanelContent,
gallery: Gallery,
};
export const initializeGenerateTabLayout = (api: GridviewApi) => {
const main = api.addPanel({
id: 'main',
component: 'main',
minimumWidth: 256,
});
const left = api.addPanel({
id: 'left',
component: 'left',
minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
position: {
direction: 'left',
referencePanel: 'main',
},
});
api.addPanel({
id: 'gallery',
component: 'gallery',
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
minimumHeight: 232,
position: {
direction: 'right',
referencePanel: 'main',
},
});
const boards = api.addPanel({
id: 'boards',
component: 'boards',
minimumHeight: 36,
position: {
direction: 'above',
referencePanel: 'gallery',
},
});
left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
boards.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX });
};
export const GenerateTabAutoLayout = memo(() => {
const [api, setApi] = useState<GridviewApi | null>(null);
const onReady = useCallback<IGridviewReactProps['onReady']>((event) => {
console.log('GenerateTabAutoLayout onReady');
setApi(event.api);
initializeGenerateTabLayout(event.api);
}, []);
return (
<AutoLayoutProvider api={api}>
<GridviewReact
className="dockview-theme-invoke"
components={generateTabComponents}
onReady={onReady}
orientation={Orientation.VERTICAL}
/>
</AutoLayoutProvider>
);
});
GenerateTabAutoLayout.displayName = 'GenerateTabAutoLayout';

View File

@@ -0,0 +1,98 @@
import type { GridviewApi, GridviewPanelApi, IGridviewPanel } from 'dockview';
import { atom } from 'nanostores';
import { useCallback, useEffect, useMemo, useState } from 'react';
const getIsCollapsed = (
panel: IGridviewPanel<GridviewPanelApi>,
orientation: 'vertical' | 'horizontal',
collapsedSize?: number
) => {
if (orientation === 'vertical') {
return panel.height <= (collapsedSize ?? panel.minimumHeight);
}
return panel.width <= (collapsedSize ?? panel.minimumWidth);
};
export const useCollapsibleGridviewPanel = (
api: GridviewApi | null,
panelId: string,
orientation: 'horizontal' | 'vertical',
defaultSize: number,
collapsedSize?: number
) => {
const $isCollapsed = useState(() => atom(false))[0];
const collapse = useCallback(() => {
if (!api) {
return;
}
const panel = api.getPanel(panelId);
if (!panel) {
return;
}
if (orientation === 'vertical') {
panel.api.setSize({ height: collapsedSize ?? panel.minimumHeight });
} else {
panel.api.setSize({ width: collapsedSize ?? panel.minimumWidth });
}
}, [api, collapsedSize, orientation, panelId]);
const expand = useCallback(() => {
if (!api) {
return;
}
const panel = api.getPanel(panelId);
if (!panel) {
return;
}
if (orientation === 'vertical') {
panel.api.setSize({ height: defaultSize });
} else {
panel.api.setSize({ width: defaultSize });
}
}, [api, defaultSize, orientation, panelId]);
const toggle = useCallback(() => {
if (!api) {
return;
}
const panel = api.getPanel(panelId);
if (!panel) {
return;
}
const isCollapsed = getIsCollapsed(panel, orientation, collapsedSize);
if (isCollapsed) {
expand();
} else {
collapse();
}
}, [api, panelId, orientation, collapsedSize, expand, collapse]);
useEffect(() => {
if (!api) {
return;
}
const panel = api.getPanel(panelId);
if (!panel) {
return;
}
const disposable = panel.api.onDidDimensionsChange(() => {
const isCollapsed = getIsCollapsed(panel, orientation, collapsedSize);
$isCollapsed.set(isCollapsed);
});
return () => {
disposable.dispose();
};
}, [$isCollapsed, api, collapsedSize, orientation, panelId]);
return useMemo(
() => ({
$isCollapsed,
expand,
collapse,
toggle,
}),
[$isCollapsed, collapse, expand, toggle]
);
};

View File

@@ -5,3 +5,5 @@ export const selectActiveTab = createSelector(selectUiSlice, (ui) => ui.activeTa
export const selectShouldShowImageDetails = createSelector(selectUiSlice, (ui) => ui.shouldShowImageDetails);
export const selectShouldShowProgressInViewer = createSelector(selectUiSlice, (ui) => ui.shouldShowProgressInViewer);
export const selectActiveTabCanvasRightPanel = createSelector(selectUiSlice, (ui) => ui.activeTabCanvasRightPanel);
export const selectShowGenerateTabSplashScreen = createSelector(selectUiSlice, (ui) => ui.showGenerateTabSplashScreen);
export const selectShowCanvasTabSplashScreen = createSelector(selectUiSlice, (ui) => ui.showCanvasTabSplashScreen);

View File

@@ -123,11 +123,11 @@ export const uiPersistConfig: PersistConfig<UIState> = {
};
const TABS_WITH_LEFT_PANEL: TabName[] = ['canvas', 'upscaling', 'workflows', 'generate'] as const;
export const LEFT_PANEL_MIN_SIZE_PX = 400;
export const LEFT_PANEL_MIN_SIZE_PX = 420;
export const $isLeftPanelOpen = atom(true);
export const selectWithLeftPanel = createSelector(selectUiSlice, (ui) => TABS_WITH_LEFT_PANEL.includes(ui.activeTab));
const TABS_WITH_RIGHT_PANEL: TabName[] = ['canvas', 'upscaling', 'workflows', 'generate'] as const;
export const RIGHT_PANEL_MIN_SIZE_PX = 390;
export const RIGHT_PANEL_MIN_SIZE_PX = 420;
export const $isRightPanelOpen = atom(true);
export const selectWithRightPanel = createSelector(selectUiSlice, (ui) => TABS_WITH_RIGHT_PANEL.includes(ui.activeTab));

View File

@@ -0,0 +1,65 @@
.dockview-theme-invoke {
--dv-paneview-active-outline-color: var(--invoke-colors-invokeBlue-300);
--dv-tabs-and-actions-container-font-size: var(--invoke-fontSizes-sm);
--dv-tabs-and-actions-container-height: var(--invoke-sizes-8);
--dv-drag-over-background-color: var(--invoke-colors-baseAlpha-400);
--dv-drag-over-border-color: var(--invoke-colors-base-300);
--dv-tabs-container-scrollbar-color: #888;
--dv-icon-hover-background-color: rgba(90, 93, 94, 0.31);
--dv-floating-box-shadow: none;
--dv-overlay-z-index: 999;
--dv-tab-font-size: inherit;
--dv-border-radius: 0;
--dv-tab-margin: 0;
--dv-sash-color: transparent;
--dv-active-sash-color: var(--invoke-colors-base-700);
--dv-active-sash-transition-duration: 0.15s;
--dv-active-sash-transition-delay: 0.1s;
--dv-group-view-background-color: var(--invoke-colors-base-900);
--dv-tabs-and-actions-container-background-color: var(--invoke-colors-base-850);
--dv-activegroup-visiblepanel-tab-color: var(--invoke-colors-base-50);
--dv-activegroup-visiblepanel-tab-background-color: var(--invoke-colors-base-700);
--dv-activegroup-hiddenpanel-tab-color: var(--invoke-colors-base-300);
--dv-activegroup-hiddenpanel-tab-background-color: var(--invoke-colors-base-850);
--dv-inactivegroup-visiblepanel-tab-color: var(--invoke-colors-base-500);
--dv-inactivegroup-visiblepanel-tab-background-color: var(--invoke-colors-base-800);
--dv-inactivegroup-hiddenpanel-tab-color: var(--invoke-colors-base-600);
--dv-inactivegroup-hiddenpanel-tab-background-color: var(--invoke-colors-base-850);
--dv-tab-divider-color: var(--invoke-colors-base-700);
--dv-inactivegroup-tab-divider-color: var(--invoke-colors-base-800);
--dv-separator-border: var(--invoke-colors-base-750);
--dv-paneview-header-border-color: rgba(204, 204, 204, 0.2);
}
.dv-default-tab-content {
margin-right: 0px !important;
}
.dv-groupview-floating {
border-radius: var(--invoke-space-2);
border-width: 1px;
border-color: var(--invoke-colors-base-800);
filter: drop-shadow(0px 0px 3px rgba(0, 0, 0, 0.4)) drop-shadow(5px 5px 10px rgba(0, 0, 0, 0.6));
}
.dv-resize-container {
border: none;
}
.dv-tab {
/* margin-right: 2px; */
}
.dv-inactive-group .dv-tabs-container.dv-horizontal .dv-tab:not(:first-child)::before {
/* this is the tab divider */
background-color: var(--dv-inactivegroup-tab-divider-color);
}

View File

@@ -0,0 +1,6 @@
import type { DockviewTheme } from 'dockview';
export const dockviewTheme: DockviewTheme = {
name: 'invoke',
className: 'dockview-theme-invoke',
};