diff --git a/invokeai/app/api/routers/boards.py b/invokeai/app/api/routers/boards.py index 55cd7c8ca2..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,11 +72,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: Optional[bool] = Query( + 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 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: # TODO: Does this need any exception handling at all? pass 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" 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..066e6f8d5f 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 image_name IN ({placeholders})" + + # Execute the query with the list of IDs as parameters + self._cursor.execute(query, image_names) + + 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]: 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')) diff --git a/invokeai/backend/model_management/models/vae.py b/invokeai/backend/model_management/models/vae.py index b582f16b30..3f0d226687 100644 --- a/invokeai/backend/model_management/models/vae.py +++ b/invokeai/backend/model_management/models/vae.py @@ -137,7 +137,6 @@ def _convert_vae_ckpt_and_cache( from .stable_diffusion import _select_ckpt_config # all sd models use same vae settings config_file = _select_ckpt_config(base_model, ModelVariantType.Normal) - else: raise Exception(f"Vae conversion not supported for model type: {base_model}") @@ -152,7 +151,7 @@ def _convert_vae_ckpt_and_cache( if "state_dict" in checkpoint: checkpoint = checkpoint["state_dict"] - config = OmegaConf.load(config_file) + config = OmegaConf.load(app_config.root_path/config_file) vae_model = convert_ldm_vae_to_diffusers( checkpoint = checkpoint, diff --git a/invokeai/frontend/web/config/common.ts b/invokeai/frontend/web/config/common.ts index 2dce54e70a..4470224225 100644 --- a/invokeai/frontend/web/config/common.ts +++ b/invokeai/frontend/web/config/common.ts @@ -3,12 +3,10 @@ import { visualizer } from 'rollup-plugin-visualizer'; import { PluginOption, UserConfig } from 'vite'; import eslint from 'vite-plugin-eslint'; import tsconfigPaths from 'vite-tsconfig-paths'; -import { nodePolyfills } from 'vite-plugin-node-polyfills'; export const commonPlugins: UserConfig['plugins'] = [ react(), eslint(), tsconfigPaths(), visualizer() as unknown as PluginOption, - nodePolyfills(), ]; diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 8c66222584..786a721d5c 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -53,7 +53,6 @@ ] }, "dependencies": { - "@apidevtools/swagger-parser": "^10.1.0", "@chakra-ui/anatomy": "^2.1.1", "@chakra-ui/icons": "^2.0.19", "@chakra-ui/react": "^2.7.1", @@ -155,7 +154,6 @@ "vite-plugin-css-injected-by-js": "^3.1.1", "vite-plugin-dts": "^2.3.0", "vite-plugin-eslint": "^1.8.1", - "vite-plugin-node-polyfills": "^0.9.0", "vite-tsconfig-paths": "^4.2.0", "yarn": "^1.22.19" } diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index b69e3b82af..1b3b790222 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -24,16 +24,13 @@ }, "common": { "hotkeysLabel": "Hotkeys", - "themeLabel": "Theme", + "darkMode": "Dark Mode", + "lightMode": "Light Mode", "languagePickerLabel": "Language", "reportBugLabel": "Report Bug", "githubLabel": "Github", "discordLabel": "Discord", "settingsLabel": "Settings", - "darkTheme": "Dark", - "lightTheme": "Light", - "greenTheme": "Green", - "oceanTheme": "Ocean", "langArabic": "العربية", "langEnglish": "English", "langDutch": "Nederlands", 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/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/app/contexts/DeleteBoardImagesContext.tsx b/invokeai/frontend/web/src/app/contexts/DeleteBoardImagesContext.tsx new file mode 100644 index 0000000000..38c89bfcf9 --- /dev/null +++ b/invokeai/frontend/web/src/app/contexts/DeleteBoardImagesContext.tsx @@ -0,0 +1,170 @@ +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 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 = { + /** + * Whether the move image dialog is open. + */ + isOpen: boolean; + /** + * Closes the move image dialog. + */ + onClose: () => void; + imagesUsage?: ImageUsage; + 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 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 [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) { + dispatch( + requestedBoardImagesDeletion({ board: boardToDelete, imagesUsage }) + ); + closeAndClearBoardToDelete(); + } + }, + [dispatch, closeAndClearBoardToDelete, boardToDelete, imagesUsage] + ); + + const handleDeleteBoardOnly = useCallback( + (boardId: string) => { + if (boardToDelete) { + deleteBoard(boardId); + closeAndClearBoardToDelete(); + } + }, + [deleteBoard, closeAndClearBoardToDelete, boardToDelete] + ); + + return ( + + {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 eab5e53ad9..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(); @@ -198,7 +200,7 @@ addControlNetImageProcessedListener(); addControlNetAutoProcessListener(); // Update image URLs on connect -addUpdateImageUrlsOnConnectListener(); +// addUpdateImageUrlsOnConnectListener(); // Boards addImageAddedToBoardFulfilledListener(); 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/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..39ec6fd245 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,100 @@ type IAIMultiSelectProps = MultiSelectProps & { const IAIMantineMultiSelect = (props: IAIMultiSelectProps) => { const { searchable = true, tooltip, ...rest } = props; + const { + base50, + base100, + base200, + base300, + base400, + base500, + base600, + base700, + base800, + base900, + accent200, + accent300, + accent400, + accent500, + accent600, + } = 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)', + backgroundColor: mode(accent400, accent600)(colorMode), + color: mode(base50, base100)(colorMode), fontWeight: 600, '&:hover': { - backgroundColor: 'var(--invokeai-colors-accent-600)', + backgroundColor: mode(accent500, accent500)(colorMode), + color: mode('white', base50)(colorMode), }, }, }, @@ -80,7 +111,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..9b023fd2d7 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, + } = useChakraThemeTokens(); + + const { colorMode } = useColorMode(); + + const [boxShadow] = useToken('shadows', ['dark-lg']); + return (