mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): first pass at boards styling
This commit is contained in:
@ -9,7 +9,7 @@ import {
|
|||||||
import { useDraggable, useDroppable } from '@dnd-kit/core';
|
import { useDraggable, useDroppable } from '@dnd-kit/core';
|
||||||
import { useCombinedRefs } from '@dnd-kit/utilities';
|
import { useCombinedRefs } from '@dnd-kit/utilities';
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
import { IAIImageFallback } from 'common/components/IAIImageFallback';
|
import { IAIImageLoadingFallback } from 'common/components/IAIImageFallback';
|
||||||
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
|
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
|
||||||
import { AnimatePresence } from 'framer-motion';
|
import { AnimatePresence } from 'framer-motion';
|
||||||
import { ReactElement, SyntheticEvent, useCallback } from 'react';
|
import { ReactElement, SyntheticEvent, useCallback } from 'react';
|
||||||
@ -53,7 +53,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
isDropDisabled = false,
|
isDropDisabled = false,
|
||||||
isDragDisabled = false,
|
isDragDisabled = false,
|
||||||
isUploadDisabled = false,
|
isUploadDisabled = false,
|
||||||
fallback = <IAIImageFallback />,
|
fallback = <IAIImageLoadingFallback />,
|
||||||
payloadImage,
|
payloadImage,
|
||||||
minSize = 24,
|
minSize = 24,
|
||||||
postUploadAction,
|
postUploadAction,
|
||||||
|
@ -1,10 +1,20 @@
|
|||||||
import { Flex, FlexProps, Spinner, SpinnerProps } from '@chakra-ui/react';
|
import {
|
||||||
|
As,
|
||||||
|
Flex,
|
||||||
|
FlexProps,
|
||||||
|
Icon,
|
||||||
|
IconProps,
|
||||||
|
Spinner,
|
||||||
|
SpinnerProps,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { ReactElement } from 'react';
|
||||||
|
import { FaImage } from 'react-icons/fa';
|
||||||
|
|
||||||
type Props = FlexProps & {
|
type Props = FlexProps & {
|
||||||
spinnerProps?: SpinnerProps;
|
spinnerProps?: SpinnerProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IAIImageFallback = (props: Props) => {
|
export const IAIImageLoadingFallback = (props: Props) => {
|
||||||
const { spinnerProps, ...rest } = props;
|
const { spinnerProps, ...rest } = props;
|
||||||
const { sx, ...restFlexProps } = rest;
|
const { sx, ...restFlexProps } = rest;
|
||||||
return (
|
return (
|
||||||
@ -25,3 +35,35 @@ export const IAIImageFallback = (props: Props) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type IAINoImageFallbackProps = {
|
||||||
|
flexProps?: FlexProps;
|
||||||
|
iconProps?: IconProps;
|
||||||
|
as?: As;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IAINoImageFallback = (props: IAINoImageFallbackProps) => {
|
||||||
|
const { sx: flexSx, ...restFlexProps } = props.flexProps ?? { sx: {} };
|
||||||
|
const { sx: iconSx, ...restIconProps } = props.iconProps ?? { sx: {} };
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
bg: 'base.900',
|
||||||
|
opacity: 0.7,
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderRadius: 'base',
|
||||||
|
...flexSx,
|
||||||
|
}}
|
||||||
|
{...restFlexProps}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
as={props.as ?? FaImage}
|
||||||
|
sx={{ color: 'base.700', ...iconSx }}
|
||||||
|
{...restIconProps}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -11,7 +11,7 @@ import IAIDndImage from 'common/components/IAIDndImage';
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { IAIImageFallback } from 'common/components/IAIImageFallback';
|
import { IAIImageLoadingFallback } from 'common/components/IAIImageFallback';
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
import { FaUndo } from 'react-icons/fa';
|
import { FaUndo } from 'react-icons/fa';
|
||||||
import { useGetImageDTOQuery } from 'services/apiSlice';
|
import { useGetImageDTOQuery } from 'services/apiSlice';
|
||||||
@ -173,7 +173,7 @@ const ControlNetImagePreview = (props: Props) => {
|
|||||||
h: 'full',
|
h: 'full',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IAIImageFallback />
|
<IAIImageLoadingFallback />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{controlImage && (
|
{controlImage && (
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { Flex, Icon, Spinner, Text } from '@chakra-ui/react';
|
import IAIButton from 'common/components/IAIButton';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { FaPlus } from 'react-icons/fa';
|
|
||||||
import { useCreateBoardMutation } from 'services/apiSlice';
|
import { useCreateBoardMutation } from 'services/apiSlice';
|
||||||
|
|
||||||
const DEFAULT_BOARD_NAME = 'My Board';
|
const DEFAULT_BOARD_NAME = 'My Board';
|
||||||
@ -13,38 +12,15 @@ const AddBoardButton = () => {
|
|||||||
}, [createBoard]);
|
}, [createBoard]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<IAIButton
|
||||||
onClick={isLoading ? undefined : handleCreateBoard}
|
isLoading={isLoading}
|
||||||
sx={{
|
aria-label="Add Board"
|
||||||
flexDir: 'column',
|
onClick={handleCreateBoard}
|
||||||
justifyContent: 'space-between',
|
size="sm"
|
||||||
alignItems: 'center',
|
sx={{ px: 4 }}
|
||||||
cursor: 'pointer',
|
|
||||||
w: 'full',
|
|
||||||
h: 'full',
|
|
||||||
gap: 1,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Flex
|
Add Board
|
||||||
sx={{
|
</IAIButton>
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
borderWidth: '1px',
|
|
||||||
borderRadius: 'base',
|
|
||||||
borderColor: 'base.800',
|
|
||||||
w: 'full',
|
|
||||||
h: 'full',
|
|
||||||
aspectRatio: '1/1',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<Spinner />
|
|
||||||
) : (
|
|
||||||
<Icon boxSize={8} color="base.700" as={FaPlus} />
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
<Text sx={{ color: 'base.200', fontSize: 'xs' }}>New Board</Text>
|
|
||||||
</Flex>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,12 +2,15 @@ import { Flex, Icon, Text } from '@chakra-ui/react';
|
|||||||
import { FaImages } from 'react-icons/fa';
|
import { FaImages } from 'react-icons/fa';
|
||||||
import { boardIdSelected } from '../../store/boardSlice';
|
import { boardIdSelected } from '../../store/boardSlice';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { IAINoImageFallback } from 'common/components/IAIImageFallback';
|
||||||
|
import { AnimatePresence } from 'framer-motion';
|
||||||
|
import { SelectedItemOverlay } from '../SelectedItemOverlay';
|
||||||
|
|
||||||
const AllImagesBoard = ({ isSelected }: { isSelected: boolean }) => {
|
const AllImagesBoard = ({ isSelected }: { isSelected: boolean }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const handleAllImagesBoardClick = () => {
|
const handleAllImagesBoardClick = () => {
|
||||||
dispatch(boardIdSelected(null));
|
dispatch(boardIdSelected());
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -19,25 +22,34 @@ const AllImagesBoard = ({ isSelected }: { isSelected: boolean }) => {
|
|||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
w: 'full',
|
w: 'full',
|
||||||
h: 'full',
|
h: 'full',
|
||||||
gap: 1,
|
borderRadius: 'base',
|
||||||
}}
|
}}
|
||||||
onClick={handleAllImagesBoardClick}
|
onClick={handleAllImagesBoardClick}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderWidth: '1px',
|
|
||||||
borderRadius: 'base',
|
borderRadius: 'base',
|
||||||
borderColor: isSelected ? 'base.500' : 'base.800',
|
|
||||||
w: 'full',
|
w: 'full',
|
||||||
h: 'full',
|
|
||||||
aspectRatio: '1/1',
|
aspectRatio: '1/1',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon boxSize={8} color="base.700" as={FaImages} />
|
<IAINoImageFallback iconProps={{ boxSize: 8 }} as={FaImages} />
|
||||||
|
<AnimatePresence>
|
||||||
|
{isSelected && <SelectedItemOverlay />}
|
||||||
|
</AnimatePresence>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Text sx={{ color: 'base.200', fontSize: 'xs' }}>All Images</Text>
|
<Text
|
||||||
|
sx={{
|
||||||
|
color: isSelected ? 'base.50' : 'base.200',
|
||||||
|
fontWeight: isSelected ? 600 : undefined,
|
||||||
|
fontSize: 'xs',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
All Images
|
||||||
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
Box,
|
Collapse,
|
||||||
Divider,
|
Flex,
|
||||||
Grid,
|
Grid,
|
||||||
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
InputGroup,
|
InputGroup,
|
||||||
InputRightElement,
|
InputRightElement,
|
||||||
Spacer,
|
|
||||||
useDisclosure,
|
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
@ -16,33 +15,36 @@ import {
|
|||||||
selectBoardsAll,
|
selectBoardsAll,
|
||||||
setBoardSearchText,
|
setBoardSearchText,
|
||||||
} from 'features/gallery/store/boardSlice';
|
} from 'features/gallery/store/boardSlice';
|
||||||
import { memo, useEffect, useState } from 'react';
|
import { memo, useState } from 'react';
|
||||||
import HoverableBoard from './HoverableBoard';
|
import HoverableBoard from './HoverableBoard';
|
||||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||||
import AddBoardButton from './AddBoardButton';
|
import AddBoardButton from './AddBoardButton';
|
||||||
import AllImagesBoard from './AllImagesBoard';
|
import AllImagesBoard from './AllImagesBoard';
|
||||||
import { searchBoardsSelector } from '../../store/boardSelectors';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import IAICollapse from '../../../../common/components/IAICollapse';
|
|
||||||
import { CloseIcon } from '@chakra-ui/icons';
|
import { CloseIcon } from '@chakra-ui/icons';
|
||||||
import { useListBoardsQuery } from 'services/apiSlice';
|
import { useListBoardsQuery } from 'services/apiSlice';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[selectBoardsAll, boardsSelector],
|
[selectBoardsAll, boardsSelector],
|
||||||
(boards, boardsState) => {
|
(boards, boardsState) => {
|
||||||
const selectedBoard = boards.find(
|
// const selectedBoard = boards.find(
|
||||||
(board) => board.board_id === boardsState.selectedBoardId
|
// (board) => board.board_id === boardsState.selectedBoardId
|
||||||
);
|
// );
|
||||||
return { selectedBoard, searchText: boardsState.searchText };
|
// return { selectedBoard, searchText: boardsState.searchText };
|
||||||
|
const { selectedBoardId, searchText } = boardsState;
|
||||||
|
return { selectedBoardId, searchText };
|
||||||
},
|
},
|
||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
const BoardsList = () => {
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BoardsList = (props: Props) => {
|
||||||
|
const { isOpen } = props;
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { selectedBoard, searchText } = useAppSelector(selector);
|
const { selectedBoardId, searchText } = useAppSelector(selector);
|
||||||
// const filteredBoards = useSelector(searchBoardsSelector);
|
// const filteredBoards = useSelector(searchBoardsSelector);
|
||||||
const { isOpen, onToggle } = useDisclosure();
|
|
||||||
|
|
||||||
const { data } = useListBoardsQuery({ offset: 0, limit: 8 });
|
const { data } = useListBoardsQuery({ offset: 0, limit: 8 });
|
||||||
|
|
||||||
@ -64,9 +66,18 @@ const BoardsList = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IAICollapse label="Select Board" isOpen={isOpen} onToggle={onToggle}>
|
<Collapse in={isOpen} animateOpacity>
|
||||||
<>
|
<Flex
|
||||||
<Box marginBottom="1rem">
|
sx={{
|
||||||
|
flexDir: 'column',
|
||||||
|
gap: 2,
|
||||||
|
bg: 'base.800',
|
||||||
|
borderRadius: 'base',
|
||||||
|
p: 2,
|
||||||
|
mt: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex sx={{ gap: 2, alignItems: 'center' }}>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search Boards..."
|
placeholder="Search Boards..."
|
||||||
@ -77,11 +88,18 @@ const BoardsList = () => {
|
|||||||
/>
|
/>
|
||||||
{searchText && searchText.length && (
|
{searchText && searchText.length && (
|
||||||
<InputRightElement>
|
<InputRightElement>
|
||||||
<CloseIcon onClick={clearBoardSearch} cursor="pointer" />
|
<IconButton
|
||||||
|
onClick={clearBoardSearch}
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
aria-label="Clear Search"
|
||||||
|
icon={<CloseIcon boxSize={3} />}
|
||||||
|
/>
|
||||||
</InputRightElement>
|
</InputRightElement>
|
||||||
)}
|
)}
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</Box>
|
<AddBoardButton />
|
||||||
|
</Flex>
|
||||||
<OverlayScrollbarsComponent
|
<OverlayScrollbarsComponent
|
||||||
defer
|
defer
|
||||||
style={{ height: '100%', width: '100%' }}
|
style={{ height: '100%', width: '100%' }}
|
||||||
@ -98,29 +116,24 @@ const BoardsList = () => {
|
|||||||
className="list-container"
|
className="list-container"
|
||||||
sx={{
|
sx={{
|
||||||
gap: 2,
|
gap: 2,
|
||||||
gridTemplateRows: '5rem 5rem',
|
gridTemplateRows: '5.5rem 5.5rem',
|
||||||
gridAutoFlow: 'column dense',
|
gridAutoFlow: 'column dense',
|
||||||
gridAutoColumns: '4rem',
|
gridAutoColumns: '4rem',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!searchMode && (
|
{!searchMode && <AllImagesBoard isSelected={!selectedBoardId} />}
|
||||||
<>
|
|
||||||
<AddBoardButton />
|
|
||||||
<AllImagesBoard isSelected={!selectedBoard} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{filteredBoards &&
|
{filteredBoards &&
|
||||||
filteredBoards.map((board) => (
|
filteredBoards.map((board) => (
|
||||||
<HoverableBoard
|
<HoverableBoard
|
||||||
key={board.board_id}
|
key={board.board_id}
|
||||||
board={board}
|
board={board}
|
||||||
isSelected={selectedBoard?.board_id === board.board_id}
|
isSelected={selectedBoardId === board.board_id}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
</OverlayScrollbarsComponent>
|
</OverlayScrollbarsComponent>
|
||||||
</>
|
</Flex>
|
||||||
</IAICollapse>
|
</Collapse>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,31 +1,22 @@
|
|||||||
import {
|
import {
|
||||||
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
Editable,
|
Editable,
|
||||||
EditableInput,
|
EditableInput,
|
||||||
EditablePreview,
|
EditablePreview,
|
||||||
Flex,
|
Flex,
|
||||||
|
Image,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
MenuList,
|
MenuList,
|
||||||
Text,
|
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { FaTrash } from 'react-icons/fa';
|
import { FaFolder, FaTrash } from 'react-icons/fa';
|
||||||
import { ContextMenu } from 'chakra-ui-contextmenu';
|
import { ContextMenu } from 'chakra-ui-contextmenu';
|
||||||
import { BoardDTO, ImageDTO } from 'services/api';
|
import { BoardDTO, ImageDTO } from 'services/api';
|
||||||
import { IAIImageFallback } from 'common/components/IAIImageFallback';
|
import { IAINoImageFallback } from 'common/components/IAIImageFallback';
|
||||||
import { boardIdSelected } from 'features/gallery/store/boardSlice';
|
import { boardIdSelected } from 'features/gallery/store/boardSlice';
|
||||||
import {
|
|
||||||
boardDeleted,
|
|
||||||
boardUpdated,
|
|
||||||
imageAddedToBoard,
|
|
||||||
} from '../../../../services/thunks/board';
|
|
||||||
import { selectImagesAll, selectImagesById } from '../../store/imagesSlice';
|
|
||||||
import IAIDndImage from '../../../../common/components/IAIDndImage';
|
|
||||||
import { defaultSelectorOptions } from '../../../../app/store/util/defaultMemoizeOptions';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { RootState } from '../../../../app/store/store';
|
|
||||||
import {
|
import {
|
||||||
useAddImageToBoardMutation,
|
useAddImageToBoardMutation,
|
||||||
useDeleteBoardMutation,
|
useDeleteBoardMutation,
|
||||||
@ -33,21 +24,10 @@ import {
|
|||||||
useUpdateBoardMutation,
|
useUpdateBoardMutation,
|
||||||
} from 'services/apiSlice';
|
} from 'services/apiSlice';
|
||||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||||
|
import { useDroppable } from '@dnd-kit/core';
|
||||||
const coverImageSelector = (imageName: string | undefined) =>
|
import { AnimatePresence } from 'framer-motion';
|
||||||
createSelector(
|
import IAIDropOverlay from 'common/components/IAIDropOverlay';
|
||||||
[(state: RootState) => state],
|
import { SelectedItemOverlay } from '../SelectedItemOverlay';
|
||||||
(state) => {
|
|
||||||
const coverImage = imageName
|
|
||||||
? selectImagesById(state, imageName)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
coverImage,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
defaultSelectorOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
interface HoverableBoardProps {
|
interface HoverableBoardProps {
|
||||||
board: BoardDTO;
|
board: BoardDTO;
|
||||||
@ -94,6 +74,17 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
|
|||||||
[addImageToBoard, board_id]
|
[addImageToBoard, board_id]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
isOver,
|
||||||
|
setNodeRef,
|
||||||
|
active: isDropActive,
|
||||||
|
} = useDroppable({
|
||||||
|
id: `board_droppable_${board_id}`,
|
||||||
|
data: {
|
||||||
|
handleDrop,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ touchAction: 'none' }}>
|
<Box sx={{ touchAction: 'none' }}>
|
||||||
<ContextMenu<HTMLDivElement>
|
<ContextMenu<HTMLDivElement>
|
||||||
@ -112,7 +103,6 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
|
|||||||
>
|
>
|
||||||
{(ref) => (
|
{(ref) => (
|
||||||
<Flex
|
<Flex
|
||||||
position="relative"
|
|
||||||
key={board_id}
|
key={board_id}
|
||||||
userSelect="none"
|
userSelect="none"
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -123,69 +113,74 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
|
|||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
w: 'full',
|
w: 'full',
|
||||||
h: 'full',
|
h: 'full',
|
||||||
gap: 1,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
|
ref={setNodeRef}
|
||||||
onClick={handleSelectBoard}
|
onClick={handleSelectBoard}
|
||||||
sx={{
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderWidth: '1px',
|
|
||||||
borderRadius: 'base',
|
borderRadius: 'base',
|
||||||
borderColor: isSelected ? 'base.500' : 'base.800',
|
|
||||||
w: 'full',
|
w: 'full',
|
||||||
h: 'full',
|
|
||||||
aspectRatio: '1/1',
|
aspectRatio: '1/1',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IAIDndImage
|
{board.cover_image_name && coverImage?.image_url && (
|
||||||
image={
|
<Image src={coverImage?.image_url} draggable={false} />
|
||||||
board.cover_image_name && coverImage ? coverImage : undefined
|
)}
|
||||||
}
|
{!(board.cover_image_name && coverImage?.image_url) && (
|
||||||
onDrop={handleDrop}
|
<IAINoImageFallback iconProps={{ boxSize: 8 }} as={FaFolder} />
|
||||||
fallback={<IAIImageFallback sx={{ bg: 'none' }} />}
|
)}
|
||||||
isUploadDisabled={true}
|
<Flex
|
||||||
/>
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
insetInlineEnd: 0,
|
||||||
|
top: 0,
|
||||||
|
p: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Badge variant="solid">{board.image_count}</Badge>
|
||||||
|
</Flex>
|
||||||
|
<AnimatePresence>
|
||||||
|
{isSelected && <SelectedItemOverlay />}
|
||||||
|
</AnimatePresence>
|
||||||
|
<AnimatePresence>
|
||||||
|
{isDropActive && <IAIDropOverlay isOver={isOver} />}
|
||||||
|
</AnimatePresence>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Editable
|
<Box sx={{ width: 'full' }}>
|
||||||
defaultValue={board_name}
|
<Editable
|
||||||
submitOnBlur={false}
|
defaultValue={board_name}
|
||||||
onSubmit={(nextValue) => {
|
submitOnBlur={false}
|
||||||
handleUpdateBoardName(nextValue);
|
onSubmit={(nextValue) => {
|
||||||
}}
|
handleUpdateBoardName(nextValue);
|
||||||
>
|
|
||||||
<EditablePreview
|
|
||||||
sx={{ color: 'base.200', fontSize: 'xs', textAlign: 'left' }}
|
|
||||||
noOfLines={1}
|
|
||||||
/>
|
|
||||||
<EditableInput
|
|
||||||
sx={{
|
|
||||||
color: 'base.200',
|
|
||||||
fontSize: 'xs',
|
|
||||||
textAlign: 'left',
|
|
||||||
borderColor: 'base.500',
|
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
</Editable>
|
<EditablePreview
|
||||||
<Flex
|
sx={{
|
||||||
sx={{
|
color: isSelected ? 'base.50' : 'base.200',
|
||||||
justifyContent: 'center',
|
fontWeight: isSelected ? 600 : undefined,
|
||||||
alignItems: 'center',
|
fontSize: 'xs',
|
||||||
pos: 'absolute',
|
textAlign: 'center',
|
||||||
color: 'base.900',
|
p: 0,
|
||||||
bg: 'accent.300',
|
}}
|
||||||
borderRadius: 'full',
|
noOfLines={1}
|
||||||
w: 4,
|
/>
|
||||||
h: 4,
|
<EditableInput
|
||||||
right: -1,
|
sx={{
|
||||||
top: -1,
|
color: 'base.50',
|
||||||
}}
|
fontSize: 'xs',
|
||||||
>
|
borderColor: 'base.500',
|
||||||
<Text fontSize="2xs">{board.image_count}</Text>
|
p: 0,
|
||||||
</Flex>
|
outline: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Editable>
|
||||||
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
|
@ -14,7 +14,7 @@ import { useAppToaster } from 'app/components/Toaster';
|
|||||||
import { imageSelected } from '../store/gallerySlice';
|
import { imageSelected } from '../store/gallerySlice';
|
||||||
import IAIDndImage from 'common/components/IAIDndImage';
|
import IAIDndImage from 'common/components/IAIDndImage';
|
||||||
import { ImageDTO } from 'services/api';
|
import { ImageDTO } from 'services/api';
|
||||||
import { IAIImageFallback } from 'common/components/IAIImageFallback';
|
import { IAIImageLoadingFallback } from 'common/components/IAIImageFallback';
|
||||||
import { RootState } from 'app/store/store';
|
import { RootState } from 'app/store/store';
|
||||||
import { selectImagesById } from '../store/imagesSlice';
|
import { selectImagesById } from '../store/imagesSlice';
|
||||||
import { useGetImageDTOQuery } from 'services/apiSlice';
|
import { useGetImageDTOQuery } from 'services/apiSlice';
|
||||||
@ -116,7 +116,7 @@ const CurrentImagePreview = () => {
|
|||||||
<IAIDndImage
|
<IAIDndImage
|
||||||
image={image}
|
image={image}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
fallback={<IAIImageFallback sx={{ bg: 'none' }} />}
|
fallback={<IAIImageLoadingFallback sx={{ bg: 'none' }} />}
|
||||||
isUploadDisabled={true}
|
isUploadDisabled={true}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
|
Button,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
Flex,
|
Flex,
|
||||||
FlexProps,
|
FlexProps,
|
||||||
Grid,
|
Grid,
|
||||||
Icon,
|
Icon,
|
||||||
Text,
|
Text,
|
||||||
|
VStack,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
|
useDisclosure,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import IAIButton from 'common/components/IAIButton';
|
import IAIButton from 'common/components/IAIButton';
|
||||||
@ -54,10 +57,10 @@ import {
|
|||||||
selectImagesAll,
|
selectImagesAll,
|
||||||
} from '../store/imagesSlice';
|
} from '../store/imagesSlice';
|
||||||
import { receivedPageOfImages } from 'services/thunks/image';
|
import { receivedPageOfImages } from 'services/thunks/image';
|
||||||
import { boardSelector } from '../store/boardSelectors';
|
|
||||||
import { boardCreated } from '../../../services/thunks/board';
|
|
||||||
import BoardsList from './Boards/BoardsList';
|
import BoardsList from './Boards/BoardsList';
|
||||||
import { selectBoardsById } from '../store/boardSlice';
|
import { boardsSelector, selectBoardsById } from '../store/boardSlice';
|
||||||
|
import { ChevronUpIcon } from '@chakra-ui/icons';
|
||||||
|
import { useListAllBoardsQuery } from 'services/apiSlice';
|
||||||
|
|
||||||
const itemSelector = createSelector(
|
const itemSelector = createSelector(
|
||||||
[(state: RootState) => state],
|
[(state: RootState) => state],
|
||||||
@ -89,7 +92,7 @@ const itemSelector = createSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const mainSelector = createSelector(
|
const mainSelector = createSelector(
|
||||||
[gallerySelector, uiSelector, boardSelector],
|
[gallerySelector, uiSelector, boardsSelector],
|
||||||
(gallery, ui, boards) => {
|
(gallery, ui, boards) => {
|
||||||
const {
|
const {
|
||||||
galleryImageMinimumWidth,
|
galleryImageMinimumWidth,
|
||||||
@ -109,7 +112,7 @@ const mainSelector = createSelector(
|
|||||||
shouldUseSingleGalleryColumn,
|
shouldUseSingleGalleryColumn,
|
||||||
selectedImage,
|
selectedImage,
|
||||||
galleryView,
|
galleryView,
|
||||||
boards,
|
selectedBoardId: boards.selectedBoardId,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
@ -142,12 +145,18 @@ const ImageGalleryContent = () => {
|
|||||||
shouldUseSingleGalleryColumn,
|
shouldUseSingleGalleryColumn,
|
||||||
selectedImage,
|
selectedImage,
|
||||||
galleryView,
|
galleryView,
|
||||||
boards,
|
selectedBoardId,
|
||||||
} = useAppSelector(mainSelector);
|
} = useAppSelector(mainSelector);
|
||||||
|
|
||||||
const { items, areMoreAvailable, isLoading, categories, selectedBoard } =
|
const { items, areMoreAvailable, isLoading, categories } =
|
||||||
useAppSelector(itemSelector);
|
useAppSelector(itemSelector);
|
||||||
|
|
||||||
|
const { selectedBoard } = useListAllBoardsQuery(undefined, {
|
||||||
|
selectFromResult: ({ data }) => ({
|
||||||
|
selectedBoard: data?.find((b) => b.board_id === selectedBoardId),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
const handleLoadMoreImages = useCallback(() => {
|
const handleLoadMoreImages = useCallback(() => {
|
||||||
dispatch(receivedPageOfImages());
|
dispatch(receivedPageOfImages());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
@ -159,6 +168,8 @@ const ImageGalleryContent = () => {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}, [areMoreAvailable, handleLoadMoreImages, isLoading]);
|
}, [areMoreAvailable, handleLoadMoreImages, isLoading]);
|
||||||
|
|
||||||
|
const { isOpen: isBoardListOpen, onToggle } = useDisclosure();
|
||||||
|
|
||||||
const handleChangeGalleryImageMinimumWidth = (v: number) => {
|
const handleChangeGalleryImageMinimumWidth = (v: number) => {
|
||||||
dispatch(setGalleryImageMinimumWidth(v));
|
dispatch(setGalleryImageMinimumWidth(v));
|
||||||
};
|
};
|
||||||
@ -197,50 +208,71 @@ const ImageGalleryContent = () => {
|
|||||||
dispatch(setGalleryView('assets'));
|
dispatch(setGalleryView('assets'));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const handleClickBoardsView = useCallback(() => {
|
|
||||||
dispatch(setGalleryView('boards'));
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<VStack
|
||||||
sx={{
|
sx={{
|
||||||
gap: 2,
|
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
h: 'full',
|
h: 'full',
|
||||||
w: 'full',
|
w: 'full',
|
||||||
borderRadius: 'base',
|
borderRadius: 'base',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
<Box sx={{ w: 'full' }}>
|
||||||
ref={resizeObserverRef}
|
<Flex
|
||||||
alignItems="center"
|
ref={resizeObserverRef}
|
||||||
justifyContent="space-between"
|
sx={{
|
||||||
gap={1}
|
alignItems: 'center',
|
||||||
>
|
justifyContent: 'space-between',
|
||||||
<ButtonGroup isAttached>
|
gap: 2,
|
||||||
<IAIIconButton
|
}}
|
||||||
tooltip={t('gallery.images')}
|
>
|
||||||
aria-label={t('gallery.images')}
|
<ButtonGroup isAttached>
|
||||||
onClick={handleClickImagesCategory}
|
<IAIIconButton
|
||||||
isChecked={galleryView === 'images'}
|
tooltip={t('gallery.images')}
|
||||||
|
aria-label={t('gallery.images')}
|
||||||
|
onClick={handleClickImagesCategory}
|
||||||
|
isChecked={galleryView === 'images'}
|
||||||
|
size="sm"
|
||||||
|
icon={<FaImage />}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
tooltip={t('gallery.assets')}
|
||||||
|
aria-label={t('gallery.assets')}
|
||||||
|
onClick={handleClickAssetsCategory}
|
||||||
|
isChecked={galleryView === 'assets'}
|
||||||
|
size="sm"
|
||||||
|
icon={<FaServer />}
|
||||||
|
/>
|
||||||
|
</ButtonGroup>
|
||||||
|
<Flex
|
||||||
|
as={Button}
|
||||||
|
onClick={onToggle}
|
||||||
size="sm"
|
size="sm"
|
||||||
icon={<FaImage />}
|
variant="ghost"
|
||||||
/>
|
sx={{
|
||||||
<IAIIconButton
|
w: 'full',
|
||||||
tooltip={t('gallery.assets')}
|
justifyContent: 'center',
|
||||||
aria-label={t('gallery.assets')}
|
alignItems: 'center',
|
||||||
onClick={handleClickAssetsCategory}
|
px: 2,
|
||||||
isChecked={galleryView === 'assets'}
|
_hover: {
|
||||||
size="sm"
|
bg: 'base.800',
|
||||||
icon={<FaServer />}
|
},
|
||||||
/>
|
}}
|
||||||
</ButtonGroup>
|
>
|
||||||
<Flex>
|
<Text
|
||||||
<Text noOfLines={1}>
|
noOfLines={1}
|
||||||
{selectedBoard ? selectedBoard.board_name : 'All Images'}
|
sx={{ w: 'full', color: 'base.200', fontWeight: 600 }}
|
||||||
</Text>
|
>
|
||||||
</Flex>
|
{selectedBoard ? selectedBoard.board_name : 'All Images'}
|
||||||
<Flex gap={2}>
|
</Text>
|
||||||
|
<ChevronUpIcon
|
||||||
|
sx={{
|
||||||
|
transform: isBoardListOpen ? 'rotate(0deg)' : 'rotate(180deg)',
|
||||||
|
transitionProperty: 'common',
|
||||||
|
transitionDuration: 'normal',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
<IAIPopover
|
<IAIPopover
|
||||||
triggerComponent={
|
triggerComponent={
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
@ -298,11 +330,11 @@ const ImageGalleryContent = () => {
|
|||||||
icon={shouldPinGallery ? <BsPinAngleFill /> : <BsPinAngle />}
|
icon={shouldPinGallery ? <BsPinAngleFill /> : <BsPinAngle />}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
<Box>
|
||||||
<Box>
|
<BoardsList isOpen={isBoardListOpen} />
|
||||||
<BoardsList />
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Flex direction="column" gap={2} h="full">
|
<Flex direction="column" gap={2} h="full" w="full">
|
||||||
{items.length || areMoreAvailable ? (
|
{items.length || areMoreAvailable ? (
|
||||||
<>
|
<>
|
||||||
<Box ref={rootRef} data-overlayscrollbars="" h="100%">
|
<Box ref={rootRef} data-overlayscrollbars="" h="100%">
|
||||||
@ -378,7 +410,7 @@ const ImageGalleryContent = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</VStack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
export const SelectedItemOverlay = () => (
|
||||||
|
<motion.div
|
||||||
|
initial={{
|
||||||
|
opacity: 0,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
opacity: 1,
|
||||||
|
transition: { duration: 0.1 },
|
||||||
|
}}
|
||||||
|
exit={{
|
||||||
|
opacity: 0,
|
||||||
|
transition: { duration: 0.1 },
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
insetInlineStart: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
boxShadow: 'inset 0px 0px 0px 2px var(--invokeai-colors-accent-300)',
|
||||||
|
borderRadius: 'var(--invokeai-radii-base)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
@ -10,7 +10,7 @@ import { generationSelector } from 'features/parameters/store/generationSelector
|
|||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import IAIDndImage from 'common/components/IAIDndImage';
|
import IAIDndImage from 'common/components/IAIDndImage';
|
||||||
import { ImageDTO } from 'services/api';
|
import { ImageDTO } from 'services/api';
|
||||||
import { IAIImageFallback } from 'common/components/IAIImageFallback';
|
import { IAIImageLoadingFallback } from 'common/components/IAIImageFallback';
|
||||||
import { useGetImageDTOQuery } from 'services/apiSlice';
|
import { useGetImageDTOQuery } from 'services/apiSlice';
|
||||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||||
|
|
||||||
@ -65,7 +65,7 @@ const InitialImagePreview = () => {
|
|||||||
image={image}
|
image={image}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
onReset={handleReset}
|
onReset={handleReset}
|
||||||
fallback={<IAIImageFallback sx={{ bg: 'none' }} />}
|
fallback={<IAIImageLoadingFallback sx={{ bg: 'none' }} />}
|
||||||
postUploadAction={{ type: 'SET_INITIAL_IMAGE' }}
|
postUploadAction={{ type: 'SET_INITIAL_IMAGE' }}
|
||||||
withResetIcon
|
withResetIcon
|
||||||
/>
|
/>
|
||||||
|
Reference in New Issue
Block a user