mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
drag and drop to move image to board, a bit of board list UI
This commit is contained in:
parent
95b9c8e505
commit
f9f3c91a83
@ -1,23 +1,7 @@
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { requestedImageDeletion } from 'features/gallery/store/actions';
|
||||
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||
import {
|
||||
PropsWithChildren,
|
||||
createContext,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { PropsWithChildren, createContext, useCallback, useState } from 'react';
|
||||
import { ImageDTO } from 'services/api';
|
||||
import { RootState } from 'app/store/store';
|
||||
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||
import { controlNetSelector } from 'features/controlNet/store/controlNetSlice';
|
||||
import { nodesSelecter } from 'features/nodes/store/nodesSlice';
|
||||
import { generationSelector } from 'features/parameters/store/generationSelectors';
|
||||
import { some } from 'lodash-es';
|
||||
import { imageAddedToBoard } from '../../services/thunks/board';
|
||||
|
||||
export type ImageUsage = {
|
||||
@ -27,48 +11,6 @@ export type ImageUsage = {
|
||||
isControlNetImage: boolean;
|
||||
};
|
||||
|
||||
export const selectImageUsage = createSelector(
|
||||
[
|
||||
generationSelector,
|
||||
canvasSelector,
|
||||
nodesSelecter,
|
||||
controlNetSelector,
|
||||
(state: RootState, image_name?: string) => image_name,
|
||||
],
|
||||
(generation, canvas, nodes, controlNet, image_name) => {
|
||||
const isInitialImage = generation.initialImage?.image_name === image_name;
|
||||
|
||||
const isCanvasImage = canvas.layerState.objects.some(
|
||||
(obj) => obj.kind === 'image' && obj.image.image_name === image_name
|
||||
);
|
||||
|
||||
const isNodesImage = nodes.nodes.some((node) => {
|
||||
return some(
|
||||
node.data.inputs,
|
||||
(input) =>
|
||||
input.type === 'image' && input.value?.image_name === image_name
|
||||
);
|
||||
});
|
||||
|
||||
const isControlNetImage = some(
|
||||
controlNet.controlNets,
|
||||
(c) =>
|
||||
c.controlImage?.image_name === image_name ||
|
||||
c.processedControlImage?.image_name === image_name
|
||||
);
|
||||
|
||||
const imageUsage: ImageUsage = {
|
||||
isInitialImage,
|
||||
isCanvasImage,
|
||||
isNodesImage,
|
||||
isControlNetImage,
|
||||
};
|
||||
|
||||
return imageUsage;
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
type AddImageToBoardContextValue = {
|
||||
/**
|
||||
* Whether the move image dialog is open.
|
||||
|
@ -1,4 +1,13 @@
|
||||
import { Box, Grid, Input, Spacer } from '@chakra-ui/react';
|
||||
import {
|
||||
Box,
|
||||
Divider,
|
||||
Grid,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputRightElement,
|
||||
Spacer,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
@ -14,19 +23,25 @@ import AddBoardButton from './AddBoardButton';
|
||||
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';
|
||||
|
||||
const selector = createSelector(
|
||||
[selectBoardsAll, boardsSelector],
|
||||
(boards, boardsState) => {
|
||||
return { boards, selectedBoardId: boardsState.selectedBoardId };
|
||||
const selectedBoard = boards.find(
|
||||
(board) => board.board_id === boardsState.selectedBoardId
|
||||
);
|
||||
return { selectedBoard, searchText: boardsState.searchText };
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
const BoardsList = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { selectedBoardId } = useAppSelector(selector);
|
||||
const { selectedBoard, searchText } = useAppSelector(selector);
|
||||
const filteredBoards = useSelector(searchBoardsSelector);
|
||||
const { isOpen, onToggle } = useDisclosure();
|
||||
|
||||
const [searchMode, setSearchMode] = useState(false);
|
||||
|
||||
@ -34,8 +49,30 @@ const BoardsList = () => {
|
||||
setSearchMode(searchTerm.length > 0);
|
||||
dispatch(setBoardSearchText(searchTerm));
|
||||
};
|
||||
const clearBoardSearch = () => {
|
||||
setSearchMode(false);
|
||||
dispatch(setBoardSearchText(''));
|
||||
};
|
||||
|
||||
return (
|
||||
<IAICollapse label="Select Board" isOpen={isOpen} onToggle={onToggle}>
|
||||
<>
|
||||
<Box marginBottom="1rem">
|
||||
<InputGroup>
|
||||
<Input
|
||||
placeholder="Search Boards..."
|
||||
value={searchText}
|
||||
onChange={(e) => {
|
||||
handleBoardSearch(e.target.value);
|
||||
}}
|
||||
/>
|
||||
{searchText && searchText.length && (
|
||||
<InputRightElement>
|
||||
<CloseIcon onClick={clearBoardSearch} cursor="pointer" />
|
||||
</InputRightElement>
|
||||
)}
|
||||
</InputGroup>
|
||||
</Box>
|
||||
<OverlayScrollbarsComponent
|
||||
defer
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
@ -48,14 +85,6 @@ const BoardsList = () => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box margin="1rem 0">
|
||||
<Input
|
||||
placeholder="Search Boards..."
|
||||
onChange={(e) => {
|
||||
handleBoardSearch(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Grid
|
||||
className="list-container"
|
||||
sx={{
|
||||
@ -68,18 +97,20 @@ const BoardsList = () => {
|
||||
{!searchMode && (
|
||||
<>
|
||||
<AddBoardButton />
|
||||
<AllImagesBoard isSelected={selectedBoardId === null} />
|
||||
<AllImagesBoard isSelected={!selectedBoard} />
|
||||
</>
|
||||
)}
|
||||
{filteredBoards.map((board) => (
|
||||
<HoverableBoard
|
||||
key={board.board_id}
|
||||
board={board}
|
||||
isSelected={selectedBoardId === board.board_id}
|
||||
isSelected={selectedBoard?.board_id === board.board_id}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
</OverlayScrollbarsComponent>
|
||||
</>
|
||||
</IAICollapse>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -4,19 +4,34 @@ import {
|
||||
EditableInput,
|
||||
EditablePreview,
|
||||
Flex,
|
||||
Icon,
|
||||
Image,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
} from '@chakra-ui/react';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { FaFolder, FaTrash } from 'react-icons/fa';
|
||||
import { FaTrash } from 'react-icons/fa';
|
||||
import { ContextMenu } from 'chakra-ui-contextmenu';
|
||||
import { BoardDTO } from 'services/api';
|
||||
import { BoardDTO, ImageDTO } from 'services/api';
|
||||
import { IAIImageFallback } from 'common/components/IAIImageFallback';
|
||||
import { boardIdSelected } from 'features/gallery/store/boardSlice';
|
||||
import { boardDeleted, boardUpdated } from '../../../../services/thunks/board';
|
||||
import {
|
||||
boardDeleted,
|
||||
boardUpdated,
|
||||
imageAddedToBoard,
|
||||
} from '../../../../services/thunks/board';
|
||||
import { selectImagesAll } from '../../store/imagesSlice';
|
||||
import IAIDndImage from '../../../../common/components/IAIDndImage';
|
||||
import { defaultSelectorOptions } from '../../../../app/store/util/defaultMemoizeOptions';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
|
||||
const selector = createSelector(
|
||||
[selectImagesAll],
|
||||
(images) => {
|
||||
return { images };
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
interface HoverableBoardProps {
|
||||
board: BoardDTO;
|
||||
@ -25,6 +40,7 @@ interface HoverableBoardProps {
|
||||
|
||||
const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { images } = useAppSelector(selector);
|
||||
|
||||
const { board_name, board_id, cover_image_url } = board;
|
||||
|
||||
@ -45,6 +61,23 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleDrop = useCallback(
|
||||
(droppedImage: ImageDTO) => {
|
||||
if (droppedImage.board_id === board_id) {
|
||||
return;
|
||||
}
|
||||
dispatch(
|
||||
imageAddedToBoard({
|
||||
requestBody: {
|
||||
board_id,
|
||||
image_name: droppedImage.image_name,
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
[board_id, dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ touchAction: 'none' }}>
|
||||
<ContextMenu<HTMLDivElement>
|
||||
@ -91,19 +124,12 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{cover_image_url ? (
|
||||
<Image
|
||||
loading="lazy"
|
||||
objectFit="cover"
|
||||
draggable={false}
|
||||
rounded="md"
|
||||
src={cover_image_url}
|
||||
fallback={<IAIImageFallback />}
|
||||
sx={{}}
|
||||
<IAIDndImage
|
||||
image={cover_image_url ? images[0] : undefined}
|
||||
onDrop={handleDrop}
|
||||
fallback={<IAIImageFallback sx={{ bg: 'none' }} />}
|
||||
isUploadDisabled={true}
|
||||
/>
|
||||
) : (
|
||||
<Icon boxSize={8} color="base.700" as={FaFolder} />
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Editable
|
||||
|
@ -22,11 +22,10 @@ import IAIMantineSelect from 'common/components/IAIMantineSelect';
|
||||
|
||||
const UpdateImageBoardModal = () => {
|
||||
const boards = useSelector(selectBoardsAll);
|
||||
const [selectedBoard, setSelectedBoard] = useState<string | null>(null);
|
||||
|
||||
const { isOpen, onClose, handleAddToBoard, image } = useContext(
|
||||
AddImageToBoardContext
|
||||
);
|
||||
const [selectedBoard, setSelectedBoard] = useState<string | null>();
|
||||
|
||||
const cancelRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
@ -50,10 +49,12 @@ const UpdateImageBoardModal = () => {
|
||||
<AlertDialogBody>
|
||||
<Box>
|
||||
<Flex direction="column" gap={3}>
|
||||
{currentBoard && (
|
||||
<Text>
|
||||
Moving this image to a board will remove it from its existing
|
||||
board.
|
||||
Moving this image from{' '}
|
||||
<strong>{currentBoard.board_name}</strong> to
|
||||
</Text>
|
||||
)}
|
||||
<IAIMantineSelect
|
||||
placeholder="Select Board"
|
||||
onChange={(v) => setSelectedBoard(v)}
|
||||
|
@ -240,39 +240,10 @@ const ImageGalleryContent = () => {
|
||||
icon={<FaServer />}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
{selectedBoard && (
|
||||
<Flex>
|
||||
<Text>{selectedBoard.board_name}</Text>
|
||||
<Text>{selectedBoard ? selectedBoard.board_name : 'All Images'}</Text>
|
||||
</Flex>
|
||||
)}
|
||||
<Flex gap={2}>
|
||||
{/* <IAIPopover
|
||||
triggerComponent={
|
||||
<IAIIconButton
|
||||
tooltip="Add Board"
|
||||
aria-label="Add Board"
|
||||
size="sm"
|
||||
icon={<FaPlus />}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Flex direction="column" gap={2}>
|
||||
<IAIInput
|
||||
label="Board Name"
|
||||
placeholder="Board Name"
|
||||
value={newBoardName}
|
||||
onChange={(e) => setNewBoardName(e.target.value)}
|
||||
/>
|
||||
<IAIButton
|
||||
size="sm"
|
||||
onClick={handleCreateNewBoard}
|
||||
disabled={true}
|
||||
isLoading={false}
|
||||
>
|
||||
Create
|
||||
</IAIButton>
|
||||
</Flex>
|
||||
</IAIPopover> */}
|
||||
<IAIPopover
|
||||
triggerComponent={
|
||||
<IAIIconButton
|
||||
|
@ -7,7 +7,7 @@ import type { ImageMetadata } from './ImageMetadata';
|
||||
import type { ResourceOrigin } from './ResourceOrigin';
|
||||
|
||||
/**
|
||||
* Deserialized image record, enriched for the frontend with URLs.
|
||||
* Deserialized image record, enriched for the frontend.
|
||||
*/
|
||||
export type ImageDTO = {
|
||||
/**
|
||||
@ -66,5 +66,9 @@ export type ImageDTO = {
|
||||
* A limited subset of the image's generation metadata. Retrieve the image's session for full metadata.
|
||||
*/
|
||||
metadata?: ImageMetadata;
|
||||
/**
|
||||
* The id of the board the image belongs to, if one exists.
|
||||
*/
|
||||
board_id?: string;
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user