From 8f6b3660c55a3868977e3b52c7e5540bbdf5a706 Mon Sep 17 00:00:00 2001 From: Brandon Rising Date: Wed, 28 Jun 2023 14:32:20 -0400 Subject: [PATCH 01/18] Set use-credentials on commercial deployment if authToken is set on canvas image calls, comment out the UpdateImageUrls on connect listener --- .../src/app/store/middleware/listenerMiddleware/index.ts | 2 +- .../web/src/features/canvas/components/IAICanvasImage.tsx | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts index eab5e53ad9..a5fde1d0c2 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -198,7 +198,7 @@ addControlNetImageProcessedListener(); addControlNetAutoProcessListener(); // Update image URLs on connect -addUpdateImageUrlsOnConnectListener(); +// addUpdateImageUrlsOnConnectListener(); // Boards addImageAddedToBoardFulfilledListener(); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasImage.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasImage.tsx index 07cad52173..eb41857e46 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasImage.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasImage.tsx @@ -3,6 +3,7 @@ import { Image, Rect } from 'react-konva'; import { useGetImageDTOQuery } from 'services/api/endpoints/images'; import useImage from 'use-image'; import { CanvasImage } from '../store/canvasTypes'; +import { $authToken } from 'services/api/client'; type IAICanvasImageProps = { canvasImage: CanvasImage; @@ -12,7 +13,10 @@ const IAICanvasImage = (props: IAICanvasImageProps) => { const { currentData: imageDTO, isError } = useGetImageDTOQuery( imageName ?? skipToken ); - const [image] = useImage(imageDTO?.image_url ?? '', 'anonymous'); + const [image] = useImage( + imageDTO?.image_url ?? '', + $authToken.get() ? 'use-credentials' : 'anonymous' + ); if (isError) { return ; From 4308d593c35164c578dd94062e5ffa9a08f20fe1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 29 Jun 2023 11:24:20 +1000 Subject: [PATCH 02/18] fix(ui): improve IDE TS performance by not resolving JSON The TS Language Server slows down immensely with our translation JSON, which is used to provide kinda-type-safe translation keys. I say "kinda", because you don't get autocomplete - you only get red squigglies when the key is incorrect. To improve the performance, we can opt out of this process entirely, at the cost of no red squigglies for translation keys. Hopefully we can resolve this in the future. It's not clear why this became an issue only recently (like past couple weeks). We've tried rolling back the app dependencies, VSCode extensions, VSCode itself, and the TS version to before the time when the issue started, but nothing seems to improve the performance. 1. Disable `resolveJsonModule` in `tsconfig.json` 2. Ignore TS in `i18n.ts` when importing the JSON 3. Comment out the custom types in `i18.d.ts` entirely It's possible that only `3` is needed to fix the issue. I've tested building the app and running the build - it works fine, and translation works fine. --- invokeai/frontend/web/src/i18.d.ts | 33 ++++++++++++++++------------- invokeai/frontend/web/src/i18n.ts | 2 ++ invokeai/frontend/web/tsconfig.json | 3 ++- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/invokeai/frontend/web/src/i18.d.ts b/invokeai/frontend/web/src/i18.d.ts index 90cee53385..aac2653d4f 100644 --- a/invokeai/frontend/web/src/i18.d.ts +++ b/invokeai/frontend/web/src/i18.d.ts @@ -1,17 +1,20 @@ -import 'i18next'; +// TODO: Disabled for IDE performance issues with our translation JSON -import en from '../public/locales/en.json'; +// import 'i18next'; -declare module 'i18next' { - // Extend CustomTypeOptions - interface CustomTypeOptions { - // Setting Default Namespace As English - defaultNS: 'en'; - // Custom Types For Resources - resources: { - en: typeof en; - }; - // Never Return Null - returnNull: false; - } -} +// import en from '../public/locales/en.json'; + +// declare module 'i18next' { +// // Extend CustomTypeOptions +// interface CustomTypeOptions { +// // Setting Default Namespace As English +// defaultNS: 'en'; +// // Custom Types For Resources +// resources: { +// en: typeof en; +// }; +// // Never Return Null +// returnNull: false; +// } +// } +export default {}; diff --git a/invokeai/frontend/web/src/i18n.ts b/invokeai/frontend/web/src/i18n.ts index 68b457eabe..706086cf51 100644 --- a/invokeai/frontend/web/src/i18n.ts +++ b/invokeai/frontend/web/src/i18n.ts @@ -3,6 +3,8 @@ import LanguageDetector from 'i18next-browser-languagedetector'; import Backend from 'i18next-http-backend'; import { initReactI18next } from 'react-i18next'; +// TODO: Disabled for IDE performance issues with our translation JSON +// @ts-ignore import translationEN from '../public/locales/en.json'; import { LOCALSTORAGE_PREFIX } from 'app/store/constants'; diff --git a/invokeai/frontend/web/tsconfig.json b/invokeai/frontend/web/tsconfig.json index 8276f461eb..6f74cf3681 100644 --- a/invokeai/frontend/web/tsconfig.json +++ b/invokeai/frontend/web/tsconfig.json @@ -11,7 +11,8 @@ "forceConsistentCasingInFileNames": true, "module": "ESNext", "moduleResolution": "Node", - "resolveJsonModule": true, + // TODO: Disabled for IDE performance issues with our translation JSON + // "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", From 30a917f70caf9df58c78fbe2b6af448f54ad1bdd Mon Sep 17 00:00:00 2001 From: mickr777 <115216705+mickr777@users.noreply.github.com> Date: Thu, 29 Jun 2023 14:45:55 +1000 Subject: [PATCH 03/18] Fix Typo in migrate_to_3.py --- invokeai/backend/install/migrate_to_3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/backend/install/migrate_to_3.py b/invokeai/backend/install/migrate_to_3.py index 713f9c5a83..c8e024f484 100644 --- a/invokeai/backend/install/migrate_to_3.py +++ b/invokeai/backend/install/migrate_to_3.py @@ -326,7 +326,7 @@ class MigrateTo3(object): vae_path = p elif repo_id := vae.get('repo_id'): if repo_id=='stabilityai/sd-vae-ft-mse': # this guy is already downloaded - vae_path = 'models/core/convert/se-vae-ft-mse' + vae_path = 'models/core/convert/sd-vae-ft-mse' else: vae_path = self._download_vae(repo_id, vae.get('subfolder')) From 73f2092ec5a5d516c47a4cc1ea5c55cd2172715e Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Mon, 26 Jun 2023 15:53:21 -0400 Subject: [PATCH 04/18] (api) add option to board delete route and logic to services --- invokeai/app/api/routers/boards.py | 10 ++++++- invokeai/app/services/image_file_storage.py | 16 ++++++----- invokeai/app/services/image_record_storage.py | 24 +++++++++++++++++ invokeai/app/services/images.py | 27 +++++++++++++++++++ 4 files changed, 70 insertions(+), 7 deletions(-) diff --git a/invokeai/app/api/routers/boards.py b/invokeai/app/api/routers/boards.py index 55cd7c8ca2..310f45b739 100644 --- a/invokeai/app/api/routers/boards.py +++ b/invokeai/app/api/routers/boards.py @@ -71,11 +71,19 @@ async def update_board( @boards_router.delete("/{board_id}", operation_id="delete_board") async def delete_board( board_id: str = Path(description="The id of board to delete"), + include_images: bool = Path( + description="Permanently delete all images on the board", default=False + ), ) -> None: """Deletes a board""" try: - ApiDependencies.invoker.services.boards.delete(board_id=board_id) + if include_images: + ApiDependencies.invoker.services.images.delete_images_on_board( + board_id=board_id + ) + else: + ApiDependencies.invoker.services.boards.delete(board_id=board_id) except Exception as e: # TODO: Does this need any exception handling at all? pass diff --git a/invokeai/app/services/image_file_storage.py b/invokeai/app/services/image_file_storage.py index b90b9b2f8b..f30499ea26 100644 --- a/invokeai/app/services/image_file_storage.py +++ b/invokeai/app/services/image_file_storage.py @@ -85,8 +85,10 @@ class DiskImageFileStorage(ImageFileStorageBase): self.__cache_ids = Queue() self.__max_cache_size = 10 # TODO: get this from config - self.__output_folder: Path = output_folder if isinstance(output_folder, Path) else Path(output_folder) - self.__thumbnails_folder = self.__output_folder / 'thumbnails' + self.__output_folder: Path = ( + output_folder if isinstance(output_folder, Path) else Path(output_folder) + ) + self.__thumbnails_folder = self.__output_folder / "thumbnails" # Validate required output folders at launch self.__validate_storage_folders() @@ -94,7 +96,7 @@ class DiskImageFileStorage(ImageFileStorageBase): def get(self, image_name: str) -> PILImageType: try: image_path = self.get_path(image_name) - + cache_item = self.__get_cache(image_path) if cache_item: return cache_item @@ -155,7 +157,7 @@ class DiskImageFileStorage(ImageFileStorageBase): # TODO: make this a bit more flexible for e.g. cloud storage def get_path(self, image_name: str, thumbnail: bool = False) -> Path: path = self.__output_folder / image_name - + if thumbnail: thumbnail_name = get_thumbnail_name(image_name) path = self.__thumbnails_folder / thumbnail_name @@ -166,7 +168,7 @@ class DiskImageFileStorage(ImageFileStorageBase): """Validates the path given for an image or thumbnail.""" path = path if isinstance(path, Path) else Path(path) return path.exists() - + def __validate_storage_folders(self) -> None: """Checks if the required output folders exist and create them if they don't""" folders: list[Path] = [self.__output_folder, self.__thumbnails_folder] @@ -179,7 +181,9 @@ class DiskImageFileStorage(ImageFileStorageBase): def __set_cache(self, image_name: Path, image: PILImageType): if not image_name in self.__cache: self.__cache[image_name] = image - self.__cache_ids.put(image_name) # TODO: this should refresh position for LRU cache + self.__cache_ids.put( + image_name + ) # TODO: this should refresh position for LRU cache if len(self.__cache) > self.__max_cache_size: cache_id = self.__cache_ids.get() if cache_id in self.__cache: diff --git a/invokeai/app/services/image_record_storage.py b/invokeai/app/services/image_record_storage.py index c34d2ca5c8..3bf931e852 100644 --- a/invokeai/app/services/image_record_storage.py +++ b/invokeai/app/services/image_record_storage.py @@ -94,6 +94,11 @@ class ImageRecordStorageBase(ABC): """Deletes an image record.""" pass + @abstractmethod + def delete_many(self, image_names: list[str]) -> None: + """Deletes many image records.""" + pass + @abstractmethod def save( self, @@ -385,6 +390,25 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): finally: self._lock.release() + def delete_many(self, image_names: list[str]) -> None: + try: + placeholders = ",".join("?" for _ in image_names) + + self._lock.acquire() + + # Construct the SQLite query with the placeholders + query = f"DELETE FROM images WHERE id_column IN ({placeholders})" + + # Execute the query with the list of IDs as parameters + self._cursor.execute(query, placeholders) + + self._conn.commit() + except sqlite3.Error as e: + self._conn.rollback() + raise ImageRecordDeleteException from e + finally: + self._lock.release() + def save( self, image_name: str, diff --git a/invokeai/app/services/images.py b/invokeai/app/services/images.py index 542f874f1d..aeb5e520d8 100644 --- a/invokeai/app/services/images.py +++ b/invokeai/app/services/images.py @@ -112,6 +112,11 @@ class ImageServiceABC(ABC): """Deletes an image.""" pass + @abstractmethod + def delete_images_on_board(self, board_id: str): + """Deletes all images on a board.""" + pass + class ImageServiceDependencies: """Service dependencies for the ImageService.""" @@ -341,6 +346,28 @@ class ImageService(ImageServiceABC): self._services.logger.error("Problem deleting image record and file") raise e + def delete_images_on_board(self, board_id: str): + try: + images = self._services.board_image_records.get_images_for_board(board_id) + image_name_list = list( + map( + lambda r: r.image_name, + images.items, + ) + ) + for image_name in image_name_list: + self._services.image_files.delete(image_name) + self._services.image_records.delete_many(image_name_list) + except ImageRecordDeleteException: + self._services.logger.error(f"Failed to delete image records") + raise + except ImageFileDeleteException: + self._services.logger.error(f"Failed to delete image files") + raise + except Exception as e: + self._services.logger.error("Problem deleting image records and files") + raise e + def _get_metadata( self, session_id: Optional[str] = None, node_id: Optional[str] = None ) -> Union[ImageMetadata, None]: From 45935caf1df3d076248b43c06a77c702e31fe757 Mon Sep 17 00:00:00 2001 From: maryhipp Date: Tue, 27 Jun 2023 13:05:32 -0700 Subject: [PATCH 05/18] fix query --- invokeai/app/api/routers/boards.py | 7 ++++--- invokeai/app/services/image_record_storage.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/invokeai/app/api/routers/boards.py b/invokeai/app/api/routers/boards.py index 310f45b739..94d8667ae4 100644 --- a/invokeai/app/api/routers/boards.py +++ b/invokeai/app/api/routers/boards.py @@ -5,6 +5,7 @@ from invokeai.app.services.board_record_storage import BoardChanges from invokeai.app.services.image_record_storage import OffsetPaginatedResults from invokeai.app.services.models.board_record import BoardDTO + from ..dependencies import ApiDependencies boards_router = APIRouter(prefix="/v1/boards", tags=["boards"]) @@ -71,17 +72,17 @@ async def update_board( @boards_router.delete("/{board_id}", operation_id="delete_board") async def delete_board( board_id: str = Path(description="The id of board to delete"), - include_images: bool = Path( + include_images: Optional[bool] = Query( description="Permanently delete all images on the board", default=False ), ) -> None: """Deletes a board""" - try: - if include_images: + if include_images is True: ApiDependencies.invoker.services.images.delete_images_on_board( board_id=board_id ) + ApiDependencies.invoker.services.boards.delete(board_id=board_id) else: ApiDependencies.invoker.services.boards.delete(board_id=board_id) except Exception as e: diff --git a/invokeai/app/services/image_record_storage.py b/invokeai/app/services/image_record_storage.py index 3bf931e852..066e6f8d5f 100644 --- a/invokeai/app/services/image_record_storage.py +++ b/invokeai/app/services/image_record_storage.py @@ -397,10 +397,10 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): self._lock.acquire() # Construct the SQLite query with the placeholders - query = f"DELETE FROM images WHERE id_column IN ({placeholders})" + query = f"DELETE FROM images WHERE image_name IN ({placeholders})" # Execute the query with the list of IDs as parameters - self._cursor.execute(query, placeholders) + self._cursor.execute(query, image_names) self._conn.commit() except sqlite3.Error as e: From ba67e57a7e540dbdd35bc86f282b2e268c4f7ab0 Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Tue, 27 Jun 2023 16:07:41 -0400 Subject: [PATCH 06/18] (wip) delete images along with board --- .../frontend/web/src/app/components/App.tsx | 2 + .../web/src/app/components/InvokeAIUI.tsx | 13 ++- .../app/contexts/DeleteBoardImagesContext.tsx | 101 ++++++++++++++++++ .../Boards/DeleteBoardImagesModal.tsx | 78 ++++++++++++++ .../components/Boards/HoverableBoard.tsx | 19 +++- .../web/src/services/api/endpoints/boards.ts | 6 +- 6 files changed, 212 insertions(+), 7 deletions(-) create mode 100644 invokeai/frontend/web/src/app/contexts/DeleteBoardImagesContext.tsx create mode 100644 invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardImagesModal.tsx diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index c93bd8791c..5b3cf5925f 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -25,6 +25,7 @@ import DeleteImageModal from 'features/gallery/components/DeleteImageModal'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; import UpdateImageBoardModal from '../../features/gallery/components/Boards/UpdateImageBoardModal'; import { useListModelsQuery } from 'services/api/endpoints/models'; +import DeleteBoardImagesModal from '../../features/gallery/components/Boards/DeleteBoardImagesModal'; const DEFAULT_CONFIG = {}; @@ -158,6 +159,7 @@ const App = ({ + diff --git a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx index 4d83a407c0..7259f6105d 100644 --- a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx +++ b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx @@ -24,6 +24,7 @@ import { import UpdateImageBoardModal from '../../features/gallery/components/Boards/UpdateImageBoardModal'; import { AddImageToBoardContextProvider } from '../contexts/AddImageToBoardContext'; import { $authToken, $baseUrl } from 'services/api/client'; +import { DeleteBoardImagesContextProvider } from '../contexts/DeleteBoardImagesContext'; const App = lazy(() => import('./App')); const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider')); @@ -86,11 +87,13 @@ const InvokeAIUI = ({ - + + + diff --git a/invokeai/frontend/web/src/app/contexts/DeleteBoardImagesContext.tsx b/invokeai/frontend/web/src/app/contexts/DeleteBoardImagesContext.tsx new file mode 100644 index 0000000000..dd50ce15a5 --- /dev/null +++ b/invokeai/frontend/web/src/app/contexts/DeleteBoardImagesContext.tsx @@ -0,0 +1,101 @@ +import { useDisclosure } from '@chakra-ui/react'; +import { PropsWithChildren, createContext, useCallback, useState } from 'react'; +import { BoardDTO } from 'services/api/types'; +import { useDeleteBoardMutation } from '../../services/api/endpoints/boards'; + +export type ImageUsage = { + isInitialImage: boolean; + isCanvasImage: boolean; + isNodesImage: boolean; + isControlNetImage: boolean; +}; + +type DeleteBoardImagesContextValue = { + /** + * Whether the move image dialog is open. + */ + isOpen: boolean; + /** + * Closes the move image dialog. + */ + onClose: () => void; + /** + * The image pending movement + */ + board?: BoardDTO; + onClickDeleteBoardImages: (board: BoardDTO) => void; + handleDeleteBoardImages: (boardId: string) => void; + handleDeleteBoardOnly: (boardId: string) => void; +}; + +export const DeleteBoardImagesContext = + createContext({ + isOpen: false, + onClose: () => undefined, + onClickDeleteBoardImages: () => undefined, + handleDeleteBoardImages: () => undefined, + handleDeleteBoardOnly: () => undefined, + }); + +type Props = PropsWithChildren; + +export const DeleteBoardImagesContextProvider = (props: Props) => { + const [boardToDelete, setBoardToDelete] = useState(); + const { isOpen, onOpen, onClose } = useDisclosure(); + + const [deleteBoardAndImages] = useDeleteBoardAndImagesMutation(); + const [deleteBoard] = useDeleteBoardMutation(); + + // Clean up after deleting or dismissing the modal + const closeAndClearBoardToDelete = useCallback(() => { + setBoardToDelete(undefined); + onClose(); + }, [onClose]); + + const onClickDeleteBoardImages = useCallback( + (board?: BoardDTO) => { + console.log({ board }); + if (!board) { + return; + } + setBoardToDelete(board); + onOpen(); + }, + [setBoardToDelete, onOpen] + ); + + const handleDeleteBoardImages = useCallback( + (boardId: string) => { + if (boardToDelete) { + deleteBoardAndImages(boardId); + closeAndClearBoardToDelete(); + } + }, + [deleteBoardAndImages, closeAndClearBoardToDelete, boardToDelete] + ); + + const handleDeleteBoardOnly = useCallback( + (boardId: string) => { + if (boardToDelete) { + deleteBoard(boardId); + closeAndClearBoardToDelete(); + } + }, + [deleteBoard, closeAndClearBoardToDelete, boardToDelete] + ); + + return ( + + {props.children} + + ); +}; diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardImagesModal.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardImagesModal.tsx new file mode 100644 index 0000000000..345a95b846 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardImagesModal.tsx @@ -0,0 +1,78 @@ +import { + AlertDialog, + AlertDialogBody, + AlertDialogContent, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogOverlay, + Divider, + Flex, + Text, +} from '@chakra-ui/react'; +import IAIButton from 'common/components/IAIButton'; +import { memo, useContext, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { DeleteBoardImagesContext } from '../../../../app/contexts/DeleteBoardImagesContext'; + +const DeleteBoardImagesModal = () => { + const { t } = useTranslation(); + + const { + isOpen, + onClose, + board, + handleDeleteBoardImages, + handleDeleteBoardOnly, + } = useContext(DeleteBoardImagesContext); + + const cancelRef = useRef(null); + + return ( + + + {board && ( + + + Delete Board + + + + + + {t('common.areYouSure')} + + This board has {board.image_count} image(s) that will be + deleted. + + + + + + Cancel + + handleDeleteBoardOnly(board.board_id)} + > + Delete Board Only + + handleDeleteBoardImages(board.board_id)} + > + Delete Board and Images + + + + )} + + + ); +}; + +export default memo(DeleteBoardImagesModal); diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/HoverableBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/HoverableBoard.tsx index ba43f792bf..535cab1b15 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/HoverableBoard.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/HoverableBoard.tsx @@ -11,7 +11,7 @@ import { } from '@chakra-ui/react'; import { useAppDispatch } from 'app/store/storeHooks'; -import { memo, useCallback } from 'react'; +import { memo, useCallback, useContext } from 'react'; import { FaFolder, FaTrash } from 'react-icons/fa'; import { ContextMenu } from 'chakra-ui-contextmenu'; import { BoardDTO, ImageDTO } from 'services/api/types'; @@ -29,6 +29,7 @@ import { useDroppable } from '@dnd-kit/core'; import { AnimatePresence } from 'framer-motion'; import IAIDropOverlay from 'common/components/IAIDropOverlay'; import { SelectedItemOverlay } from '../SelectedItemOverlay'; +import { DeleteBoardImagesContext } from '../../../../app/contexts/DeleteBoardImagesContext'; interface HoverableBoardProps { board: BoardDTO; @@ -44,6 +45,8 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => { const { board_name, board_id } = board; + const { onClickDeleteBoardImages } = useContext(DeleteBoardImagesContext); + const handleSelectBoard = useCallback(() => { dispatch(boardIdSelected(board_id)); }, [board_id, dispatch]); @@ -65,6 +68,11 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => { deleteBoard(board_id); }, [board_id, deleteBoard]); + const handleDeleteBoardAndImages = useCallback(() => { + console.log({ board }); + onClickDeleteBoardImages(board); + }, [board, onClickDeleteBoardImages]); + const handleDrop = useCallback( (droppedImage: ImageDTO) => { if (droppedImage.board_id === board_id) { @@ -92,6 +100,15 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => { menuProps={{ size: 'sm', isLazy: true }} renderMenu={() => ( + {board.image_count > 0 && ( + } + onClickCapture={handleDeleteBoardAndImages} + > + Delete Board and Images + + )} } diff --git a/invokeai/frontend/web/src/services/api/endpoints/boards.ts b/invokeai/frontend/web/src/services/api/endpoints/boards.ts index 9816d88eb9..64ab21075d 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/boards.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/boards.ts @@ -82,11 +82,14 @@ export const boardsApi = api.injectEndpoints({ { type: 'Board', id: arg.board_id }, ], }), - deleteBoard: build.mutation({ query: (board_id) => ({ url: `boards/${board_id}`, method: 'DELETE' }), invalidatesTags: (result, error, arg) => [{ type: 'Board', id: arg }], }), + deleteBoardAndImages: build.mutation({ + query: (board_id) => ({ url: `boards/${board_id}`, method: 'DELETE', params: { include_images: true } }), + invalidatesTags: (result, error, arg) => [{ type: 'Board', id: arg }, { type: 'Image', id: LIST_TAG }], + }), }), }); @@ -96,4 +99,5 @@ export const { useCreateBoardMutation, useUpdateBoardMutation, useDeleteBoardMutation, + useDeleteBoardAndImagesMutation } = boardsApi; From 723d68e4961302638d4a81299fcd3c525cb97c28 Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Wed, 28 Jun 2023 11:43:04 -0400 Subject: [PATCH 07/18] add image usage for board images and listener to handle actual deletion --- .../app/contexts/DeleteBoardImagesContext.tsx | 93 ++++++++++++++++--- .../middleware/listenerMiddleware/index.ts | 2 + .../listeners/boardImagesDeleted.ts | 79 ++++++++++++++++ .../Boards/DeleteBoardImagesModal.tsx | 36 +++++++ .../web/src/features/gallery/store/actions.ts | 12 ++- .../src/features/gallery/store/imagesSlice.ts | 4 + 6 files changed, 213 insertions(+), 13 deletions(-) create mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardImagesDeleted.ts diff --git a/invokeai/frontend/web/src/app/contexts/DeleteBoardImagesContext.tsx b/invokeai/frontend/web/src/app/contexts/DeleteBoardImagesContext.tsx index dd50ce15a5..38c89bfcf9 100644 --- a/invokeai/frontend/web/src/app/contexts/DeleteBoardImagesContext.tsx +++ b/invokeai/frontend/web/src/app/contexts/DeleteBoardImagesContext.tsx @@ -2,13 +2,76 @@ import { useDisclosure } from '@chakra-ui/react'; import { PropsWithChildren, createContext, useCallback, useState } from 'react'; import { BoardDTO } from 'services/api/types'; import { useDeleteBoardMutation } from '../../services/api/endpoints/boards'; +import { defaultSelectorOptions } from '../store/util/defaultMemoizeOptions'; +import { createSelector } from '@reduxjs/toolkit'; +import { some } from 'lodash-es'; +import { canvasSelector } from '../../features/canvas/store/canvasSelectors'; +import { controlNetSelector } from '../../features/controlNet/store/controlNetSlice'; +import { selectImagesById } from '../../features/gallery/store/imagesSlice'; +import { nodesSelector } from '../../features/nodes/store/nodesSlice'; +import { generationSelector } from '../../features/parameters/store/generationSelectors'; +import { RootState } from '../store/store'; +import { useAppDispatch, useAppSelector } from '../store/storeHooks'; +import { ImageUsage } from './DeleteImageContext'; +import { requestedBoardImagesDeletion } from '../../features/gallery/store/actions'; -export type ImageUsage = { - isInitialImage: boolean; - isCanvasImage: boolean; - isNodesImage: boolean; - isControlNetImage: boolean; -}; +export const selectBoardImagesUsage = createSelector( + [ + (state: RootState) => state, + generationSelector, + canvasSelector, + nodesSelector, + controlNetSelector, + (state: RootState, board_id?: string) => board_id, + ], + (state, generation, canvas, nodes, controlNet, board_id) => { + const initialImage = generation.initialImage + ? selectImagesById(state, generation.initialImage.imageName) + : undefined; + const isInitialImage = initialImage?.board_id === board_id; + + const isCanvasImage = canvas.layerState.objects.some((obj) => { + if (obj.kind === 'image') { + const image = selectImagesById(state, obj.imageName); + return image?.board_id === board_id; + } + return false; + }); + + const isNodesImage = nodes.nodes.some((node) => { + return some(node.data.inputs, (input) => { + if (input.type === 'image' && input.value) { + const image = selectImagesById(state, input.value.image_name); + return image?.board_id === board_id; + } + return false; + }); + }); + + const isControlNetImage = some(controlNet.controlNets, (c) => { + const controlImage = c.controlImage + ? selectImagesById(state, c.controlImage) + : undefined; + const processedControlImage = c.processedControlImage + ? selectImagesById(state, c.processedControlImage) + : undefined; + return ( + controlImage?.board_id === board_id || + processedControlImage?.board_id === board_id + ); + }); + + const imageUsage: ImageUsage = { + isInitialImage, + isCanvasImage, + isNodesImage, + isControlNetImage, + }; + + return imageUsage; + }, + defaultSelectorOptions +); type DeleteBoardImagesContextValue = { /** @@ -19,9 +82,7 @@ type DeleteBoardImagesContextValue = { * Closes the move image dialog. */ onClose: () => void; - /** - * The image pending movement - */ + imagesUsage?: ImageUsage; board?: BoardDTO; onClickDeleteBoardImages: (board: BoardDTO) => void; handleDeleteBoardImages: (boardId: string) => void; @@ -42,8 +103,13 @@ type Props = PropsWithChildren; export const DeleteBoardImagesContextProvider = (props: Props) => { const [boardToDelete, setBoardToDelete] = useState(); const { isOpen, onOpen, onClose } = useDisclosure(); + const dispatch = useAppDispatch(); + + // Check where the board images to be deleted are used (eg init image, controlnet, etc.) + const imagesUsage = useAppSelector((state) => + selectBoardImagesUsage(state, boardToDelete?.board_id) + ); - const [deleteBoardAndImages] = useDeleteBoardAndImagesMutation(); const [deleteBoard] = useDeleteBoardMutation(); // Clean up after deleting or dismissing the modal @@ -67,11 +133,13 @@ export const DeleteBoardImagesContextProvider = (props: Props) => { const handleDeleteBoardImages = useCallback( (boardId: string) => { if (boardToDelete) { - deleteBoardAndImages(boardId); + dispatch( + requestedBoardImagesDeletion({ board: boardToDelete, imagesUsage }) + ); closeAndClearBoardToDelete(); } }, - [deleteBoardAndImages, closeAndClearBoardToDelete, boardToDelete] + [dispatch, closeAndClearBoardToDelete, boardToDelete, imagesUsage] ); const handleDeleteBoardOnly = useCallback( @@ -93,6 +161,7 @@ export const DeleteBoardImagesContextProvider = (props: Props) => { onClickDeleteBoardImages, handleDeleteBoardImages, handleDeleteBoardOnly, + imagesUsage, }} > {props.children} diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts index a5fde1d0c2..a36141fafc 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -83,6 +83,7 @@ import { addImageRemovedFromBoardRejectedListener, } from './listeners/imageRemovedFromBoard'; import { addReceivedOpenAPISchemaListener } from './listeners/receivedOpenAPISchema'; +import { addRequestedBoardImageDeletionListener } from './listeners/boardImagesDeleted'; export const listenerMiddleware = createListenerMiddleware(); @@ -124,6 +125,7 @@ addRequestedImageDeletionListener(); addImageDeletedPendingListener(); addImageDeletedFulfilledListener(); addImageDeletedRejectedListener(); +addRequestedBoardImageDeletionListener(); // Image metadata addImageMetadataReceivedFulfilledListener(); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardImagesDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardImagesDeleted.ts new file mode 100644 index 0000000000..c4d3c5f0ba --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardImagesDeleted.ts @@ -0,0 +1,79 @@ +import { requestedBoardImagesDeletion } from 'features/gallery/store/actions'; +import { startAppListening } from '..'; +import { imageSelected } from 'features/gallery/store/gallerySlice'; +import { + imagesRemoved, + selectImagesAll, + selectImagesById, +} from 'features/gallery/store/imagesSlice'; +import { resetCanvas } from 'features/canvas/store/canvasSlice'; +import { controlNetReset } from 'features/controlNet/store/controlNetSlice'; +import { clearInitialImage } from 'features/parameters/store/generationSlice'; +import { nodeEditorReset } from 'features/nodes/store/nodesSlice'; +import { LIST_TAG, api } from 'services/api'; +import { boardsApi } from '../../../../../services/api/endpoints/boards'; + +export const addRequestedBoardImageDeletionListener = () => { + startAppListening({ + actionCreator: requestedBoardImagesDeletion, + effect: async (action, { dispatch, getState, condition }) => { + const { board, imagesUsage } = action.payload; + + const { board_id } = board; + + const state = getState(); + const selectedImage = state.gallery.selectedImage + ? selectImagesById(state, state.gallery.selectedImage) + : undefined; + + if (selectedImage && selectedImage.board_id === board_id) { + dispatch(imageSelected()); + } + + // We need to reset the features where the board images are in use - none of these work if their image(s) don't exist + + if (imagesUsage.isCanvasImage) { + dispatch(resetCanvas()); + } + + if (imagesUsage.isControlNetImage) { + dispatch(controlNetReset()); + } + + if (imagesUsage.isInitialImage) { + dispatch(clearInitialImage()); + } + + if (imagesUsage.isNodesImage) { + dispatch(nodeEditorReset()); + } + + // Preemptively remove from gallery + const images = selectImagesAll(state).reduce((acc: string[], img) => { + if (img.board_id === board_id) { + acc.push(img.image_name); + } + return acc; + }, []); + dispatch(imagesRemoved(images)); + + // Delete from server + dispatch(boardsApi.endpoints.deleteBoardAndImages.initiate(board_id)); + const result = + boardsApi.endpoints.deleteBoardAndImages.select(board_id)(state); + const { isSuccess } = result; + + // Wait for successful deletion, then trigger boards to re-fetch + const wasBoardDeleted = await condition(() => !!isSuccess, 30000); + + if (wasBoardDeleted) { + dispatch( + api.util.invalidateTags([ + { type: 'Board', id: board_id }, + { type: 'Image', id: LIST_TAG }, + ]) + ); + } + }, + }); +}; diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardImagesModal.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardImagesModal.tsx index 345a95b846..736d72f862 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardImagesModal.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardImagesModal.tsx @@ -7,12 +7,46 @@ import { AlertDialogOverlay, Divider, Flex, + ListItem, Text, + UnorderedList, } from '@chakra-ui/react'; import IAIButton from 'common/components/IAIButton'; import { memo, useContext, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { DeleteBoardImagesContext } from '../../../../app/contexts/DeleteBoardImagesContext'; +import { some } from 'lodash-es'; +import { ImageUsage } from '../../../../app/contexts/DeleteImageContext'; + +const BoardImageInUseMessage = (props: { imagesUsage?: ImageUsage }) => { + const { imagesUsage } = props; + + if (!imagesUsage) { + return null; + } + + if (!some(imagesUsage)) { + return null; + } + + return ( + <> + + An image from this board is currently in use in the following features: + + + {imagesUsage.isInitialImage && Image to Image} + {imagesUsage.isCanvasImage && Unified Canvas} + {imagesUsage.isControlNetImage && ControlNet} + {imagesUsage.isNodesImage && Node Editor} + + + If you delete images from this board, those features will immediately be + reset. + + + ); +}; const DeleteBoardImagesModal = () => { const { t } = useTranslation(); @@ -23,6 +57,7 @@ const DeleteBoardImagesModal = () => { board, handleDeleteBoardImages, handleDeleteBoardOnly, + imagesUsage, } = useContext(DeleteBoardImagesContext); const cancelRef = useRef(null); @@ -43,6 +78,7 @@ const DeleteBoardImagesModal = () => { + {t('common.areYouSure')} diff --git a/invokeai/frontend/web/src/features/gallery/store/actions.ts b/invokeai/frontend/web/src/features/gallery/store/actions.ts index aa767b1422..4234778120 100644 --- a/invokeai/frontend/web/src/features/gallery/store/actions.ts +++ b/invokeai/frontend/web/src/features/gallery/store/actions.ts @@ -1,6 +1,6 @@ import { createAction } from '@reduxjs/toolkit'; import { ImageUsage } from 'app/contexts/DeleteImageContext'; -import { ImageDTO } from 'services/api/types'; +import { ImageDTO, BoardDTO } from 'services/api/types'; export type RequestedImageDeletionArg = { image: ImageDTO; @@ -11,6 +11,16 @@ export const requestedImageDeletion = createAction( 'gallery/requestedImageDeletion' ); +export type RequestedBoardImagesDeletionArg = { + board: BoardDTO; + imagesUsage: ImageUsage; +}; + +export const requestedBoardImagesDeletion = + createAction( + 'gallery/requestedBoardImagesDeletion' + ); + export const sentImageToCanvas = createAction('gallery/sentImageToCanvas'); export const sentImageToImg2Img = createAction('gallery/sentImageToImg2Img'); diff --git a/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts b/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts index 4bdaa796cd..8041ffd5c5 100644 --- a/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts @@ -60,6 +60,9 @@ const imagesSlice = createSlice({ imageRemoved: (state, action: PayloadAction) => { imagesAdapter.removeOne(state, action.payload); }, + imagesRemoved: (state, action: PayloadAction) => { + imagesAdapter.removeMany(state, action.payload); + }, imageCategoriesChanged: (state, action: PayloadAction) => { state.categories = action.payload; }, @@ -117,6 +120,7 @@ export const { imageUpserted, imageUpdatedOne, imageRemoved, + imagesRemoved, imageCategoriesChanged, } = imagesSlice.actions; From 2c5b050d82a98c509e974d858fd6ec1b11bb9f02 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Mon, 26 Jun 2023 14:16:10 -0400 Subject: [PATCH 08/18] add image board support to invokeai-node-cli --- invokeai/app/cli_app.py | 55 ++++++++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/invokeai/app/cli_app.py b/invokeai/app/cli_app.py index 26e058166b..07193c8500 100644 --- a/invokeai/app/cli_app.py +++ b/invokeai/app/cli_app.py @@ -18,8 +18,17 @@ config = InvokeAIAppConfig.get_config() config.parse_args() logger = InvokeAILogger().getLogger(config=config) +from invokeai.app.services.board_image_record_storage import ( + SqliteBoardImageRecordStorage, +) +from invokeai.app.services.board_images import ( + BoardImagesService, + BoardImagesServiceDependencies, +) +from invokeai.app.services.board_record_storage import SqliteBoardRecordStorage +from invokeai.app.services.boards import BoardService, BoardServiceDependencies from invokeai.app.services.image_record_storage import SqliteImageRecordStorage -from invokeai.app.services.images import ImageService +from invokeai.app.services.images import ImageService, ImageServiceDependencies from invokeai.app.services.metadata import CoreMetadataService from invokeai.app.services.resource_name import SimpleNameService from invokeai.app.services.urls import LocalUrlService @@ -230,21 +239,49 @@ def invoke_cli(): image_file_storage = DiskImageFileStorage(f"{output_folder}/images") names = SimpleNameService() - images = ImageService( - image_record_storage=image_record_storage, - image_file_storage=image_file_storage, - metadata=metadata, - url=urls, - logger=logger, - names=names, - graph_execution_manager=graph_execution_manager, + board_record_storage = SqliteBoardRecordStorage(db_location) + board_image_record_storage = SqliteBoardImageRecordStorage(db_location) + + boards = BoardService( + services=BoardServiceDependencies( + board_image_record_storage=board_image_record_storage, + board_record_storage=board_record_storage, + image_record_storage=image_record_storage, + url=urls, + logger=logger, + ) ) + board_images = BoardImagesService( + services=BoardImagesServiceDependencies( + board_image_record_storage=board_image_record_storage, + board_record_storage=board_record_storage, + image_record_storage=image_record_storage, + url=urls, + logger=logger, + ) + ) + + images = ImageService( + services=ImageServiceDependencies( + board_image_record_storage=board_image_record_storage, + image_record_storage=image_record_storage, + image_file_storage=image_file_storage, + metadata=metadata, + url=urls, + logger=logger, + names=names, + graph_execution_manager=graph_execution_manager, + ) + ) + services = InvocationServices( model_manager=model_manager, events=events, latents = ForwardCacheLatentsStorage(DiskLatentsStorage(f'{output_folder}/latents')), images=images, + boards=boards, + board_images=board_images, queue=MemoryInvocationQueue(), graph_library=SqliteItemStorage[LibraryGraph]( filename=db_location, table_name="graphs" From 032c7e68d0f078e3bd22ef776017cdda572e370f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 27 Jun 2023 00:12:33 +1000 Subject: [PATCH 09/18] feat(ui): remove themes, add hand-crafted dark and light modes Themes are very fun but due to the differences in perceived saturation and lightness across the the color spectrum, it's impossible to have have multiple themes that look great without hand- crafting *every* shade for *every* theme. We've ended up with 4 OK themes (well, 3, because the light theme was pretty bad). I've removed the themes and added color mode support. There is now a single dark and light mode, each with their own color palette and the classic grey / purple / yellow invoke colors that @blessedcoolant first designed. I've re-styled almost everything except the model manager and lightbox, which I keep forgetting to work on. One new concept is the Chakra `layerStyle`. This lets us define "layers" - think body, first layer, second layer, etc - that can be applied on various components. By defining layers, we can be more consistent about the z-axis and its relationship to color and lightness. --- .../app/components/ThemeLocaleProvider.tsx | 31 +---- .../web/src/common/components/IAICollapse.tsx | 29 +++- .../web/src/common/components/IAIDndImage.tsx | 11 +- .../src/common/components/IAIDropOverlay.tsx | 14 +- .../common/components/IAIImageFallback.tsx | 12 +- .../components/IAIMantineMultiSelect.tsx | 86 ++++++++---- .../common/components/IAIMantineSelect.tsx | 88 ++++++++++--- .../common/components/IAISimpleCheckbox.tsx | 11 +- .../src/common/hooks/useChakraThemeTokens.ts | 124 ++++++++++++++++++ .../canvas/components/IAICanvasGrid.tsx | 17 ++- .../canvas/components/IAICanvasStatusText.tsx | 5 +- .../controlNet/components/ControlNet.tsx | 11 +- .../components/Boards/AllImagesBoard.tsx | 8 +- .../gallery/components/Boards/BoardsList.tsx | 4 +- .../components/Boards/HoverableBoard.tsx | 14 +- .../components/CurrentImageDisplay.tsx | 1 - .../components/ImageGalleryContent.tsx | 12 +- .../components/SelectedItemOverlay.tsx | 62 +++++---- .../components/IAINode/IAINodeHeader.tsx | 35 +++-- .../nodes/components/InvocationComponent.tsx | 12 +- .../features/nodes/components/NodeEditor.tsx | 4 +- .../nodes/components/NodeGraphOverlay.tsx | 25 ++-- .../nodes/components/panels/MinimapPanel.tsx | 40 +++--- .../ImageToImage/InitialImageDisplay.tsx | 4 +- .../ProcessButtons/CancelButton.tsx | 6 +- .../ProcessButtons/InvokeButton.tsx | 2 +- .../system/components/ColorModeButton.tsx | 25 ++++ .../SettingsModal/SettingsModal.tsx | 50 ++++--- .../features/system/components/SiteHeader.tsx | 6 +- .../system/components/StatusIndicator.tsx | 26 +++- .../system/components/ThemeChanger.tsx | 60 --------- .../ui/components/FloatingGalleryButton.tsx | 1 + .../FloatingParametersPanelButtons.tsx | 1 + .../ResizableDrawer/ResizableDrawer.tsx | 10 +- .../ui/components/tabs/ResizeHandle.tsx | 20 ++- .../tabs/TextToImage/TextToImageTabMain.tsx | 4 +- .../UnifiedCanvas/UnifiedCanvasContent.tsx | 8 +- .../web/src/features/ui/store/uiSlice.ts | 5 - .../web/src/features/ui/store/uiTypes.ts | 1 - .../frontend/web/src/mantine-theme/theme.ts | 2 +- .../frontend/web/src/theme/colors/greenTea.ts | 18 --- .../frontend/web/src/theme/colors/invokeAI.ts | 18 --- .../web/src/theme/colors/lightTheme.ts | 18 --- .../web/src/theme/colors/oceanBlue.ts | 18 --- .../web/src/theme/components/accordion.ts | 13 +- .../web/src/theme/components/button.ts | 41 +++--- .../web/src/theme/components/checkbox.ts | 23 ++-- .../web/src/theme/components/formLabel.ts | 5 +- .../frontend/web/src/theme/components/menu.ts | 24 ++-- .../web/src/theme/components/modal.ts | 19 +-- .../web/src/theme/components/numberInput.ts | 7 +- .../web/src/theme/components/popover.ts | 17 ++- .../web/src/theme/components/select.ts | 5 +- .../web/src/theme/components/slider.ts | 14 +- .../web/src/theme/components/switch.ts | 7 +- .../frontend/web/src/theme/components/tabs.ts | 64 ++++++--- .../frontend/web/src/theme/components/text.ts | 5 +- .../web/src/theme/components/tooltip.ts | 17 +++ invokeai/frontend/web/src/theme/theme.ts | 63 ++++++--- .../src/theme/util/generateColorPalette.ts | 39 ++---- .../src/theme/util/getInputOutlineStyles.ts | 30 ++--- invokeai/frontend/web/src/theme/util/mode.ts | 3 + 62 files changed, 839 insertions(+), 516 deletions(-) create mode 100644 invokeai/frontend/web/src/common/hooks/useChakraThemeTokens.ts create mode 100644 invokeai/frontend/web/src/features/system/components/ColorModeButton.tsx delete mode 100644 invokeai/frontend/web/src/features/system/components/ThemeChanger.tsx delete mode 100644 invokeai/frontend/web/src/theme/colors/greenTea.ts delete mode 100644 invokeai/frontend/web/src/theme/colors/invokeAI.ts delete mode 100644 invokeai/frontend/web/src/theme/colors/lightTheme.ts delete mode 100644 invokeai/frontend/web/src/theme/colors/oceanBlue.ts create mode 100644 invokeai/frontend/web/src/theme/components/tooltip.ts create mode 100644 invokeai/frontend/web/src/theme/util/mode.ts diff --git a/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx b/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx index 5eea4bb940..1e86e0ce1b 100644 --- a/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx +++ b/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx @@ -3,17 +3,10 @@ import { createLocalStorageManager, extendTheme, } from '@chakra-ui/react'; -import { RootState } from 'app/store/store'; -import { useAppSelector } from 'app/store/storeHooks'; -import { ReactNode, useEffect } from 'react'; +import { ReactNode, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { theme as invokeAITheme } from 'theme/theme'; -import { greenTeaThemeColors } from 'theme/colors/greenTea'; -import { invokeAIThemeColors } from 'theme/colors/invokeAI'; -import { lightThemeColors } from 'theme/colors/lightTheme'; -import { oceanBlueColors } from 'theme/colors/oceanBlue'; - import '@fontsource-variable/inter'; import { MantineProvider } from '@mantine/core'; import { mantineTheme } from 'mantine-theme/theme'; @@ -24,29 +17,19 @@ type ThemeLocaleProviderProps = { children: ReactNode; }; -const THEMES = { - dark: invokeAIThemeColors, - light: lightThemeColors, - green: greenTeaThemeColors, - ocean: oceanBlueColors, -}; - const manager = createLocalStorageManager('@@invokeai-color-mode'); function ThemeLocaleProvider({ children }: ThemeLocaleProviderProps) { const { i18n } = useTranslation(); - const currentTheme = useAppSelector( - (state: RootState) => state.ui.currentTheme - ); - const direction = i18n.dir(); - const theme = extendTheme({ - ...invokeAITheme, - colors: THEMES[currentTheme as keyof typeof THEMES], - direction, - }); + const theme = useMemo(() => { + return extendTheme({ + ...invokeAITheme, + direction, + }); + }, [direction]); useEffect(() => { document.body.dir = direction; diff --git a/invokeai/frontend/web/src/common/components/IAICollapse.tsx b/invokeai/frontend/web/src/common/components/IAICollapse.tsx index ec23793741..5db26f3841 100644 --- a/invokeai/frontend/web/src/common/components/IAICollapse.tsx +++ b/invokeai/frontend/web/src/common/components/IAICollapse.tsx @@ -1,6 +1,14 @@ import { ChevronUpIcon } from '@chakra-ui/icons'; -import { Box, Collapse, Flex, Spacer, Switch } from '@chakra-ui/react'; +import { + Box, + Collapse, + Flex, + Spacer, + Switch, + useColorMode, +} from '@chakra-ui/react'; import { PropsWithChildren, memo } from 'react'; +import { mode } from 'theme/util/mode'; export type IAIToggleCollapseProps = PropsWithChildren & { label: string; @@ -11,6 +19,7 @@ export type IAIToggleCollapseProps = PropsWithChildren & { const IAICollapse = (props: IAIToggleCollapseProps) => { const { label, isOpen, onToggle, children, withSwitch = false } = props; + const { colorMode } = useColorMode(); return ( { px: 4, borderTopRadius: 'base', borderBottomRadius: isOpen ? 0 : 'base', - bg: isOpen ? 'base.750' : 'base.800', - color: 'base.100', + bg: isOpen + ? mode('base.200', 'base.750')(colorMode) + : mode('base.150', 'base.800')(colorMode), + color: mode('base.900', 'base.100')(colorMode), _hover: { - bg: isOpen ? 'base.700' : 'base.750', + bg: isOpen + ? mode('base.250', 'base.700')(colorMode) + : mode('base.200', 'base.750')(colorMode), }, fontSize: 'sm', fontWeight: 600, @@ -50,7 +63,13 @@ const IAICollapse = (props: IAIToggleCollapseProps) => { )} - + {children} diff --git a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx index bdf22c2df1..d0652dc8b9 100644 --- a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx +++ b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx @@ -5,6 +5,7 @@ import { Icon, IconButtonProps, Image, + useColorMode, } from '@chakra-ui/react'; import { useDraggable, useDroppable } from '@dnd-kit/core'; import { useCombinedRefs } from '@dnd-kit/utilities'; @@ -20,6 +21,7 @@ import { v4 as uuidv4 } from 'uuid'; import IAIDropOverlay from './IAIDropOverlay'; import { PostUploadAction } from 'services/api/thunks/image'; import { useImageUploadButton } from 'common/hooks/useImageUploadButton'; +import { mode } from 'theme/util/mode'; type IAIDndImageProps = { image: ImageDTO | null | undefined; @@ -62,6 +64,7 @@ const IAIDndImage = (props: IAIDndImageProps) => { } = props; const dndId = useRef(uuidv4()); + const { colorMode } = useColorMode(); const { isOver, @@ -99,10 +102,10 @@ const IAIDndImage = (props: IAIDndImageProps) => { ? {} : { cursor: 'pointer', - bg: 'base.800', + bg: mode('base.200', 'base.800')(colorMode), _hover: { - bg: 'base.750', - color: 'base.300', + bg: mode('base.300', 'base.650')(colorMode), + color: mode('base.500', 'base.300')(colorMode), }, }; @@ -181,7 +184,7 @@ const IAIDndImage = (props: IAIDndImageProps) => { borderRadius: 'base', transitionProperty: 'common', transitionDuration: '0.1s', - color: 'base.500', + color: mode('base.500', 'base.500')(colorMode), ...uploadButtonStyles, }} {...getUploadButtonProps()} diff --git a/invokeai/frontend/web/src/common/components/IAIDropOverlay.tsx b/invokeai/frontend/web/src/common/components/IAIDropOverlay.tsx index 79105c00d6..8ae54c30ab 100644 --- a/invokeai/frontend/web/src/common/components/IAIDropOverlay.tsx +++ b/invokeai/frontend/web/src/common/components/IAIDropOverlay.tsx @@ -1,6 +1,7 @@ -import { Flex, Text } from '@chakra-ui/react'; +import { Flex, Text, useColorMode } from '@chakra-ui/react'; import { motion } from 'framer-motion'; import { memo, useRef } from 'react'; +import { mode } from 'theme/util/mode'; import { v4 as uuidv4 } from 'uuid'; type Props = { @@ -11,6 +12,7 @@ type Props = { export const IAIDropOverlay = (props: Props) => { const { isOver, label = 'Drop' } = props; const motionId = useRef(uuidv4()); + const { colorMode } = useColorMode(); return ( { insetInlineStart: 0, w: 'full', h: 'full', - bg: 'base.900', + bg: mode('base.700', 'base.900')(colorMode), opacity: 0.7, borderRadius: 'base', alignItems: 'center', @@ -61,7 +63,9 @@ export const IAIDropOverlay = (props: Props) => { h: 'full', opacity: 1, borderWidth: 2, - borderColor: isOver ? 'base.200' : 'base.500', + borderColor: isOver + ? mode('base.50', 'base.200')(colorMode) + : mode('base.100', 'base.500')(colorMode), borderRadius: 'base', borderStyle: 'dashed', transitionProperty: 'common', @@ -75,7 +79,9 @@ export const IAIDropOverlay = (props: Props) => { fontSize: '2xl', fontWeight: 600, transform: isOver ? 'scale(1.1)' : 'scale(1)', - color: isOver ? 'base.100' : 'base.500', + color: isOver + ? mode('base.100', 'base.100')(colorMode) + : mode('base.200', 'base.500')(colorMode), transitionProperty: 'common', transitionDuration: '0.1s', }} diff --git a/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx b/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx index 03a00d5b1c..4cff351aee 100644 --- a/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx +++ b/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx @@ -6,9 +6,10 @@ import { IconProps, Spinner, SpinnerProps, + useColorMode, } from '@chakra-ui/react'; -import { ReactElement } from 'react'; import { FaImage } from 'react-icons/fa'; +import { mode } from 'theme/util/mode'; type Props = FlexProps & { spinnerProps?: SpinnerProps; @@ -17,10 +18,11 @@ type Props = FlexProps & { export const IAIImageLoadingFallback = (props: Props) => { const { spinnerProps, ...rest } = props; const { sx, ...restFlexProps } = rest; + const { colorMode } = useColorMode(); return ( { const { sx: flexSx, ...restFlexProps } = props.flexProps ?? { sx: {} }; const { sx: iconSx, ...restIconProps } = props.iconProps ?? { sx: {} }; + const { colorMode } = useColorMode(); + return ( { > diff --git a/invokeai/frontend/web/src/common/components/IAIMantineMultiSelect.tsx b/invokeai/frontend/web/src/common/components/IAIMantineMultiSelect.tsx index 90be25d04d..0a324d3221 100644 --- a/invokeai/frontend/web/src/common/components/IAIMantineMultiSelect.tsx +++ b/invokeai/frontend/web/src/common/components/IAIMantineMultiSelect.tsx @@ -1,6 +1,8 @@ -import { Tooltip } from '@chakra-ui/react'; +import { Tooltip, useColorMode, useToken } from '@chakra-ui/react'; import { MultiSelect, MultiSelectProps } from '@mantine/core'; +import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens'; import { memo } from 'react'; +import { mode } from 'theme/util/mode'; type IAIMultiSelectProps = MultiSelectProps & { tooltip?: string; @@ -8,71 +10,101 @@ type IAIMultiSelectProps = MultiSelectProps & { const IAIMantineMultiSelect = (props: IAIMultiSelectProps) => { const { searchable = true, tooltip, ...rest } = props; + const { + base50, + base100, + base200, + base250, + base300, + base400, + base500, + base600, + base700, + base800, + base900, + accent200, + accent300, + accent400, + accent500, + accent600, + accent700, + } = useChakraThemeTokens(); + const [boxShadow] = useToken('shadows', ['dark-lg']); + const { colorMode } = useColorMode(); + return ( ({ label: { - color: 'var(--invokeai-colors-base-300)', + color: mode(base700, base300)(colorMode), fontWeight: 'normal', }, searchInput: { - '::placeholder': { - color: 'var(--invokeai-colors-base-700)', + ':placeholder': { + color: mode(base300, base700)(colorMode), }, }, input: { - backgroundColor: 'var(--invokeai-colors-base-900)', + backgroundColor: mode(base50, base900)(colorMode), borderWidth: '2px', - borderColor: 'var(--invokeai-colors-base-800)', - color: 'var(--invokeai-colors-base-100)', + borderColor: mode(base200, base800)(colorMode), + color: mode(base900, base100)(colorMode), paddingRight: 24, fontWeight: 600, - '&:hover': { borderColor: 'var(--invokeai-colors-base-700)' }, + '&:hover': { borderColor: mode(base300, base600)(colorMode) }, '&:focus': { - borderColor: 'var(--invokeai-colors-accent-600)', + borderColor: mode(accent300, accent600)(colorMode), + }, + '&:is(:focus, :hover)': { + borderColor: mode(base400, base500)(colorMode), }, '&:focus-within': { - borderColor: 'var(--invokeai-colors-accent-600)', + borderColor: mode(accent200, accent600)(colorMode), + }, + '&:disabled': { + backgroundColor: mode(base300, base700)(colorMode), + color: mode(base600, base400)(colorMode), }, }, value: { - backgroundColor: 'var(--invokeai-colors-base-800)', - color: 'var(--invokeai-colors-base-100)', + backgroundColor: mode(base200, base800)(colorMode), + color: mode(base900, base100)(colorMode), button: { - color: 'var(--invokeai-colors-base-100)', + color: mode(base900, base100)(colorMode), }, '&:hover': { - backgroundColor: 'var(--invokeai-colors-base-700)', + backgroundColor: mode(base300, base700)(colorMode), cursor: 'pointer', }, }, dropdown: { - backgroundColor: 'var(--invokeai-colors-base-800)', - borderColor: 'var(--invokeai-colors-base-700)', + backgroundColor: mode(base200, base800)(colorMode), + borderColor: mode(base200, base800)(colorMode), + boxShadow, }, item: { - backgroundColor: 'var(--invokeai-colors-base-800)', - color: 'var(--invokeai-colors-base-200)', + backgroundColor: mode(base200, base800)(colorMode), + color: mode(base800, base200)(colorMode), padding: 6, '&[data-hovered]': { - color: 'var(--invokeai-colors-base-100)', - backgroundColor: 'var(--invokeai-colors-base-750)', + color: mode(base900, base100)(colorMode), + backgroundColor: mode(base300, base700)(colorMode), }, '&[data-active]': { - backgroundColor: 'var(--invokeai-colors-base-750)', + backgroundColor: mode(base300, base700)(colorMode), '&:hover': { - color: 'var(--invokeai-colors-base-100)', - backgroundColor: 'var(--invokeai-colors-base-750)', + color: mode(base900, base100)(colorMode), + backgroundColor: mode(base300, base700)(colorMode), }, }, '&[data-selected]': { - color: 'var(--invokeai-colors-base-50)', - backgroundColor: 'var(--invokeai-colors-accent-650)', + color: mode(base900, base50)(colorMode), + backgroundColor: mode(accent300, accent600)(colorMode), fontWeight: 600, '&:hover': { - backgroundColor: 'var(--invokeai-colors-accent-600)', + backgroundColor: mode(accent400, accent500)(colorMode), }, }, }, @@ -80,7 +112,7 @@ const IAIMantineMultiSelect = (props: IAIMultiSelectProps) => { width: 24, padding: 20, button: { - color: 'var(--invokeai-colors-base-100)', + color: mode(base900, base100)(colorMode), }, }, })} diff --git a/invokeai/frontend/web/src/common/components/IAIMantineSelect.tsx b/invokeai/frontend/web/src/common/components/IAIMantineSelect.tsx index 5f02140904..3638b59562 100644 --- a/invokeai/frontend/web/src/common/components/IAIMantineSelect.tsx +++ b/invokeai/frontend/web/src/common/components/IAIMantineSelect.tsx @@ -1,6 +1,8 @@ -import { Tooltip } from '@chakra-ui/react'; +import { Tooltip, useColorMode, useToken } from '@chakra-ui/react'; import { Select, SelectProps } from '@mantine/core'; +import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens'; import { memo } from 'react'; +import { mode } from 'theme/util/mode'; export type IAISelectDataType = { value: string; @@ -14,61 +16,105 @@ type IAISelectProps = SelectProps & { const IAIMantineSelect = (props: IAISelectProps) => { const { searchable = true, tooltip, ...rest } = props; + const { + base50, + base100, + base200, + base300, + base400, + base500, + base600, + base700, + base800, + base900, + accent200, + accent300, + accent400, + accent500, + accent600, + accent700, + } = useChakraThemeTokens(); + + const { colorMode } = useColorMode(); + + const [boxShadow] = useToken('shadows', ['dark-lg']); + return (