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 { useDisclosure } from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { PropsWithChildren, createContext, useCallback, useState } from 'react';
|
||||||
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 { ImageDTO } from 'services/api';
|
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';
|
import { imageAddedToBoard } from '../../services/thunks/board';
|
||||||
|
|
||||||
export type ImageUsage = {
|
export type ImageUsage = {
|
||||||
@ -27,48 +11,6 @@ export type ImageUsage = {
|
|||||||
isControlNetImage: boolean;
|
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 = {
|
type AddImageToBoardContextValue = {
|
||||||
/**
|
/**
|
||||||
* Whether the move image dialog is open.
|
* 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 { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
@ -14,19 +23,25 @@ import AddBoardButton from './AddBoardButton';
|
|||||||
import AllImagesBoard from './AllImagesBoard';
|
import AllImagesBoard from './AllImagesBoard';
|
||||||
import { searchBoardsSelector } from '../../store/boardSelectors';
|
import { searchBoardsSelector } from '../../store/boardSelectors';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
import IAICollapse from '../../../../common/components/IAICollapse';
|
||||||
|
import { CloseIcon } from '@chakra-ui/icons';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[selectBoardsAll, boardsSelector],
|
[selectBoardsAll, boardsSelector],
|
||||||
(boards, boardsState) => {
|
(boards, boardsState) => {
|
||||||
return { boards, selectedBoardId: boardsState.selectedBoardId };
|
const selectedBoard = boards.find(
|
||||||
|
(board) => board.board_id === boardsState.selectedBoardId
|
||||||
|
);
|
||||||
|
return { selectedBoard, searchText: boardsState.searchText };
|
||||||
},
|
},
|
||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
const BoardsList = () => {
|
const BoardsList = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { selectedBoardId } = useAppSelector(selector);
|
const { selectedBoard, searchText } = useAppSelector(selector);
|
||||||
const filteredBoards = useSelector(searchBoardsSelector);
|
const filteredBoards = useSelector(searchBoardsSelector);
|
||||||
|
const { isOpen, onToggle } = useDisclosure();
|
||||||
|
|
||||||
const [searchMode, setSearchMode] = useState(false);
|
const [searchMode, setSearchMode] = useState(false);
|
||||||
|
|
||||||
@ -34,8 +49,30 @@ const BoardsList = () => {
|
|||||||
setSearchMode(searchTerm.length > 0);
|
setSearchMode(searchTerm.length > 0);
|
||||||
dispatch(setBoardSearchText(searchTerm));
|
dispatch(setBoardSearchText(searchTerm));
|
||||||
};
|
};
|
||||||
|
const clearBoardSearch = () => {
|
||||||
|
setSearchMode(false);
|
||||||
|
dispatch(setBoardSearchText(''));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
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
|
<OverlayScrollbarsComponent
|
||||||
defer
|
defer
|
||||||
style={{ height: '100%', width: '100%' }}
|
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
|
<Grid
|
||||||
className="list-container"
|
className="list-container"
|
||||||
sx={{
|
sx={{
|
||||||
@ -68,18 +97,20 @@ const BoardsList = () => {
|
|||||||
{!searchMode && (
|
{!searchMode && (
|
||||||
<>
|
<>
|
||||||
<AddBoardButton />
|
<AddBoardButton />
|
||||||
<AllImagesBoard isSelected={selectedBoardId === null} />
|
<AllImagesBoard isSelected={!selectedBoard} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{filteredBoards.map((board) => (
|
{filteredBoards.map((board) => (
|
||||||
<HoverableBoard
|
<HoverableBoard
|
||||||
key={board.board_id}
|
key={board.board_id}
|
||||||
board={board}
|
board={board}
|
||||||
isSelected={selectedBoardId === board.board_id}
|
isSelected={selectedBoard?.board_id === board.board_id}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
</OverlayScrollbarsComponent>
|
</OverlayScrollbarsComponent>
|
||||||
|
</>
|
||||||
|
</IAICollapse>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,19 +4,34 @@ import {
|
|||||||
EditableInput,
|
EditableInput,
|
||||||
EditablePreview,
|
EditablePreview,
|
||||||
Flex,
|
Flex,
|
||||||
Icon,
|
|
||||||
Image,
|
|
||||||
MenuItem,
|
MenuItem,
|
||||||
MenuList,
|
MenuList,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { memo, useCallback } from 'react';
|
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 { ContextMenu } from 'chakra-ui-contextmenu';
|
||||||
import { BoardDTO } from 'services/api';
|
import { BoardDTO, ImageDTO } from 'services/api';
|
||||||
import { IAIImageFallback } from 'common/components/IAIImageFallback';
|
import { IAIImageFallback } from 'common/components/IAIImageFallback';
|
||||||
import { boardIdSelected } from 'features/gallery/store/boardSlice';
|
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 {
|
interface HoverableBoardProps {
|
||||||
board: BoardDTO;
|
board: BoardDTO;
|
||||||
@ -25,6 +40,7 @@ interface HoverableBoardProps {
|
|||||||
|
|
||||||
const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
|
const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const { images } = useAppSelector(selector);
|
||||||
|
|
||||||
const { board_name, board_id, cover_image_url } = board;
|
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 (
|
return (
|
||||||
<Box sx={{ touchAction: 'none' }}>
|
<Box sx={{ touchAction: 'none' }}>
|
||||||
<ContextMenu<HTMLDivElement>
|
<ContextMenu<HTMLDivElement>
|
||||||
@ -91,19 +124,12 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{cover_image_url ? (
|
<IAIDndImage
|
||||||
<Image
|
image={cover_image_url ? images[0] : undefined}
|
||||||
loading="lazy"
|
onDrop={handleDrop}
|
||||||
objectFit="cover"
|
fallback={<IAIImageFallback sx={{ bg: 'none' }} />}
|
||||||
draggable={false}
|
isUploadDisabled={true}
|
||||||
rounded="md"
|
|
||||||
src={cover_image_url}
|
|
||||||
fallback={<IAIImageFallback />}
|
|
||||||
sx={{}}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<Icon boxSize={8} color="base.700" as={FaFolder} />
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Editable
|
<Editable
|
||||||
|
@ -22,11 +22,10 @@ import IAIMantineSelect from 'common/components/IAIMantineSelect';
|
|||||||
|
|
||||||
const UpdateImageBoardModal = () => {
|
const UpdateImageBoardModal = () => {
|
||||||
const boards = useSelector(selectBoardsAll);
|
const boards = useSelector(selectBoardsAll);
|
||||||
const [selectedBoard, setSelectedBoard] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const { isOpen, onClose, handleAddToBoard, image } = useContext(
|
const { isOpen, onClose, handleAddToBoard, image } = useContext(
|
||||||
AddImageToBoardContext
|
AddImageToBoardContext
|
||||||
);
|
);
|
||||||
|
const [selectedBoard, setSelectedBoard] = useState<string | null>();
|
||||||
|
|
||||||
const cancelRef = useRef<HTMLButtonElement>(null);
|
const cancelRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
@ -50,10 +49,12 @@ const UpdateImageBoardModal = () => {
|
|||||||
<AlertDialogBody>
|
<AlertDialogBody>
|
||||||
<Box>
|
<Box>
|
||||||
<Flex direction="column" gap={3}>
|
<Flex direction="column" gap={3}>
|
||||||
|
{currentBoard && (
|
||||||
<Text>
|
<Text>
|
||||||
Moving this image to a board will remove it from its existing
|
Moving this image from{' '}
|
||||||
board.
|
<strong>{currentBoard.board_name}</strong> to
|
||||||
</Text>
|
</Text>
|
||||||
|
)}
|
||||||
<IAIMantineSelect
|
<IAIMantineSelect
|
||||||
placeholder="Select Board"
|
placeholder="Select Board"
|
||||||
onChange={(v) => setSelectedBoard(v)}
|
onChange={(v) => setSelectedBoard(v)}
|
||||||
|
@ -240,39 +240,10 @@ const ImageGalleryContent = () => {
|
|||||||
icon={<FaServer />}
|
icon={<FaServer />}
|
||||||
/>
|
/>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
{selectedBoard && (
|
|
||||||
<Flex>
|
<Flex>
|
||||||
<Text>{selectedBoard.board_name}</Text>
|
<Text>{selectedBoard ? selectedBoard.board_name : 'All Images'}</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
|
||||||
<Flex gap={2}>
|
<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
|
<IAIPopover
|
||||||
triggerComponent={
|
triggerComponent={
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
|
@ -7,7 +7,7 @@ import type { ImageMetadata } from './ImageMetadata';
|
|||||||
import type { ResourceOrigin } from './ResourceOrigin';
|
import type { ResourceOrigin } from './ResourceOrigin';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deserialized image record, enriched for the frontend with URLs.
|
* Deserialized image record, enriched for the frontend.
|
||||||
*/
|
*/
|
||||||
export type ImageDTO = {
|
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.
|
* A limited subset of the image's generation metadata. Retrieve the image's session for full metadata.
|
||||||
*/
|
*/
|
||||||
metadata?: ImageMetadata;
|
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