feat(ui): first pass at boards styling

This commit is contained in:
psychedelicious
2023-06-21 17:58:22 +10:00
parent 2489d5459f
commit bd533426fc
11 changed files with 300 additions and 204 deletions

View File

@ -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,

View File

@ -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>
);
};

View File

@ -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 && (

View File

@ -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>
); );
}; };

View File

@ -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>
); );
}; };

View File

@ -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>
); );
}; };

View File

@ -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,33 +113,46 @@ 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>
<Box sx={{ width: 'full' }}>
<Editable <Editable
defaultValue={board_name} defaultValue={board_name}
submitOnBlur={false} submitOnBlur={false}
@ -158,34 +161,26 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
}} }}
> >
<EditablePreview <EditablePreview
sx={{ color: 'base.200', fontSize: 'xs', textAlign: 'left' }} sx={{
color: isSelected ? 'base.50' : 'base.200',
fontWeight: isSelected ? 600 : undefined,
fontSize: 'xs',
textAlign: 'center',
p: 0,
}}
noOfLines={1} noOfLines={1}
/> />
<EditableInput <EditableInput
sx={{ sx={{
color: 'base.200', color: 'base.50',
fontSize: 'xs', fontSize: 'xs',
textAlign: 'left',
borderColor: 'base.500', borderColor: 'base.500',
p: 0,
outline: 0,
}} }}
/> />
</Editable> </Editable>
<Flex </Box>
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>
</Flex> </Flex>
)} )}
</ContextMenu> </ContextMenu>

View File

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

View File

@ -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,25 +208,23 @@ 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',
}} }}
> >
<Box sx={{ w: 'full' }}>
<Flex <Flex
ref={resizeObserverRef} ref={resizeObserverRef}
alignItems="center" sx={{
justifyContent="space-between" alignItems: 'center',
gap={1} justifyContent: 'space-between',
gap: 2,
}}
> >
<ButtonGroup isAttached> <ButtonGroup isAttached>
<IAIIconButton <IAIIconButton
@ -235,12 +244,35 @@ const ImageGalleryContent = () => {
icon={<FaServer />} icon={<FaServer />}
/> />
</ButtonGroup> </ButtonGroup>
<Flex> <Flex
<Text noOfLines={1}> as={Button}
onClick={onToggle}
size="sm"
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'} {selectedBoard ? selectedBoard.board_name : 'All Images'}
</Text> </Text>
<ChevronUpIcon
sx={{
transform: isBoardListOpen ? 'rotate(0deg)' : 'rotate(180deg)',
transitionProperty: 'common',
transitionDuration: 'normal',
}}
/>
</Flex> </Flex>
<Flex gap={2}>
<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 /> <BoardsList isOpen={isBoardListOpen} />
</Box> </Box>
<Flex direction="column" gap={2} h="full"> </Box>
<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>
); );
}; };

View File

@ -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)',
}}
/>
);

View File

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