mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-12 21:25:12 -05:00
wip
This commit is contained in:
@@ -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",
|
||||
|
||||
16
invokeai/frontend/web/pnpm-lock.yaml
generated
16
invokeai/frontend/web/pnpm-lock.yaml
generated
@@ -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'}
|
||||
|
||||
@@ -17,6 +17,7 @@ const Loading = () => {
|
||||
right={0}
|
||||
bottom={0}
|
||||
left={0}
|
||||
zIndex={99999}
|
||||
>
|
||||
<Image src={InvokeLogoWhite} w="8rem" h="8rem" />
|
||||
<Spinner
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 },
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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} />;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
45
invokeai/frontend/web/src/features/ui/layouts/AutoLayout.tsx
Normal file
45
invokeai/frontend/web/src/features/ui/layouts/AutoLayout.tsx
Normal 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';
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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]
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
6
invokeai/frontend/web/src/features/ui/styles/theme.ts
Normal file
6
invokeai/frontend/web/src/features/ui/styles/theme.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { DockviewTheme } from 'dockview';
|
||||
|
||||
export const dockviewTheme: DockviewTheme = {
|
||||
name: 'invoke',
|
||||
className: 'dockview-theme-invoke',
|
||||
};
|
||||
Reference in New Issue
Block a user