mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
add view that displays private boards with shared boards
This commit is contained in:
parent
8b55900035
commit
6437ef3f82
@ -32,6 +32,7 @@ class DeleteBoardResult(BaseModel):
|
|||||||
)
|
)
|
||||||
async def create_board(
|
async def create_board(
|
||||||
board_name: str = Query(description="The name of the board to create"),
|
board_name: str = Query(description="The name of the board to create"),
|
||||||
|
private_board: bool = Query(default=False, description="Whether the board is private"),
|
||||||
) -> BoardDTO:
|
) -> BoardDTO:
|
||||||
"""Creates a board"""
|
"""Creates a board"""
|
||||||
try:
|
try:
|
||||||
|
@ -65,6 +65,7 @@ export type AppConfig = {
|
|||||||
*/
|
*/
|
||||||
shouldUpdateImagesOnConnect: boolean;
|
shouldUpdateImagesOnConnect: boolean;
|
||||||
shouldFetchMetadataFromApi: boolean;
|
shouldFetchMetadataFromApi: boolean;
|
||||||
|
allowPrivateBoards: boolean;
|
||||||
disabledTabs: InvokeTabName[];
|
disabledTabs: InvokeTabName[];
|
||||||
disabledFeatures: AppFeature[];
|
disabledFeatures: AppFeature[];
|
||||||
disabledSDFeatures: SDFeature[];
|
disabledSDFeatures: SDFeature[];
|
||||||
|
@ -46,11 +46,10 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
|
|||||||
);
|
);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Box w="full" h="full" userSelect="none">
|
<Box w="full" userSelect="none">
|
||||||
<Flex
|
<Flex
|
||||||
onMouseOver={handleMouseOver}
|
onMouseOver={handleMouseOver}
|
||||||
onMouseOut={handleMouseOut}
|
onMouseOut={handleMouseOut}
|
||||||
position="relative"
|
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
aspectRatio="1/1"
|
aspectRatio="1/1"
|
||||||
@ -65,47 +64,32 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
onClick={handleSelectBoard}
|
onClick={handleSelectBoard}
|
||||||
w="full"
|
w="full"
|
||||||
h="full"
|
|
||||||
position="relative"
|
position="relative"
|
||||||
justifyContent="center"
|
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
borderRadius="base"
|
borderRadius="base"
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
bg="base.800"
|
bg="base.800"
|
||||||
>
|
>
|
||||||
<Flex w="full" h="full" justifyContent="center" alignItems="center">
|
<Image
|
||||||
<Image
|
src={InvokeLogoSVG}
|
||||||
src={InvokeLogoSVG}
|
alt="invoke-ai-logo"
|
||||||
alt="invoke-ai-logo"
|
opacity={0.7}
|
||||||
opacity={0.7}
|
mixBlendMode="overlay"
|
||||||
mixBlendMode="overlay"
|
w={8}
|
||||||
mt={-6}
|
h={8}
|
||||||
w={16}
|
minW={8}
|
||||||
h={16}
|
minH={8}
|
||||||
minW={16}
|
userSelect="none"
|
||||||
minH={16}
|
/>
|
||||||
userSelect="none"
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
{autoAddBoardId === 'none' && <AutoAddIcon />}
|
{autoAddBoardId === 'none' && <AutoAddIcon />}
|
||||||
<Flex
|
<Text
|
||||||
position="absolute"
|
|
||||||
bottom={0}
|
|
||||||
left={0}
|
|
||||||
p={1}
|
p={1}
|
||||||
justifyContent="center"
|
|
||||||
alignItems="center"
|
|
||||||
w="full"
|
|
||||||
maxW="full"
|
|
||||||
borderBottomRadius="base"
|
|
||||||
bg={isSelected ? 'invokeBlue.400' : 'base.600'}
|
|
||||||
color={isSelected ? 'base.800' : 'base.100'}
|
|
||||||
lineHeight="short"
|
lineHeight="short"
|
||||||
fontSize="xs"
|
fontSize="xs"
|
||||||
fontWeight={isSelected ? 'bold' : 'normal'}
|
color={isSelected ? 'blue' : 'white'}
|
||||||
>
|
>
|
||||||
{boardName}
|
{boardName}
|
||||||
</Flex>
|
</Text>
|
||||||
<SelectionOverlay isSelected={isSelected} isSelectedForCompare={false} isHovered={isHovered} />
|
<SelectionOverlay isSelected={isSelected} isSelectedForCompare={false} isHovered={isHovered} />
|
||||||
<IAIDroppable data={droppableData} dropLabel={<Text fontSize="md">{t('unifiedCanvas.move')}</Text>} />
|
<IAIDroppable data={droppableData} dropLabel={<Text fontSize="md">{t('unifiedCanvas.move')}</Text>} />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
import { Icon } from '@invoke-ai/ui-library';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiPlusBold } from 'react-icons/pi';
|
||||||
|
import { useCreateBoardMutation } from 'services/api/endpoints/boards';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
privateBoard: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AddBoardButton = ({ privateBoard }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [createBoard] = useCreateBoardMutation();
|
||||||
|
const DEFAULT_BOARD_NAME = t('boards.myBoard');
|
||||||
|
const handleCreateBoard = useCallback(() => {
|
||||||
|
createBoard({ DEFAULT_BOARD_NAME, privateBoard });
|
||||||
|
}, [createBoard, DEFAULT_BOARD_NAME, privateBoard]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
as={PiPlusBold}
|
||||||
|
boxSize={6}
|
||||||
|
transitionProperty="common"
|
||||||
|
transitionDuration="normal"
|
||||||
|
color="base.400"
|
||||||
|
onClick={handleCreateBoard}
|
||||||
|
cursor="pointer"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(AddBoardButton);
|
@ -0,0 +1,22 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useGetBoardAssetsTotalQuery, useGetBoardImagesTotalQuery } from 'services/api/endpoints/boards';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
board_id: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BoardTotalsTooltip = ({ board_id, isArchived }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { imagesTotal } = useGetBoardImagesTotalQuery(board_id, {
|
||||||
|
selectFromResult: ({ data }) => {
|
||||||
|
return { imagesTotal: data?.total ?? 0 };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { assetsTotal } = useGetBoardAssetsTotalQuery(board_id, {
|
||||||
|
selectFromResult: ({ data }) => {
|
||||||
|
return { assetsTotal: data?.total ?? 0 };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return `${t('boards.imagesWithCount', { count: imagesTotal })}, ${t('boards.assetsWithCount', { count: assetsTotal })}${isArchived ? ` (${t('boards.archived')})` : ''}`;
|
||||||
|
};
|
@ -0,0 +1,120 @@
|
|||||||
|
import { Collapse, Flex, Icon, Text } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
||||||
|
import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModal';
|
||||||
|
import GallerySettingsPopover from 'features/gallery/components/GallerySettingsPopover/GallerySettingsPopover';
|
||||||
|
import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||||
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||||
|
import type { CSSProperties } from 'react';
|
||||||
|
import { memo, useState } from 'react';
|
||||||
|
import { PiCaretUpBold, PiPlusBold } from 'react-icons/pi';
|
||||||
|
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
||||||
|
import type { BoardDTO } from 'services/api/types';
|
||||||
|
|
||||||
|
import AddBoardButton from './AddBoardButton';
|
||||||
|
import BoardsSearch from './BoardsSearch';
|
||||||
|
import GalleryBoard from './GalleryBoard';
|
||||||
|
import NoBoardBoard from './NoBoardBoard';
|
||||||
|
|
||||||
|
const overlayScrollbarsStyles: CSSProperties = {
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
};
|
||||||
|
|
||||||
|
const BoardsListWithPrivate = () => {
|
||||||
|
const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId);
|
||||||
|
const boardSearchText = useAppSelector((s) => s.gallery.boardSearchText);
|
||||||
|
const queryArgs = useAppSelector(selectListBoardsQueryArgs);
|
||||||
|
const { data: boards } = useListAllBoardsQuery(queryArgs);
|
||||||
|
const filteredPrivateBoards = boardSearchText
|
||||||
|
? boards?.filter((board) => board.is_private && board.board_name.toLowerCase().includes(boardSearchText.toLowerCase()))
|
||||||
|
: boards?.filter((board) => board.is_private);
|
||||||
|
const filteredSharedBoards = boardSearchText
|
||||||
|
? boards?.filter((board) => !board.is_private && board.board_name.toLowerCase().includes(boardSearchText.toLowerCase()))
|
||||||
|
: boards?.filter((board) => !board.is_private);
|
||||||
|
const [boardToDelete, setBoardToDelete] = useState<BoardDTO>();
|
||||||
|
const [isPrivateBoardsOpen, setIsPrivateBoardsOpen] = useState(true);
|
||||||
|
const [isSharedBoardsOpen, setIsSharedBoardsOpen] = useState(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Flex layerStyle="first" flexDir="column" gap={2} p={2} mt={2} borderRadius="base">
|
||||||
|
<Flex gap={2} alignItems="center">
|
||||||
|
<BoardsSearch />
|
||||||
|
<GallerySettingsPopover />
|
||||||
|
</Flex>
|
||||||
|
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
|
||||||
|
<Flex borderBottom="1px" borderColor="base.400" my="2" justifyContent="space-between">
|
||||||
|
<Flex
|
||||||
|
onClick={() => setIsPrivateBoardsOpen(!isPrivateBoardsOpen)}
|
||||||
|
gap={2}
|
||||||
|
alignItems="center"
|
||||||
|
cursor="pointer"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
as={PiCaretUpBold}
|
||||||
|
boxSize={6}
|
||||||
|
transform={isPrivateBoardsOpen ? 'rotate(0deg)' : 'rotate(180deg)'}
|
||||||
|
transitionProperty="common"
|
||||||
|
transitionDuration="normal"
|
||||||
|
color="base.400"
|
||||||
|
/>
|
||||||
|
<Text fontSize="md" fontWeight="medium">Private</Text>
|
||||||
|
</Flex>
|
||||||
|
<AddBoardButton privateBoard={true} />
|
||||||
|
</Flex>
|
||||||
|
<Collapse in={isPrivateBoardsOpen} animateOpacity>
|
||||||
|
<Flex direction="column">
|
||||||
|
<NoBoardBoard isSelected={selectedBoardId === 'none'} />
|
||||||
|
{filteredPrivateBoards &&
|
||||||
|
filteredPrivateBoards.map((board) => (
|
||||||
|
<GalleryBoard
|
||||||
|
board={board}
|
||||||
|
isSelected={selectedBoardId === board.board_id}
|
||||||
|
setBoardToDelete={setBoardToDelete}
|
||||||
|
key={board.board_id}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Collapse>
|
||||||
|
<Flex borderBottom="1px" borderColor="base.400" my="2" justifyContent="space-between">
|
||||||
|
<Flex
|
||||||
|
onClick={() => setIsSharedBoardsOpen(!isSharedBoardsOpen)}
|
||||||
|
gap={2}
|
||||||
|
alignItems="center"
|
||||||
|
cursor="pointer"
|
||||||
|
>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
as={PiCaretUpBold}
|
||||||
|
boxSize={6}
|
||||||
|
transform={isSharedBoardsOpen ? 'rotate(0deg)' : 'rotate(180deg)'}
|
||||||
|
transitionProperty="common"
|
||||||
|
transitionDuration="normal"
|
||||||
|
color="base.400"
|
||||||
|
/>
|
||||||
|
<Text fontSize="md" fontWeight="medium">Shared</Text>
|
||||||
|
</Flex>
|
||||||
|
<AddBoardButton privateBoard={false} />
|
||||||
|
</Flex>
|
||||||
|
<Collapse in={isSharedBoardsOpen} animateOpacity>
|
||||||
|
<Flex direction="column">
|
||||||
|
{filteredSharedBoards &&
|
||||||
|
filteredSharedBoards.map((board) => (
|
||||||
|
<GalleryBoard
|
||||||
|
board={board}
|
||||||
|
isSelected={selectedBoardId === board.board_id}
|
||||||
|
setBoardToDelete={setBoardToDelete}
|
||||||
|
key={board.board_id}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Collapse>
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
|
</Flex>
|
||||||
|
<DeleteBoardModal boardToDelete={boardToDelete} setBoardToDelete={setBoardToDelete} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(BoardsListWithPrivate);
|
@ -0,0 +1,66 @@
|
|||||||
|
import { IconButton, Input, InputGroup, InputRightElement } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
|
||||||
|
import type { ChangeEvent, KeyboardEvent } from 'react';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiXBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
const BoardsSearch = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const boardSearchText = useAppSelector((s) => s.gallery.boardSearchText);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleBoardSearch = useCallback(
|
||||||
|
(searchTerm: string) => {
|
||||||
|
dispatch(boardSearchTextChanged(searchTerm));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearBoardSearch = useCallback(() => {
|
||||||
|
dispatch(boardSearchTextChanged(''));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleKeydown = useCallback(
|
||||||
|
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
// exit search mode on escape
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
clearBoardSearch();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[clearBoardSearch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
handleBoardSearch(e.target.value);
|
||||||
|
},
|
||||||
|
[handleBoardSearch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputGroup>
|
||||||
|
<Input
|
||||||
|
placeholder={t('boards.searchBoard')}
|
||||||
|
value={boardSearchText}
|
||||||
|
onKeyDown={handleKeydown}
|
||||||
|
onChange={handleChange}
|
||||||
|
data-testid="board-search-input"
|
||||||
|
/>
|
||||||
|
{boardSearchText && boardSearchText.length && (
|
||||||
|
<InputRightElement h="full" pe={2}>
|
||||||
|
<IconButton
|
||||||
|
onClick={clearBoardSearch}
|
||||||
|
size="sm"
|
||||||
|
variant="link"
|
||||||
|
aria-label={t('boards.clearSearch')}
|
||||||
|
icon={<PiXBold />}
|
||||||
|
/>
|
||||||
|
</InputRightElement>
|
||||||
|
)}
|
||||||
|
</InputGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(BoardsSearch);
|
@ -0,0 +1,194 @@
|
|||||||
|
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||||
|
import { Box, Editable, EditableInput, EditablePreview, Flex, Icon, Image, Text, Tooltip } from '@invoke-ai/ui-library';
|
||||||
|
import { skipToken } from '@reduxjs/toolkit/query';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import IAIDroppable from 'common/components/IAIDroppable';
|
||||||
|
import SelectionOverlay from 'common/components/SelectionOverlay';
|
||||||
|
import type { AddToBoardDropData } from 'features/dnd/types';
|
||||||
|
import BoardContextMenu from 'features/gallery/components/Boards/BoardContextMenu';
|
||||||
|
import { BoardTotalsTooltip } from 'features/gallery/components/Boards/BoardsList/BoardTotalsTooltip';
|
||||||
|
import { autoAddBoardIdChanged, boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||||
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiArchiveBold, PiImagesSquare } from 'react-icons/pi';
|
||||||
|
import { useUpdateBoardMutation } from 'services/api/endpoints/boards';
|
||||||
|
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||||
|
import type { BoardDTO } from 'services/api/types';
|
||||||
|
|
||||||
|
const editableInputStyles: SystemStyleObject = {
|
||||||
|
p: 0,
|
||||||
|
fontSize: 'md',
|
||||||
|
w: '100%',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ArchivedIcon = () => {
|
||||||
|
return (
|
||||||
|
<Box position="absolute" top={1} insetInlineEnd={2} p={0} minW={0}>
|
||||||
|
<Icon as={PiArchiveBold} fill="base.300" filter="drop-shadow(0px 0px 0.1rem var(--invoke-colors-base-800))" />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface GalleryBoardProps {
|
||||||
|
board: BoardDTO;
|
||||||
|
isSelected: boolean;
|
||||||
|
setBoardToDelete: (board?: BoardDTO) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GalleryBoard = ({ board, isSelected, setBoardToDelete }: GalleryBoardProps) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const autoAssignBoardOnClick = useAppSelector((s) => s.gallery.autoAssignBoardOnClick);
|
||||||
|
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
const handleMouseOver = useCallback(() => {
|
||||||
|
setIsHovered(true);
|
||||||
|
}, []);
|
||||||
|
const handleMouseOut = useCallback(() => {
|
||||||
|
setIsHovered(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { currentData: coverImage } = useGetImageDTOQuery(board.cover_image_name ?? skipToken);
|
||||||
|
|
||||||
|
const { board_name, board_id } = board;
|
||||||
|
const [localBoardName, setLocalBoardName] = useState(board_name);
|
||||||
|
|
||||||
|
const handleSelectBoard = useCallback(() => {
|
||||||
|
dispatch(boardIdSelected({ boardId: board_id }));
|
||||||
|
if (autoAssignBoardOnClick && !board.archived) {
|
||||||
|
dispatch(autoAddBoardIdChanged(board_id));
|
||||||
|
}
|
||||||
|
}, [board_id, autoAssignBoardOnClick, dispatch, board.archived]);
|
||||||
|
|
||||||
|
const [updateBoard, { isLoading: isUpdateBoardLoading }] = useUpdateBoardMutation();
|
||||||
|
|
||||||
|
const droppableData: AddToBoardDropData = useMemo(
|
||||||
|
() => ({
|
||||||
|
id: board_id,
|
||||||
|
actionType: 'ADD_TO_BOARD',
|
||||||
|
context: { boardId: board_id },
|
||||||
|
}),
|
||||||
|
[board_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(
|
||||||
|
async (newBoardName: string) => {
|
||||||
|
// empty strings are not allowed
|
||||||
|
if (!newBoardName.trim()) {
|
||||||
|
setLocalBoardName(board_name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't updated the board name if it hasn't changed
|
||||||
|
if (newBoardName === board_name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { board_name } = await updateBoard({
|
||||||
|
board_id,
|
||||||
|
changes: { board_name: newBoardName },
|
||||||
|
}).unwrap();
|
||||||
|
|
||||||
|
// update local state
|
||||||
|
setLocalBoardName(board_name);
|
||||||
|
} catch {
|
||||||
|
// revert on error
|
||||||
|
setLocalBoardName(board_name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[board_id, board_name, updateBoard]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = useCallback((newBoardName: string) => {
|
||||||
|
setLocalBoardName(newBoardName);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box w="full" userSelect="none" px="1">
|
||||||
|
<Flex
|
||||||
|
onMouseOver={handleMouseOver}
|
||||||
|
onMouseOut={handleMouseOut}
|
||||||
|
position="relative"
|
||||||
|
alignItems="center"
|
||||||
|
borderRadius="base"
|
||||||
|
w="full"
|
||||||
|
my="2"
|
||||||
|
userSelect="none"
|
||||||
|
>
|
||||||
|
<BoardContextMenu board={board} setBoardToDelete={setBoardToDelete}>
|
||||||
|
{(ref) => (
|
||||||
|
<Tooltip
|
||||||
|
label={<BoardTotalsTooltip board_id={board.board_id} isArchived={Boolean(board.archived)} />}
|
||||||
|
openDelay={1000}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
ref={ref}
|
||||||
|
onClick={handleSelectBoard}
|
||||||
|
w="full"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="space-between"
|
||||||
|
borderRadius="base"
|
||||||
|
cursor="pointer"
|
||||||
|
gap="6"
|
||||||
|
p="1"
|
||||||
|
>
|
||||||
|
<Flex gap="6">
|
||||||
|
{board.archived && <ArchivedIcon />}
|
||||||
|
{coverImage?.thumbnail_url ? (
|
||||||
|
<Image
|
||||||
|
src={coverImage?.thumbnail_url}
|
||||||
|
draggable={false}
|
||||||
|
objectFit="cover"
|
||||||
|
w="8"
|
||||||
|
h="8"
|
||||||
|
borderRadius="base"
|
||||||
|
borderBottomRadius="lg"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Flex w="8" h="8" justifyContent="center" alignItems="center">
|
||||||
|
<Icon boxSize={8} as={PiImagesSquare} opacity={0.7} color="base.500" />
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SelectionOverlay isSelected={isSelected} isSelectedForCompare={false} isHovered={isHovered} />
|
||||||
|
<Flex
|
||||||
|
p={1}
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
color={isSelected ? 'base.100' : 'base.400'}
|
||||||
|
lineHeight="short"
|
||||||
|
fontSize="md"
|
||||||
|
>
|
||||||
|
<Editable
|
||||||
|
value={localBoardName}
|
||||||
|
isDisabled={isUpdateBoardLoading}
|
||||||
|
submitOnBlur={true}
|
||||||
|
onChange={handleChange}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
<EditablePreview
|
||||||
|
p={0}
|
||||||
|
fontSize="md"
|
||||||
|
textOverflow="ellipsis"
|
||||||
|
noOfLines={1}
|
||||||
|
color="inherit"
|
||||||
|
w="fit-content"
|
||||||
|
/>
|
||||||
|
<EditableInput sx={editableInputStyles} />
|
||||||
|
</Editable>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Text justifySelf="end" color="base.600">{board.image_count} images</Text>
|
||||||
|
|
||||||
|
<IAIDroppable data={droppableData} dropLabel={<Text fontSize="md">{t('unifiedCanvas.move')}</Text>} />
|
||||||
|
</Flex>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</BoardContextMenu>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(GalleryBoard);
|
@ -0,0 +1,96 @@
|
|||||||
|
import { Box, Flex, Image, Text, Tooltip } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import IAIDroppable from 'common/components/IAIDroppable';
|
||||||
|
import SelectionOverlay from 'common/components/SelectionOverlay';
|
||||||
|
import type { RemoveFromBoardDropData } from 'features/dnd/types';
|
||||||
|
import { BoardTotalsTooltip } from 'features/gallery/components/Boards/BoardsList/BoardTotalsTooltip';
|
||||||
|
import NoBoardBoardContextMenu from 'features/gallery/components/Boards/NoBoardBoardContextMenu';
|
||||||
|
import { autoAddBoardIdChanged, boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||||
|
import InvokeLogoSVG from 'public/assets/images/invoke-symbol-wht-lrg.svg';
|
||||||
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useBoardName } from 'services/api/hooks/useBoardName';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isSelected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NoBoardBoard = memo(({ isSelected }: Props) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const autoAssignBoardOnClick = useAppSelector((s) => s.gallery.autoAssignBoardOnClick);
|
||||||
|
const boardName = useBoardName('none');
|
||||||
|
const handleSelectBoard = useCallback(() => {
|
||||||
|
dispatch(boardIdSelected({ boardId: 'none' }));
|
||||||
|
if (autoAssignBoardOnClick) {
|
||||||
|
dispatch(autoAddBoardIdChanged('none'));
|
||||||
|
}
|
||||||
|
}, [dispatch, autoAssignBoardOnClick]);
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
|
const handleMouseOver = useCallback(() => {
|
||||||
|
setIsHovered(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleMouseOut = useCallback(() => {
|
||||||
|
setIsHovered(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const droppableData: RemoveFromBoardDropData = useMemo(
|
||||||
|
() => ({
|
||||||
|
id: 'no_board',
|
||||||
|
actionType: 'REMOVE_FROM_BOARD',
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Box w="full" userSelect="none" px="1">
|
||||||
|
<Flex
|
||||||
|
onMouseOver={handleMouseOver}
|
||||||
|
onMouseOut={handleMouseOut}
|
||||||
|
position="relative"
|
||||||
|
alignItems="center"
|
||||||
|
borderRadius="base"
|
||||||
|
w="full"
|
||||||
|
my="2"
|
||||||
|
userSelect="none"
|
||||||
|
>
|
||||||
|
<NoBoardBoardContextMenu>
|
||||||
|
{(ref) => (
|
||||||
|
<Tooltip label={<BoardTotalsTooltip board_id="none" isArchived={false} />} openDelay={1000}>
|
||||||
|
<Flex
|
||||||
|
ref={ref}
|
||||||
|
onClick={handleSelectBoard}
|
||||||
|
w="full"
|
||||||
|
alignItems="center"
|
||||||
|
borderRadius="base"
|
||||||
|
cursor="pointer"
|
||||||
|
gap="6"
|
||||||
|
p="1"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={InvokeLogoSVG}
|
||||||
|
alt="invoke-ai-logo"
|
||||||
|
opacity={0.7}
|
||||||
|
mixBlendMode="overlay"
|
||||||
|
userSelect="none"
|
||||||
|
height="6"
|
||||||
|
width="6"
|
||||||
|
/>
|
||||||
|
<Text fontSize="md" color={isSelected ? 'base.100' : 'base.400'}>
|
||||||
|
{boardName}
|
||||||
|
</Text>
|
||||||
|
<SelectionOverlay isSelected={isSelected} isSelectedForCompare={false} isHovered={isHovered} />
|
||||||
|
<IAIDroppable data={droppableData} dropLabel={<Text fontSize="md">{t('unifiedCanvas.move')}</Text>} />
|
||||||
|
</Flex>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</NoBoardBoardContextMenu>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
NoBoardBoard.displayName = 'HoverableBoard';
|
||||||
|
|
||||||
|
export default memo(NoBoardBoard);
|
@ -9,6 +9,7 @@ import { PiImagesBold } from 'react-icons/pi';
|
|||||||
import { RiServerLine } from 'react-icons/ri';
|
import { RiServerLine } from 'react-icons/ri';
|
||||||
|
|
||||||
import BoardsList from './Boards/BoardsList/BoardsList';
|
import BoardsList from './Boards/BoardsList/BoardsList';
|
||||||
|
import BoardsListWithPrivate from './Boards/BoardsListWithPrivate/BoardsListWithPrivate';
|
||||||
import GalleryBoardName from './GalleryBoardName';
|
import GalleryBoardName from './GalleryBoardName';
|
||||||
import GallerySettingsPopover from './GallerySettingsPopover/GallerySettingsPopover';
|
import GallerySettingsPopover from './GallerySettingsPopover/GallerySettingsPopover';
|
||||||
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
|
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
|
||||||
@ -18,6 +19,7 @@ import { GallerySearch } from './ImageGrid/GallerySearch';
|
|||||||
const ImageGalleryContent = () => {
|
const ImageGalleryContent = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const galleryView = useAppSelector((s) => s.gallery.galleryView);
|
const galleryView = useAppSelector((s) => s.gallery.galleryView);
|
||||||
|
const allowPrivateBoards = useAppSelector((s) => s.config.allowPrivateBoards);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const galleryHeader = useStore($galleryHeader);
|
const galleryHeader = useStore($galleryHeader);
|
||||||
const { isOpen: isBoardListOpen, onToggle: onToggleBoardList } = useDisclosure({ defaultIsOpen: true });
|
const { isOpen: isBoardListOpen, onToggle: onToggleBoardList } = useDisclosure({ defaultIsOpen: true });
|
||||||
@ -42,15 +44,21 @@ const ImageGalleryContent = () => {
|
|||||||
gap={2}
|
gap={2}
|
||||||
>
|
>
|
||||||
{galleryHeader}
|
{galleryHeader}
|
||||||
<Box>
|
{true ? (
|
||||||
<Flex alignItems="center" justifyContent="space-between" gap={2}>
|
|
||||||
<GalleryBoardName isOpen={isBoardListOpen} onToggle={onToggleBoardList} />
|
|
||||||
<GallerySettingsPopover />
|
|
||||||
</Flex>
|
|
||||||
<Box>
|
<Box>
|
||||||
<BoardsList isOpen={isBoardListOpen} />
|
<BoardsListWithPrivate isOpen={isBoardListOpen} />
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box>
|
||||||
|
<Flex alignItems="center" justifyContent="space-between" gap={2}>
|
||||||
|
<GalleryBoardName isOpen={isBoardListOpen} onToggle={onToggleBoardList} />
|
||||||
|
<GallerySettingsPopover />
|
||||||
|
</Flex>
|
||||||
|
<Box>
|
||||||
|
<BoardsList isOpen={isBoardListOpen} />
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
)}
|
||||||
<Flex alignItems="center" justifyContent="space-between" gap={2}>
|
<Flex alignItems="center" justifyContent="space-between" gap={2}>
|
||||||
<Tabs index={galleryView === 'images' ? 0 : 1} variant="unstyled" size="sm" w="full">
|
<Tabs index={galleryView === 'images' ? 0 : 1} variant="unstyled" size="sm" w="full">
|
||||||
<TabList>
|
<TabList>
|
||||||
|
@ -18,6 +18,7 @@ const initialConfigState: AppConfig = {
|
|||||||
isLocal: true,
|
isLocal: true,
|
||||||
shouldUpdateImagesOnConnect: false,
|
shouldUpdateImagesOnConnect: false,
|
||||||
shouldFetchMetadataFromApi: false,
|
shouldFetchMetadataFromApi: false,
|
||||||
|
allowPrivateBoards: false,
|
||||||
disabledTabs: [],
|
disabledTabs: [],
|
||||||
disabledFeatures: ['lightbox', 'faceRestore', 'batches'],
|
disabledFeatures: ['lightbox', 'faceRestore', 'batches'],
|
||||||
disabledSDFeatures: ['variation', 'symmetry', 'hires', 'perlinNoise', 'noiseThreshold'],
|
disabledSDFeatures: ['variation', 'symmetry', 'hires', 'perlinNoise', 'noiseThreshold'],
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
import { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
||||||
import type { BoardDTO, ListBoardsArgs, OffsetPaginatedResults_ImageDTO_, UpdateBoardArg } from 'services/api/types';
|
import type { BoardDTO, CreateBoardArg, ListBoardsArgs, OffsetPaginatedResults_ImageDTO_, UpdateBoardArg } from 'services/api/types';
|
||||||
import { getListImagesUrl } from 'services/api/util';
|
import { getListImagesUrl } from 'services/api/util';
|
||||||
|
|
||||||
import type { ApiTagDescription } from '..';
|
import type { ApiTagDescription } from '..';
|
||||||
@ -87,11 +87,11 @@ export const boardsApi = api.injectEndpoints({
|
|||||||
* Boards Mutations
|
* Boards Mutations
|
||||||
*/
|
*/
|
||||||
|
|
||||||
createBoard: build.mutation<BoardDTO, string>({
|
createBoard: build.mutation<BoardDTO, CreateBoardArg>({
|
||||||
query: (board_name) => ({
|
query: ({ board_name, private_board }) => ({
|
||||||
url: buildBoardsUrl(),
|
url: buildBoardsUrl(),
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
params: { board_name },
|
params: { board_name, private_board },
|
||||||
}),
|
}),
|
||||||
invalidatesTags: [{ type: 'Board', id: LIST_TAG }],
|
invalidatesTags: [{ type: 'Board', id: LIST_TAG }],
|
||||||
}),
|
}),
|
||||||
|
@ -11,6 +11,10 @@ export type ListBoardsArgs = NonNullable<paths['/api/v1/boards/']['get']['parame
|
|||||||
export type DeleteBoardResult =
|
export type DeleteBoardResult =
|
||||||
paths['/api/v1/boards/{board_id}']['delete']['responses']['200']['content']['application/json'];
|
paths['/api/v1/boards/{board_id}']['delete']['responses']['200']['content']['application/json'];
|
||||||
|
|
||||||
|
export type CreateBoardArg = paths['/api/v1/boards/']['post']['parameters']['query'] & {
|
||||||
|
changes: paths['/api/v1/boards/']['post']['parameters']['query'];
|
||||||
|
};
|
||||||
|
|
||||||
export type UpdateBoardArg = paths['/api/v1/boards/{board_id}']['patch']['parameters']['path'] & {
|
export type UpdateBoardArg = paths['/api/v1/boards/{board_id}']['patch']['parameters']['path'] & {
|
||||||
changes: paths['/api/v1/boards/{board_id}']['patch']['requestBody']['content']['application/json'];
|
changes: paths['/api/v1/boards/{board_id}']['patch']['requestBody']['content']['application/json'];
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user