mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): rough out boards UI
This commit is contained in:
parent
5865ecd530
commit
d306a84447
@ -0,0 +1,36 @@
|
||||
import { Flex, Icon, Text } from '@chakra-ui/react';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
|
||||
const AddBoardButton = () => {
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
flexDir: 'column',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: '1px',
|
||||
borderRadius: 'base',
|
||||
borderColor: 'base.800',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
aspectRatio: '1/1',
|
||||
}}
|
||||
>
|
||||
<Icon boxSize={8} color="base.700" as={FaPlus} />
|
||||
</Flex>
|
||||
<Text sx={{ color: 'base.200', fontSize: 'xs' }}>New Board</Text>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddBoardButton;
|
@ -0,0 +1,53 @@
|
||||
import { Grid } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { selectBoardsAll } from 'features/gallery/store/boardSlice';
|
||||
import { memo } from 'react';
|
||||
import HoverableBoard from './HoverableBoard';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||
import AddBoardButton from './AddBoardButton';
|
||||
|
||||
const selector = createSelector(
|
||||
selectBoardsAll,
|
||||
(boards) => {
|
||||
return { boards };
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
const BoardsList = () => {
|
||||
const { boards } = useAppSelector(selector);
|
||||
|
||||
return (
|
||||
<OverlayScrollbarsComponent
|
||||
defer
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
options={{
|
||||
scrollbars: {
|
||||
visibility: 'auto',
|
||||
autoHide: 'move',
|
||||
autoHideDelay: 1300,
|
||||
theme: 'os-theme-dark',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Grid
|
||||
className="list-container"
|
||||
sx={{
|
||||
gap: 2,
|
||||
gridTemplateRows: '5rem 5rem',
|
||||
gridAutoFlow: 'column dense',
|
||||
gridAutoColumns: '4rem',
|
||||
}}
|
||||
>
|
||||
<AddBoardButton />
|
||||
{boards.map((board) => (
|
||||
<HoverableBoard key={board.board_id} board={board} />
|
||||
))}
|
||||
</Grid>
|
||||
</OverlayScrollbarsComponent>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(BoardsList);
|
@ -1,7 +1,15 @@
|
||||
import { Box, Image, MenuItem, MenuList, Text } from '@chakra-ui/react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Icon,
|
||||
Image,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Text,
|
||||
} from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
import { FaImage } from 'react-icons/fa';
|
||||
import { PropsWithChildren, memo, useCallback, useState } from 'react';
|
||||
import { FaFolder, FaImage } from 'react-icons/fa';
|
||||
import { ContextMenu } from 'chakra-ui-contextmenu';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||
@ -11,10 +19,12 @@ import { EntityId, createSelector } from '@reduxjs/toolkit';
|
||||
import {
|
||||
selectFilteredImagesIds,
|
||||
selectImagesById,
|
||||
} from '../store/imagesSlice';
|
||||
import { RootState } from '../../../app/store/store';
|
||||
import { defaultSelectorOptions } from '../../../app/store/util/defaultMemoizeOptions';
|
||||
} from '../../store/imagesSlice';
|
||||
import { RootState } from '../../../../app/store/store';
|
||||
import { defaultSelectorOptions } from '../../../../app/store/util/defaultMemoizeOptions';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { IAIImageFallback } from 'common/components/IAIImageFallback';
|
||||
import { boardIdSelected } from 'features/gallery/store/boardSlice';
|
||||
|
||||
interface HoverableBoardProps {
|
||||
board: BoardDTO;
|
||||
@ -26,20 +36,16 @@ interface HoverableBoardProps {
|
||||
const HoverableBoard = memo(({ board }: HoverableBoardProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { board_name, board_id, cover_image_name } = board;
|
||||
|
||||
const coverImage = useAppSelector((state) =>
|
||||
selectImagesById(state, cover_image_name as EntityId)
|
||||
);
|
||||
const { board_name, board_id, cover_image_url } = board;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleSelectBoard = useCallback(() => {
|
||||
// dispatch(imageSelected(board_id));
|
||||
}, []);
|
||||
dispatch(boardIdSelected(board_id));
|
||||
}, [board_id, dispatch]);
|
||||
|
||||
return (
|
||||
<Box sx={{ w: 'full', h: 'full', touchAction: 'none' }}>
|
||||
<Box sx={{ touchAction: 'none' }}>
|
||||
<ContextMenu<HTMLDivElement>
|
||||
menuProps={{ size: 'sm', isLazy: true }}
|
||||
renderMenu={() => (
|
||||
@ -54,42 +60,50 @@ const HoverableBoard = memo(({ board }: HoverableBoardProps) => {
|
||||
)}
|
||||
>
|
||||
{(ref) => (
|
||||
<Box
|
||||
<Flex
|
||||
position="relative"
|
||||
key={board_id}
|
||||
userSelect="none"
|
||||
onClick={handleSelectBoard}
|
||||
ref={ref}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDir: 'column',
|
||||
justifyContent: 'center',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
transition: 'transform 0.2s ease-out',
|
||||
aspectRatio: '1/1',
|
||||
cursor: 'pointer',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
loading="lazy"
|
||||
// objectFit={
|
||||
// shouldUseSingleGalleryColumn ? 'contain' : galleryImageObjectFit
|
||||
// }
|
||||
draggable={false}
|
||||
rounded="md"
|
||||
src={coverImage ? coverImage.thumbnail_url : undefined}
|
||||
fallback={<FaImage />}
|
||||
<Flex
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: '1px',
|
||||
borderRadius: 'base',
|
||||
borderColor: 'base.800',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
aspectRatio: '1/1',
|
||||
}}
|
||||
/>
|
||||
<Text textAlign="center">{board_name}</Text>
|
||||
</Box>
|
||||
>
|
||||
{cover_image_url ? (
|
||||
<Image
|
||||
loading="lazy"
|
||||
objectFit="cover"
|
||||
draggable={false}
|
||||
rounded="md"
|
||||
src={cover_image_url}
|
||||
fallback={<IAIImageFallback />}
|
||||
sx={{}}
|
||||
/>
|
||||
) : (
|
||||
<Icon boxSize={8} color="base.700" as={FaFolder} />
|
||||
)}
|
||||
</Flex>
|
||||
<Text sx={{ color: 'base.200', fontSize: 'xs' }}>{board_name}</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</ContextMenu>
|
||||
</Box>
|
@ -57,9 +57,11 @@ import { receivedPageOfImages } from 'services/thunks/image';
|
||||
import { boardSelector } from '../store/boardSelectors';
|
||||
import { BoardDTO, ImageDTO } from '../../../services/api';
|
||||
import { isBoardDTO, isImageDTO } from '../../../services/types/guards';
|
||||
import HoverableBoard from './HoverableBoard';
|
||||
import HoverableBoard from './Boards/HoverableBoard';
|
||||
import IAIInput from '../../../common/components/IAIInput';
|
||||
import { boardCreated } from '../../../services/thunks/board';
|
||||
import BoardsList from './Boards/BoardsList';
|
||||
import { selectBoardsById } from '../store/boardSlice';
|
||||
|
||||
const itemSelector = createSelector(
|
||||
[(state: RootState) => state],
|
||||
@ -70,24 +72,23 @@ const itemSelector = createSelector(
|
||||
let areMoreAvailable = false;
|
||||
let isLoading = true;
|
||||
|
||||
if (gallery.galleryView === 'images' || gallery.galleryView === 'assets') {
|
||||
const { categories } = images;
|
||||
const { categories } = images;
|
||||
|
||||
const allImages = selectImagesAll(state);
|
||||
items = allImages.filter((i) => categories.includes(i.image_category));
|
||||
areMoreAvailable = items.length < images.total;
|
||||
isLoading = images.isLoading;
|
||||
} else if (gallery.galleryView === 'boards') {
|
||||
items = Object.values(boards.entities) as BoardDTO[];
|
||||
areMoreAvailable = items.length < boards.total;
|
||||
isLoading = boards.isLoading;
|
||||
}
|
||||
const allImages = selectImagesAll(state);
|
||||
items = allImages.filter((i) => categories.includes(i.image_category));
|
||||
areMoreAvailable = items.length < images.total;
|
||||
isLoading = images.isLoading;
|
||||
|
||||
const selectedBoard = boards.selectedBoardId
|
||||
? selectBoardsById(state, boards.selectedBoardId)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
items,
|
||||
isLoading,
|
||||
areMoreAvailable,
|
||||
categories: images.categories,
|
||||
selectedBoard,
|
||||
};
|
||||
},
|
||||
defaultSelectorOptions
|
||||
@ -153,7 +154,7 @@ const ImageGalleryContent = () => {
|
||||
boards,
|
||||
} = useAppSelector(mainSelector);
|
||||
|
||||
const { items, areMoreAvailable, isLoading, categories } =
|
||||
const { items, areMoreAvailable, isLoading, categories, selectedBoard } =
|
||||
useAppSelector(itemSelector);
|
||||
|
||||
const handleLoadMoreImages = useCallback(() => {
|
||||
@ -247,17 +248,14 @@ const ImageGalleryContent = () => {
|
||||
size="sm"
|
||||
icon={<FaServer />}
|
||||
/>
|
||||
<IAIIconButton
|
||||
tooltip={t('gallery.boards')}
|
||||
aria-label={t('gallery.boards')}
|
||||
onClick={handleClickBoardsView}
|
||||
isChecked={galleryView === 'boards'}
|
||||
size="sm"
|
||||
icon={<FaFolder />}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
{selectedBoard && (
|
||||
<Flex>
|
||||
<Text>{selectedBoard.board_name}</Text>
|
||||
</Flex>
|
||||
)}
|
||||
<Flex gap={2}>
|
||||
<IAIPopover
|
||||
{/* <IAIPopover
|
||||
triggerComponent={
|
||||
<IAIIconButton
|
||||
tooltip="Add Board"
|
||||
@ -283,7 +281,7 @@ const ImageGalleryContent = () => {
|
||||
Create
|
||||
</IAIButton>
|
||||
</Flex>
|
||||
</IAIPopover>
|
||||
</IAIPopover> */}
|
||||
<IAIPopover
|
||||
triggerComponent={
|
||||
<IAIIconButton
|
||||
@ -342,6 +340,9 @@ const ImageGalleryContent = () => {
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Box>
|
||||
<BoardsList />
|
||||
</Box>
|
||||
<Flex direction="column" gap={2} h="full">
|
||||
{items.length || areMoreAvailable ? (
|
||||
<>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
EntityId,
|
||||
PayloadAction,
|
||||
Update,
|
||||
createEntityAdapter,
|
||||
@ -19,6 +20,7 @@ type AdditionalBoardsState = {
|
||||
limit: number;
|
||||
total: number;
|
||||
isLoading: boolean;
|
||||
selectedBoardId: EntityId | null;
|
||||
};
|
||||
|
||||
export const initialBoardsState =
|
||||
@ -27,6 +29,7 @@ export const initialBoardsState =
|
||||
limit: 0,
|
||||
total: 0,
|
||||
isLoading: false,
|
||||
selectedBoardId: null,
|
||||
});
|
||||
|
||||
export type BoardsState = typeof initialBoardsState;
|
||||
@ -44,6 +47,9 @@ const boardsSlice = createSlice({
|
||||
boardRemoved: (state, action: PayloadAction<string>) => {
|
||||
boardsAdapter.removeOne(state, action.payload);
|
||||
},
|
||||
boardIdSelected: (state, action: PayloadAction<string>) => {
|
||||
state.selectedBoardId = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(receivedBoards.pending, (state) => {
|
||||
@ -71,7 +77,9 @@ export const {
|
||||
selectTotal: selectBoardsTotal,
|
||||
} = boardsAdapter.getSelectors<RootState>((state) => state.boards);
|
||||
|
||||
export const { boardUpserted, boardUpdatedOne, boardRemoved } =
|
||||
export const { boardUpserted, boardUpdatedOne, boardRemoved, boardIdSelected } =
|
||||
boardsSlice.actions;
|
||||
|
||||
export const boardsSelector = (state: RootState) => state.boards;
|
||||
|
||||
export default boardsSlice.reducer;
|
||||
|
Loading…
Reference in New Issue
Block a user