mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-19 01:54:22 -05:00
Merge branch 'main' into ryan/spandrel-upscale
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
import { Badge } from '@invoke-ai/ui-library';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const AutoAddBadge = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Badge color="invokeBlue.400" borderColor="invokeBlue.700" borderWidth={1} bg="transparent" flexShrink={0}>
|
||||
{t('common.auto')}
|
||||
</Badge>
|
||||
);
|
||||
});
|
||||
|
||||
AutoAddBadge.displayName = 'AutoAddBadge';
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Badge, Flex } from '@invoke-ai/ui-library';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const AutoAddIcon = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Flex position="absolute" insetInlineEnd={0} top={0} p={1}>
|
||||
<Badge variant="solid" bg="invokeBlue.400">
|
||||
{t('common.auto')}
|
||||
</Badge>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(AutoAddIcon);
|
||||
@@ -1,26 +1,48 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiPlusBold } from 'react-icons/pi';
|
||||
import { useCreateBoardMutation } from 'services/api/endpoints/boards';
|
||||
|
||||
const AddBoardButton = () => {
|
||||
type Props = {
|
||||
isPrivateBoard: boolean;
|
||||
};
|
||||
|
||||
const AddBoardButton = ({ isPrivateBoard }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const allowPrivateBoards = useAppSelector((s) => s.config.allowPrivateBoards);
|
||||
const [createBoard, { isLoading }] = useCreateBoardMutation();
|
||||
const DEFAULT_BOARD_NAME = t('boards.myBoard');
|
||||
const handleCreateBoard = useCallback(() => {
|
||||
createBoard(DEFAULT_BOARD_NAME);
|
||||
}, [createBoard, DEFAULT_BOARD_NAME]);
|
||||
const label = useMemo(() => {
|
||||
if (!allowPrivateBoards) {
|
||||
return t('boards.addBoard');
|
||||
}
|
||||
if (isPrivateBoard) {
|
||||
return t('boards.addPrivateBoard');
|
||||
}
|
||||
return t('boards.addSharedBoard');
|
||||
}, [allowPrivateBoards, isPrivateBoard, t]);
|
||||
const handleCreateBoard = useCallback(async () => {
|
||||
try {
|
||||
const board = await createBoard({ board_name: t('boards.myBoard'), is_private: isPrivateBoard }).unwrap();
|
||||
dispatch(boardIdSelected({ boardId: board.board_id }));
|
||||
} catch {
|
||||
//no-op
|
||||
}
|
||||
}, [t, createBoard, isPrivateBoard, dispatch]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
icon={<PiPlusBold />}
|
||||
isLoading={isLoading}
|
||||
tooltip={t('boards.addBoard')}
|
||||
aria-label={t('boards.addBoard')}
|
||||
tooltip={label}
|
||||
aria-label={label}
|
||||
onClick={handleCreateBoard}
|
||||
size="sm"
|
||||
size="md"
|
||||
data-testid="add-board-button"
|
||||
variant="ghost"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { Collapse, Flex, Grid, GridItem } from '@invoke-ai/ui-library';
|
||||
import { Collapse, Flex, Icon, Text, useDisclosure } from '@invoke-ai/ui-library';
|
||||
import { EMPTY_ARRAY } from 'app/store/constants';
|
||||
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 { memo, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCaretUpBold } from 'react-icons/pi';
|
||||
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
||||
import type { BoardDTO } from 'services/api/types';
|
||||
|
||||
@@ -19,56 +23,112 @@ const overlayScrollbarsStyles: CSSProperties = {
|
||||
width: '100%',
|
||||
};
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
};
|
||||
|
||||
const BoardsList = (props: Props) => {
|
||||
const { isOpen } = props;
|
||||
const BoardsList = () => {
|
||||
const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId);
|
||||
const boardSearchText = useAppSelector((s) => s.gallery.boardSearchText);
|
||||
const allowPrivateBoards = useAppSelector((s) => s.config.allowPrivateBoards);
|
||||
const queryArgs = useAppSelector(selectListBoardsQueryArgs);
|
||||
const { data: boards } = useListAllBoardsQuery(queryArgs);
|
||||
const filteredBoards = boardSearchText
|
||||
? boards?.filter((board) => board.board_name.toLowerCase().includes(boardSearchText.toLowerCase()))
|
||||
: boards;
|
||||
const [boardToDelete, setBoardToDelete] = useState<BoardDTO>();
|
||||
const privateBoardsDisclosure = useDisclosure({ defaultIsOpen: false });
|
||||
const sharedBoardsDisclosure = useDisclosure({ defaultIsOpen: false });
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { filteredPrivateBoards, filteredSharedBoards } = useMemo(() => {
|
||||
const filteredBoards = boardSearchText
|
||||
? boards?.filter((board) => board.board_name.toLowerCase().includes(boardSearchText.toLowerCase()))
|
||||
: boards;
|
||||
const filteredPrivateBoards = filteredBoards?.filter((board) => board.is_private) ?? EMPTY_ARRAY;
|
||||
const filteredSharedBoards = filteredBoards?.filter((board) => !board.is_private) ?? EMPTY_ARRAY;
|
||||
return { filteredPrivateBoards, filteredSharedBoards };
|
||||
}, [boardSearchText, boards]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collapse in={isOpen} animateOpacity>
|
||||
<Flex layerStyle="first" flexDir="column" gap={2} p={2} mt={2} borderRadius="base">
|
||||
<Flex gap={2} alignItems="center">
|
||||
<BoardsSearch />
|
||||
<AddBoardButton />
|
||||
</Flex>
|
||||
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
|
||||
<Grid
|
||||
className="list-container"
|
||||
data-testid="boards-list"
|
||||
gridTemplateColumns="repeat(auto-fill, minmax(90px, 1fr))"
|
||||
maxH={346}
|
||||
>
|
||||
<GridItem p={1.5} data-testid="no-board">
|
||||
<NoBoardBoard isSelected={selectedBoardId === 'none'} />
|
||||
</GridItem>
|
||||
{filteredBoards &&
|
||||
filteredBoards.map((board, index) => (
|
||||
<GridItem key={board.board_id} p={1.5} data-testid={`board-${index}`}>
|
||||
<Flex layerStyle="first" flexDir="column" borderRadius="base">
|
||||
<Flex gap={2} alignItems="center" pb={2}>
|
||||
<BoardsSearch />
|
||||
<GallerySettingsPopover />
|
||||
</Flex>
|
||||
{allowPrivateBoards && (
|
||||
<>
|
||||
<Flex w="full" gap={2}>
|
||||
<Flex
|
||||
flexGrow={1}
|
||||
onClick={privateBoardsDisclosure.onToggle}
|
||||
gap={2}
|
||||
alignItems="center"
|
||||
cursor="pointer"
|
||||
>
|
||||
<Icon
|
||||
as={PiCaretUpBold}
|
||||
boxSize={4}
|
||||
transform={privateBoardsDisclosure.isOpen ? 'rotate(0deg)' : 'rotate(180deg)'}
|
||||
transitionProperty="common"
|
||||
transitionDuration="normal"
|
||||
color="base.400"
|
||||
/>
|
||||
<Text fontSize="md" fontWeight="medium" userSelect="none">
|
||||
{t('boards.private')}
|
||||
</Text>
|
||||
</Flex>
|
||||
<AddBoardButton isPrivateBoard={true} />
|
||||
</Flex>
|
||||
<Collapse in={privateBoardsDisclosure.isOpen} animateOpacity>
|
||||
<OverlayScrollbarsComponent
|
||||
defer
|
||||
style={overlayScrollbarsStyles}
|
||||
options={overlayScrollbarsParams.options}
|
||||
>
|
||||
<Flex direction="column" maxH={346} gap={1}>
|
||||
{allowPrivateBoards && <NoBoardBoard isSelected={selectedBoardId === 'none'} />}
|
||||
{filteredPrivateBoards.map((board) => (
|
||||
<GalleryBoard
|
||||
board={board}
|
||||
isSelected={selectedBoardId === board.board_id}
|
||||
setBoardToDelete={setBoardToDelete}
|
||||
key={board.board_id}
|
||||
/>
|
||||
</GridItem>
|
||||
))}
|
||||
</Grid>
|
||||
</OverlayScrollbarsComponent>
|
||||
))}
|
||||
</Flex>
|
||||
</OverlayScrollbarsComponent>
|
||||
</Collapse>
|
||||
</>
|
||||
)}
|
||||
<Flex w="full" gap={2}>
|
||||
<Flex onClick={sharedBoardsDisclosure.onToggle} gap={2} alignItems="center" cursor="pointer" flexGrow={1}>
|
||||
<Icon
|
||||
as={PiCaretUpBold}
|
||||
boxSize={4}
|
||||
transform={sharedBoardsDisclosure.isOpen ? 'rotate(0deg)' : 'rotate(180deg)'}
|
||||
transitionProperty="common"
|
||||
transitionDuration="normal"
|
||||
color="base.400"
|
||||
/>
|
||||
<Text fontSize="md" fontWeight="medium" userSelect="none">
|
||||
{allowPrivateBoards ? t('boards.shared') : t('boards.boards')}
|
||||
</Text>
|
||||
</Flex>
|
||||
<AddBoardButton isPrivateBoard={false} />
|
||||
</Flex>
|
||||
</Collapse>
|
||||
<Collapse in={sharedBoardsDisclosure.isOpen} animateOpacity>
|
||||
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
|
||||
<Flex direction="column" maxH={346} gap={1}>
|
||||
{!allowPrivateBoards && <NoBoardBoard isSelected={selectedBoardId === 'none'} />}
|
||||
{filteredSharedBoards.map((board) => (
|
||||
<GalleryBoard
|
||||
board={board}
|
||||
isSelected={selectedBoardId === board.board_id}
|
||||
setBoardToDelete={setBoardToDelete}
|
||||
key={board.board_id}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
</OverlayScrollbarsComponent>
|
||||
</Collapse>
|
||||
</Flex>
|
||||
<DeleteBoardModal boardToDelete={boardToDelete} setBoardToDelete={setBoardToDelete} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(BoardsList);
|
||||
|
||||
@@ -1,36 +1,41 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Box, Editable, EditableInput, EditablePreview, Flex, Icon, Image, Text, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import {
|
||||
Editable,
|
||||
EditableInput,
|
||||
EditablePreview,
|
||||
Flex,
|
||||
Icon,
|
||||
Image,
|
||||
Text,
|
||||
Tooltip,
|
||||
useDisclosure,
|
||||
} 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 AutoAddIcon from 'features/gallery/components/Boards/AutoAddIcon';
|
||||
import { AutoAddBadge } from 'features/gallery/components/Boards/AutoAddBadge';
|
||||
import BoardContextMenu from 'features/gallery/components/Boards/BoardContextMenu';
|
||||
import { BoardTotalsTooltip } from 'features/gallery/components/Boards/BoardsList/BoardTotalsTooltip';
|
||||
import { autoAddBoardIdChanged, boardIdSelected, selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
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 { PiArchiveBold, PiImageSquare } 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%',
|
||||
_focusVisible: {
|
||||
p: 0,
|
||||
textAlign: 'center',
|
||||
},
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
const _hover: SystemStyleObject = {
|
||||
bg: 'base.800',
|
||||
};
|
||||
|
||||
interface GalleryBoardProps {
|
||||
@@ -42,71 +47,53 @@ interface GalleryBoardProps {
|
||||
const GalleryBoard = ({ board, isSelected, setBoardToDelete }: GalleryBoardProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId);
|
||||
const autoAssignBoardOnClick = useAppSelector((s) => s.gallery.autoAssignBoardOnClick);
|
||||
const selectIsSelectedForAutoAdd = useMemo(
|
||||
() => createSelector(selectGallerySlice, (gallery) => board.board_id === gallery.autoAddBoardId),
|
||||
[board.board_id]
|
||||
);
|
||||
|
||||
const isSelectedForAutoAdd = useAppSelector(selectIsSelectedForAutoAdd);
|
||||
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 editingDisclosure = useDisclosure();
|
||||
const [localBoardName, setLocalBoardName] = useState(board.board_name);
|
||||
|
||||
const handleSelectBoard = useCallback(() => {
|
||||
dispatch(boardIdSelected({ boardId: board_id }));
|
||||
dispatch(boardIdSelected({ boardId: board.board_id }));
|
||||
if (autoAssignBoardOnClick) {
|
||||
dispatch(autoAddBoardIdChanged(board_id));
|
||||
dispatch(autoAddBoardIdChanged(board.board_id));
|
||||
}
|
||||
}, [board_id, autoAssignBoardOnClick, dispatch]);
|
||||
}, [dispatch, board.board_id, autoAssignBoardOnClick]);
|
||||
|
||||
const [updateBoard, { isLoading: isUpdateBoardLoading }] = useUpdateBoardMutation();
|
||||
|
||||
const droppableData: AddToBoardDropData = useMemo(
|
||||
() => ({
|
||||
id: board_id,
|
||||
id: board.board_id,
|
||||
actionType: 'ADD_TO_BOARD',
|
||||
context: { boardId: board_id },
|
||||
context: { boardId: board.board_id },
|
||||
}),
|
||||
[board_id]
|
||||
[board.board_id]
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (newBoardName: string) => {
|
||||
// empty strings are not allowed
|
||||
if (!newBoardName.trim()) {
|
||||
setLocalBoardName(board_name);
|
||||
return;
|
||||
}
|
||||
// empty strings are not allowed
|
||||
setLocalBoardName(board.board_name);
|
||||
} else if (newBoardName === board.board_name) {
|
||||
// don't updated the board name if it hasn't changed
|
||||
} else {
|
||||
try {
|
||||
const { board_name } = await updateBoard({
|
||||
board_id: board.board_id,
|
||||
changes: { board_name: newBoardName },
|
||||
}).unwrap();
|
||||
|
||||
// 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);
|
||||
// update local state
|
||||
setLocalBoardName(board_name);
|
||||
} catch {
|
||||
// revert on error
|
||||
setLocalBoardName(board.board_name);
|
||||
}
|
||||
}
|
||||
editingDisclosure.onClose();
|
||||
},
|
||||
[board_id, board_name, updateBoard]
|
||||
[board.board_id, board.board_name, editingDisclosure, updateBoard]
|
||||
);
|
||||
|
||||
const handleChange = useCallback((newBoardName: string) => {
|
||||
@@ -114,98 +101,91 @@ const GalleryBoard = ({ board, isSelected, setBoardToDelete }: GalleryBoardProps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box w="full" h="full" userSelect="none">
|
||||
<Flex
|
||||
onMouseOver={handleMouseOver}
|
||||
onMouseOut={handleMouseOut}
|
||||
position="relative"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
aspectRatio="1/1"
|
||||
w="full"
|
||||
h="full"
|
||||
>
|
||||
<BoardContextMenu board={board} setBoardToDelete={setBoardToDelete}>
|
||||
{(ref) => (
|
||||
<Tooltip
|
||||
label={<BoardTotalsTooltip board_id={board.board_id} isArchived={Boolean(board.archived)} />}
|
||||
openDelay={1000}
|
||||
<BoardContextMenu board={board} setBoardToDelete={setBoardToDelete}>
|
||||
{(ref) => (
|
||||
<Tooltip
|
||||
label={<BoardTotalsTooltip board_id={board.board_id} isArchived={Boolean(board.archived)} />}
|
||||
openDelay={1000}
|
||||
>
|
||||
<Flex
|
||||
position="relative"
|
||||
ref={ref}
|
||||
onClick={handleSelectBoard}
|
||||
w="full"
|
||||
alignItems="center"
|
||||
borderRadius="base"
|
||||
cursor="pointer"
|
||||
py={1}
|
||||
px={2}
|
||||
gap={2}
|
||||
bg={isSelected ? 'base.800' : undefined}
|
||||
_hover={_hover}
|
||||
>
|
||||
<CoverImage board={board} />
|
||||
<Editable
|
||||
as={Flex}
|
||||
alignItems="center"
|
||||
gap={4}
|
||||
flexGrow={1}
|
||||
onEdit={editingDisclosure.onOpen}
|
||||
value={localBoardName}
|
||||
isDisabled={isUpdateBoardLoading}
|
||||
submitOnBlur={true}
|
||||
onChange={handleChange}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Flex
|
||||
ref={ref}
|
||||
onClick={handleSelectBoard}
|
||||
w="full"
|
||||
h="full"
|
||||
position="relative"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
borderRadius="base"
|
||||
cursor="pointer"
|
||||
bg="base.800"
|
||||
>
|
||||
{board.archived && <ArchivedIcon />}
|
||||
{coverImage?.thumbnail_url ? (
|
||||
<Image
|
||||
src={coverImage?.thumbnail_url}
|
||||
draggable={false}
|
||||
objectFit="cover"
|
||||
w="full"
|
||||
h="full"
|
||||
maxH="full"
|
||||
borderRadius="base"
|
||||
borderBottomRadius="lg"
|
||||
/>
|
||||
) : (
|
||||
<Flex w="full" h="full" justifyContent="center" alignItems="center">
|
||||
<Icon boxSize={14} as={PiImagesSquare} mt={-6} opacity={0.7} color="base.500" />
|
||||
</Flex>
|
||||
)}
|
||||
{isSelectedForAutoAdd && <AutoAddIcon />}
|
||||
<SelectionOverlay isSelected={isSelected} isSelectedForCompare={false} isHovered={isHovered} />
|
||||
<Flex
|
||||
position="absolute"
|
||||
bottom={0}
|
||||
left={0}
|
||||
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"
|
||||
fontSize="xs"
|
||||
>
|
||||
<Editable
|
||||
value={localBoardName}
|
||||
isDisabled={isUpdateBoardLoading}
|
||||
submitOnBlur={true}
|
||||
onChange={handleChange}
|
||||
onSubmit={handleSubmit}
|
||||
w="full"
|
||||
>
|
||||
<EditablePreview
|
||||
p={0}
|
||||
fontWeight={isSelected ? 'bold' : 'normal'}
|
||||
textAlign="center"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
noOfLines={1}
|
||||
color="inherit"
|
||||
/>
|
||||
<EditableInput sx={editableInputStyles} />
|
||||
</Editable>
|
||||
</Flex>
|
||||
<EditablePreview
|
||||
p={0}
|
||||
fontSize="md"
|
||||
textOverflow="ellipsis"
|
||||
noOfLines={1}
|
||||
w="fit-content"
|
||||
wordBreak="break-all"
|
||||
color={isSelected ? 'base.100' : 'base.400'}
|
||||
fontWeight={isSelected ? 'semibold' : 'normal'}
|
||||
/>
|
||||
<EditableInput sx={editableInputStyles} />
|
||||
</Editable>
|
||||
{autoAddBoardId === board.board_id && !editingDisclosure.isOpen && <AutoAddBadge />}
|
||||
{board.archived && !editingDisclosure.isOpen && (
|
||||
<Icon
|
||||
as={PiArchiveBold}
|
||||
fill="base.300"
|
||||
filter="drop-shadow(0px 0px 0.1rem var(--invoke-colors-base-800))"
|
||||
/>
|
||||
)}
|
||||
{!editingDisclosure.isOpen && <Text variant="subtext">{board.image_count}</Text>}
|
||||
|
||||
<IAIDroppable data={droppableData} dropLabel={<Text fontSize="md">{t('unifiedCanvas.move')}</Text>} />
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
)}
|
||||
</BoardContextMenu>
|
||||
</Flex>
|
||||
</Box>
|
||||
<IAIDroppable data={droppableData} dropLabel={<Text fontSize="md">{t('unifiedCanvas.move')}</Text>} />
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
)}
|
||||
</BoardContextMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(GalleryBoard);
|
||||
|
||||
const CoverImage = ({ board }: { board: BoardDTO }) => {
|
||||
const { currentData: coverImage } = useGetImageDTOQuery(board.cover_image_name ?? skipToken);
|
||||
|
||||
if (coverImage) {
|
||||
return (
|
||||
<Image
|
||||
src={coverImage.thumbnail_url}
|
||||
draggable={false}
|
||||
objectFit="cover"
|
||||
w={8}
|
||||
h={8}
|
||||
borderRadius="base"
|
||||
borderBottomRadius="lg"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex w={8} h={8} justifyContent="center" alignItems="center">
|
||||
<Icon boxSize={8} as={PiImageSquare} opacity={0.7} color="base.500" />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,23 +1,32 @@
|
||||
import { Box, Flex, Image, Text, Tooltip } from '@invoke-ai/ui-library';
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Flex, Icon, 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 AutoAddIcon from 'features/gallery/components/Boards/AutoAddIcon';
|
||||
import { AutoAddBadge } from 'features/gallery/components/Boards/AutoAddBadge';
|
||||
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 { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useGetBoardImagesTotalQuery } from 'services/api/endpoints/boards';
|
||||
import { useBoardName } from 'services/api/hooks/useBoardName';
|
||||
|
||||
interface Props {
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
const _hover: SystemStyleObject = {
|
||||
bg: 'base.800',
|
||||
};
|
||||
|
||||
const NoBoardBoard = memo(({ isSelected }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { imagesTotal } = useGetBoardImagesTotalQuery('none', {
|
||||
selectFromResult: ({ data }) => {
|
||||
return { imagesTotal: data?.total ?? 0 };
|
||||
},
|
||||
});
|
||||
const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId);
|
||||
const autoAssignBoardOnClick = useAppSelector((s) => s.gallery.autoAssignBoardOnClick);
|
||||
const boardName = useBoardName('none');
|
||||
@@ -27,15 +36,6 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
|
||||
dispatch(autoAddBoardIdChanged('none'));
|
||||
}
|
||||
}, [dispatch, autoAssignBoardOnClick]);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const handleMouseOver = useCallback(() => {
|
||||
setIsHovered(true);
|
||||
}, []);
|
||||
|
||||
const handleMouseOut = useCallback(() => {
|
||||
setIsHovered(false);
|
||||
}, []);
|
||||
|
||||
const droppableData: RemoveFromBoardDropData = useMemo(
|
||||
() => ({
|
||||
@@ -46,74 +46,49 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box w="full" h="full" userSelect="none">
|
||||
<Flex
|
||||
onMouseOver={handleMouseOver}
|
||||
onMouseOut={handleMouseOut}
|
||||
position="relative"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
aspectRatio="1/1"
|
||||
borderRadius="base"
|
||||
w="full"
|
||||
h="full"
|
||||
>
|
||||
<NoBoardBoardContextMenu>
|
||||
{(ref) => (
|
||||
<Tooltip label={<BoardTotalsTooltip board_id="none" isArchived={false} />} openDelay={1000}>
|
||||
<Flex
|
||||
ref={ref}
|
||||
onClick={handleSelectBoard}
|
||||
w="full"
|
||||
h="full"
|
||||
position="relative"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
borderRadius="base"
|
||||
cursor="pointer"
|
||||
bg="base.800"
|
||||
>
|
||||
<Flex w="full" h="full" justifyContent="center" alignItems="center">
|
||||
<Image
|
||||
src={InvokeLogoSVG}
|
||||
alt="invoke-ai-logo"
|
||||
opacity={0.7}
|
||||
mixBlendMode="overlay"
|
||||
mt={-6}
|
||||
w={16}
|
||||
h={16}
|
||||
minW={16}
|
||||
minH={16}
|
||||
userSelect="none"
|
||||
/>
|
||||
</Flex>
|
||||
{autoAddBoardId === 'none' && <AutoAddIcon />}
|
||||
<Flex
|
||||
position="absolute"
|
||||
bottom={0}
|
||||
left={0}
|
||||
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"
|
||||
fontSize="xs"
|
||||
fontWeight={isSelected ? 'bold' : 'normal'}
|
||||
>
|
||||
{boardName}
|
||||
</Flex>
|
||||
<SelectionOverlay isSelected={isSelected} isSelectedForCompare={false} isHovered={isHovered} />
|
||||
<IAIDroppable data={droppableData} dropLabel={<Text fontSize="md">{t('unifiedCanvas.move')}</Text>} />
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
)}
|
||||
</NoBoardBoardContextMenu>
|
||||
</Flex>
|
||||
</Box>
|
||||
<NoBoardBoardContextMenu>
|
||||
{(ref) => (
|
||||
<Tooltip label={<BoardTotalsTooltip board_id="none" isArchived={false} />} openDelay={1000}>
|
||||
<Flex
|
||||
position="relative"
|
||||
ref={ref}
|
||||
onClick={handleSelectBoard}
|
||||
w="full"
|
||||
alignItems="center"
|
||||
borderRadius="base"
|
||||
cursor="pointer"
|
||||
px={2}
|
||||
py={1}
|
||||
gap={2}
|
||||
bg={isSelected ? 'base.800' : undefined}
|
||||
_hover={_hover}
|
||||
>
|
||||
<Flex w={8} h={8} justifyContent="center" alignItems="center">
|
||||
{/* iconified from public/assets/images/invoke-symbol-wht-lrg.svg */}
|
||||
<Icon boxSize={6} opacity={1} stroke="base.500" viewBox="0 0 66 66" fill="none">
|
||||
<path
|
||||
d="M43.9137 16H63.1211V3H3.12109V16H22.3285L43.9137 50H63.1211V63H3.12109V50H22.3285"
|
||||
strokeWidth="5"
|
||||
/>
|
||||
</Icon>
|
||||
</Flex>
|
||||
|
||||
<Text
|
||||
fontSize="md"
|
||||
color={isSelected ? 'base.100' : 'base.400'}
|
||||
fontWeight={isSelected ? 'semibold' : 'normal'}
|
||||
noOfLines={1}
|
||||
flexGrow={1}
|
||||
>
|
||||
{boardName}
|
||||
</Text>
|
||||
{autoAddBoardId === 'none' && <AutoAddBadge />}
|
||||
<Text variant="subtext">{imagesTotal}</Text>
|
||||
<IAIDroppable data={droppableData} dropLabel={<Text fontSize="md">{t('unifiedCanvas.move')}</Text>} />
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
)}
|
||||
</NoBoardBoardContextMenu>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,48 +1,17 @@
|
||||
import { Button, Flex, Icon, Spacer } from '@invoke-ai/ui-library';
|
||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { PiCaretUpBold } from 'react-icons/pi';
|
||||
import { memo } from 'react';
|
||||
import { useBoardName } from 'services/api/hooks/useBoardName';
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onToggle: () => void;
|
||||
};
|
||||
|
||||
const GalleryBoardName = (props: Props) => {
|
||||
const { isOpen, onToggle } = props;
|
||||
const GalleryBoardName = () => {
|
||||
const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId);
|
||||
const boardName = useBoardName(selectedBoardId);
|
||||
|
||||
const formattedBoardName = useMemo(() => {
|
||||
if (boardName.length > 20) {
|
||||
return `${boardName.substring(0, 20)}...`;
|
||||
}
|
||||
return boardName;
|
||||
}, [boardName]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
as={Button}
|
||||
onClick={onToggle}
|
||||
size="sm"
|
||||
position="relative"
|
||||
gap={2}
|
||||
w="full"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
px={2}
|
||||
>
|
||||
<Spacer />
|
||||
{formattedBoardName}
|
||||
<Spacer />
|
||||
<Icon
|
||||
as={PiCaretUpBold}
|
||||
boxSize={4}
|
||||
transform={isOpen ? 'rotate(0deg)' : 'rotate(180deg)'}
|
||||
transitionProperty="common"
|
||||
transitionDuration="normal"
|
||||
/>
|
||||
<Flex w="full" borderWidth={1} borderRadius="base" alignItems="center" justifyContent="center" px={2}>
|
||||
<Text fontWeight="semibold" fontSize="md" noOfLines={1} wordBreak="break-all" color="base.200">
|
||||
{boardName}
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, Button, ButtonGroup, Flex, Tab, TabList, Tabs, useDisclosure } from '@invoke-ai/ui-library';
|
||||
import { Button, ButtonGroup, Flex, Tab, TabList, Tabs } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $galleryHeader } from 'app/store/nanostores/galleryHeader';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
@@ -10,7 +10,6 @@ import { RiServerLine } from 'react-icons/ri';
|
||||
|
||||
import BoardsList from './Boards/BoardsList/BoardsList';
|
||||
import GalleryBoardName from './GalleryBoardName';
|
||||
import GallerySettingsPopover from './GallerySettingsPopover/GallerySettingsPopover';
|
||||
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
|
||||
import { GalleryPagination } from './ImageGrid/GalleryPagination';
|
||||
import { GallerySearch } from './ImageGrid/GallerySearch';
|
||||
@@ -20,7 +19,6 @@ const ImageGalleryContent = () => {
|
||||
const galleryView = useAppSelector((s) => s.gallery.galleryView);
|
||||
const dispatch = useAppDispatch();
|
||||
const galleryHeader = useStore($galleryHeader);
|
||||
const { isOpen: isBoardListOpen, onToggle: onToggleBoardList } = useDisclosure({ defaultIsOpen: true });
|
||||
|
||||
const handleClickImages = useCallback(() => {
|
||||
dispatch(galleryViewChanged('images'));
|
||||
@@ -42,15 +40,8 @@ const ImageGalleryContent = () => {
|
||||
gap={2}
|
||||
>
|
||||
{galleryHeader}
|
||||
<Box>
|
||||
<Flex alignItems="center" justifyContent="space-between" gap={2}>
|
||||
<GalleryBoardName isOpen={isBoardListOpen} onToggle={onToggleBoardList} />
|
||||
<GallerySettingsPopover />
|
||||
</Flex>
|
||||
<Box>
|
||||
<BoardsList isOpen={isBoardListOpen} />
|
||||
</Box>
|
||||
</Box>
|
||||
<BoardsList />
|
||||
<GalleryBoardName />
|
||||
<Flex alignItems="center" justifyContent="space-between" gap={2}>
|
||||
<Tabs index={galleryView === 'images' ? 0 : 1} variant="unstyled" size="sm" w="full">
|
||||
<TabList>
|
||||
|
||||
@@ -19,7 +19,7 @@ const initialGalleryState: GalleryState = {
|
||||
limit: 20,
|
||||
offset: 0,
|
||||
starredFirst: true,
|
||||
orderDir: 'ASC',
|
||||
orderDir: 'DESC',
|
||||
searchTerm: '',
|
||||
isImageViewerOpen: true,
|
||||
imageToCompare: null,
|
||||
@@ -122,6 +122,7 @@ export const gallerySlice = createSlice({
|
||||
},
|
||||
searchTermChanged: (state, action: PayloadAction<string>) => {
|
||||
state.searchTerm = action.payload;
|
||||
state.offset = 0;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ const initialConfigState: AppConfig = {
|
||||
isLocal: true,
|
||||
shouldUpdateImagesOnConnect: false,
|
||||
shouldFetchMetadataFromApi: false,
|
||||
allowPrivateBoards: false,
|
||||
disabledTabs: [],
|
||||
disabledFeatures: ['lightbox', 'faceRestore', 'batches'],
|
||||
disabledSDFeatures: ['variation', 'symmetry', 'hires', 'perlinNoise', 'noiseThreshold'],
|
||||
|
||||
Reference in New Issue
Block a user