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 />