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