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