diff --git a/invokeai/app/api/routers/boards.py b/invokeai/app/api/routers/boards.py index 77d6956033..926c0f7fd2 100644 --- a/invokeai/app/api/routers/boards.py +++ b/invokeai/app/api/routers/boards.py @@ -31,6 +31,7 @@ class DeleteBoardResult(BaseModel): ) async def create_board( board_name: str = Query(description="The name of the board to create"), + is_private: bool = Query(default=False, description="Whether the board is private"), ) -> BoardDTO: """Creates a board""" try: diff --git a/invokeai/app/services/board_records/board_records_common.py b/invokeai/app/services/board_records/board_records_common.py index d763480a9f..0dda8a8b6b 100644 --- a/invokeai/app/services/board_records/board_records_common.py +++ b/invokeai/app/services/board_records/board_records_common.py @@ -24,6 +24,8 @@ class BoardRecord(BaseModelExcludeNull): """The name of the cover image of the board.""" archived: bool = Field(description="Whether or not the board is archived.") """Whether or not the board is archived.""" + is_private: Optional[bool] = Field(default=None, description="Whether the board is private.") + """Whether the board is private.""" def deserialize_board_record(board_dict: dict) -> BoardRecord: @@ -38,6 +40,7 @@ def deserialize_board_record(board_dict: dict) -> BoardRecord: updated_at = board_dict.get("updated_at", get_iso_timestamp()) deleted_at = board_dict.get("deleted_at", get_iso_timestamp()) archived = board_dict.get("archived", False) + is_private = board_dict.get("is_private", False) return BoardRecord( board_id=board_id, @@ -47,6 +50,7 @@ def deserialize_board_record(board_dict: dict) -> BoardRecord: updated_at=updated_at, deleted_at=deleted_at, archived=archived, + is_private=is_private, ) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index dc09ac313f..4b102d6cf3 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -17,9 +17,12 @@ }, "boards": { "addBoard": "Add Board", + "addPrivateBoard": "Add Private Board", + "addSharedBoard": "Add Shared Board", "archiveBoard": "Archive Board", "archived": "Archived", "autoAddBoard": "Auto-Add Board", + "boards": "Boards", "selectedForAutoAdd": "Selected for Auto-Add", "bottomMessage": "Deleting this board and its images will reset any features currently using them.", "cancel": "Cancel", @@ -36,8 +39,10 @@ "movingImagesToBoard_other": "Moving {{count}} images to board:", "myBoard": "My Board", "noMatching": "No matching Boards", + "private": "Private", "searchBoard": "Search Boards...", "selectBoard": "Select a Board", + "shared": "Shared", "topMessage": "This board contains images used in the following features:", "unarchiveBoard": "Unarchive Board", "uncategorized": "Uncategorized", diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addArchivedOrDeletedBoardListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addArchivedOrDeletedBoardListener.ts index 0915929245..c569a6e36d 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addArchivedOrDeletedBoardListener.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addArchivedOrDeletedBoardListener.ts @@ -15,8 +15,6 @@ export const addArchivedOrDeletedBoardListener = (startAppListening: AppStartLis matcher: isAnyOf( // Updating a board may change its archived status boardsApi.endpoints.updateBoard.matchFulfilled, - // If the selected/auto-add board was deleted from a different session, we'll only know during the list request, - boardsApi.endpoints.listAllBoards.matchFulfilled, // If a board is deleted, we'll need to reset the auto-add board imagesApi.endpoints.deleteBoard.matchFulfilled, imagesApi.endpoints.deleteBoardAndImages.matchFulfilled, diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index 21636ada49..6d7416d95d 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -65,6 +65,7 @@ export type AppConfig = { */ shouldUpdateImagesOnConnect: boolean; shouldFetchMetadataFromApi: boolean; + allowPrivateBoards: boolean; disabledTabs: InvokeTabName[]; disabledFeatures: AppFeature[]; disabledSDFeatures: SDFeature[]; diff --git a/invokeai/frontend/web/src/common/components/IAIDropOverlay.tsx b/invokeai/frontend/web/src/common/components/IAIDropOverlay.tsx index cd3e0cbee1..51e5583bc6 100644 --- a/invokeai/frontend/web/src/common/components/IAIDropOverlay.tsx +++ b/invokeai/frontend/web/src/common/components/IAIDropOverlay.tsx @@ -52,8 +52,8 @@ const IAIDropOverlay = (props: Props) => { bottom={0.5} opacity={1} borderWidth={2} - borderColor={isOver ? 'base.50' : 'base.300'} - borderRadius="lg" + borderColor={isOver ? 'base.300' : 'base.500'} + borderRadius="base" borderStyle="dashed" transitionProperty="common" transitionDuration="0.1s" diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/AutoAddBadge.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/AutoAddBadge.tsx new file mode 100644 index 0000000000..a8b1f9f4fb --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/AutoAddBadge.tsx @@ -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 ( + + {t('common.auto')} + + ); +}); + +AutoAddBadge.displayName = 'AutoAddBadge'; diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/AutoAddIcon.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/AutoAddIcon.tsx deleted file mode 100644 index 9dd6a59c49..0000000000 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/AutoAddIcon.tsx +++ /dev/null @@ -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 ( - - - {t('common.auto')} - - - ); -}; - -export default memo(AutoAddIcon); diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/AddBoardButton.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/AddBoardButton.tsx index 5cd4d001f4..c6ddb85daa 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/AddBoardButton.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/AddBoardButton.tsx @@ -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 ( } 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" /> ); }; diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsList.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsList.tsx index e47edd21fc..6f37dbcdb5 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsList.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsList.tsx @@ -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(); + 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 ( <> - - - - - - - - - - - - {filteredBoards && - filteredBoards.map((board, index) => ( - + + + + + + {allowPrivateBoards && ( + <> + + + + + {t('boards.private')} + + + + + + + + {allowPrivateBoards && } + {filteredPrivateBoards.map((board) => ( - - ))} - - + ))} + + + + + )} + + + + + {allowPrivateBoards ? t('boards.shared') : t('boards.boards')} + + + - + + + + {!allowPrivateBoards && } + {filteredSharedBoards.map((board) => ( + + ))} + + + + ); }; - export default memo(BoardsList); diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx index ad40fdbf6a..32ed84558c 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx @@ -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 ( - - - - ); +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 ( - - - - {(ref) => ( - } - openDelay={1000} + + {(ref) => ( + } + openDelay={1000} + > + + + - - {board.archived && } - {coverImage?.thumbnail_url ? ( - - ) : ( - - - - )} - {isSelectedForAutoAdd && } - - - - - - - + + + + {autoAddBoardId === board.board_id && !editingDisclosure.isOpen && } + {board.archived && !editingDisclosure.isOpen && ( + + )} + {!editingDisclosure.isOpen && {board.image_count}} - {t('unifiedCanvas.move')}} /> - - - )} - - - + {t('unifiedCanvas.move')}} /> + + + )} + ); }; export default memo(GalleryBoard); + +const CoverImage = ({ board }: { board: BoardDTO }) => { + const { currentData: coverImage } = useGetImageDTOQuery(board.cover_image_name ?? skipToken); + + if (coverImage) { + return ( + + ); + } + + return ( + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx index 2e823ea25b..fb47bf5810 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx @@ -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 ( - - - - {(ref) => ( - } openDelay={1000}> - - - invoke-ai-logo - - {autoAddBoardId === 'none' && } - - {boardName} - - - {t('unifiedCanvas.move')}} /> - - - )} - - - + + {(ref) => ( + } openDelay={1000}> + + + {/* iconified from public/assets/images/invoke-symbol-wht-lrg.svg */} + + + + + + + {boardName} + + {autoAddBoardId === 'none' && } + {imagesTotal} + {t('unifiedCanvas.move')}} /> + + + )} + ); }); diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryBoardName.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryBoardName.tsx index 55aec17ab2..233aa8a8c1 100644 --- a/invokeai/frontend/web/src/features/gallery/components/GalleryBoardName.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/GalleryBoardName.tsx @@ -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 ( - - - {formattedBoardName} - - + + + {boardName} + ); }; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx index b0b147b510..665d96a006 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx @@ -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} - - - - - - - - - + + diff --git a/invokeai/frontend/web/src/features/system/store/configSlice.ts b/invokeai/frontend/web/src/features/system/store/configSlice.ts index 7d26dbd34c..8901365556 100644 --- a/invokeai/frontend/web/src/features/system/store/configSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/configSlice.ts @@ -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'], diff --git a/invokeai/frontend/web/src/services/api/endpoints/boards.ts b/invokeai/frontend/web/src/services/api/endpoints/boards.ts index 177aa0e340..55ebeab318 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/boards.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/boards.ts @@ -1,5 +1,11 @@ import { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types'; -import type { BoardDTO, ListBoardsArgs, OffsetPaginatedResults_ImageDTO_, UpdateBoardArg } from 'services/api/types'; +import type { + BoardDTO, + CreateBoardArg, + ListBoardsArgs, + OffsetPaginatedResults_ImageDTO_, + UpdateBoardArg, +} from 'services/api/types'; import { getListImagesUrl } from 'services/api/util'; import type { ApiTagDescription } from '..'; @@ -87,11 +93,11 @@ export const boardsApi = api.injectEndpoints({ * Boards Mutations */ - createBoard: build.mutation({ - query: (board_name) => ({ + createBoard: build.mutation({ + query: ({ board_name, is_private }) => ({ url: buildBoardsUrl(), method: 'POST', - params: { board_name }, + params: { board_name, is_private }, }), invalidatesTags: [{ type: 'Board', id: LIST_TAG }], }), diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index f64fba6e77..7780ccbdc8 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -1058,6 +1058,11 @@ export type components = { * @description Whether or not the board is archived. */ archived: boolean; + /** + * Is Private + * @description Whether the board is private. + */ + is_private?: boolean | null; /** * Image Count * @description The number of images in the board. @@ -6561,6 +6566,12 @@ export type components = { * @default false */ tiled?: boolean; + /** + * Tile Size + * @description The tile size for VAE tiling in pixels (image space). If set to 0, the default tile size for the + * @default 0 + */ + tile_size?: number; /** * Fp32 * @description Whether or not to use full float32 precision @@ -7293,145 +7304,145 @@ export type components = { project_id: string | null; }; InvocationOutputMap: { - save_image: components["schemas"]["ImageOutput"]; - integer_math: components["schemas"]["IntegerOutput"]; - segment_anything_processor: components["schemas"]["ImageOutput"]; - sdxl_refiner_compel_prompt: components["schemas"]["ConditioningOutput"]; - zoe_depth_image_processor: components["schemas"]["ImageOutput"]; - collect: components["schemas"]["CollectInvocationOutput"]; - range: components["schemas"]["IntegerCollectionOutput"]; - unsharp_mask: components["schemas"]["ImageOutput"]; - string_replace: components["schemas"]["StringOutput"]; - face_identifier: components["schemas"]["ImageOutput"]; - heuristic_resize: components["schemas"]["ImageOutput"]; - range_of_size: components["schemas"]["IntegerCollectionOutput"]; - latents_collection: components["schemas"]["LatentsCollectionOutput"]; - color_map_image_processor: components["schemas"]["ImageOutput"]; - img_ilerp: components["schemas"]["ImageOutput"]; - infill_patchmatch: components["schemas"]["ImageOutput"]; - face_off: components["schemas"]["FaceOffOutput"]; - string_collection: components["schemas"]["StringCollectionOutput"]; - sdxl_model_loader: components["schemas"]["SDXLModelLoaderOutput"]; - sdxl_lora_loader: components["schemas"]["SDXLLoRALoaderOutput"]; - string_join: components["schemas"]["StringOutput"]; - lblend: components["schemas"]["LatentsOutput"]; - conditioning_collection: components["schemas"]["ConditioningCollectionOutput"]; - string_split_neg: components["schemas"]["StringPosNegOutput"]; - img_watermark: components["schemas"]["ImageOutput"]; - infill_lama: components["schemas"]["ImageOutput"]; - div: components["schemas"]["IntegerOutput"]; - show_image: components["schemas"]["ImageOutput"]; - tile_to_properties: components["schemas"]["TileToPropertiesOutput"]; - sub: components["schemas"]["IntegerOutput"]; - normalbae_image_processor: components["schemas"]["ImageOutput"]; - invert_tensor_mask: components["schemas"]["MaskOutput"]; - create_gradient_mask: components["schemas"]["GradientMaskOutput"]; - string_split: components["schemas"]["String2Output"]; - step_param_easing: components["schemas"]["FloatCollectionOutput"]; - metadata: components["schemas"]["MetadataOutput"]; - img_pad_crop: components["schemas"]["ImageOutput"]; - integer: components["schemas"]["IntegerOutput"]; - img_mul: components["schemas"]["ImageOutput"]; - calculate_image_tiles: components["schemas"]["CalculateImageTilesOutput"]; - color: components["schemas"]["ColorOutput"]; - infill_rgba: components["schemas"]["ImageOutput"]; - t2i_adapter: components["schemas"]["T2IAdapterOutput"]; - denoise_latents: components["schemas"]["LatentsOutput"]; - img_lerp: components["schemas"]["ImageOutput"]; - img_channel_offset: components["schemas"]["ImageOutput"]; - img_crop: components["schemas"]["ImageOutput"]; - alpha_mask_to_tensor: components["schemas"]["MaskOutput"]; - color_correct: components["schemas"]["ImageOutput"]; - calculate_image_tiles_min_overlap: components["schemas"]["CalculateImageTilesOutput"]; - img_hue_adjust: components["schemas"]["ImageOutput"]; - lresize: components["schemas"]["LatentsOutput"]; - img_blur: components["schemas"]["ImageOutput"]; - compel: components["schemas"]["ConditioningOutput"]; - sdxl_lora_collection_loader: components["schemas"]["SDXLLoRALoaderOutput"]; - float_to_int: components["schemas"]["IntegerOutput"]; - boolean: components["schemas"]["BooleanOutput"]; - string_join_three: components["schemas"]["StringOutput"]; - add: components["schemas"]["IntegerOutput"]; - merge_tiles_to_image: components["schemas"]["ImageOutput"]; - core_metadata: components["schemas"]["MetadataOutput"]; - lscale: components["schemas"]["LatentsOutput"]; - mlsd_image_processor: components["schemas"]["ImageOutput"]; - image_collection: components["schemas"]["ImageCollectionOutput"]; - crop_latents: components["schemas"]["LatentsOutput"]; - image_mask_to_tensor: components["schemas"]["MaskOutput"]; - lora_collection_loader: components["schemas"]["LoRALoaderOutput"]; - ip_adapter: components["schemas"]["IPAdapterOutput"]; - pidi_image_processor: components["schemas"]["ImageOutput"]; - rand_int: components["schemas"]["IntegerOutput"]; - img_conv: components["schemas"]["ImageOutput"]; - scheduler: components["schemas"]["SchedulerOutput"]; - img_paste: components["schemas"]["ImageOutput"]; - noise: components["schemas"]["NoiseOutput"]; - img_scale: components["schemas"]["ImageOutput"]; - i2l: components["schemas"]["LatentsOutput"]; - main_model_loader: components["schemas"]["ModelLoaderOutput"]; - blank_image: components["schemas"]["ImageOutput"]; - mask_edge: components["schemas"]["ImageOutput"]; - seamless: components["schemas"]["SeamlessModeOutput"]; - esrgan: components["schemas"]["ImageOutput"]; - canvas_paste_back: components["schemas"]["ImageOutput"]; - mul: components["schemas"]["IntegerOutput"]; - dynamic_prompt: components["schemas"]["StringCollectionOutput"]; - controlnet: components["schemas"]["ControlOutput"]; - l2i: components["schemas"]["ImageOutput"]; - ideal_size: components["schemas"]["IdealSizeOutput"]; - latents: components["schemas"]["LatentsOutput"]; - midas_depth_image_processor: components["schemas"]["ImageOutput"]; - tomask: components["schemas"]["ImageOutput"]; - float_math: components["schemas"]["FloatOutput"]; - round_float: components["schemas"]["FloatOutput"]; - cv_inpaint: components["schemas"]["ImageOutput"]; - create_denoise_mask: components["schemas"]["DenoiseMaskOutput"]; - model_identifier: components["schemas"]["ModelIdentifierOutput"]; - pair_tile_image: components["schemas"]["PairTileImageOutput"]; - lineart_image_processor: components["schemas"]["ImageOutput"]; - img_nsfw: components["schemas"]["ImageOutput"]; - infill_cv2: components["schemas"]["ImageOutput"]; - clip_skip: components["schemas"]["CLIPSkipInvocationOutput"]; - dw_openpose_image_processor: components["schemas"]["ImageOutput"]; - img_resize: components["schemas"]["ImageOutput"]; - iterate: components["schemas"]["IterateInvocationOutput"]; rectangle_mask: components["schemas"]["MaskOutput"]; - canny_image_processor: components["schemas"]["ImageOutput"]; - calculate_image_tiles_even_split: components["schemas"]["CalculateImageTilesOutput"]; + hed_image_processor: components["schemas"]["ImageOutput"]; + compel: components["schemas"]["ConditioningOutput"]; + img_resize: components["schemas"]["ImageOutput"]; + ideal_size: components["schemas"]["IdealSizeOutput"]; + rand_int: components["schemas"]["IntegerOutput"]; + clip_skip: components["schemas"]["CLIPSkipInvocationOutput"]; + string_collection: components["schemas"]["StringCollectionOutput"]; + create_gradient_mask: components["schemas"]["GradientMaskOutput"]; + round_float: components["schemas"]["FloatOutput"]; + scheduler: components["schemas"]["SchedulerOutput"]; + main_model_loader: components["schemas"]["ModelLoaderOutput"]; + string_split: components["schemas"]["String2Output"]; mask_from_id: components["schemas"]["ImageOutput"]; - metadata_item: components["schemas"]["MetadataItemOutput"]; - infill_tile: components["schemas"]["ImageOutput"]; - tiled_multi_diffusion_denoise_latents: components["schemas"]["LatentsOutput"]; - img_channel_multiply: components["schemas"]["ImageOutput"]; + collect: components["schemas"]["CollectInvocationOutput"]; + heuristic_resize: components["schemas"]["ImageOutput"]; + tomask: components["schemas"]["ImageOutput"]; boolean_collection: components["schemas"]["BooleanCollectionOutput"]; - lora_loader: components["schemas"]["LoRALoaderOutput"]; - float_collection: components["schemas"]["FloatCollectionOutput"]; - string: components["schemas"]["StringOutput"]; - freeu: components["schemas"]["UNetOutput"]; - lineart_anime_image_processor: components["schemas"]["ImageOutput"]; - depth_anything_image_processor: components["schemas"]["ImageOutput"]; - image: components["schemas"]["ImageOutput"]; + core_metadata: components["schemas"]["MetadataOutput"]; + canny_image_processor: components["schemas"]["ImageOutput"]; + string_replace: components["schemas"]["StringOutput"]; face_mask_detection: components["schemas"]["FaceMaskOutput"]; + integer: components["schemas"]["IntegerOutput"]; + img_watermark: components["schemas"]["ImageOutput"]; + img_crop: components["schemas"]["ImageOutput"]; + t2i_adapter: components["schemas"]["T2IAdapterOutput"]; + create_denoise_mask: components["schemas"]["DenoiseMaskOutput"]; rand_float: components["schemas"]["FloatOutput"]; - float: components["schemas"]["FloatOutput"]; - random_range: components["schemas"]["IntegerCollectionOutput"]; - integer_collection: components["schemas"]["IntegerCollectionOutput"]; - sdxl_refiner_model_loader: components["schemas"]["SDXLRefinerModelLoaderOutput"]; - mask_combine: components["schemas"]["ImageOutput"]; - tile_image_processor: components["schemas"]["ImageOutput"]; + zoe_depth_image_processor: components["schemas"]["ImageOutput"]; + face_off: components["schemas"]["FaceOffOutput"]; + tile_to_properties: components["schemas"]["TileToPropertiesOutput"]; + color_map_image_processor: components["schemas"]["ImageOutput"]; + lineart_anime_image_processor: components["schemas"]["ImageOutput"]; + face_identifier: components["schemas"]["ImageOutput"]; + float_math: components["schemas"]["FloatOutput"]; + mediapipe_face_processor: components["schemas"]["ImageOutput"]; + img_channel_multiply: components["schemas"]["ImageOutput"]; + metadata_item: components["schemas"]["MetadataItemOutput"]; + img_ilerp: components["schemas"]["ImageOutput"]; + conditioning: components["schemas"]["ConditioningOutput"]; + pidi_image_processor: components["schemas"]["ImageOutput"]; + seamless: components["schemas"]["SeamlessModeOutput"]; + latents: components["schemas"]["LatentsOutput"]; img_chan: components["schemas"]["ImageOutput"]; - vae_loader: components["schemas"]["VAEOutput"]; + model_identifier: components["schemas"]["ModelIdentifierOutput"]; + noise: components["schemas"]["NoiseOutput"]; + string_join: components["schemas"]["StringOutput"]; + blank_image: components["schemas"]["ImageOutput"]; + calculate_image_tiles: components["schemas"]["CalculateImageTilesOutput"]; + invert_tensor_mask: components["schemas"]["MaskOutput"]; + save_image: components["schemas"]["ImageOutput"]; + unsharp_mask: components["schemas"]["ImageOutput"]; + image_mask_to_tensor: components["schemas"]["MaskOutput"]; + step_param_easing: components["schemas"]["FloatCollectionOutput"]; + merge_tiles_to_image: components["schemas"]["ImageOutput"]; + integer_collection: components["schemas"]["IntegerCollectionOutput"]; + calculate_image_tiles_even_split: components["schemas"]["CalculateImageTilesOutput"]; + integer_math: components["schemas"]["IntegerOutput"]; + range: components["schemas"]["IntegerCollectionOutput"]; prompt_from_file: components["schemas"]["StringCollectionOutput"]; + segment_anything_processor: components["schemas"]["ImageOutput"]; + freeu: components["schemas"]["UNetOutput"]; + sub: components["schemas"]["IntegerOutput"]; + lresize: components["schemas"]["LatentsOutput"]; + float: components["schemas"]["FloatOutput"]; + float_collection: components["schemas"]["FloatCollectionOutput"]; + dynamic_prompt: components["schemas"]["StringCollectionOutput"]; + infill_lama: components["schemas"]["ImageOutput"]; + l2i: components["schemas"]["ImageOutput"]; + img_lerp: components["schemas"]["ImageOutput"]; + ip_adapter: components["schemas"]["IPAdapterOutput"]; + lora_collection_loader: components["schemas"]["LoRALoaderOutput"]; + color: components["schemas"]["ColorOutput"]; + tiled_multi_diffusion_denoise_latents: components["schemas"]["LatentsOutput"]; + cv_inpaint: components["schemas"]["ImageOutput"]; + lscale: components["schemas"]["LatentsOutput"]; + string: components["schemas"]["StringOutput"]; + sdxl_refiner_compel_prompt: components["schemas"]["ConditioningOutput"]; + string_join_three: components["schemas"]["StringOutput"]; + midas_depth_image_processor: components["schemas"]["ImageOutput"]; + esrgan: components["schemas"]["ImageOutput"]; + sdxl_refiner_model_loader: components["schemas"]["SDXLRefinerModelLoaderOutput"]; + mul: components["schemas"]["IntegerOutput"]; + normalbae_image_processor: components["schemas"]["ImageOutput"]; + infill_rgba: components["schemas"]["ImageOutput"]; + sdxl_model_loader: components["schemas"]["SDXLModelLoaderOutput"]; + vae_loader: components["schemas"]["VAEOutput"]; + float_to_int: components["schemas"]["IntegerOutput"]; + lora_selector: components["schemas"]["LoRASelectorOutput"]; + crop_latents: components["schemas"]["LatentsOutput"]; + img_mul: components["schemas"]["ImageOutput"]; float_range: components["schemas"]["FloatCollectionOutput"]; merge_metadata: components["schemas"]["MetadataOutput"]; - sdxl_compel_prompt: components["schemas"]["ConditioningOutput"]; - hed_image_processor: components["schemas"]["ImageOutput"]; - lora_selector: components["schemas"]["LoRASelectorOutput"]; - conditioning: components["schemas"]["ConditioningOutput"]; + img_blur: components["schemas"]["ImageOutput"]; + boolean: components["schemas"]["BooleanOutput"]; + tile_image_processor: components["schemas"]["ImageOutput"]; + mlsd_image_processor: components["schemas"]["ImageOutput"]; + infill_patchmatch: components["schemas"]["ImageOutput"]; + img_pad_crop: components["schemas"]["ImageOutput"]; leres_image_processor: components["schemas"]["ImageOutput"]; - mediapipe_face_processor: components["schemas"]["ImageOutput"]; + sdxl_lora_loader: components["schemas"]["SDXLLoRALoaderOutput"]; + dw_openpose_image_processor: components["schemas"]["ImageOutput"]; + img_scale: components["schemas"]["ImageOutput"]; + pair_tile_image: components["schemas"]["PairTileImageOutput"]; + lblend: components["schemas"]["LatentsOutput"]; + range_of_size: components["schemas"]["IntegerCollectionOutput"]; + image_collection: components["schemas"]["ImageCollectionOutput"]; + calculate_image_tiles_min_overlap: components["schemas"]["CalculateImageTilesOutput"]; + img_channel_offset: components["schemas"]["ImageOutput"]; + alpha_mask_to_tensor: components["schemas"]["MaskOutput"]; + infill_cv2: components["schemas"]["ImageOutput"]; + mask_combine: components["schemas"]["ImageOutput"]; + string_split_neg: components["schemas"]["StringPosNegOutput"]; + sdxl_lora_collection_loader: components["schemas"]["SDXLLoRALoaderOutput"]; + lineart_image_processor: components["schemas"]["ImageOutput"]; + img_nsfw: components["schemas"]["ImageOutput"]; + image: components["schemas"]["ImageOutput"]; content_shuffle_image_processor: components["schemas"]["ImageOutput"]; + canvas_paste_back: components["schemas"]["ImageOutput"]; + iterate: components["schemas"]["IterateInvocationOutput"]; + div: components["schemas"]["IntegerOutput"]; + latents_collection: components["schemas"]["LatentsCollectionOutput"]; + img_conv: components["schemas"]["ImageOutput"]; + mask_edge: components["schemas"]["ImageOutput"]; + conditioning_collection: components["schemas"]["ConditioningCollectionOutput"]; + img_hue_adjust: components["schemas"]["ImageOutput"]; + depth_anything_image_processor: components["schemas"]["ImageOutput"]; + lora_loader: components["schemas"]["LoRALoaderOutput"]; + sdxl_compel_prompt: components["schemas"]["ConditioningOutput"]; + add: components["schemas"]["IntegerOutput"]; + controlnet: components["schemas"]["ControlOutput"]; + color_correct: components["schemas"]["ImageOutput"]; + random_range: components["schemas"]["IntegerCollectionOutput"]; + denoise_latents: components["schemas"]["LatentsOutput"]; + metadata: components["schemas"]["MetadataOutput"]; + i2l: components["schemas"]["LatentsOutput"]; + show_image: components["schemas"]["ImageOutput"]; + img_paste: components["schemas"]["ImageOutput"]; + infill_tile: components["schemas"]["ImageOutput"]; }; /** * InvocationStartedEvent @@ -7769,6 +7780,12 @@ export type components = { * @default false */ tiled?: boolean; + /** + * Tile Size + * @description The tile size for VAE tiling in pixels (image space). If set to 0, the default tile size for the + * @default 0 + */ + tile_size?: number; /** * Fp32 * @description Whether or not to use full float32 precision @@ -15014,6 +15031,8 @@ export type operations = { query: { /** @description The name of the board to create */ board_name: string; + /** @description Whether the board is private */ + is_private?: boolean; }; }; responses: { diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index 162bdf6abc..5beb5cbbf5 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -11,6 +11,8 @@ export type ListBoardsArgs = NonNullable