mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): boards styling
This commit is contained in:
parent
8d77c5ca96
commit
190ba5af59
@ -17,13 +17,13 @@ import {
|
|||||||
} from 'common/components/IAIImageFallback';
|
} from 'common/components/IAIImageFallback';
|
||||||
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
|
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
|
||||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||||
|
import ImageContextMenu from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
|
||||||
import { MouseEvent, ReactElement, SyntheticEvent, memo } from 'react';
|
import { MouseEvent, ReactElement, SyntheticEvent, memo } from 'react';
|
||||||
import { FaImage, FaUndo, FaUpload } from 'react-icons/fa';
|
import { FaImage, FaUndo, FaUpload } from 'react-icons/fa';
|
||||||
import { ImageDTO, PostUploadAction } from 'services/api/types';
|
import { ImageDTO, PostUploadAction } from 'services/api/types';
|
||||||
import { mode } from 'theme/util/mode';
|
import { mode } from 'theme/util/mode';
|
||||||
import IAIDraggable from './IAIDraggable';
|
import IAIDraggable from './IAIDraggable';
|
||||||
import IAIDroppable from './IAIDroppable';
|
import IAIDroppable from './IAIDroppable';
|
||||||
import ImageContextMenu from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
|
|
||||||
|
|
||||||
type IAIDndImageProps = {
|
type IAIDndImageProps = {
|
||||||
imageDTO: ImageDTO | undefined;
|
imageDTO: ImageDTO | undefined;
|
||||||
@ -148,7 +148,9 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
maxH: 'full',
|
maxH: 'full',
|
||||||
borderRadius: 'base',
|
borderRadius: 'base',
|
||||||
shadow: isSelected ? 'selected.light' : undefined,
|
shadow: isSelected ? 'selected.light' : undefined,
|
||||||
_dark: { shadow: isSelected ? 'selected.dark' : undefined },
|
_dark: {
|
||||||
|
shadow: isSelected ? 'selected.dark' : undefined,
|
||||||
|
},
|
||||||
...imageSx,
|
...imageSx,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -183,13 +185,6 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!imageDTO && isUploadDisabled && noContentFallback}
|
{!imageDTO && isUploadDisabled && noContentFallback}
|
||||||
{!isDropDisabled && (
|
|
||||||
<IAIDroppable
|
|
||||||
data={droppableData}
|
|
||||||
disabled={isDropDisabled}
|
|
||||||
dropLabel={dropLabel}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{imageDTO && !isDragDisabled && (
|
{imageDTO && !isDragDisabled && (
|
||||||
<IAIDraggable
|
<IAIDraggable
|
||||||
data={draggableData}
|
data={draggableData}
|
||||||
@ -197,6 +192,13 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{!isDropDisabled && (
|
||||||
|
<IAIDroppable
|
||||||
|
data={droppableData}
|
||||||
|
disabled={isDropDisabled}
|
||||||
|
dropLabel={dropLabel}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{onClickReset && withResetIcon && imageDTO && (
|
{onClickReset && withResetIcon && imageDTO && (
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
onClick={onClickReset}
|
onClick={onClickReset}
|
||||||
|
@ -13,10 +13,11 @@ type IAIDroppableProps = {
|
|||||||
dropLabel?: ReactNode;
|
dropLabel?: ReactNode;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
data?: TypesafeDroppableData;
|
data?: TypesafeDroppableData;
|
||||||
|
hoverRef?: React.Ref<HTMLDivElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const IAIDroppable = (props: IAIDroppableProps) => {
|
const IAIDroppable = (props: IAIDroppableProps) => {
|
||||||
const { dropLabel, data, disabled } = props;
|
const { dropLabel, data, disabled, hoverRef } = props;
|
||||||
const dndId = useRef(uuidv4());
|
const dndId = useRef(uuidv4());
|
||||||
|
|
||||||
const { isOver, setNodeRef, active } = useDroppable({
|
const { isOver, setNodeRef, active } = useDroppable({
|
||||||
|
@ -23,7 +23,6 @@ const BoardContextMenu = memo(
|
|||||||
dispatch(boardIdSelected(board?.board_id ?? board_id));
|
dispatch(boardIdSelected(board?.board_id ?? board_id));
|
||||||
}, [board?.board_id, board_id, dispatch]);
|
}, [board?.board_id, board_id, dispatch]);
|
||||||
return (
|
return (
|
||||||
<Box sx={{ touchAction: 'none', height: 'full' }}>
|
|
||||||
<ContextMenu<HTMLDivElement>
|
<ContextMenu<HTMLDivElement>
|
||||||
menuProps={{ size: 'sm', isLazy: true }}
|
menuProps={{ size: 'sm', isLazy: true }}
|
||||||
menuButtonProps={{
|
menuButtonProps={{
|
||||||
@ -50,7 +49,6 @@ const BoardContextMenu = memo(
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -1,27 +1,21 @@
|
|||||||
import {
|
import { ButtonGroup, Collapse, Flex, Grid, GridItem } from '@chakra-ui/react';
|
||||||
Collapse,
|
|
||||||
Flex,
|
|
||||||
Grid,
|
|
||||||
GridItem,
|
|
||||||
useDisclosure,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||||
import { memo, useState } from 'react';
|
import { memo, useCallback, useState } from 'react';
|
||||||
|
import { FaSearch } from 'react-icons/fa';
|
||||||
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
||||||
|
import { BoardDTO } from 'services/api/types';
|
||||||
import { useFeatureStatus } from '../../../../system/hooks/useFeatureStatus';
|
import { useFeatureStatus } from '../../../../system/hooks/useFeatureStatus';
|
||||||
|
import DeleteBoardModal from '../DeleteBoardModal';
|
||||||
import AddBoardButton from './AddBoardButton';
|
import AddBoardButton from './AddBoardButton';
|
||||||
import AllAssetsBoard from './AllAssetsBoard';
|
|
||||||
import AllImagesBoard from './AllImagesBoard';
|
|
||||||
import BatchBoard from './BatchBoard';
|
|
||||||
import BoardsSearch from './BoardsSearch';
|
import BoardsSearch from './BoardsSearch';
|
||||||
import GalleryBoard from './GalleryBoard';
|
import GalleryBoard from './GalleryBoard';
|
||||||
import NoBoardBoard from './NoBoardBoard';
|
import SystemBoardButton from './SystemBoardButton';
|
||||||
import DeleteBoardModal from '../DeleteBoardModal';
|
|
||||||
import { BoardDTO } from 'services/api/types';
|
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[stateSelector],
|
[stateSelector],
|
||||||
@ -48,7 +42,10 @@ const BoardsList = (props: Props) => {
|
|||||||
)
|
)
|
||||||
: boards;
|
: boards;
|
||||||
const [boardToDelete, setBoardToDelete] = useState<BoardDTO>();
|
const [boardToDelete, setBoardToDelete] = useState<BoardDTO>();
|
||||||
const [searchMode, setSearchMode] = useState(false);
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
|
const handleClickSearchIcon = useCallback(() => {
|
||||||
|
setIsSearching((v) => !v);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -64,7 +61,54 @@ const BoardsList = (props: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex sx={{ gap: 2, alignItems: 'center' }}>
|
<Flex sx={{ gap: 2, alignItems: 'center' }}>
|
||||||
<BoardsSearch setSearchMode={setSearchMode} />
|
<AnimatePresence mode="popLayout">
|
||||||
|
{isSearching ? (
|
||||||
|
<motion.div
|
||||||
|
key="boards-search"
|
||||||
|
initial={{
|
||||||
|
opacity: 0,
|
||||||
|
}}
|
||||||
|
exit={{
|
||||||
|
opacity: 0,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
opacity: 1,
|
||||||
|
transition: { duration: 0.1 },
|
||||||
|
}}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
<BoardsSearch setIsSearching={setIsSearching} />
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
key="system-boards-select"
|
||||||
|
initial={{
|
||||||
|
opacity: 0,
|
||||||
|
}}
|
||||||
|
exit={{
|
||||||
|
opacity: 0,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
opacity: 1,
|
||||||
|
transition: { duration: 0.1 },
|
||||||
|
}}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
<ButtonGroup sx={{ w: 'full', ps: 1.5 }} isAttached>
|
||||||
|
<SystemBoardButton board_id="images" />
|
||||||
|
<SystemBoardButton board_id="assets" />
|
||||||
|
<SystemBoardButton board_id="no_board" />
|
||||||
|
</ButtonGroup>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Search Boards"
|
||||||
|
size="sm"
|
||||||
|
isChecked={isSearching}
|
||||||
|
onClick={handleClickSearchIcon}
|
||||||
|
icon={<FaSearch />}
|
||||||
|
/>
|
||||||
<AddBoardButton />
|
<AddBoardButton />
|
||||||
</Flex>
|
</Flex>
|
||||||
<OverlayScrollbarsComponent
|
<OverlayScrollbarsComponent
|
||||||
@ -82,29 +126,10 @@ const BoardsList = (props: Props) => {
|
|||||||
<Grid
|
<Grid
|
||||||
className="list-container"
|
className="list-container"
|
||||||
sx={{
|
sx={{
|
||||||
gridTemplateRows: '6.5rem 6.5rem',
|
gridTemplateColumns: `repeat(auto-fill, minmax(96px, 1fr));`,
|
||||||
gridAutoFlow: 'column dense',
|
maxH: 346,
|
||||||
gridAutoColumns: '5rem',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!searchMode && (
|
|
||||||
<>
|
|
||||||
<GridItem sx={{ p: 1.5 }}>
|
|
||||||
<AllImagesBoard isSelected={selectedBoardId === 'images'} />
|
|
||||||
</GridItem>
|
|
||||||
<GridItem sx={{ p: 1.5 }}>
|
|
||||||
<AllAssetsBoard isSelected={selectedBoardId === 'assets'} />
|
|
||||||
</GridItem>
|
|
||||||
<GridItem sx={{ p: 1.5 }}>
|
|
||||||
<NoBoardBoard isSelected={selectedBoardId === 'no_board'} />
|
|
||||||
</GridItem>
|
|
||||||
{isBatchEnabled && (
|
|
||||||
<GridItem sx={{ p: 1.5 }}>
|
|
||||||
<BatchBoard isSelected={selectedBoardId === 'batch'} />
|
|
||||||
</GridItem>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{filteredBoards &&
|
{filteredBoards &&
|
||||||
filteredBoards.map((board) => (
|
filteredBoards.map((board) => (
|
||||||
<GridItem key={board.board_id} sx={{ p: 1.5 }}>
|
<GridItem key={board.board_id} sx={{ p: 1.5 }}>
|
||||||
|
@ -10,7 +10,14 @@ import { stateSelector } from 'app/store/store';
|
|||||||
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';
|
||||||
import { setBoardSearchText } from 'features/gallery/store/boardSlice';
|
import { setBoardSearchText } from 'features/gallery/store/boardSlice';
|
||||||
import { memo } from 'react';
|
import {
|
||||||
|
ChangeEvent,
|
||||||
|
KeyboardEvent,
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[stateSelector],
|
[stateSelector],
|
||||||
@ -22,31 +29,60 @@ const selector = createSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
setSearchMode: (searchMode: boolean) => void;
|
setIsSearching: (isSearching: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const BoardsSearch = (props: Props) => {
|
const BoardsSearch = (props: Props) => {
|
||||||
const { setSearchMode } = props;
|
const { setIsSearching } = props;
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { searchText } = useAppSelector(selector);
|
const { searchText } = useAppSelector(selector);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const handleBoardSearch = (searchTerm: string) => {
|
const handleBoardSearch = useCallback(
|
||||||
setSearchMode(searchTerm.length > 0);
|
(searchTerm: string) => {
|
||||||
dispatch(setBoardSearchText(searchTerm));
|
dispatch(setBoardSearchText(searchTerm));
|
||||||
};
|
},
|
||||||
const clearBoardSearch = () => {
|
[dispatch]
|
||||||
setSearchMode(false);
|
);
|
||||||
|
|
||||||
|
const clearBoardSearch = useCallback(() => {
|
||||||
dispatch(setBoardSearchText(''));
|
dispatch(setBoardSearchText(''));
|
||||||
};
|
setIsSearching(false);
|
||||||
|
}, [dispatch, setIsSearching]);
|
||||||
|
|
||||||
|
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]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// focus the search box on mount
|
||||||
|
if (!inputRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
inputRef.current.focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<Input
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
placeholder="Search Boards..."
|
placeholder="Search Boards..."
|
||||||
value={searchText}
|
value={searchText}
|
||||||
onChange={(e) => {
|
onKeyDown={handleKeydown}
|
||||||
handleBoardSearch(e.target.value);
|
onChange={handleChange}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{searchText && searchText.length && (
|
{searchText && searchText.length && (
|
||||||
<InputRightElement>
|
<InputRightElement>
|
||||||
@ -55,7 +91,8 @@ const BoardsSearch = (props: Props) => {
|
|||||||
size="xs"
|
size="xs"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
aria-label="Clear Search"
|
aria-label="Clear Search"
|
||||||
icon={<CloseIcon boxSize={3} />}
|
opacity={0.5}
|
||||||
|
icon={<CloseIcon boxSize={2} />}
|
||||||
/>
|
/>
|
||||||
</InputRightElement>
|
</InputRightElement>
|
||||||
)}
|
)}
|
||||||
|
@ -6,9 +6,9 @@ import {
|
|||||||
EditableInput,
|
EditableInput,
|
||||||
EditablePreview,
|
EditablePreview,
|
||||||
Flex,
|
Flex,
|
||||||
|
Icon,
|
||||||
Image,
|
Image,
|
||||||
Text,
|
Text,
|
||||||
useColorMode,
|
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||||
@ -17,14 +17,12 @@ import { stateSelector } from 'app/store/store';
|
|||||||
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';
|
||||||
import IAIDroppable from 'common/components/IAIDroppable';
|
import IAIDroppable from 'common/components/IAIDroppable';
|
||||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
|
||||||
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { FaUser } from 'react-icons/fa';
|
import { FaFolder } from 'react-icons/fa';
|
||||||
import { useUpdateBoardMutation } from 'services/api/endpoints/boards';
|
import { useUpdateBoardMutation } from 'services/api/endpoints/boards';
|
||||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||||
import { BoardDTO } from 'services/api/types';
|
import { BoardDTO } from 'services/api/types';
|
||||||
import { mode } from 'theme/util/mode';
|
|
||||||
import BoardContextMenu from '../BoardContextMenu';
|
import BoardContextMenu from '../BoardContextMenu';
|
||||||
|
|
||||||
const AUTO_ADD_BADGE_STYLES: ChakraProps['sx'] = {
|
const AUTO_ADD_BADGE_STYLES: ChakraProps['sx'] = {
|
||||||
@ -66,8 +64,9 @@ const GalleryBoard = memo(
|
|||||||
board.cover_image_name ?? skipToken
|
board.cover_image_name ?? skipToken
|
||||||
);
|
);
|
||||||
|
|
||||||
const { colorMode } = useColorMode();
|
|
||||||
const { board_name, board_id } = board;
|
const { board_name, board_id } = board;
|
||||||
|
const [localBoardName, setLocalBoardName] = useState(board_name);
|
||||||
|
|
||||||
const handleSelectBoard = useCallback(() => {
|
const handleSelectBoard = useCallback(() => {
|
||||||
dispatch(boardIdSelected(board_id));
|
dispatch(boardIdSelected(board_id));
|
||||||
}, [board_id, dispatch]);
|
}, [board_id, dispatch]);
|
||||||
@ -75,10 +74,6 @@ const GalleryBoard = memo(
|
|||||||
const [updateBoard, { isLoading: isUpdateBoardLoading }] =
|
const [updateBoard, { isLoading: isUpdateBoardLoading }] =
|
||||||
useUpdateBoardMutation();
|
useUpdateBoardMutation();
|
||||||
|
|
||||||
const handleUpdateBoardName = (newBoardName: string) => {
|
|
||||||
updateBoard({ board_id, changes: { board_name: newBoardName } });
|
|
||||||
};
|
|
||||||
|
|
||||||
const droppableData: MoveBoardDropData = useMemo(
|
const droppableData: MoveBoardDropData = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
id: board_id,
|
id: board_id,
|
||||||
@ -88,8 +83,49 @@ const GalleryBoard = memo(
|
|||||||
[board_id]
|
[board_id]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(
|
||||||
|
(newBoardName: string) => {
|
||||||
|
if (!newBoardName) {
|
||||||
|
// empty strings are not allowed
|
||||||
|
setLocalBoardName(board_name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newBoardName === board_name) {
|
||||||
|
// don't updated the board name if it hasn't changed
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateBoard({ board_id, changes: { board_name: newBoardName } })
|
||||||
|
.unwrap()
|
||||||
|
.then((response) => {
|
||||||
|
// update local state
|
||||||
|
setLocalBoardName(response.board_name);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// revert on error
|
||||||
|
setLocalBoardName(board_name);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[board_id, board_name, updateBoard]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = useCallback((newBoardName: string) => {
|
||||||
|
setLocalBoardName(newBoardName);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ touchAction: 'none', height: 'full' }}>
|
<Box
|
||||||
|
sx={{ w: 'full', h: 'full', touchAction: 'none', userSelect: 'none' }}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
aspectRatio: '1/1',
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<BoardContextMenu
|
<BoardContextMenu
|
||||||
board={board}
|
board={board}
|
||||||
board_id={board_id}
|
board_id={board_id}
|
||||||
@ -97,50 +133,66 @@ const GalleryBoard = memo(
|
|||||||
>
|
>
|
||||||
{(ref) => (
|
{(ref) => (
|
||||||
<Flex
|
<Flex
|
||||||
key={board_id}
|
|
||||||
userSelect="none"
|
|
||||||
ref={ref}
|
ref={ref}
|
||||||
sx={{
|
|
||||||
flexDir: 'column',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
cursor: 'pointer',
|
|
||||||
w: 'full',
|
|
||||||
h: 'full',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
onClick={handleSelectBoard}
|
onClick={handleSelectBoard}
|
||||||
sx={{
|
sx={{
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderRadius: 'base',
|
borderRadius: 'base',
|
||||||
w: 'full',
|
cursor: 'pointer',
|
||||||
aspectRatio: '1/1',
|
|
||||||
overflow: 'hidden',
|
|
||||||
shadow: isSelected ? 'selected.light' : undefined,
|
|
||||||
_dark: { shadow: isSelected ? 'selected.dark' : undefined },
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{board.cover_image_name && coverImage?.thumbnail_url && (
|
<Flex
|
||||||
<Image src={coverImage?.thumbnail_url} draggable={false} />
|
|
||||||
)}
|
|
||||||
{!(board.cover_image_name && coverImage?.thumbnail_url) && (
|
|
||||||
<IAINoContentFallback
|
|
||||||
boxSize={8}
|
|
||||||
icon={FaUser}
|
|
||||||
sx={{
|
sx={{
|
||||||
borderWidth: '2px',
|
w: 'full',
|
||||||
borderStyle: 'solid',
|
h: 'full',
|
||||||
borderColor: 'base.200',
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: 'base',
|
||||||
|
bg: 'base.200',
|
||||||
_dark: {
|
_dark: {
|
||||||
borderColor: 'base.800',
|
bg: 'base.800',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{coverImage?.thumbnail_url ? (
|
||||||
|
<Image
|
||||||
|
src={coverImage?.thumbnail_url}
|
||||||
|
draggable={false}
|
||||||
|
sx={{
|
||||||
|
maxW: 'full',
|
||||||
|
maxH: 'full',
|
||||||
|
borderRadius: 'base',
|
||||||
|
borderBottomRadius: 'lg',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
boxSize={12}
|
||||||
|
as={FaFolder}
|
||||||
|
sx={{
|
||||||
|
mt: -3,
|
||||||
|
opacity: 0.7,
|
||||||
|
color: 'base.500',
|
||||||
|
_dark: {
|
||||||
|
color: 'base.500',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</Flex>
|
||||||
)}
|
)}
|
||||||
|
</Flex>
|
||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@ -160,37 +212,55 @@ const GalleryBoard = memo(
|
|||||||
{board.image_count}
|
{board.image_count}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Flex>
|
</Flex>
|
||||||
<IAIDroppable
|
<Box
|
||||||
data={droppableData}
|
className="selection-box"
|
||||||
dropLabel={<Text fontSize="md">Move</Text>}
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
insetInlineEnd: 0,
|
||||||
|
bottom: 0,
|
||||||
|
insetInlineStart: 0,
|
||||||
|
borderRadius: 'base',
|
||||||
|
transitionProperty: 'common',
|
||||||
|
transitionDuration: 'common',
|
||||||
|
shadow: isSelected ? 'selected.light' : undefined,
|
||||||
|
_dark: {
|
||||||
|
shadow: isSelected ? 'selected.dark' : undefined,
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
width: 'full',
|
position: 'absolute',
|
||||||
height: 'full',
|
bottom: 0,
|
||||||
|
p: 1,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
w: 'full',
|
||||||
|
maxW: 'full',
|
||||||
|
borderBottomRadius: 'base',
|
||||||
|
bg: 'accent.400',
|
||||||
|
color: isSelected ? 'base.50' : 'base.100',
|
||||||
|
_dark: { color: 'base.800', bg: 'accent.200' },
|
||||||
|
lineHeight: 'short',
|
||||||
|
fontSize: 'xs',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Editable
|
<Editable
|
||||||
defaultValue={board_name}
|
value={localBoardName}
|
||||||
|
isDisabled={isUpdateBoardLoading}
|
||||||
submitOnBlur={true}
|
submitOnBlur={true}
|
||||||
onSubmit={(nextValue) => {
|
onChange={handleChange}
|
||||||
handleUpdateBoardName(nextValue);
|
onSubmit={handleSubmit}
|
||||||
|
sx={{
|
||||||
|
w: 'full',
|
||||||
}}
|
}}
|
||||||
sx={{ maxW: 'full' }}
|
|
||||||
>
|
>
|
||||||
<EditablePreview
|
<EditablePreview
|
||||||
sx={{
|
sx={{
|
||||||
color: isSelected
|
|
||||||
? mode('base.900', 'base.50')(colorMode)
|
|
||||||
: mode('base.700', 'base.200')(colorMode),
|
|
||||||
fontWeight: isSelected ? 600 : undefined,
|
|
||||||
fontSize: 'xs',
|
|
||||||
textAlign: 'center',
|
|
||||||
p: 0,
|
p: 0,
|
||||||
|
fontWeight: isSelected ? 700 : 500,
|
||||||
|
textAlign: 'center',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
}}
|
}}
|
||||||
@ -198,18 +268,25 @@ const GalleryBoard = memo(
|
|||||||
/>
|
/>
|
||||||
<EditableInput
|
<EditableInput
|
||||||
sx={{
|
sx={{
|
||||||
color: mode('base.900', 'base.50')(colorMode),
|
|
||||||
fontSize: 'xs',
|
|
||||||
borderColor: mode('base.500', 'base.500')(colorMode),
|
|
||||||
p: 0,
|
p: 0,
|
||||||
outline: 0,
|
_focusVisible: {
|
||||||
|
p: 0,
|
||||||
|
// get rid of the edit border
|
||||||
|
boxShadow: 'none',
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Editable>
|
</Editable>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
|
<IAIDroppable
|
||||||
|
data={droppableData}
|
||||||
|
dropLabel={<Text fontSize="md">Move</Text>}
|
||||||
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</BoardContextMenu>
|
</BoardContextMenu>
|
||||||
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -92,7 +92,7 @@ const GenericBoard = (props: GenericBoardProps) => {
|
|||||||
h: 'full',
|
h: 'full',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
fontWeight: isSelected ? 600 : undefined,
|
fontWeight: isSelected ? 600 : undefined,
|
||||||
fontSize: 'xs',
|
fontSize: 'sm',
|
||||||
color: isSelected ? 'base.900' : 'base.700',
|
color: isSelected ? 'base.900' : 'base.700',
|
||||||
_dark: { color: isSelected ? 'base.50' : 'base.200' },
|
_dark: { color: isSelected ? 'base.50' : 'base.200' },
|
||||||
}}
|
}}
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { stateSelector } from 'app/store/store';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
|
import IAIButton from 'common/components/IAIButton';
|
||||||
|
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||||
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
|
import { useBoardName } from 'services/api/hooks/useBoardName';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
board_id: 'images' | 'assets' | 'no_board';
|
||||||
|
};
|
||||||
|
|
||||||
|
const SystemBoardButton = ({ board_id }: Props) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const selector = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(
|
||||||
|
[stateSelector],
|
||||||
|
({ gallery }) => {
|
||||||
|
const { selectedBoardId } = gallery;
|
||||||
|
return { isSelected: selectedBoardId === board_id };
|
||||||
|
},
|
||||||
|
defaultSelectorOptions
|
||||||
|
),
|
||||||
|
[board_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { isSelected } = useAppSelector(selector);
|
||||||
|
|
||||||
|
const boardName = useBoardName(board_id);
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
dispatch(boardIdSelected(board_id));
|
||||||
|
}, [board_id, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IAIButton
|
||||||
|
onClick={handleClick}
|
||||||
|
size="sm"
|
||||||
|
isChecked={isSelected}
|
||||||
|
sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
borderRadius: 'base',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{boardName}
|
||||||
|
</IAIButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(SystemBoardButton);
|
@ -109,7 +109,7 @@ const GalleryDrawer = () => {
|
|||||||
isResizable={true}
|
isResizable={true}
|
||||||
isOpen={shouldShowGallery}
|
isOpen={shouldShowGallery}
|
||||||
onClose={handleCloseGallery}
|
onClose={handleCloseGallery}
|
||||||
minWidth={337}
|
minWidth={400}
|
||||||
>
|
>
|
||||||
<ImageGalleryContent />
|
<ImageGalleryContent />
|
||||||
</ResizableDrawer>
|
</ResizableDrawer>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Box } from '@chakra-ui/react';
|
import { Box, Flex } from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { TypesafeDraggableData } from 'app/components/ImageDnd/typesafeDnd';
|
import { TypesafeDraggableData } from 'app/components/ImageDnd/typesafeDnd';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
@ -86,15 +86,10 @@ const GalleryImage = (props: HoverableImageProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ w: 'full', h: 'full', touchAction: 'none' }}>
|
<Box sx={{ w: 'full', h: 'full', touchAction: 'none' }}>
|
||||||
<ImageContextMenu imageDTO={imageDTO}>
|
<Flex
|
||||||
{(ref) => (
|
|
||||||
<Box
|
|
||||||
position="relative"
|
|
||||||
key={imageName}
|
|
||||||
userSelect="none"
|
userSelect="none"
|
||||||
ref={ref}
|
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
position: 'relative',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
aspectRatio: '1/1',
|
aspectRatio: '1/1',
|
||||||
@ -115,9 +110,7 @@ const GalleryImage = (props: HoverableImageProps) => {
|
|||||||
// resetTooltip="Delete image"
|
// resetTooltip="Delete image"
|
||||||
// withResetIcon // removed bc it's too easy to accidentally delete images
|
// withResetIcon // removed bc it's too easy to accidentally delete images
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Flex>
|
||||||
)}
|
|
||||||
</ImageContextMenu>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -105,7 +105,7 @@ const enabledTabsSelector = createSelector(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const MIN_GALLERY_WIDTH = 300;
|
const MIN_GALLERY_WIDTH = 350;
|
||||||
const DEFAULT_GALLERY_PCT = 20;
|
const DEFAULT_GALLERY_PCT = 20;
|
||||||
export const NO_GALLERY_TABS: InvokeTabName[] = ['modelManager'];
|
export const NO_GALLERY_TABS: InvokeTabName[] = ['modelManager'];
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { memo } from 'react';
|
|||||||
import { PanelResizeHandle } from 'react-resizable-panels';
|
import { PanelResizeHandle } from 'react-resizable-panels';
|
||||||
import { mode } from 'theme/util/mode';
|
import { mode } from 'theme/util/mode';
|
||||||
|
|
||||||
type ResizeHandleProps = FlexProps & {
|
type ResizeHandleProps = Omit<FlexProps, 'direction'> & {
|
||||||
direction?: 'horizontal' | 'vertical';
|
direction?: 'horizontal' | 'vertical';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -6,9 +6,9 @@ export const useBoardName = (board_id: BoardId | null | undefined) => {
|
|||||||
selectFromResult: ({ data }) => {
|
selectFromResult: ({ data }) => {
|
||||||
let boardName = '';
|
let boardName = '';
|
||||||
if (board_id === 'images') {
|
if (board_id === 'images') {
|
||||||
boardName = 'All Images';
|
boardName = 'Images';
|
||||||
} else if (board_id === 'assets') {
|
} else if (board_id === 'assets') {
|
||||||
boardName = 'All Assets';
|
boardName = 'Assets';
|
||||||
} else if (board_id === 'no_board') {
|
} else if (board_id === 'no_board') {
|
||||||
boardName = 'No Board';
|
boardName = 'No Board';
|
||||||
} else if (board_id === 'batch') {
|
} else if (board_id === 'batch') {
|
||||||
|
56
invokeai/frontend/web/src/theme/components/editable.ts
Normal file
56
invokeai/frontend/web/src/theme/components/editable.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { editableAnatomy as parts } from '@chakra-ui/anatomy';
|
||||||
|
import {
|
||||||
|
createMultiStyleConfigHelpers,
|
||||||
|
defineStyle,
|
||||||
|
} from '@chakra-ui/styled-system';
|
||||||
|
import { mode } from '@chakra-ui/theme-tools';
|
||||||
|
|
||||||
|
const { definePartsStyle, defineMultiStyleConfig } =
|
||||||
|
createMultiStyleConfigHelpers(parts.keys);
|
||||||
|
|
||||||
|
const baseStylePreview = defineStyle({
|
||||||
|
borderRadius: 'md',
|
||||||
|
py: '1',
|
||||||
|
transitionProperty: 'common',
|
||||||
|
transitionDuration: 'normal',
|
||||||
|
});
|
||||||
|
|
||||||
|
const baseStyleInput = defineStyle((props) => ({
|
||||||
|
borderRadius: 'md',
|
||||||
|
py: '1',
|
||||||
|
transitionProperty: 'common',
|
||||||
|
transitionDuration: 'normal',
|
||||||
|
width: 'full',
|
||||||
|
_focusVisible: { boxShadow: 'outline' },
|
||||||
|
_placeholder: { opacity: 0.6 },
|
||||||
|
'::selection': {
|
||||||
|
color: mode('accent.900', 'accent.50')(props),
|
||||||
|
bg: mode('accent.200', 'accent.400')(props),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const baseStyleTextarea = defineStyle({
|
||||||
|
borderRadius: 'md',
|
||||||
|
py: '1',
|
||||||
|
transitionProperty: 'common',
|
||||||
|
transitionDuration: 'normal',
|
||||||
|
width: 'full',
|
||||||
|
_focusVisible: { boxShadow: 'outline' },
|
||||||
|
_placeholder: { opacity: 0.6 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const invokeAI = definePartsStyle((props) => ({
|
||||||
|
preview: baseStylePreview,
|
||||||
|
input: baseStyleInput(props),
|
||||||
|
textarea: baseStyleTextarea,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const editableTheme = defineMultiStyleConfig({
|
||||||
|
variants: {
|
||||||
|
invokeAI,
|
||||||
|
},
|
||||||
|
defaultProps: {
|
||||||
|
size: 'sm',
|
||||||
|
variant: 'invokeAI',
|
||||||
|
},
|
||||||
|
});
|
@ -20,6 +20,7 @@ import { tabsTheme } from './components/tabs';
|
|||||||
import { textTheme } from './components/text';
|
import { textTheme } from './components/text';
|
||||||
import { textareaTheme } from './components/textarea';
|
import { textareaTheme } from './components/textarea';
|
||||||
import { tooltipTheme } from './components/tooltip';
|
import { tooltipTheme } from './components/tooltip';
|
||||||
|
import { editableTheme } from './components/editable';
|
||||||
|
|
||||||
export const theme: ThemeOverride = {
|
export const theme: ThemeOverride = {
|
||||||
config: {
|
config: {
|
||||||
@ -74,12 +75,23 @@ export const theme: ThemeOverride = {
|
|||||||
'0px 0px 0px 1px var(--invokeai-colors-base-150), 0px 0px 0px 4px var(--invokeai-colors-accent-400)',
|
'0px 0px 0px 1px var(--invokeai-colors-base-150), 0px 0px 0px 4px var(--invokeai-colors-accent-400)',
|
||||||
dark: '0px 0px 0px 1px var(--invokeai-colors-base-900), 0px 0px 0px 4px var(--invokeai-colors-accent-400)',
|
dark: '0px 0px 0px 1px var(--invokeai-colors-base-900), 0px 0px 0px 4px var(--invokeai-colors-accent-400)',
|
||||||
},
|
},
|
||||||
|
hoverSelected: {
|
||||||
|
light:
|
||||||
|
'0px 0px 0px 1px var(--invokeai-colors-base-150), 0px 0px 0px 4px var(--invokeai-colors-accent-500)',
|
||||||
|
dark: '0px 0px 0px 1px var(--invokeai-colors-base-900), 0px 0px 0px 4px var(--invokeai-colors-accent-300)',
|
||||||
|
},
|
||||||
|
hoverUnselected: {
|
||||||
|
light:
|
||||||
|
'0px 0px 0px 1px var(--invokeai-colors-base-150), 0px 0px 0px 4px var(--invokeai-colors-accent-200)',
|
||||||
|
dark: '0px 0px 0px 1px var(--invokeai-colors-base-900), 0px 0px 0px 4px var(--invokeai-colors-accent-600)',
|
||||||
|
},
|
||||||
nodeSelectedOutline: `0 0 0 2px var(--invokeai-colors-accent-450)`,
|
nodeSelectedOutline: `0 0 0 2px var(--invokeai-colors-accent-450)`,
|
||||||
},
|
},
|
||||||
colors: InvokeAIColors,
|
colors: InvokeAIColors,
|
||||||
components: {
|
components: {
|
||||||
Button: buttonTheme, // Button and IconButton
|
Button: buttonTheme, // Button and IconButton
|
||||||
Input: inputTheme,
|
Input: inputTheme,
|
||||||
|
Editable: editableTheme,
|
||||||
Textarea: textareaTheme,
|
Textarea: textareaTheme,
|
||||||
Tabs: tabsTheme,
|
Tabs: tabsTheme,
|
||||||
Progress: progressTheme,
|
Progress: progressTheme,
|
||||||
|
@ -37,4 +37,7 @@ export const getInputOutlineStyles = (props: StyleFunctionProps) => ({
|
|||||||
_placeholder: {
|
_placeholder: {
|
||||||
color: mode('base.700', 'base.400')(props),
|
color: mode('base.700', 'base.400')(props),
|
||||||
},
|
},
|
||||||
|
'::selection': {
|
||||||
|
bg: mode('accent.200', 'accent.400')(props),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user