From bd533426fc0d238a5afdec9a2e6aeb308f5b5d51 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 21 Jun 2023 17:58:22 +1000
Subject: [PATCH] feat(ui): first pass at boards styling
---
.../web/src/common/components/IAIDndImage.tsx | 4 +-
.../common/components/IAIImageFallback.tsx | 46 +++++-
.../components/ControlNetImagePreview.tsx | 4 +-
.../components/Boards/AddBoardButton.tsx | 42 ++---
.../components/Boards/AllImagesBoard.tsx | 26 ++-
.../gallery/components/Boards/BoardsList.tsx | 73 +++++----
.../components/Boards/HoverableBoard.tsx | 149 +++++++++---------
.../components/CurrentImagePreview.tsx | 4 +-
.../components/ImageGalleryContent.tsx | 126 +++++++++------
.../components/SelectedItemOverlay.tsx | 26 +++
.../ImageToImage/InitialImagePreview.tsx | 4 +-
11 files changed, 300 insertions(+), 204 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/gallery/components/SelectedItemOverlay.tsx
diff --git a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx
index 669a68c88a..e54b4a8872 100644
--- a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx
+++ b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx
@@ -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 = ,
+ fallback = ,
payloadImage,
minSize = 24,
postUploadAction,
diff --git a/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx b/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx
index 3d34fbca9e..03a00d5b1c 100644
--- a/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx
+++ b/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx
@@ -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) => {
);
};
+
+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 (
+
+
+
+ );
+};
diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx
index a121875f59..217caf9461 100644
--- a/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx
+++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx
@@ -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',
}}
>
-
+
)}
{controlImage && (
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/AddBoardButton.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/AddBoardButton.tsx
index 284e6558ac..632cebcb33 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/AddBoardButton.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/AddBoardButton.tsx
@@ -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 (
-
-
- {isLoading ? (
-
- ) : (
-
- )}
-
- New Board
-
+ Add Board
+
);
};
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/AllImagesBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/AllImagesBoard.tsx
index 51a7609678..51e95b64c4 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/AllImagesBoard.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/AllImagesBoard.tsx
@@ -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}
>
-
+
+
+ {isSelected && }
+
- All Images
+
+ All Images
+
);
};
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList.tsx
index 5854c3fe7c..fb68021dee 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList.tsx
@@ -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 (
-
- <>
-
+
+
+
{
/>
{searchText && searchText.length && (
-
+ }
+ />
)}
-
+
+
{
className="list-container"
sx={{
gap: 2,
- gridTemplateRows: '5rem 5rem',
+ gridTemplateRows: '5.5rem 5.5rem',
gridAutoFlow: 'column dense',
gridAutoColumns: '4rem',
}}
>
- {!searchMode && (
- <>
-
-
- >
- )}
+ {!searchMode && }
{filteredBoards &&
filteredBoards.map((board) => (
))}
- >
-
+
+
);
};
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/HoverableBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/HoverableBoard.tsx
index 71e080ff17..a2c07e4870 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/HoverableBoard.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/HoverableBoard.tsx
@@ -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 (
@@ -112,7 +103,6 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
>
{(ref) => (
{
cursor: 'pointer',
w: 'full',
h: 'full',
- gap: 1,
}}
>
- }
- isUploadDisabled={true}
- />
+ {board.cover_image_name && coverImage?.image_url && (
+
+ )}
+ {!(board.cover_image_name && coverImage?.image_url) && (
+
+ )}
+
+ {board.image_count}
+
+
+ {isSelected && }
+
+
+ {isDropActive && }
+
- {
- handleUpdateBoardName(nextValue);
- }}
- >
-
-
+ {
+ handleUpdateBoardName(nextValue);
}}
- />
-
-
- {board.image_count}
-
+ >
+
+
+
+
)}
diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx
index bff32f1d78..49376b4807 100644
--- a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx
@@ -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 = () => {
}
+ fallback={}
isUploadDisabled={true}
/>
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx
index 48bd2bde74..8648962c8c 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx
@@ -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 (
-
-
-
-
+
+
+ }
+ />
+ }
+ />
+
+ }
- />
- }
- />
-
-
-
- {selectedBoard ? selectedBoard.board_name : 'All Images'}
-
-
-
+ variant="ghost"
+ sx={{
+ w: 'full',
+ justifyContent: 'center',
+ alignItems: 'center',
+ px: 2,
+ _hover: {
+ bg: 'base.800',
+ },
+ }}
+ >
+
+ {selectedBoard ? selectedBoard.board_name : 'All Images'}
+
+
+
{
icon={shouldPinGallery ? : }
/>
-
-
-
+
+
+
-
+
{items.length || areMoreAvailable ? (
<>
@@ -378,7 +410,7 @@ const ImageGalleryContent = () => {
)}
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/gallery/components/SelectedItemOverlay.tsx b/invokeai/frontend/web/src/features/gallery/components/SelectedItemOverlay.tsx
new file mode 100644
index 0000000000..7038b4b64f
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/SelectedItemOverlay.tsx
@@ -0,0 +1,26 @@
+import { motion } from 'framer-motion';
+
+export const SelectedItemOverlay = () => (
+
+);
diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx
index e4b3a9191e..fbfa00e2a1 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx
@@ -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={}
+ fallback={}
postUploadAction={{ type: 'SET_INITIAL_IMAGE' }}
withResetIcon
/>