Merge branch 'main' into release/invokeai-3-0-alpha

This commit is contained in:
Lincoln Stein 2023-07-01 14:34:38 -04:00 committed by GitHub
commit 6c80620c25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
97 changed files with 1720 additions and 1389 deletions

View File

@ -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.image_record_storage import OffsetPaginatedResults
from invokeai.app.services.models.board_record import BoardDTO from invokeai.app.services.models.board_record import BoardDTO
from ..dependencies import ApiDependencies from ..dependencies import ApiDependencies
boards_router = APIRouter(prefix="/v1/boards", tags=["boards"]) boards_router = APIRouter(prefix="/v1/boards", tags=["boards"])
@ -71,10 +72,18 @@ async def update_board(
@boards_router.delete("/{board_id}", operation_id="delete_board") @boards_router.delete("/{board_id}", operation_id="delete_board")
async def delete_board( async def delete_board(
board_id: str = Path(description="The id of board to delete"), 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: ) -> None:
"""Deletes a board""" """Deletes a board"""
try: try:
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) ApiDependencies.invoker.services.boards.delete(board_id=board_id)
except Exception as e: except Exception as e:
# TODO: Does this need any exception handling at all? # TODO: Does this need any exception handling at all?

View File

@ -18,8 +18,17 @@ config = InvokeAIAppConfig.get_config()
config.parse_args() config.parse_args()
logger = InvokeAILogger().getLogger(config=config) 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.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.metadata import CoreMetadataService
from invokeai.app.services.resource_name import SimpleNameService from invokeai.app.services.resource_name import SimpleNameService
from invokeai.app.services.urls import LocalUrlService from invokeai.app.services.urls import LocalUrlService
@ -230,7 +239,32 @@ def invoke_cli():
image_file_storage = DiskImageFileStorage(f"{output_folder}/images") image_file_storage = DiskImageFileStorage(f"{output_folder}/images")
names = SimpleNameService() names = SimpleNameService()
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( images = ImageService(
services=ImageServiceDependencies(
board_image_record_storage=board_image_record_storage,
image_record_storage=image_record_storage, image_record_storage=image_record_storage,
image_file_storage=image_file_storage, image_file_storage=image_file_storage,
metadata=metadata, metadata=metadata,
@ -239,12 +273,15 @@ def invoke_cli():
names=names, names=names,
graph_execution_manager=graph_execution_manager, graph_execution_manager=graph_execution_manager,
) )
)
services = InvocationServices( services = InvocationServices(
model_manager=model_manager, model_manager=model_manager,
events=events, events=events,
latents = ForwardCacheLatentsStorage(DiskLatentsStorage(f'{output_folder}/latents')), latents = ForwardCacheLatentsStorage(DiskLatentsStorage(f'{output_folder}/latents')),
images=images, images=images,
boards=boards,
board_images=board_images,
queue=MemoryInvocationQueue(), queue=MemoryInvocationQueue(),
graph_library=SqliteItemStorage[LibraryGraph]( graph_library=SqliteItemStorage[LibraryGraph](
filename=db_location, table_name="graphs" filename=db_location, table_name="graphs"

View File

@ -85,8 +85,10 @@ class DiskImageFileStorage(ImageFileStorageBase):
self.__cache_ids = Queue() self.__cache_ids = Queue()
self.__max_cache_size = 10 # TODO: get this from config 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.__output_folder: Path = (
self.__thumbnails_folder = self.__output_folder / 'thumbnails' output_folder if isinstance(output_folder, Path) else Path(output_folder)
)
self.__thumbnails_folder = self.__output_folder / "thumbnails"
# Validate required output folders at launch # Validate required output folders at launch
self.__validate_storage_folders() self.__validate_storage_folders()
@ -179,7 +181,9 @@ class DiskImageFileStorage(ImageFileStorageBase):
def __set_cache(self, image_name: Path, image: PILImageType): def __set_cache(self, image_name: Path, image: PILImageType):
if not image_name in self.__cache: if not image_name in self.__cache:
self.__cache[image_name] = image 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: if len(self.__cache) > self.__max_cache_size:
cache_id = self.__cache_ids.get() cache_id = self.__cache_ids.get()
if cache_id in self.__cache: if cache_id in self.__cache:

View File

@ -94,6 +94,11 @@ class ImageRecordStorageBase(ABC):
"""Deletes an image record.""" """Deletes an image record."""
pass pass
@abstractmethod
def delete_many(self, image_names: list[str]) -> None:
"""Deletes many image records."""
pass
@abstractmethod @abstractmethod
def save( def save(
self, self,
@ -385,6 +390,25 @@ class SqliteImageRecordStorage(ImageRecordStorageBase):
finally: finally:
self._lock.release() 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( def save(
self, self,
image_name: str, image_name: str,

View File

@ -112,6 +112,11 @@ class ImageServiceABC(ABC):
"""Deletes an image.""" """Deletes an image."""
pass pass
@abstractmethod
def delete_images_on_board(self, board_id: str):
"""Deletes all images on a board."""
pass
class ImageServiceDependencies: class ImageServiceDependencies:
"""Service dependencies for the ImageService.""" """Service dependencies for the ImageService."""
@ -341,6 +346,28 @@ class ImageService(ImageServiceABC):
self._services.logger.error("Problem deleting image record and file") self._services.logger.error("Problem deleting image record and file")
raise e 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( def _get_metadata(
self, session_id: Optional[str] = None, node_id: Optional[str] = None self, session_id: Optional[str] = None, node_id: Optional[str] = None
) -> Union[ImageMetadata, None]: ) -> Union[ImageMetadata, None]:

View File

@ -326,7 +326,7 @@ class MigrateTo3(object):
vae_path = p vae_path = p
elif repo_id := vae.get('repo_id'): elif repo_id := vae.get('repo_id'):
if repo_id=='stabilityai/sd-vae-ft-mse': # this guy is already downloaded 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: else:
vae_path = self._download_vae(repo_id, vae.get('subfolder')) vae_path = self._download_vae(repo_id, vae.get('subfolder'))

View File

@ -137,7 +137,6 @@ def _convert_vae_ckpt_and_cache(
from .stable_diffusion import _select_ckpt_config from .stable_diffusion import _select_ckpt_config
# all sd models use same vae settings # all sd models use same vae settings
config_file = _select_ckpt_config(base_model, ModelVariantType.Normal) config_file = _select_ckpt_config(base_model, ModelVariantType.Normal)
else: else:
raise Exception(f"Vae conversion not supported for model type: {base_model}") 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: if "state_dict" in checkpoint:
checkpoint = checkpoint["state_dict"] 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( vae_model = convert_ldm_vae_to_diffusers(
checkpoint = checkpoint, checkpoint = checkpoint,

View File

@ -3,12 +3,10 @@ import { visualizer } from 'rollup-plugin-visualizer';
import { PluginOption, UserConfig } from 'vite'; import { PluginOption, UserConfig } from 'vite';
import eslint from 'vite-plugin-eslint'; import eslint from 'vite-plugin-eslint';
import tsconfigPaths from 'vite-tsconfig-paths'; import tsconfigPaths from 'vite-tsconfig-paths';
import { nodePolyfills } from 'vite-plugin-node-polyfills';
export const commonPlugins: UserConfig['plugins'] = [ export const commonPlugins: UserConfig['plugins'] = [
react(), react(),
eslint(), eslint(),
tsconfigPaths(), tsconfigPaths(),
visualizer() as unknown as PluginOption, visualizer() as unknown as PluginOption,
nodePolyfills(),
]; ];

View File

@ -53,7 +53,6 @@
] ]
}, },
"dependencies": { "dependencies": {
"@apidevtools/swagger-parser": "^10.1.0",
"@chakra-ui/anatomy": "^2.1.1", "@chakra-ui/anatomy": "^2.1.1",
"@chakra-ui/icons": "^2.0.19", "@chakra-ui/icons": "^2.0.19",
"@chakra-ui/react": "^2.7.1", "@chakra-ui/react": "^2.7.1",
@ -155,7 +154,6 @@
"vite-plugin-css-injected-by-js": "^3.1.1", "vite-plugin-css-injected-by-js": "^3.1.1",
"vite-plugin-dts": "^2.3.0", "vite-plugin-dts": "^2.3.0",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-plugin-node-polyfills": "^0.9.0",
"vite-tsconfig-paths": "^4.2.0", "vite-tsconfig-paths": "^4.2.0",
"yarn": "^1.22.19" "yarn": "^1.22.19"
} }

View File

@ -24,16 +24,13 @@
}, },
"common": { "common": {
"hotkeysLabel": "Hotkeys", "hotkeysLabel": "Hotkeys",
"themeLabel": "Theme", "darkMode": "Dark Mode",
"lightMode": "Light Mode",
"languagePickerLabel": "Language", "languagePickerLabel": "Language",
"reportBugLabel": "Report Bug", "reportBugLabel": "Report Bug",
"githubLabel": "Github", "githubLabel": "Github",
"discordLabel": "Discord", "discordLabel": "Discord",
"settingsLabel": "Settings", "settingsLabel": "Settings",
"darkTheme": "Dark",
"lightTheme": "Light",
"greenTheme": "Green",
"oceanTheme": "Ocean",
"langArabic": "العربية", "langArabic": "العربية",
"langEnglish": "English", "langEnglish": "English",
"langDutch": "Nederlands", "langDutch": "Nederlands",

View File

@ -25,6 +25,7 @@ import DeleteImageModal from 'features/gallery/components/DeleteImageModal';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
import UpdateImageBoardModal from '../../features/gallery/components/Boards/UpdateImageBoardModal'; import UpdateImageBoardModal from '../../features/gallery/components/Boards/UpdateImageBoardModal';
import { useListModelsQuery } from 'services/api/endpoints/models'; import { useListModelsQuery } from 'services/api/endpoints/models';
import DeleteBoardImagesModal from '../../features/gallery/components/Boards/DeleteBoardImagesModal';
const DEFAULT_CONFIG = {}; const DEFAULT_CONFIG = {};
@ -158,6 +159,7 @@ const App = ({
</Grid> </Grid>
<DeleteImageModal /> <DeleteImageModal />
<UpdateImageBoardModal /> <UpdateImageBoardModal />
<DeleteBoardImagesModal />
<Toaster /> <Toaster />
<GlobalHotkeys /> <GlobalHotkeys />
</> </>

View File

@ -24,6 +24,7 @@ import {
import UpdateImageBoardModal from '../../features/gallery/components/Boards/UpdateImageBoardModal'; import UpdateImageBoardModal from '../../features/gallery/components/Boards/UpdateImageBoardModal';
import { AddImageToBoardContextProvider } from '../contexts/AddImageToBoardContext'; import { AddImageToBoardContextProvider } from '../contexts/AddImageToBoardContext';
import { $authToken, $baseUrl } from 'services/api/client'; import { $authToken, $baseUrl } from 'services/api/client';
import { DeleteBoardImagesContextProvider } from '../contexts/DeleteBoardImagesContext';
const App = lazy(() => import('./App')); const App = lazy(() => import('./App'));
const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider')); const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider'));
@ -86,11 +87,13 @@ const InvokeAIUI = ({
<ImageDndContext> <ImageDndContext>
<DeleteImageContextProvider> <DeleteImageContextProvider>
<AddImageToBoardContextProvider> <AddImageToBoardContextProvider>
<DeleteBoardImagesContextProvider>
<App <App
config={config} config={config}
headerComponent={headerComponent} headerComponent={headerComponent}
setIsReady={setIsReady} setIsReady={setIsReady}
/> />
</DeleteBoardImagesContextProvider>
</AddImageToBoardContextProvider> </AddImageToBoardContextProvider>
</DeleteImageContextProvider> </DeleteImageContextProvider>
</ImageDndContext> </ImageDndContext>

View File

@ -3,17 +3,10 @@ import {
createLocalStorageManager, createLocalStorageManager,
extendTheme, extendTheme,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { RootState } from 'app/store/store'; import { ReactNode, useEffect, useMemo } from 'react';
import { useAppSelector } from 'app/store/storeHooks';
import { ReactNode, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { theme as invokeAITheme } from 'theme/theme'; 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 '@fontsource-variable/inter';
import { MantineProvider } from '@mantine/core'; import { MantineProvider } from '@mantine/core';
import { mantineTheme } from 'mantine-theme/theme'; import { mantineTheme } from 'mantine-theme/theme';
@ -24,29 +17,19 @@ type ThemeLocaleProviderProps = {
children: ReactNode; children: ReactNode;
}; };
const THEMES = {
dark: invokeAIThemeColors,
light: lightThemeColors,
green: greenTeaThemeColors,
ocean: oceanBlueColors,
};
const manager = createLocalStorageManager('@@invokeai-color-mode'); const manager = createLocalStorageManager('@@invokeai-color-mode');
function ThemeLocaleProvider({ children }: ThemeLocaleProviderProps) { function ThemeLocaleProvider({ children }: ThemeLocaleProviderProps) {
const { i18n } = useTranslation(); const { i18n } = useTranslation();
const currentTheme = useAppSelector(
(state: RootState) => state.ui.currentTheme
);
const direction = i18n.dir(); const direction = i18n.dir();
const theme = extendTheme({ const theme = useMemo(() => {
return extendTheme({
...invokeAITheme, ...invokeAITheme,
colors: THEMES[currentTheme as keyof typeof THEMES],
direction, direction,
}); });
}, [direction]);
useEffect(() => { useEffect(() => {
document.body.dir = direction; document.body.dir = direction;

View File

@ -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<DeleteBoardImagesContextValue>({
isOpen: false,
onClose: () => undefined,
onClickDeleteBoardImages: () => undefined,
handleDeleteBoardImages: () => undefined,
handleDeleteBoardOnly: () => undefined,
});
type Props = PropsWithChildren;
export const DeleteBoardImagesContextProvider = (props: Props) => {
const [boardToDelete, setBoardToDelete] = useState<BoardDTO>();
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 (
<DeleteBoardImagesContext.Provider
value={{
isOpen,
board: boardToDelete,
onClose: closeAndClearBoardToDelete,
onClickDeleteBoardImages,
handleDeleteBoardImages,
handleDeleteBoardOnly,
imagesUsage,
}}
>
{props.children}
</DeleteBoardImagesContext.Provider>
);
};

View File

@ -83,6 +83,7 @@ import {
addImageRemovedFromBoardRejectedListener, addImageRemovedFromBoardRejectedListener,
} from './listeners/imageRemovedFromBoard'; } from './listeners/imageRemovedFromBoard';
import { addReceivedOpenAPISchemaListener } from './listeners/receivedOpenAPISchema'; import { addReceivedOpenAPISchemaListener } from './listeners/receivedOpenAPISchema';
import { addRequestedBoardImageDeletionListener } from './listeners/boardImagesDeleted';
export const listenerMiddleware = createListenerMiddleware(); export const listenerMiddleware = createListenerMiddleware();
@ -124,6 +125,7 @@ addRequestedImageDeletionListener();
addImageDeletedPendingListener(); addImageDeletedPendingListener();
addImageDeletedFulfilledListener(); addImageDeletedFulfilledListener();
addImageDeletedRejectedListener(); addImageDeletedRejectedListener();
addRequestedBoardImageDeletionListener();
// Image metadata // Image metadata
addImageMetadataReceivedFulfilledListener(); addImageMetadataReceivedFulfilledListener();
@ -198,7 +200,7 @@ addControlNetImageProcessedListener();
addControlNetAutoProcessListener(); addControlNetAutoProcessListener();
// Update image URLs on connect // Update image URLs on connect
addUpdateImageUrlsOnConnectListener(); // addUpdateImageUrlsOnConnectListener();
// Boards // Boards
addImageAddedToBoardFulfilledListener(); addImageAddedToBoardFulfilledListener();

View File

@ -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 },
])
);
}
},
});
};

View File

@ -1,6 +1,14 @@
import { ChevronUpIcon } from '@chakra-ui/icons'; 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 { PropsWithChildren, memo } from 'react';
import { mode } from 'theme/util/mode';
export type IAIToggleCollapseProps = PropsWithChildren & { export type IAIToggleCollapseProps = PropsWithChildren & {
label: string; label: string;
@ -11,6 +19,7 @@ export type IAIToggleCollapseProps = PropsWithChildren & {
const IAICollapse = (props: IAIToggleCollapseProps) => { const IAICollapse = (props: IAIToggleCollapseProps) => {
const { label, isOpen, onToggle, children, withSwitch = false } = props; const { label, isOpen, onToggle, children, withSwitch = false } = props;
const { colorMode } = useColorMode();
return ( return (
<Box> <Box>
<Flex <Flex
@ -21,10 +30,14 @@ const IAICollapse = (props: IAIToggleCollapseProps) => {
px: 4, px: 4,
borderTopRadius: 'base', borderTopRadius: 'base',
borderBottomRadius: isOpen ? 0 : 'base', borderBottomRadius: isOpen ? 0 : 'base',
bg: isOpen ? 'base.750' : 'base.800', bg: isOpen
color: 'base.100', ? mode('base.200', 'base.750')(colorMode)
: mode('base.150', 'base.800')(colorMode),
color: mode('base.900', 'base.100')(colorMode),
_hover: { _hover: {
bg: isOpen ? 'base.700' : 'base.750', bg: isOpen
? mode('base.250', 'base.700')(colorMode)
: mode('base.200', 'base.750')(colorMode),
}, },
fontSize: 'sm', fontSize: 'sm',
fontWeight: 600, fontWeight: 600,
@ -50,7 +63,13 @@ const IAICollapse = (props: IAIToggleCollapseProps) => {
)} )}
</Flex> </Flex>
<Collapse in={isOpen} animateOpacity style={{ overflow: 'unset' }}> <Collapse in={isOpen} animateOpacity style={{ overflow: 'unset' }}>
<Box sx={{ p: 4, borderBottomRadius: 'base', bg: 'base.800' }}> <Box
sx={{
p: 4,
borderBottomRadius: 'base',
bg: mode('base.100', 'base.800')(colorMode),
}}
>
{children} {children}
</Box> </Box>
</Collapse> </Collapse>

View File

@ -5,6 +5,7 @@ import {
Icon, Icon,
IconButtonProps, IconButtonProps,
Image, Image,
useColorMode,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useDraggable, useDroppable } from '@dnd-kit/core'; import { useDraggable, useDroppable } from '@dnd-kit/core';
import { useCombinedRefs } from '@dnd-kit/utilities'; import { useCombinedRefs } from '@dnd-kit/utilities';
@ -20,6 +21,7 @@ import { v4 as uuidv4 } from 'uuid';
import IAIDropOverlay from './IAIDropOverlay'; import IAIDropOverlay from './IAIDropOverlay';
import { PostUploadAction } from 'services/api/thunks/image'; import { PostUploadAction } from 'services/api/thunks/image';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton'; import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import { mode } from 'theme/util/mode';
type IAIDndImageProps = { type IAIDndImageProps = {
image: ImageDTO | null | undefined; image: ImageDTO | null | undefined;
@ -62,6 +64,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
} = props; } = props;
const dndId = useRef(uuidv4()); const dndId = useRef(uuidv4());
const { colorMode } = useColorMode();
const { const {
isOver, isOver,
@ -99,10 +102,10 @@ const IAIDndImage = (props: IAIDndImageProps) => {
? {} ? {}
: { : {
cursor: 'pointer', cursor: 'pointer',
bg: 'base.800', bg: mode('base.200', 'base.800')(colorMode),
_hover: { _hover: {
bg: 'base.750', bg: mode('base.300', 'base.650')(colorMode),
color: 'base.300', color: mode('base.500', 'base.300')(colorMode),
}, },
}; };
@ -181,7 +184,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
borderRadius: 'base', borderRadius: 'base',
transitionProperty: 'common', transitionProperty: 'common',
transitionDuration: '0.1s', transitionDuration: '0.1s',
color: 'base.500', color: mode('base.500', 'base.500')(colorMode),
...uploadButtonStyles, ...uploadButtonStyles,
}} }}
{...getUploadButtonProps()} {...getUploadButtonProps()}

View File

@ -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 { motion } from 'framer-motion';
import { memo, useRef } from 'react'; import { memo, useRef } from 'react';
import { mode } from 'theme/util/mode';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
type Props = { type Props = {
@ -11,6 +12,7 @@ type Props = {
export const IAIDropOverlay = (props: Props) => { export const IAIDropOverlay = (props: Props) => {
const { isOver, label = 'Drop' } = props; const { isOver, label = 'Drop' } = props;
const motionId = useRef(uuidv4()); const motionId = useRef(uuidv4());
const { colorMode } = useColorMode();
return ( return (
<motion.div <motion.div
key={motionId.current} key={motionId.current}
@ -42,7 +44,7 @@ export const IAIDropOverlay = (props: Props) => {
insetInlineStart: 0, insetInlineStart: 0,
w: 'full', w: 'full',
h: 'full', h: 'full',
bg: 'base.900', bg: mode('base.700', 'base.900')(colorMode),
opacity: 0.7, opacity: 0.7,
borderRadius: 'base', borderRadius: 'base',
alignItems: 'center', alignItems: 'center',
@ -61,7 +63,9 @@ export const IAIDropOverlay = (props: Props) => {
h: 'full', h: 'full',
opacity: 1, opacity: 1,
borderWidth: 2, borderWidth: 2,
borderColor: isOver ? 'base.200' : 'base.500', borderColor: isOver
? mode('base.50', 'base.200')(colorMode)
: mode('base.100', 'base.500')(colorMode),
borderRadius: 'base', borderRadius: 'base',
borderStyle: 'dashed', borderStyle: 'dashed',
transitionProperty: 'common', transitionProperty: 'common',
@ -75,7 +79,9 @@ export const IAIDropOverlay = (props: Props) => {
fontSize: '2xl', fontSize: '2xl',
fontWeight: 600, fontWeight: 600,
transform: isOver ? 'scale(1.1)' : 'scale(1)', 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', transitionProperty: 'common',
transitionDuration: '0.1s', transitionDuration: '0.1s',
}} }}

View File

@ -6,9 +6,10 @@ import {
IconProps, IconProps,
Spinner, Spinner,
SpinnerProps, SpinnerProps,
useColorMode,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { ReactElement } from 'react';
import { FaImage } from 'react-icons/fa'; import { FaImage } from 'react-icons/fa';
import { mode } from 'theme/util/mode';
type Props = FlexProps & { type Props = FlexProps & {
spinnerProps?: SpinnerProps; spinnerProps?: SpinnerProps;
@ -17,10 +18,11 @@ type Props = FlexProps & {
export const IAIImageLoadingFallback = (props: Props) => { export const IAIImageLoadingFallback = (props: Props) => {
const { spinnerProps, ...rest } = props; const { spinnerProps, ...rest } = props;
const { sx, ...restFlexProps } = rest; const { sx, ...restFlexProps } = rest;
const { colorMode } = useColorMode();
return ( return (
<Flex <Flex
sx={{ sx={{
bg: 'base.900', bg: mode('base.200', 'base.900')(colorMode),
opacity: 0.7, opacity: 0.7,
w: 'full', w: 'full',
h: 'full', h: 'full',
@ -45,10 +47,12 @@ type IAINoImageFallbackProps = {
export const IAINoImageFallback = (props: IAINoImageFallbackProps) => { export const IAINoImageFallback = (props: IAINoImageFallbackProps) => {
const { sx: flexSx, ...restFlexProps } = props.flexProps ?? { sx: {} }; const { sx: flexSx, ...restFlexProps } = props.flexProps ?? { sx: {} };
const { sx: iconSx, ...restIconProps } = props.iconProps ?? { sx: {} }; const { sx: iconSx, ...restIconProps } = props.iconProps ?? { sx: {} };
const { colorMode } = useColorMode();
return ( return (
<Flex <Flex
sx={{ sx={{
bg: 'base.900', bg: mode('base.200', 'base.900')(colorMode),
opacity: 0.7, opacity: 0.7,
w: 'full', w: 'full',
h: 'full', h: 'full',
@ -61,7 +65,7 @@ export const IAINoImageFallback = (props: IAINoImageFallbackProps) => {
> >
<Icon <Icon
as={props.as ?? FaImage} as={props.as ?? FaImage}
sx={{ color: 'base.700', ...iconSx }} sx={{ color: mode('base.700', 'base.500')(colorMode), ...iconSx }}
{...restIconProps} {...restIconProps}
/> />
</Flex> </Flex>

View File

@ -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 { MultiSelect, MultiSelectProps } from '@mantine/core';
import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens';
import { memo } from 'react'; import { memo } from 'react';
import { mode } from 'theme/util/mode';
type IAIMultiSelectProps = MultiSelectProps & { type IAIMultiSelectProps = MultiSelectProps & {
tooltip?: string; tooltip?: string;
@ -8,71 +10,100 @@ type IAIMultiSelectProps = MultiSelectProps & {
const IAIMantineMultiSelect = (props: IAIMultiSelectProps) => { const IAIMantineMultiSelect = (props: IAIMultiSelectProps) => {
const { searchable = true, tooltip, ...rest } = props; 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 ( return (
<Tooltip label={tooltip} placement="top" hasArrow> <Tooltip label={tooltip} placement="top" hasArrow>
<MultiSelect <MultiSelect
searchable={searchable} searchable={searchable}
styles={() => ({ styles={() => ({
label: { label: {
color: 'var(--invokeai-colors-base-300)', color: mode(base700, base300)(colorMode),
fontWeight: 'normal', fontWeight: 'normal',
}, },
searchInput: { searchInput: {
'::placeholder': { ':placeholder': {
color: 'var(--invokeai-colors-base-700)', color: mode(base300, base700)(colorMode),
}, },
}, },
input: { input: {
backgroundColor: 'var(--invokeai-colors-base-900)', backgroundColor: mode(base50, base900)(colorMode),
borderWidth: '2px', borderWidth: '2px',
borderColor: 'var(--invokeai-colors-base-800)', borderColor: mode(base200, base800)(colorMode),
color: 'var(--invokeai-colors-base-100)', color: mode(base900, base100)(colorMode),
paddingRight: 24, paddingRight: 24,
fontWeight: 600, fontWeight: 600,
'&:hover': { borderColor: 'var(--invokeai-colors-base-700)' }, '&:hover': { borderColor: mode(base300, base600)(colorMode) },
'&:focus': { '&:focus': {
borderColor: 'var(--invokeai-colors-accent-600)', borderColor: mode(accent300, accent600)(colorMode),
},
'&:is(:focus, :hover)': {
borderColor: mode(base400, base500)(colorMode),
}, },
'&:focus-within': { '&: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: { value: {
backgroundColor: 'var(--invokeai-colors-base-800)', backgroundColor: mode(base200, base800)(colorMode),
color: 'var(--invokeai-colors-base-100)', color: mode(base900, base100)(colorMode),
button: { button: {
color: 'var(--invokeai-colors-base-100)', color: mode(base900, base100)(colorMode),
}, },
'&:hover': { '&:hover': {
backgroundColor: 'var(--invokeai-colors-base-700)', backgroundColor: mode(base300, base700)(colorMode),
cursor: 'pointer', cursor: 'pointer',
}, },
}, },
dropdown: { dropdown: {
backgroundColor: 'var(--invokeai-colors-base-800)', backgroundColor: mode(base200, base800)(colorMode),
borderColor: 'var(--invokeai-colors-base-700)', borderColor: mode(base200, base800)(colorMode),
boxShadow,
}, },
item: { item: {
backgroundColor: 'var(--invokeai-colors-base-800)', backgroundColor: mode(base200, base800)(colorMode),
color: 'var(--invokeai-colors-base-200)', color: mode(base800, base200)(colorMode),
padding: 6, padding: 6,
'&[data-hovered]': { '&[data-hovered]': {
color: 'var(--invokeai-colors-base-100)', color: mode(base900, base100)(colorMode),
backgroundColor: 'var(--invokeai-colors-base-750)', backgroundColor: mode(base300, base700)(colorMode),
}, },
'&[data-active]': { '&[data-active]': {
backgroundColor: 'var(--invokeai-colors-base-750)', backgroundColor: mode(base300, base700)(colorMode),
'&:hover': { '&:hover': {
color: 'var(--invokeai-colors-base-100)', color: mode(base900, base100)(colorMode),
backgroundColor: 'var(--invokeai-colors-base-750)', backgroundColor: mode(base300, base700)(colorMode),
}, },
}, },
'&[data-selected]': { '&[data-selected]': {
color: 'var(--invokeai-colors-base-50)', backgroundColor: mode(accent400, accent600)(colorMode),
backgroundColor: 'var(--invokeai-colors-accent-650)', color: mode(base50, base100)(colorMode),
fontWeight: 600, fontWeight: 600,
'&:hover': { '&: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, width: 24,
padding: 20, padding: 20,
button: { button: {
color: 'var(--invokeai-colors-base-100)', color: mode(base900, base100)(colorMode),
}, },
}, },
})} })}

View File

@ -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 { Select, SelectProps } from '@mantine/core';
import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens';
import { memo } from 'react'; import { memo } from 'react';
import { mode } from 'theme/util/mode';
export type IAISelectDataType = { export type IAISelectDataType = {
value: string; value: string;
@ -14,61 +16,105 @@ type IAISelectProps = SelectProps & {
const IAIMantineSelect = (props: IAISelectProps) => { const IAIMantineSelect = (props: IAISelectProps) => {
const { searchable = true, tooltip, ...rest } = props; 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 ( return (
<Tooltip label={tooltip} placement="top" hasArrow> <Tooltip label={tooltip} placement="top" hasArrow>
<Select <Select
searchable={searchable} searchable={searchable}
styles={() => ({ styles={() => ({
label: { label: {
color: 'var(--invokeai-colors-base-300)', color: mode(base700, base300)(colorMode),
fontWeight: 'normal', fontWeight: 'normal',
}, },
input: { input: {
backgroundColor: 'var(--invokeai-colors-base-900)', backgroundColor: mode(base50, base900)(colorMode),
borderWidth: '2px', borderWidth: '2px',
borderColor: 'var(--invokeai-colors-base-800)', borderColor: mode(base200, base800)(colorMode),
color: 'var(--invokeai-colors-base-100)', color: mode(base900, base100)(colorMode),
paddingRight: 24, paddingRight: 24,
fontWeight: 600, fontWeight: 600,
'&:hover': { borderColor: 'var(--invokeai-colors-base-700)' }, '&:hover': { borderColor: mode(base300, base600)(colorMode) },
'&:focus': { '&:focus': {
borderColor: 'var(--invokeai-colors-accent-600)', borderColor: mode(accent300, accent600)(colorMode),
},
'&:is(:focus, :hover)': {
borderColor: mode(base400, base500)(colorMode),
},
'&:focus-within': {
borderColor: mode(accent200, accent600)(colorMode),
}, },
'&:disabled': { '&:disabled': {
backgroundColor: 'var(--invokeai-colors-base-700)', backgroundColor: mode(base300, base700)(colorMode),
color: 'var(--invokeai-colors-base-400)', color: mode(base600, base400)(colorMode),
},
},
value: {
backgroundColor: mode(base100, base900)(colorMode),
color: mode(base900, base100)(colorMode),
button: {
color: mode(base900, base100)(colorMode),
},
'&:hover': {
backgroundColor: mode(base300, base700)(colorMode),
cursor: 'pointer',
}, },
}, },
dropdown: { dropdown: {
backgroundColor: 'var(--invokeai-colors-base-800)', backgroundColor: mode(base200, base800)(colorMode),
borderColor: 'var(--invokeai-colors-base-700)', borderColor: mode(base200, base800)(colorMode),
boxShadow,
}, },
item: { item: {
backgroundColor: 'var(--invokeai-colors-base-800)', backgroundColor: mode(base200, base800)(colorMode),
color: 'var(--invokeai-colors-base-200)', color: mode(base800, base200)(colorMode),
padding: 6, padding: 6,
'&[data-hovered]': { '&[data-hovered]': {
color: 'var(--invokeai-colors-base-100)', color: mode(base900, base100)(colorMode),
backgroundColor: 'var(--invokeai-colors-base-750)', backgroundColor: mode(base300, base700)(colorMode),
}, },
'&[data-active]': { '&[data-active]': {
backgroundColor: 'var(--invokeai-colors-base-750)', backgroundColor: mode(base300, base700)(colorMode),
'&:hover': { '&:hover': {
color: 'var(--invokeai-colors-base-100)', color: mode(base900, base100)(colorMode),
backgroundColor: 'var(--invokeai-colors-base-750)', backgroundColor: mode(base300, base700)(colorMode),
}, },
}, },
'&[data-selected]': { '&[data-selected]': {
color: 'var(--invokeai-colors-base-50)', backgroundColor: mode(accent400, accent600)(colorMode),
backgroundColor: 'var(--invokeai-colors-accent-650)', color: mode(base50, base100)(colorMode),
fontWeight: 600, fontWeight: 600,
'&:hover': { '&:hover': {
backgroundColor: 'var(--invokeai-colors-accent-600)', backgroundColor: mode(accent500, accent500)(colorMode),
color: mode('white', base50)(colorMode),
}, },
}, },
}, },
rightSection: { rightSection: {
width: 32, width: 32,
button: {
color: mode(base900, base100)(colorMode),
},
}, },
})} })}
{...rest} {...rest}

View File

@ -1,5 +1,6 @@
import { Checkbox, CheckboxProps, Text } from '@chakra-ui/react'; import { Checkbox, CheckboxProps, Text, useColorMode } from '@chakra-ui/react';
import { memo, ReactElement } from 'react'; import { memo, ReactElement } from 'react';
import { mode } from 'theme/util/mode';
type IAISimpleCheckboxProps = CheckboxProps & { type IAISimpleCheckboxProps = CheckboxProps & {
label: string | ReactElement; label: string | ReactElement;
@ -7,9 +8,15 @@ type IAISimpleCheckboxProps = CheckboxProps & {
const IAISimpleCheckbox = (props: IAISimpleCheckboxProps) => { const IAISimpleCheckbox = (props: IAISimpleCheckboxProps) => {
const { label, ...rest } = props; const { label, ...rest } = props;
const { colorMode } = useColorMode();
return ( return (
<Checkbox colorScheme="accent" {...rest}> <Checkbox colorScheme="accent" {...rest}>
<Text color="base.200" fontSize="md"> <Text
sx={{
fontSize: 'sm',
color: mode('base.800', 'base.200')(colorMode),
}}
>
{label} {label}
</Text> </Text>
</Checkbox> </Checkbox>

View File

@ -0,0 +1,124 @@
import { useToken } from '@chakra-ui/react';
export const useChakraThemeTokens = () => {
const [
base50,
base100,
base150,
base200,
base250,
base300,
base350,
base400,
base450,
base500,
base550,
base600,
base650,
base700,
base750,
base800,
base850,
base900,
base950,
accent50,
accent100,
accent150,
accent200,
accent250,
accent300,
accent350,
accent400,
accent450,
accent500,
accent550,
accent600,
accent650,
accent700,
accent750,
accent800,
accent850,
accent900,
accent950,
] = useToken('colors', [
'base.50',
'base.100',
'base.150',
'base.200',
'base.250',
'base.300',
'base.350',
'base.400',
'base.450',
'base.500',
'base.550',
'base.600',
'base.650',
'base.700',
'base.750',
'base.800',
'base.850',
'base.900',
'base.950',
'accent.50',
'accent.100',
'accent.150',
'accent.200',
'accent.250',
'accent.300',
'accent.350',
'accent.400',
'accent.450',
'accent.500',
'accent.550',
'accent.600',
'accent.650',
'accent.700',
'accent.750',
'accent.800',
'accent.850',
'accent.900',
'accent.950',
]);
return {
base50,
base100,
base150,
base200,
base250,
base300,
base350,
base400,
base450,
base500,
base550,
base600,
base650,
base700,
base750,
base800,
base850,
base900,
base950,
accent50,
accent100,
accent150,
accent200,
accent250,
accent300,
accent350,
accent400,
accent450,
accent500,
accent550,
accent600,
accent650,
accent700,
accent750,
accent800,
accent850,
accent900,
accent950,
};
};

View File

@ -1,8 +1,7 @@
// Grid drawing adapted from https://longviewcoder.com/2021/12/08/konva-a-better-grid/ // Grid drawing adapted from https://longviewcoder.com/2021/12/08/konva-a-better-grid/
import { useToken } from '@chakra-ui/react'; import { useColorMode, useToken } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { canvasSelector } from 'features/canvas/store/canvasSelectors'; import { canvasSelector } from 'features/canvas/store/canvasSelectors';
import { isEqual, range } from 'lodash-es'; import { isEqual, range } from 'lodash-es';
@ -24,14 +23,14 @@ const selector = createSelector(
); );
const IAICanvasGrid = () => { const IAICanvasGrid = () => {
const currentTheme = useAppSelector(
(state: RootState) => state.ui.currentTheme
);
const { stageScale, stageCoordinates, stageDimensions } = const { stageScale, stageCoordinates, stageDimensions } =
useAppSelector(selector); useAppSelector(selector);
const { colorMode } = useColorMode();
const [gridLines, setGridLines] = useState<ReactNode[]>([]); const [gridLines, setGridLines] = useState<ReactNode[]>([]);
const [darkGridLineColor, lightGridLineColor] = useToken('colors', [
const [gridLineColor] = useToken('colors', ['gridLineColor']); 'base.800',
'base.200',
]);
const unscale = useCallback( const unscale = useCallback(
(value: number) => { (value: number) => {
@ -89,7 +88,7 @@ const IAICanvasGrid = () => {
x={fullRect.x1 + i * 64} x={fullRect.x1 + i * 64}
y={fullRect.y1} y={fullRect.y1}
points={[0, 0, 0, ySize]} points={[0, 0, 0, ySize]}
stroke={gridLineColor} stroke={colorMode === 'dark' ? darkGridLineColor : lightGridLineColor}
strokeWidth={1} strokeWidth={1}
/> />
)); ));
@ -99,7 +98,7 @@ const IAICanvasGrid = () => {
x={fullRect.x1} x={fullRect.x1}
y={fullRect.y1 + i * 64} y={fullRect.y1 + i * 64}
points={[0, 0, xSize, 0]} points={[0, 0, xSize, 0]}
stroke={gridLineColor} stroke={colorMode === 'dark' ? darkGridLineColor : lightGridLineColor}
strokeWidth={1} strokeWidth={1}
/> />
)); ));
@ -109,9 +108,10 @@ const IAICanvasGrid = () => {
stageScale, stageScale,
stageCoordinates, stageCoordinates,
stageDimensions, stageDimensions,
currentTheme,
unscale, unscale,
gridLineColor, colorMode,
darkGridLineColor,
lightGridLineColor,
]); ]);
return <Group>{gridLines}</Group>; return <Group>{gridLines}</Group>;

View File

@ -3,6 +3,7 @@ import { Image, Rect } from 'react-konva';
import { useGetImageDTOQuery } from 'services/api/endpoints/images'; import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import useImage from 'use-image'; import useImage from 'use-image';
import { CanvasImage } from '../store/canvasTypes'; import { CanvasImage } from '../store/canvasTypes';
import { $authToken } from 'services/api/client';
type IAICanvasImageProps = { type IAICanvasImageProps = {
canvasImage: CanvasImage; canvasImage: CanvasImage;
@ -12,7 +13,10 @@ const IAICanvasImage = (props: IAICanvasImageProps) => {
const { currentData: imageDTO, isError } = useGetImageDTOQuery( const { currentData: imageDTO, isError } = useGetImageDTOQuery(
imageName ?? skipToken imageName ?? skipToken
); );
const [image] = useImage(imageDTO?.image_url ?? '', 'anonymous'); const [image] = useImage(
imageDTO?.image_url ?? '',
$authToken.get() ? 'use-credentials' : 'anonymous'
);
if (isError) { if (isError) {
return <Rect x={x} y={y} width={width} height={height} fill="red" />; return <Rect x={x} y={y} width={width} height={height} fill="red" />;

View File

@ -104,7 +104,10 @@ const IAICanvasStatusText = () => {
margin: 1, margin: 1,
borderRadius: 'base', borderRadius: 'base',
pointerEvents: 'none', pointerEvents: 'none',
bg: 'base.200',
_dark: {
bg: 'base.800', bg: 'base.800',
},
}} }}
> >
<Box <Box

View File

@ -1,4 +1,4 @@
import { Box, ChakraProps, Flex } from '@chakra-ui/react'; import { Box, ChakraProps, Flex, useColorMode } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { FaCopy, FaTrash } from 'react-icons/fa'; import { FaCopy, FaTrash } from 'react-icons/fa';
@ -22,6 +22,7 @@ import ParamControlNetShouldAutoConfig from './ParamControlNetShouldAutoConfig';
import ParamControlNetBeginEnd from './parameters/ParamControlNetBeginEnd'; import ParamControlNetBeginEnd from './parameters/ParamControlNetBeginEnd';
import ParamControlNetControlMode from './parameters/ParamControlNetControlMode'; import ParamControlNetControlMode from './parameters/ParamControlNetControlMode';
import ParamControlNetProcessorSelect from './parameters/ParamControlNetProcessorSelect'; import ParamControlNetProcessorSelect from './parameters/ParamControlNetProcessorSelect';
import { mode } from 'theme/util/mode';
const expandedControlImageSx: ChakraProps['sx'] = { maxH: 96 }; const expandedControlImageSx: ChakraProps['sx'] = { maxH: 96 };
@ -46,7 +47,7 @@ const ControlNet = (props: ControlNetProps) => {
} = props.controlNet; } = props.controlNet;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [isExpanded, toggleIsExpanded] = useToggle(false); const [isExpanded, toggleIsExpanded] = useToggle(false);
const { colorMode } = useColorMode();
const handleDelete = useCallback(() => { const handleDelete = useCallback(() => {
dispatch(controlNetRemoved({ controlNetId })); dispatch(controlNetRemoved({ controlNetId }));
}, [controlNetId, dispatch]); }, [controlNetId, dispatch]);
@ -67,7 +68,7 @@ const ControlNet = (props: ControlNetProps) => {
flexDir: 'column', flexDir: 'column',
gap: 2, gap: 2,
p: 3, p: 3,
bg: 'base.850', bg: mode('base.200', 'base.850')(colorMode),
borderRadius: 'base', borderRadius: 'base',
position: 'relative', position: 'relative',
}} }}
@ -115,7 +116,7 @@ const ControlNet = (props: ControlNetProps) => {
<ChevronUpIcon <ChevronUpIcon
sx={{ sx={{
boxSize: 4, boxSize: 4,
color: 'base.300', color: mode('base.700', 'base.300')(colorMode),
transform: isExpanded ? 'rotate(0deg)' : 'rotate(180deg)', transform: isExpanded ? 'rotate(0deg)' : 'rotate(180deg)',
transitionProperty: 'common', transitionProperty: 'common',
transitionDuration: 'normal', transitionDuration: 'normal',
@ -130,7 +131,7 @@ const ControlNet = (props: ControlNetProps) => {
w: 1.5, w: 1.5,
h: 1.5, h: 1.5,
borderRadius: 'full', borderRadius: 'full',
bg: 'error.200', bg: mode('error.700', 'error.200')(colorMode),
top: 4, top: 4,
insetInlineEnd: 4, insetInlineEnd: 4,
}} }}

View File

@ -1,4 +1,4 @@
import { Flex, Text } from '@chakra-ui/react'; import { Flex, Text, useColorMode } from '@chakra-ui/react';
import { FaImages } from 'react-icons/fa'; import { FaImages } from 'react-icons/fa';
import { boardIdSelected } from '../../store/boardSlice'; import { boardIdSelected } from '../../store/boardSlice';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
@ -10,9 +10,11 @@ import { ImageDTO } from 'services/api/types';
import { useRemoveImageFromBoardMutation } from 'services/api/endpoints/boardImages'; import { useRemoveImageFromBoardMutation } from 'services/api/endpoints/boardImages';
import { useDroppable } from '@dnd-kit/core'; import { useDroppable } from '@dnd-kit/core';
import IAIDropOverlay from 'common/components/IAIDropOverlay'; import IAIDropOverlay from 'common/components/IAIDropOverlay';
import { mode } from 'theme/util/mode';
const AllImagesBoard = ({ isSelected }: { isSelected: boolean }) => { const AllImagesBoard = ({ isSelected }: { isSelected: boolean }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { colorMode } = useColorMode();
const handleAllImagesBoardClick = () => { const handleAllImagesBoardClick = () => {
dispatch(boardIdSelected()); dispatch(boardIdSelected());
@ -79,7 +81,9 @@ const AllImagesBoard = ({ isSelected }: { isSelected: boolean }) => {
</Flex> </Flex>
<Text <Text
sx={{ sx={{
color: isSelected ? 'base.50' : 'base.200', color: isSelected
? mode('base.900', 'base.50')(colorMode)
: mode('base.700', 'base.200')(colorMode),
fontWeight: isSelected ? 600 : undefined, fontWeight: isSelected ? 600 : undefined,
fontSize: 'xs', fontSize: 'xs',
}} }}

View File

@ -62,13 +62,13 @@ const BoardsList = (props: Props) => {
return ( return (
<Collapse in={isOpen} animateOpacity> <Collapse in={isOpen} animateOpacity>
<Flex <Flex
layerStyle={'first'}
sx={{ sx={{
flexDir: 'column', flexDir: 'column',
gap: 2, gap: 2,
bg: 'base.800',
borderRadius: 'base',
p: 2, p: 2,
mt: 2, mt: 2,
borderRadius: 'base',
}} }}
> >
<Flex sx={{ gap: 2, alignItems: 'center' }}> <Flex sx={{ gap: 2, alignItems: 'center' }}>

View File

@ -0,0 +1,114 @@
import {
AlertDialog,
AlertDialogBody,
AlertDialogContent,
AlertDialogFooter,
AlertDialogHeader,
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 (
<>
<Text>
An image from this board is currently in use in the following features:
</Text>
<UnorderedList sx={{ paddingInlineStart: 6 }}>
{imagesUsage.isInitialImage && <ListItem>Image to Image</ListItem>}
{imagesUsage.isCanvasImage && <ListItem>Unified Canvas</ListItem>}
{imagesUsage.isControlNetImage && <ListItem>ControlNet</ListItem>}
{imagesUsage.isNodesImage && <ListItem>Node Editor</ListItem>}
</UnorderedList>
<Text>
If you delete images from this board, those features will immediately be
reset.
</Text>
</>
);
};
const DeleteBoardImagesModal = () => {
const { t } = useTranslation();
const {
isOpen,
onClose,
board,
handleDeleteBoardImages,
handleDeleteBoardOnly,
imagesUsage,
} = useContext(DeleteBoardImagesContext);
const cancelRef = useRef<HTMLButtonElement>(null);
return (
<AlertDialog
isOpen={isOpen}
leastDestructiveRef={cancelRef}
onClose={onClose}
isCentered
>
<AlertDialogOverlay>
{board && (
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
Delete Board
</AlertDialogHeader>
<AlertDialogBody>
<Flex direction="column" gap={3}>
<BoardImageInUseMessage imagesUsage={imagesUsage} />
<Divider />
<Text>{t('common.areYouSure')}</Text>
<Text fontWeight="bold">
This board has {board.image_count} image(s) that will be
deleted.
</Text>
</Flex>
</AlertDialogBody>
<AlertDialogFooter gap={3}>
<IAIButton ref={cancelRef} onClick={onClose}>
Cancel
</IAIButton>
<IAIButton
colorScheme="warning"
onClick={() => handleDeleteBoardOnly(board.board_id)}
>
Delete Board Only
</IAIButton>
<IAIButton
colorScheme="error"
onClick={() => handleDeleteBoardImages(board.board_id)}
>
Delete Board and Images
</IAIButton>
</AlertDialogFooter>
</AlertDialogContent>
)}
</AlertDialogOverlay>
</AlertDialog>
);
};
export default memo(DeleteBoardImagesModal);

View File

@ -8,10 +8,11 @@ import {
Image, Image,
MenuItem, MenuItem,
MenuList, MenuList,
useColorMode,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks'; 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 { FaFolder, FaTrash } from 'react-icons/fa';
import { ContextMenu } from 'chakra-ui-contextmenu'; import { ContextMenu } from 'chakra-ui-contextmenu';
import { BoardDTO, ImageDTO } from 'services/api/types'; import { BoardDTO, ImageDTO } from 'services/api/types';
@ -29,6 +30,8 @@ import { useDroppable } from '@dnd-kit/core';
import { AnimatePresence } from 'framer-motion'; import { AnimatePresence } from 'framer-motion';
import IAIDropOverlay from 'common/components/IAIDropOverlay'; import IAIDropOverlay from 'common/components/IAIDropOverlay';
import { SelectedItemOverlay } from '../SelectedItemOverlay'; import { SelectedItemOverlay } from '../SelectedItemOverlay';
import { DeleteBoardImagesContext } from '../../../../app/contexts/DeleteBoardImagesContext';
import { mode } from 'theme/util/mode';
interface HoverableBoardProps { interface HoverableBoardProps {
board: BoardDTO; board: BoardDTO;
@ -42,8 +45,12 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
board.cover_image_name ?? skipToken board.cover_image_name ?? skipToken
); );
const { colorMode } = useColorMode();
const { board_name, board_id } = board; const { board_name, board_id } = board;
const { onClickDeleteBoardImages } = useContext(DeleteBoardImagesContext);
const handleSelectBoard = useCallback(() => { const handleSelectBoard = useCallback(() => {
dispatch(boardIdSelected(board_id)); dispatch(boardIdSelected(board_id));
}, [board_id, dispatch]); }, [board_id, dispatch]);
@ -65,6 +72,11 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
deleteBoard(board_id); deleteBoard(board_id);
}, [board_id, deleteBoard]); }, [board_id, deleteBoard]);
const handleDeleteBoardAndImages = useCallback(() => {
console.log({ board });
onClickDeleteBoardImages(board);
}, [board, onClickDeleteBoardImages]);
const handleDrop = useCallback( const handleDrop = useCallback(
(droppedImage: ImageDTO) => { (droppedImage: ImageDTO) => {
if (droppedImage.board_id === board_id) { if (droppedImage.board_id === board_id) {
@ -92,9 +104,18 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
menuProps={{ size: 'sm', isLazy: true }} menuProps={{ size: 'sm', isLazy: true }}
renderMenu={() => ( renderMenu={() => (
<MenuList sx={{ visibility: 'visible !important' }}> <MenuList sx={{ visibility: 'visible !important' }}>
{board.image_count > 0 && (
<MenuItem <MenuItem
sx={{ color: 'error.300' }} sx={{ color: 'error.300' }}
icon={<FaTrash />} icon={<FaTrash />}
onClickCapture={handleDeleteBoardAndImages}
>
Delete Board and Images
</MenuItem>
)}
<MenuItem
sx={{ color: mode('error.700', 'error.300')(colorMode) }}
icon={<FaTrash />}
onClickCapture={handleDeleteBoard} onClickCapture={handleDeleteBoard}
> >
Delete Board Delete Board
@ -163,7 +184,9 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
> >
<EditablePreview <EditablePreview
sx={{ sx={{
color: isSelected ? 'base.50' : 'base.200', color: isSelected
? mode('base.900', 'base.50')(colorMode)
: mode('base.700', 'base.200')(colorMode),
fontWeight: isSelected ? 600 : undefined, fontWeight: isSelected ? 600 : undefined,
fontSize: 'xs', fontSize: 'xs',
textAlign: 'center', textAlign: 'center',
@ -173,9 +196,9 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
/> />
<EditableInput <EditableInput
sx={{ sx={{
color: 'base.50', color: mode('base.900', 'base.50')(colorMode),
fontSize: 'xs', fontSize: 'xs',
borderColor: 'base.500', borderColor: mode('base.500', 'base.500')(colorMode),
p: 0, p: 0,
outline: 0, outline: 0,
}} }}

View File

@ -32,7 +32,6 @@ const CurrentImageDisplay = () => {
height: '100%', height: '100%',
width: '100%', width: '100%',
rowGap: 4, rowGap: 4,
borderRadius: 'base',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}} }}

View File

@ -120,7 +120,7 @@ const GalleryDrawer = () => {
isResizable={true} isResizable={true}
isOpen={shouldShowGallery} isOpen={shouldShowGallery}
onClose={handleCloseGallery} onClose={handleCloseGallery}
minWidth={200} minWidth={337}
> >
<ImageGalleryContent /> <ImageGalleryContent />
</ResizableDrawer> </ResizableDrawer>

View File

@ -9,6 +9,7 @@ import {
Text, Text,
VStack, VStack,
forwardRef, forwardRef,
useColorMode,
useDisclosure, useDisclosure,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
@ -61,6 +62,7 @@ import BoardsList from './Boards/BoardsList';
import { boardsSelector } from '../store/boardSlice'; import { boardsSelector } from '../store/boardSlice';
import { ChevronUpIcon } from '@chakra-ui/icons'; import { ChevronUpIcon } from '@chakra-ui/icons';
import { useListAllBoardsQuery } from 'services/api/endpoints/boards'; import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
import { mode } from 'theme/util/mode';
const itemSelector = createSelector( const itemSelector = createSelector(
[(state: RootState) => state], [(state: RootState) => state],
@ -135,6 +137,8 @@ const ImageGalleryContent = () => {
}, },
}); });
const { colorMode } = useColorMode();
const { const {
shouldPinGallery, shouldPinGallery,
galleryImageMinimumWidth, galleryImageMinimumWidth,
@ -267,13 +271,17 @@ const ImageGalleryContent = () => {
alignItems: 'center', alignItems: 'center',
px: 2, px: 2,
_hover: { _hover: {
bg: 'base.800', bg: mode('base.100', 'base.800')(colorMode),
}, },
}} }}
> >
<Text <Text
noOfLines={1} noOfLines={1}
sx={{ w: 'full', color: 'base.200', fontWeight: 600 }} sx={{
w: 'full',
color: mode('base.800', 'base.200')(colorMode),
fontWeight: 600,
}}
> >
{selectedBoard ? selectedBoard.board_name : 'All Images'} {selectedBoard ? selectedBoard.board_name : 'All Images'}
</Text> </Text>

View File

@ -1,6 +1,16 @@
import { useColorMode, useToken } from '@chakra-ui/react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { mode } from 'theme/util/mode';
export const SelectedItemOverlay = () => ( export const SelectedItemOverlay = () => {
const [accent400, accent500] = useToken('colors', [
'accent.400',
'accent.500',
]);
const { colorMode } = useColorMode();
return (
<motion.div <motion.div
initial={{ initial={{
opacity: 0, opacity: 0,
@ -19,8 +29,12 @@ export const SelectedItemOverlay = () => (
insetInlineStart: 0, insetInlineStart: 0,
width: '100%', width: '100%',
height: '100%', height: '100%',
boxShadow: 'inset 0px 0px 0px 2px var(--invokeai-colors-accent-300)', boxShadow: `inset 0px 0px 0px 2px ${mode(
accent400,
accent500
)(colorMode)}`,
borderRadius: 'var(--invokeai-radii-base)', borderRadius: 'var(--invokeai-radii-base)',
}} }}
/> />
); );
};

View File

@ -1,6 +1,6 @@
import { createAction } from '@reduxjs/toolkit'; import { createAction } from '@reduxjs/toolkit';
import { ImageUsage } from 'app/contexts/DeleteImageContext'; import { ImageUsage } from 'app/contexts/DeleteImageContext';
import { ImageDTO } from 'services/api/types'; import { ImageDTO, BoardDTO } from 'services/api/types';
export type RequestedImageDeletionArg = { export type RequestedImageDeletionArg = {
image: ImageDTO; image: ImageDTO;
@ -11,6 +11,16 @@ export const requestedImageDeletion = createAction<RequestedImageDeletionArg>(
'gallery/requestedImageDeletion' 'gallery/requestedImageDeletion'
); );
export type RequestedBoardImagesDeletionArg = {
board: BoardDTO;
imagesUsage: ImageUsage;
};
export const requestedBoardImagesDeletion =
createAction<RequestedBoardImagesDeletionArg>(
'gallery/requestedBoardImagesDeletion'
);
export const sentImageToCanvas = createAction('gallery/sentImageToCanvas'); export const sentImageToCanvas = createAction('gallery/sentImageToCanvas');
export const sentImageToImg2Img = createAction('gallery/sentImageToImg2Img'); export const sentImageToImg2Img = createAction('gallery/sentImageToImg2Img');

View File

@ -60,6 +60,9 @@ const imagesSlice = createSlice({
imageRemoved: (state, action: PayloadAction<string>) => { imageRemoved: (state, action: PayloadAction<string>) => {
imagesAdapter.removeOne(state, action.payload); imagesAdapter.removeOne(state, action.payload);
}, },
imagesRemoved: (state, action: PayloadAction<string[]>) => {
imagesAdapter.removeMany(state, action.payload);
},
imageCategoriesChanged: (state, action: PayloadAction<ImageCategory[]>) => { imageCategoriesChanged: (state, action: PayloadAction<ImageCategory[]>) => {
state.categories = action.payload; state.categories = action.payload;
}, },
@ -117,6 +120,7 @@ export const {
imageUpserted, imageUpserted,
imageUpdatedOne, imageUpdatedOne,
imageRemoved, imageRemoved,
imagesRemoved,
imageCategoriesChanged, imageCategoriesChanged,
} = imagesSlice.actions; } = imagesSlice.actions;

View File

@ -12,15 +12,25 @@ const IAINodeHeader = (props: IAINodeHeaderProps) => {
const { nodeId, template } = props; const { nodeId, template } = props;
return ( return (
<Flex <Flex
borderTopRadius="md" sx={{
justifyContent="space-between" borderTopRadius: 'md',
background="base.700" alignItems: 'center',
px={2} justifyContent: 'space-between',
py={1} px: 2,
alignItems="center" py: 1,
bg: 'base.300',
_dark: { bg: 'base.700' },
}}
> >
<Tooltip label={nodeId}> <Tooltip label={nodeId}>
<Heading size="xs" fontWeight={600} color="base.100"> <Heading
size="xs"
sx={{
fontWeight: 600,
color: 'base.900',
_dark: { color: 'base.100' },
}}
>
{template.title} {template.title}
</Heading> </Heading>
</Tooltip> </Tooltip>
@ -30,7 +40,16 @@ const IAINodeHeader = (props: IAINodeHeaderProps) => {
hasArrow hasArrow
shouldWrapChildren shouldWrapChildren
> >
<Icon color="base.300" as={FaInfoCircle} h="min-content" /> <Icon
sx={{
h: 'min-content',
color: 'base.700',
_dark: {
color: 'base.300',
},
}}
as={FaInfoCircle}
/>
</Tooltip> </Tooltip>
</Flex> </Flex>
); );

View File

@ -72,7 +72,14 @@ export const InvocationComponent = memo((props: NodeProps<InvocationValue>) => {
return ( return (
<InvocationComponentWrapper selected={selected}> <InvocationComponentWrapper selected={selected}>
<Flex sx={{ alignItems: 'center', justifyContent: 'center' }}> <Flex sx={{ alignItems: 'center', justifyContent: 'center' }}>
<Icon color="base.400" boxSize={32} as={FaExclamationCircle}></Icon> <Icon
as={FaExclamationCircle}
sx={{
boxSize: 32,
color: 'base.600',
_dark: { color: 'base.400' },
}}
></Icon>
<IAINodeResizer /> <IAINodeResizer />
</Flex> </Flex>
</InvocationComponentWrapper> </InvocationComponentWrapper>
@ -86,8 +93,9 @@ export const InvocationComponent = memo((props: NodeProps<InvocationValue>) => {
sx={{ sx={{
flexDirection: 'column', flexDirection: 'column',
borderBottomRadius: 'md', borderBottomRadius: 'md',
bg: 'base.800',
py: 2, py: 2,
bg: 'base.200',
_dark: { bg: 'base.800' },
}} }}
> >
<IAINodeOutputs nodeId={nodeId} outputs={outputs} template={template} /> <IAINodeOutputs nodeId={nodeId} outputs={outputs} template={template} />

View File

@ -8,12 +8,12 @@ import { memo } from 'react';
const NodeEditor = () => { const NodeEditor = () => {
return ( return (
<Box <Box
layerStyle={'first'}
sx={{ sx={{
position: 'relative', position: 'relative',
width: 'full', width: 'full',
height: 'full', height: 'full',
borderRadius: 'md', borderRadius: 'base',
bg: 'base.850',
}} }}
> >
<ReactFlowProvider> <ReactFlowProvider>

View File

@ -11,17 +11,20 @@ const NodeGraphOverlay = () => {
return ( return (
<Box <Box
as="pre" as="pre"
fontFamily="monospace" sx={{
position="absolute" fontFamily: 'monospace',
top={2} position: 'absolute',
right={2} top: 2,
opacity={0.7} right: 2,
background="base.800" opacity: 0.7,
p={2} p: 2,
maxHeight={500} maxHeight: 500,
maxWidth={500} maxWidth: 500,
overflowY="scroll" overflowY: 'scroll',
borderRadius="md" borderRadius: 'base',
bg: 'base.200',
_dark: { bg: 'base.800' },
}}
> >
{JSON.stringify(graph, null, 2)} {JSON.stringify(graph, null, 2)}
</Box> </Box>

View File

@ -1,15 +1,25 @@
import { RootState } from 'app/store/store'; import { useColorModeValue } from '@chakra-ui/react';
import { useAppSelector } from 'app/store/storeHooks'; import { memo } from 'react';
import { CSSProperties, memo } from 'react';
import { MiniMap } from 'reactflow'; import { MiniMap } from 'reactflow';
const MinimapStyle: CSSProperties = {
background: 'var(--invokeai-colors-base-500)',
};
const MinimapPanel = () => { const MinimapPanel = () => {
const currentTheme = useAppSelector( const miniMapStyle = useColorModeValue(
(state: RootState) => state.ui.currentTheme {
background: 'var(--invokeai-colors-base-200)',
},
{
background: 'var(--invokeai-colors-base-500)',
}
);
const nodeColor = useColorModeValue(
'var(--invokeai-colors-accent-300)',
'var(--invokeai-colors-accent-700)'
);
const maskColor = useColorModeValue(
'var(--invokeai-colors-blackAlpha-300)',
'var(--invokeai-colors-blackAlpha-600)'
); );
return ( return (
@ -18,15 +28,9 @@ const MinimapPanel = () => {
pannable pannable
zoomable zoomable
nodeBorderRadius={30} nodeBorderRadius={30}
style={MinimapStyle} style={miniMapStyle}
nodeColor={ nodeColor={nodeColor}
currentTheme === 'light' maskColor={maskColor}
? 'var(--invokeai-colors-accent-700)'
: currentTheme === 'green'
? 'var(--invokeai-colors-accent-600)'
: 'var(--invokeai-colors-accent-700)'
}
maskColor="var(--invokeai-colors-base-700)"
/> />
); );
}; };

View File

@ -333,7 +333,7 @@ export type TypeHints = {
}; };
export type InvocationSchemaExtra = { export type InvocationSchemaExtra = {
output: OpenAPIV3.SchemaObject; // the output of the invocation output: OpenAPIV3.ReferenceObject; // the output of the invocation
ui?: { ui?: {
tags?: string[]; tags?: string[];
type_hints?: TypeHints; type_hints?: TypeHints;

View File

@ -349,11 +349,21 @@ export const getFieldType = (
if (typeHints && name in typeHints) { if (typeHints && name in typeHints) {
rawFieldType = typeHints[name]; rawFieldType = typeHints[name];
} else if (!schemaObject.type && schemaObject.allOf) { } else if (!schemaObject.type) {
// if schemaObject has no type, then it should have one of allOf // if schemaObject has no type, then it should have one of allOf, anyOf, oneOf
rawFieldType = if (schemaObject.allOf) {
(schemaObject.allOf[0] as OpenAPIV3.SchemaObject).title ?? rawFieldType = refObjectToFieldType(
'Missing Field Type'; schemaObject.allOf![0] as OpenAPIV3.ReferenceObject
);
} else if (schemaObject.anyOf) {
rawFieldType = refObjectToFieldType(
schemaObject.anyOf![0] as OpenAPIV3.ReferenceObject
);
} else if (schemaObject.oneOf) {
rawFieldType = refObjectToFieldType(
schemaObject.oneOf![0] as OpenAPIV3.ReferenceObject
);
}
} else if (schemaObject.enum) { } else if (schemaObject.enum) {
rawFieldType = 'enum'; rawFieldType = 'enum';
} else if (schemaObject.type) { } else if (schemaObject.type) {

View File

@ -5,48 +5,33 @@ import {
InputFieldTemplate, InputFieldTemplate,
InvocationSchemaObject, InvocationSchemaObject,
InvocationTemplate, InvocationTemplate,
isInvocationSchemaObject,
OutputFieldTemplate, OutputFieldTemplate,
} from '../types/types'; } from '../types/types';
import { buildInputFieldTemplate, getFieldType } from './fieldTemplateBuilders'; import {
import { O } from 'ts-toolbelt'; buildInputFieldTemplate,
buildOutputFieldTemplates,
// recursively exclude all properties of type U from T } from './fieldTemplateBuilders';
type DeepExclude<T, U> = T extends U
? never
: T extends object
? {
[K in keyof T]: DeepExclude<T[K], U>;
}
: T;
// The schema from swagger-parser is dereferenced, and we know `components` and `components.schemas` exist
type DereferencedOpenAPIDocument = DeepExclude<
O.Required<OpenAPIV3.Document, 'schemas' | 'components', 'deep'>,
OpenAPIV3.ReferenceObject
>;
const RESERVED_FIELD_NAMES = ['id', 'type', 'is_intermediate']; const RESERVED_FIELD_NAMES = ['id', 'type', 'is_intermediate'];
const invocationDenylist = ['Graph', 'InvocationMeta']; const invocationDenylist = ['Graph', 'InvocationMeta'];
const nodeFilter = ( export const parseSchema = (openAPI: OpenAPIV3.Document) => {
schema: DereferencedOpenAPIDocument['components']['schemas'][string], // filter out non-invocation schemas, plus some tricky invocations for now
key: string const filteredSchemas = filter(
) => openAPI.components!.schemas,
(schema, key) =>
key.includes('Invocation') && key.includes('Invocation') &&
!key.includes('InvocationOutput') && !key.includes('InvocationOutput') &&
!invocationDenylist.some((denylistItem) => key.includes(denylistItem)); !invocationDenylist.some((denylistItem) => key.includes(denylistItem))
) as (OpenAPIV3.ReferenceObject | InvocationSchemaObject)[];
export const parseSchema = (openAPI: DereferencedOpenAPIDocument) => {
// filter out non-invocation schemas, plus some tricky invocations for now
const filteredSchemas = filter(openAPI.components.schemas, nodeFilter);
const invocations = filteredSchemas.reduce< const invocations = filteredSchemas.reduce<
Record<string, InvocationTemplate> Record<string, InvocationTemplate>
>((acc, s) => { >((acc, schema) => {
// cast to InvocationSchemaObject, we know the shape // only want SchemaObjects
const schema = s as InvocationSchemaObject; if (isInvocationSchemaObject(schema)) {
const type = schema.properties.type.default; const type = schema.properties.type.default;
const title = schema.ui?.title ?? schema.title.replace('Invocation', ''); const title = schema.ui?.title ?? schema.title.replace('Invocation', '');
@ -56,8 +41,10 @@ export const parseSchema = (openAPI: DereferencedOpenAPIDocument) => {
const inputs: Record<string, InputFieldTemplate> = {}; const inputs: Record<string, InputFieldTemplate> = {};
if (type === 'collect') { if (type === 'collect') {
// Special handling for the Collect node const itemProperty = schema.properties[
const itemProperty = schema.properties['item'] as InvocationSchemaObject; 'item'
] as InvocationSchemaObject;
// Handle the special Collect node
inputs.item = { inputs.item = {
type: 'item', type: 'item',
name: 'item', name: 'item',
@ -68,7 +55,6 @@ export const parseSchema = (openAPI: DereferencedOpenAPIDocument) => {
default: undefined, default: undefined,
}; };
} else if (type === 'iterate') { } else if (type === 'iterate') {
// Special handling for the Iterate node
const itemProperty = schema.properties[ const itemProperty = schema.properties[
'collection' 'collection'
] as InvocationSchemaObject; ] as InvocationSchemaObject;
@ -105,12 +91,16 @@ export const parseSchema = (openAPI: DereferencedOpenAPIDocument) => {
); );
} }
const rawOutput = (schema as InvocationSchemaObject).output;
let outputs: Record<string, OutputFieldTemplate>; let outputs: Record<string, OutputFieldTemplate>;
// some special handling is needed for collect, iterate and range nodes
if (type === 'iterate') { if (type === 'iterate') {
// Special handling for the Iterate node output // this is guaranteed to be a SchemaObject
const iterationOutput = const iterationOutput = openAPI.components!.schemas![
openAPI.components.schemas['IterateInvocationOutput']; 'IterateInvocationOutput'
] as OpenAPIV3.SchemaObject;
outputs = { outputs = {
item: { item: {
@ -121,25 +111,7 @@ export const parseSchema = (openAPI: DereferencedOpenAPIDocument) => {
}, },
}; };
} else { } else {
// All other node outputs outputs = buildOutputFieldTemplates(rawOutput, openAPI, typeHints);
outputs = reduce(
schema.output.properties as OpenAPIV3.SchemaObject,
(outputsAccumulator, property, propertyName) => {
if (!['type', 'id'].includes(propertyName)) {
const fieldType = getFieldType(property, propertyName, typeHints);
outputsAccumulator[propertyName] = {
name: propertyName,
title: property.title ?? '',
description: property.description ?? '',
type: fieldType,
};
}
return outputsAccumulator;
},
{} as Record<string, OutputFieldTemplate>
);
} }
const invocation: InvocationTemplate = { const invocation: InvocationTemplate = {
@ -152,6 +124,7 @@ export const parseSchema = (openAPI: DereferencedOpenAPIDocument) => {
}; };
Object.assign(acc, { [type]: invocation }); Object.assign(acc, { [type]: invocation });
}
return acc; return acc;
}, {}); }, {});

View File

@ -4,17 +4,17 @@ import InitialImagePreview from './InitialImagePreview';
const InitialImageDisplay = () => { const InitialImageDisplay = () => {
return ( return (
<Flex <Flex
layerStyle={'first'}
sx={{ sx={{
position: 'relative', position: 'relative',
flexDirection: 'column', flexDirection: 'column',
height: '100%', height: '100%',
width: '100%', width: '100%',
rowGap: 4, rowGap: 4,
borderRadius: 'base',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
bg: 'base.850',
p: 4, p: 4,
borderRadius: 'base',
}} }}
> >
<Flex <Flex

View File

@ -1,33 +1,32 @@
import {
ButtonGroup,
ButtonProps,
ButtonSpinner,
Menu,
MenuButton,
MenuItemOption,
MenuList,
MenuOptionGroup,
} from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIIconButton, { import IAIIconButton from 'common/components/IAIIconButton';
IAIIconButtonProps,
} from 'common/components/IAIIconButton';
import { systemSelector } from 'features/system/store/systemSelectors'; import { systemSelector } from 'features/system/store/systemSelectors';
import { import {
CancelStrategy,
SystemState, SystemState,
cancelScheduled, cancelScheduled,
cancelTypeChanged, cancelTypeChanged,
CancelStrategy,
} from 'features/system/store/systemSlice'; } from 'features/system/store/systemSlice';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { useCallback, memo, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import {
ButtonSpinner,
ButtonGroup,
Menu,
MenuButton,
MenuList,
MenuOptionGroup,
MenuItemOption,
} from '@chakra-ui/react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { MdCancel, MdCancelScheduleSend } from 'react-icons/md'; import { MdCancel, MdCancelScheduleSend } from 'react-icons/md';
import { sessionCanceled } from 'services/api/thunks/session';
import { ChevronDownIcon } from '@chakra-ui/icons'; import { ChevronDownIcon } from '@chakra-ui/icons';
import { sessionCanceled } from 'services/api/thunks/session';
const cancelButtonSelector = createSelector( const cancelButtonSelector = createSelector(
systemSelector, systemSelector,
@ -55,7 +54,7 @@ interface CancelButtonProps {
} }
const CancelButton = ( const CancelButton = (
props: CancelButtonProps & Omit<IAIIconButtonProps, 'aria-label'> props: CancelButtonProps & Omit<ButtonProps, 'aria-label'>
) => { ) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { btnGroupWidth = 'auto', ...rest } = props; const { btnGroupWidth = 'auto', ...rest } = props;
@ -145,6 +144,7 @@ const CancelButton = (
paddingY={0} paddingY={0}
colorScheme="error" colorScheme="error"
minWidth={5} minWidth={5}
{...rest}
/> />
<MenuList minWidth="240px"> <MenuList minWidth="240px">
<MenuOptionGroup <MenuOptionGroup

View File

@ -71,7 +71,7 @@ export default function InvokeButton(props: InvokeButton) {
flexGrow={1} flexGrow={1}
w="100%" w="100%"
tooltip={t('parameters.invoke')} tooltip={t('parameters.invoke')}
tooltipProps={{ placement: 'bottom' }} tooltipProps={{ placement: 'top' }}
colorScheme="accent" colorScheme="accent"
id="invoke-button" id="invoke-button"
{...rest} {...rest}

View File

@ -0,0 +1,32 @@
import { useColorMode } from '@chakra-ui/react';
import IAIIconButton from 'common/components/IAIIconButton';
import { useTranslation } from 'react-i18next';
import { FaMoon, FaSun } from 'react-icons/fa';
const ColorModeButton = () => {
const { colorMode, toggleColorMode } = useColorMode();
const { t } = useTranslation();
return (
<IAIIconButton
aria-label={
colorMode === 'dark' ? t('common.lightMode') : t('common.darkMode')
}
tooltip={
colorMode === 'dark' ? t('common.lightMode') : t('common.darkMode')
}
size="sm"
icon={
colorMode === 'dark' ? (
<FaSun fontSize={19} />
) : (
<FaMoon fontSize={18} />
)
}
onClick={toggleColorMode}
variant="link"
/>
);
};
export default ColorModeButton;

View File

@ -1,5 +1,4 @@
import { import {
ChakraProps,
Flex, Flex,
Heading, Heading,
Modal, Modal,
@ -39,6 +38,7 @@ import { UIState } from 'features/ui/store/uiTypes';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { import {
ChangeEvent, ChangeEvent,
PropsWithChildren,
ReactElement, ReactElement,
cloneElement, cloneElement,
useCallback, useCallback,
@ -83,14 +83,6 @@ const selector = createSelector(
} }
); );
const modalSectionStyles: ChakraProps['sx'] = {
flexDirection: 'column',
gap: 2,
p: 4,
bg: 'base.900',
borderRadius: 'base',
};
type ConfigOptions = { type ConfigOptions = {
shouldShowDeveloperSettings: boolean; shouldShowDeveloperSettings: boolean;
shouldShowResetWebUiText: boolean; shouldShowResetWebUiText: boolean;
@ -183,12 +175,12 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
isCentered isCentered
> >
<ModalOverlay /> <ModalOverlay />
<ModalContent paddingInlineEnd={4}> <ModalContent>
<ModalHeader>{t('common.settingsLabel')}</ModalHeader> <ModalHeader>{t('common.settingsLabel')}</ModalHeader>
<ModalCloseButton /> <ModalCloseButton />
<ModalBody> <ModalBody>
<Flex sx={{ gap: 4, flexDirection: 'column' }}> <Flex sx={{ gap: 4, flexDirection: 'column' }}>
<Flex sx={modalSectionStyles}> <StyledFlex>
<Heading size="sm">{t('settings.general')}</Heading> <Heading size="sm">{t('settings.general')}</Heading>
<IAISwitch <IAISwitch
label={t('settings.confirmOnDelete')} label={t('settings.confirmOnDelete')}
@ -197,14 +189,14 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
dispatch(setShouldConfirmOnDelete(e.target.checked)) dispatch(setShouldConfirmOnDelete(e.target.checked))
} }
/> />
</Flex> </StyledFlex>
<Flex sx={modalSectionStyles}> <StyledFlex>
<Heading size="sm">{t('settings.generation')}</Heading> <Heading size="sm">{t('settings.generation')}</Heading>
<SettingsSchedulers /> <SettingsSchedulers />
</Flex> </StyledFlex>
<Flex sx={modalSectionStyles}> <StyledFlex>
<Heading size="sm">{t('settings.ui')}</Heading> <Heading size="sm">{t('settings.ui')}</Heading>
<IAISwitch <IAISwitch
label={t('settings.displayHelpIcons')} label={t('settings.displayHelpIcons')}
@ -245,10 +237,10 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
) )
} }
/> />
</Flex> </StyledFlex>
{shouldShowDeveloperSettings && ( {shouldShowDeveloperSettings && (
<Flex sx={modalSectionStyles}> <StyledFlex>
<Heading size="sm">{t('settings.developer')}</Heading> <Heading size="sm">{t('settings.developer')}</Heading>
<IAISwitch <IAISwitch
label={t('settings.shouldLogToConsole')} label={t('settings.shouldLogToConsole')}
@ -269,10 +261,10 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
dispatch(setEnableImageDebugging(e.target.checked)) dispatch(setEnableImageDebugging(e.target.checked))
} }
/> />
</Flex> </StyledFlex>
)} )}
<Flex sx={modalSectionStyles}> <StyledFlex>
<Heading size="sm">{t('settings.resetWebUI')}</Heading> <Heading size="sm">{t('settings.resetWebUI')}</Heading>
<IAIButton colorScheme="error" onClick={handleClickResetWebUI}> <IAIButton colorScheme="error" onClick={handleClickResetWebUI}>
{t('settings.resetWebUI')} {t('settings.resetWebUI')}
@ -283,7 +275,7 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
<Text>{t('settings.resetWebUIDesc2')}</Text> <Text>{t('settings.resetWebUIDesc2')}</Text>
</> </>
)} )}
</Flex> </StyledFlex>
</Flex> </Flex>
</ModalBody> </ModalBody>
@ -319,3 +311,19 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
}; };
export default SettingsModal; export default SettingsModal;
const StyledFlex = (props: PropsWithChildren) => {
return (
<Flex
layerStyle="second"
sx={{
flexDirection: 'column',
gap: 2,
p: 4,
borderRadius: 'base',
}}
>
{props.children}
</Flex>
);
};

View File

@ -12,8 +12,8 @@ import InvokeAILogoComponent from './InvokeAILogoComponent';
import LanguagePicker from './LanguagePicker'; import LanguagePicker from './LanguagePicker';
import ModelManagerModal from './ModelManager/ModelManagerModal'; import ModelManagerModal from './ModelManager/ModelManagerModal';
import SettingsModal from './SettingsModal/SettingsModal'; import SettingsModal from './SettingsModal/SettingsModal';
import ThemeChanger from './ThemeChanger';
import { useFeatureStatus } from '../hooks/useFeatureStatus'; import { useFeatureStatus } from '../hooks/useFeatureStatus';
import ColorModeButton from './ColorModeButton';
const SiteHeader = () => { const SiteHeader = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -63,8 +63,6 @@ const SiteHeader = () => {
/> />
</HotkeysModal> </HotkeysModal>
<ThemeChanger />
{isLocalizationEnabled && <LanguagePicker />} {isLocalizationEnabled && <LanguagePicker />}
{isBugLinkEnabled && ( {isBugLinkEnabled && (
@ -121,6 +119,8 @@ const SiteHeader = () => {
</Link> </Link>
)} )}
<ColorModeButton />
<SettingsModal> <SettingsModal>
<IAIIconButton <IAIIconButton
aria-label={t('common.settingsLabel')} aria-label={t('common.settingsLabel')}

View File

@ -1,12 +1,12 @@
import { Flex, Link } from '@chakra-ui/react'; import { Flex, Link } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaCube, FaKeyboard, FaBug, FaGithub, FaDiscord } from 'react-icons/fa'; import { FaBug, FaCube, FaDiscord, FaGithub, FaKeyboard } from 'react-icons/fa';
import { MdSettings } from 'react-icons/md'; import { MdSettings } from 'react-icons/md';
import HotkeysModal from './HotkeysModal/HotkeysModal'; import HotkeysModal from './HotkeysModal/HotkeysModal';
import LanguagePicker from './LanguagePicker'; import LanguagePicker from './LanguagePicker';
import ModelManagerModal from './ModelManager/ModelManagerModal'; import ModelManagerModal from './ModelManager/ModelManagerModal';
import SettingsModal from './SettingsModal/SettingsModal'; import SettingsModal from './SettingsModal/SettingsModal';
import ThemeChanger from './ThemeChanger';
import IAIIconButton from 'common/components/IAIIconButton'; import IAIIconButton from 'common/components/IAIIconButton';
import { useFeatureStatus } from '../hooks/useFeatureStatus'; import { useFeatureStatus } from '../hooks/useFeatureStatus';
@ -53,8 +53,6 @@ const SiteHeaderMenu = () => {
/> />
</HotkeysModal> </HotkeysModal>
<ThemeChanger />
{isLocalizationEnabled && <LanguagePicker />} {isLocalizationEnabled && <LanguagePicker />}
{isBugLinkEnabled && ( {isBugLinkEnabled && (

View File

@ -35,6 +35,18 @@ const statusIndicatorSelector = createSelector(
defaultSelectorOptions defaultSelectorOptions
); );
const DARK_COLOR_MAP = {
ok: 'green.400',
working: 'yellow.400',
error: 'red.400',
};
const LIGHT_COLOR_MAP = {
ok: 'green.600',
working: 'yellow.500',
error: 'red.500',
};
const StatusIndicator = () => { const StatusIndicator = () => {
const { const {
isConnected, isConnected,
@ -46,7 +58,7 @@ const StatusIndicator = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const ref = useRef(null); const ref = useRef(null);
const statusColorScheme = useMemo(() => { const statusString = useMemo(() => {
if (isProcessing) { if (isProcessing) {
return 'working'; return 'working';
} }
@ -90,9 +102,10 @@ const StatusIndicator = () => {
sx={{ sx={{
fontSize: 'sm', fontSize: 'sm',
fontWeight: '600', fontWeight: '600',
color: `${statusColorScheme}.400`,
pb: '1px', pb: '1px',
userSelect: 'none', userSelect: 'none',
color: LIGHT_COLOR_MAP[statusString],
_dark: { color: DARK_COLOR_MAP[statusString] },
}} }}
> >
{t(statusTranslationKey as ResourceKey)} {t(statusTranslationKey as ResourceKey)}
@ -101,7 +114,14 @@ const StatusIndicator = () => {
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
<Icon as={FaCircle} boxSize="0.5rem" color={`${statusColorScheme}.400`} /> <Icon
as={FaCircle}
sx={{
boxSize: '0.5rem',
color: LIGHT_COLOR_MAP[statusString],
_dark: { color: DARK_COLOR_MAP[statusString] },
}}
/>
</Flex> </Flex>
); );
}; };

View File

@ -1,60 +0,0 @@
import {
IconButton,
Menu,
MenuButton,
MenuItemOption,
MenuList,
MenuOptionGroup,
Tooltip,
} from '@chakra-ui/react';
import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { setCurrentTheme } from 'features/ui/store/uiSlice';
import i18n from 'i18n';
import { map } from 'lodash-es';
import { useTranslation } from 'react-i18next';
import { FaPalette } from 'react-icons/fa';
export const THEMES = {
dark: i18n.t('common.darkTheme'),
light: i18n.t('common.lightTheme'),
green: i18n.t('common.greenTheme'),
ocean: i18n.t('common.oceanTheme'),
};
export default function ThemeChanger() {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const currentTheme = useAppSelector(
(state: RootState) => state.ui.currentTheme
);
return (
<Menu closeOnSelect={false}>
<Tooltip label={t('common.themeLabel')} hasArrow>
<MenuButton
as={IconButton}
icon={<FaPalette />}
variant="link"
aria-label={t('common.themeLabel')}
fontSize={20}
minWidth={8}
/>
</Tooltip>
<MenuList>
<MenuOptionGroup value={currentTheme}>
{map(THEMES, (themeName, themeKey: keyof typeof THEMES) => (
<MenuItemOption
key={themeKey}
value={themeKey}
onClick={() => dispatch(setCurrentTheme(themeKey))}
>
{themeName}
</MenuItemOption>
))}
</MenuOptionGroup>
</MenuList>
</Menu>
);
}

View File

@ -51,6 +51,7 @@ const FloatingGalleryButton = () => {
w: 8, w: 8,
borderStartEndRadius: 0, borderStartEndRadius: 0,
borderEndEndRadius: 0, borderEndEndRadius: 0,
shadow: '2xl',
}} }}
> >
<MdPhotoLibrary /> <MdPhotoLibrary />

View File

@ -19,6 +19,7 @@ import { FaSlidersH } from 'react-icons/fa';
const floatingButtonStyles: ChakraProps['sx'] = { const floatingButtonStyles: ChakraProps['sx'] = {
borderStartStartRadius: 0, borderStartStartRadius: 0,
borderEndStartRadius: 0, borderEndStartRadius: 0,
shadow: '2xl',
}; };
export const floatingParametersPanelButtonSelector = createSelector( export const floatingParametersPanelButtonSelector = createSelector(

View File

@ -36,6 +36,7 @@ import { FaFont, FaImage } from 'react-icons/fa';
import ResizeHandle from './tabs/ResizeHandle'; import ResizeHandle from './tabs/ResizeHandle';
import ImageTab from './tabs/ImageToImage/ImageToImageTab'; import ImageTab from './tabs/ImageToImage/ImageToImageTab';
import AuxiliaryProgressIndicator from 'app/components/AuxiliaryProgressIndicator'; import AuxiliaryProgressIndicator from 'app/components/AuxiliaryProgressIndicator';
import { useMinimumPanelSize } from '../hooks/useMinimumPanelSize';
export interface InvokeTabInfo { export interface InvokeTabInfo {
id: InvokeTabName; id: InvokeTabName;
@ -78,6 +79,9 @@ const enabledTabsSelector = createSelector(
} }
); );
const MIN_GALLERY_WIDTH = 300;
const DEFAULT_GALLERY_PCT = 20;
const InvokeTabs = () => { const InvokeTabs = () => {
const activeTab = useAppSelector(activeTabIndexSelector); const activeTab = useAppSelector(activeTabIndexSelector);
const activeTabName = useAppSelector(activeTabNameSelector); const activeTabName = useAppSelector(activeTabNameSelector);
@ -150,6 +154,9 @@ const InvokeTabs = () => {
[enabledTabs] [enabledTabs]
); );
const { ref: galleryPanelRef, minSizePct: galleryMinSizePct } =
useMinimumPanelSize(MIN_GALLERY_WIDTH, DEFAULT_GALLERY_PCT, 'app');
return ( return (
<Tabs <Tabs
defaultIndex={activeTab} defaultIndex={activeTab}
@ -175,6 +182,7 @@ const InvokeTabs = () => {
<AuxiliaryProgressIndicator /> <AuxiliaryProgressIndicator />
</TabList> </TabList>
<PanelGroup <PanelGroup
id="app"
autoSaveId="app" autoSaveId="app"
direction="horizontal" direction="horizontal"
style={{ height: '100%', width: '100%' }} style={{ height: '100%', width: '100%' }}
@ -188,11 +196,16 @@ const InvokeTabs = () => {
<> <>
<ResizeHandle /> <ResizeHandle />
<Panel <Panel
ref={galleryPanelRef}
onResize={handleResizeGallery} onResize={handleResizeGallery}
id="gallery" id="gallery"
order={3} order={3}
defaultSize={10} defaultSize={
minSize={10} galleryMinSizePct > DEFAULT_GALLERY_PCT
? galleryMinSizePct
: DEFAULT_GALLERY_PCT
}
minSize={galleryMinSizePct}
maxSize={50} maxSize={50}
> >
<ImageGalleryContent /> <ImageGalleryContent />

View File

@ -6,6 +6,7 @@ import {
useOutsideClick, useOutsideClick,
useTheme, useTheme,
SlideDirection, SlideDirection,
useColorMode,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { import {
Resizable, Resizable,
@ -21,6 +22,7 @@ import {
getSlideDirection, getSlideDirection,
getStyles, getStyles,
} from './util'; } from './util';
import { mode } from 'theme/util/mode';
type ResizableDrawerProps = ResizableProps & { type ResizableDrawerProps = ResizableProps & {
children: ReactNode; children: ReactNode;
@ -64,7 +66,7 @@ const ResizableDrawer = ({
sx = {}, sx = {},
}: ResizableDrawerProps) => { }: ResizableDrawerProps) => {
const langDirection = useTheme().direction as LangDirection; const langDirection = useTheme().direction as LangDirection;
const { colorMode } = useColorMode();
const outsideClickRef = useRef<HTMLDivElement>(null); const outsideClickRef = useRef<HTMLDivElement>(null);
const defaultWidth = useMemo( const defaultWidth = useMemo(
@ -160,11 +162,11 @@ const ResizableDrawer = ({
handleStyles={handleStyles} handleStyles={handleStyles}
{...minMaxDimensions} {...minMaxDimensions}
sx={{ sx={{
borderColor: 'base.800', borderColor: mode('base.200', 'base.800')(colorMode),
p: 4, p: 4,
bg: 'base.900', bg: mode('base.100', 'base.900')(colorMode),
height: 'full', height: 'full',
boxShadow: '0 0 4rem 0 rgba(0, 0, 0, 0.8)', shadow: isOpen ? 'dark-lg' : undefined,
...containerStyles, ...containerStyles,
...sx, ...sx,
}} }}

View File

@ -1,6 +1,7 @@
import { Box, Flex, FlexProps } from '@chakra-ui/react'; import { Box, Flex, FlexProps, useColorMode } from '@chakra-ui/react';
import { memo } from 'react'; import { memo } from 'react';
import { PanelResizeHandle } from 'react-resizable-panels'; import { PanelResizeHandle } from 'react-resizable-panels';
import { mode } from 'theme/util/mode';
type ResizeHandleProps = FlexProps & { type ResizeHandleProps = FlexProps & {
direction?: 'horizontal' | 'vertical'; direction?: 'horizontal' | 'vertical';
@ -8,6 +9,7 @@ type ResizeHandleProps = FlexProps & {
const ResizeHandle = (props: ResizeHandleProps) => { const ResizeHandle = (props: ResizeHandleProps) => {
const { direction = 'horizontal', ...rest } = props; const { direction = 'horizontal', ...rest } = props;
const { colorMode } = useColorMode();
if (direction === 'horizontal') { if (direction === 'horizontal') {
return ( return (
@ -21,7 +23,13 @@ const ResizeHandle = (props: ResizeHandleProps) => {
}} }}
{...rest} {...rest}
> >
<Box sx={{ w: 0.5, h: 'calc(100% - 4px)', bg: 'base.850' }} /> <Box
sx={{
w: 0.5,
h: 'calc(100% - 4px)',
bg: mode('base.100', 'base.850')(colorMode),
}}
/>
</Flex> </Flex>
</PanelResizeHandle> </PanelResizeHandle>
); );
@ -38,7 +46,13 @@ const ResizeHandle = (props: ResizeHandleProps) => {
}} }}
{...rest} {...rest}
> >
<Box sx={{ w: 'calc(100% - 4px)', h: 0.5, bg: 'base.850' }} /> <Box
sx={{
w: 'calc(100% - 4px)',
h: 0.5,
bg: mode('base.100', 'base.850')(colorMode),
}}
/>
</Flex> </Flex>
</PanelResizeHandle> </PanelResizeHandle>
); );

View File

@ -4,13 +4,13 @@ import CurrentImageDisplay from 'features/gallery/components/CurrentImageDisplay
const TextToImageTabMain = () => { const TextToImageTabMain = () => {
return ( return (
<Box <Box
layerStyle={'first'}
sx={{ sx={{
position: 'relative', position: 'relative',
width: '100%', width: '100%',
height: '100%', height: '100%',
borderRadius: 'base',
bg: 'base.850',
p: 4, p: 4,
borderRadius: 'base',
}} }}
> >
<Flex <Flex

View File

@ -67,14 +67,14 @@ const UnifiedCanvasContent = () => {
if (shouldUseCanvasBetaLayout) { if (shouldUseCanvasBetaLayout) {
return ( return (
<Box <Box
layerStyle="first"
ref={setDroppableRef} ref={setDroppableRef}
tabIndex={0} tabIndex={0}
sx={{ sx={{
w: 'full', w: 'full',
h: 'full', h: 'full',
borderRadius: 'base',
bg: 'base.850',
p: 4, p: 4,
borderRadius: 'base',
}} }}
> >
<Flex <Flex
@ -110,11 +110,11 @@ const UnifiedCanvasContent = () => {
ref={setDroppableRef} ref={setDroppableRef}
tabIndex={-1} tabIndex={-1}
sx={{ sx={{
layerStyle: 'first',
w: 'full', w: 'full',
h: 'full', h: 'full',
borderRadius: 'base',
bg: 'base.850',
p: 4, p: 4,
borderRadius: 'base',
}} }}
> >
<Flex <Flex

View File

@ -0,0 +1,70 @@
// adapted from https://github.com/bvaughn/react-resizable-panels/issues/141#issuecomment-1540048714
import {
RefObject,
useCallback,
useLayoutEffect,
useRef,
useState,
} from 'react';
import { ImperativePanelHandle } from 'react-resizable-panels';
export const useMinimumPanelSize = (
minSizePx: number,
defaultSizePct: number,
groupId: string,
orientation: 'horizontal' | 'vertical' = 'horizontal'
): { ref: RefObject<ImperativePanelHandle>; minSizePct: number } => {
const ref = useRef<ImperativePanelHandle>(null);
const [minSizePct, setMinSizePct] = useState(defaultSizePct);
const handleWindowResize = useCallback(() => {
const size = ref.current?.getSize();
if (size !== undefined && size < minSizePct) {
ref.current?.resize(minSizePct);
}
}, [minSizePct]);
useLayoutEffect(() => {
const panelGroup = document.querySelector(
`[data-panel-group-id="${groupId}"]`
);
const resizeHandles = document.querySelectorAll(
'[data-panel-resize-handle-id]'
);
if (!panelGroup) {
return;
}
const observer = new ResizeObserver(() => {
let dim =
orientation === 'horizontal'
? panelGroup.getBoundingClientRect().width
: panelGroup.getBoundingClientRect().height;
resizeHandles.forEach((resizeHandle) => {
dim -=
orientation === 'horizontal'
? resizeHandle.getBoundingClientRect().width
: resizeHandle.getBoundingClientRect().height;
});
// Minimum size in pixels is a percentage of the PanelGroup's width/height
setMinSizePct((minSizePx / dim) * 100);
});
observer.observe(panelGroup);
resizeHandles.forEach((resizeHandle) => {
observer.observe(resizeHandle);
});
window.addEventListener('resize', handleWindowResize);
return () => {
observer.disconnect();
window.removeEventListener('resize', handleWindowResize);
};
}, [groupId, handleWindowResize, minSizePct, minSizePx, orientation]);
return { ref, minSizePct };
};

View File

@ -8,7 +8,6 @@ import { SchedulerParam } from 'features/parameters/store/parameterZodSchemas';
export const initialUIState: UIState = { export const initialUIState: UIState = {
activeTab: 0, activeTab: 0,
currentTheme: 'dark',
shouldPinParametersPanel: true, shouldPinParametersPanel: true,
shouldShowParametersPanel: true, shouldShowParametersPanel: true,
shouldShowImageDetails: false, shouldShowImageDetails: false,
@ -30,9 +29,6 @@ export const uiSlice = createSlice({
setActiveTab: (state, action: PayloadAction<number | InvokeTabName>) => { setActiveTab: (state, action: PayloadAction<number | InvokeTabName>) => {
setActiveTabReducer(state, action.payload); setActiveTabReducer(state, action.payload);
}, },
setCurrentTheme: (state, action: PayloadAction<string>) => {
state.currentTheme = action.payload;
},
setShouldPinParametersPanel: (state, action: PayloadAction<boolean>) => { setShouldPinParametersPanel: (state, action: PayloadAction<boolean>) => {
state.shouldPinParametersPanel = action.payload; state.shouldPinParametersPanel = action.payload;
state.shouldShowParametersPanel = true; state.shouldShowParametersPanel = true;
@ -110,7 +106,6 @@ export const uiSlice = createSlice({
export const { export const {
setActiveTab, setActiveTab,
setCurrentTheme,
setShouldPinParametersPanel, setShouldPinParametersPanel,
setShouldShowParametersPanel, setShouldShowParametersPanel,
setShouldShowImageDetails, setShouldShowImageDetails,

View File

@ -16,7 +16,6 @@ export type Rect = Coordinates & Dimensions;
export interface UIState { export interface UIState {
activeTab: number; activeTab: number;
currentTheme: string;
shouldPinParametersPanel: boolean; shouldPinParametersPanel: boolean;
shouldShowParametersPanel: boolean; shouldShowParametersPanel: boolean;
shouldShowImageDetails: boolean; shouldShowImageDetails: boolean;

View File

@ -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' { // import en from '../public/locales/en.json';
// Extend CustomTypeOptions
interface CustomTypeOptions { // declare module 'i18next' {
// Setting Default Namespace As English // // Extend CustomTypeOptions
defaultNS: 'en'; // interface CustomTypeOptions {
// Custom Types For Resources // // Setting Default Namespace As English
resources: { // defaultNS: 'en';
en: typeof en; // // Custom Types For Resources
}; // resources: {
// Never Return Null // en: typeof en;
returnNull: false; // };
} // // Never Return Null
} // returnNull: false;
// }
// }
export default {};

View File

@ -3,6 +3,8 @@ import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend'; import Backend from 'i18next-http-backend';
import { initReactI18next } from 'react-i18next'; 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 translationEN from '../public/locales/en.json';
import { LOCALSTORAGE_PREFIX } from 'app/store/constants'; import { LOCALSTORAGE_PREFIX } from 'app/store/constants';

View File

@ -1,9 +1,9 @@
export { default as InvokeAiLogoComponent } from './features/system/components/InvokeAILogoComponent';
export { default as ThemeChanger } from './features/system/components/ThemeChanger';
export { default as IAIPopover } from './common/components/IAIPopover';
export { default as IAIIconButton } from './common/components/IAIIconButton';
export { default as SettingsModal } from './features/system/components/SettingsModal/SettingsModal';
export { default as StatusIndicator } from './features/system/components/StatusIndicator';
export { default as ModelSelect } from './features/system/components/ModelSelect';
export { default as InvokeAIUI } from './app/components/InvokeAIUI'; export { default as InvokeAIUI } from './app/components/InvokeAIUI';
export type { PartialAppConfig } from './app/types/invokeai'; export type { PartialAppConfig } from './app/types/invokeai';
export { default as IAIIconButton } from './common/components/IAIIconButton';
export { default as IAIPopover } from './common/components/IAIPopover';
export { default as InvokeAiLogoComponent } from './features/system/components/InvokeAILogoComponent';
export { default as ModelSelect } from './features/system/components/ModelSelect';
export { default as SettingsModal } from './features/system/components/SettingsModal/SettingsModal';
export { default as StatusIndicator } from './features/system/components/StatusIndicator';
export { default as ColorModeButton } from './features/system/components/ColorModeButton';

View File

@ -2,7 +2,7 @@ import { MantineThemeOverride } from '@mantine/core';
export const mantineTheme: MantineThemeOverride = { export const mantineTheme: MantineThemeOverride = {
colorScheme: 'dark', colorScheme: 'dark',
fontFamily: `'InterVariable', sans-serif`, fontFamily: `'Inter Variable', sans-serif`,
components: { components: {
ScrollArea: { ScrollArea: {
defaultProps: { defaultProps: {

View File

@ -82,11 +82,14 @@ export const boardsApi = api.injectEndpoints({
{ type: 'Board', id: arg.board_id }, { type: 'Board', id: arg.board_id },
], ],
}), }),
deleteBoard: build.mutation<void, string>({ deleteBoard: build.mutation<void, string>({
query: (board_id) => ({ url: `boards/${board_id}`, method: 'DELETE' }), query: (board_id) => ({ url: `boards/${board_id}`, method: 'DELETE' }),
invalidatesTags: (result, error, arg) => [{ type: 'Board', id: arg }], invalidatesTags: (result, error, arg) => [{ type: 'Board', id: arg }],
}), }),
deleteBoardAndImages: build.mutation<void, string>({
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, useCreateBoardMutation,
useUpdateBoardMutation, useUpdateBoardMutation,
useDeleteBoardMutation, useDeleteBoardMutation,
useDeleteBoardAndImagesMutation
} = boardsApi; } = boardsApi;

View File

@ -1,7 +1,5 @@
import SwaggerParser from '@apidevtools/swagger-parser';
import { createAsyncThunk } from '@reduxjs/toolkit'; import { createAsyncThunk } from '@reduxjs/toolkit';
import { log } from 'app/logging/useLogger'; import { log } from 'app/logging/useLogger';
import { OpenAPIV3 } from 'openapi-types';
const schemaLog = log.child({ namespace: 'schema' }); const schemaLog = log.child({ namespace: 'schema' });
@ -29,12 +27,13 @@ export const receivedOpenAPISchema = createAsyncThunk(
'nodes/receivedOpenAPISchema', 'nodes/receivedOpenAPISchema',
async (_, { dispatch, rejectWithValue }) => { async (_, { dispatch, rejectWithValue }) => {
try { try {
const dereferencedSchema = (await SwaggerParser.dereference( const response = await fetch(`openapi.json`);
'openapi.json' const openAPISchema = await response.json();
)) as OpenAPIV3.Document;
schemaLog.info({ openAPISchema }, 'Received OpenAPI schema');
const schemaJSON = JSON.parse( const schemaJSON = JSON.parse(
JSON.stringify(dereferencedSchema, getCircularReplacer()) JSON.stringify(openAPISchema, getCircularReplacer())
); );
return schemaJSON; return schemaJSON;

View File

@ -0,0 +1,24 @@
import { InvokeAIThemeColors } from 'theme/themeTypes';
import { generateColorPalette } from 'theme/util/generateColorPalette';
const BASE = { H: 220, S: 16 };
const ACCENT = { H: 250, S: 52 };
const WORKING = { H: 47, S: 50 };
const WARNING = { H: 28, S: 50 };
const OK = { H: 113, S: 50 };
const ERROR = { H: 0, S: 50 };
export const InvokeAIColors: InvokeAIThemeColors = {
base: generateColorPalette(BASE.H, BASE.S),
baseAlpha: generateColorPalette(BASE.H, BASE.S, true),
accent: generateColorPalette(ACCENT.H, ACCENT.S),
accentAlpha: generateColorPalette(ACCENT.H, ACCENT.S, true),
working: generateColorPalette(WORKING.H, WORKING.S),
workingAlpha: generateColorPalette(WORKING.H, WORKING.S, true),
warning: generateColorPalette(WARNING.H, WARNING.S),
warningAlpha: generateColorPalette(WARNING.H, WARNING.S, true),
ok: generateColorPalette(OK.H, OK.S),
okAlpha: generateColorPalette(OK.H, OK.S, true),
error: generateColorPalette(ERROR.H, ERROR.S),
errorAlpha: generateColorPalette(ERROR.H, ERROR.S, true),
};

View File

@ -1,18 +0,0 @@
import { InvokeAIThemeColors } from 'theme/themeTypes';
import { generateColorPalette } from '../util/generateColorPalette';
export const greenTeaThemeColors: InvokeAIThemeColors = {
base: generateColorPalette(223, 10),
baseAlpha: generateColorPalette(223, 10, false, true),
accent: generateColorPalette(160, 60),
accentAlpha: generateColorPalette(160, 60, false, true),
working: generateColorPalette(47, 68),
workingAlpha: generateColorPalette(47, 68, false, true),
warning: generateColorPalette(28, 75),
warningAlpha: generateColorPalette(28, 75, false, true),
ok: generateColorPalette(122, 49),
okAlpha: generateColorPalette(122, 49, false, true),
error: generateColorPalette(0, 50),
errorAlpha: generateColorPalette(0, 50, false, true),
gridLineColor: 'rgba(255, 255, 255, 0.15)',
};

View File

@ -1,18 +0,0 @@
import { InvokeAIThemeColors } from 'theme/themeTypes';
import { generateColorPalette } from 'theme/util/generateColorPalette';
export const invokeAIThemeColors: InvokeAIThemeColors = {
base: generateColorPalette(220, 15),
baseAlpha: generateColorPalette(220, 15, false, true),
accent: generateColorPalette(250, 50),
accentAlpha: generateColorPalette(250, 50, false, true),
working: generateColorPalette(47, 67),
workingAlpha: generateColorPalette(47, 67, false, true),
warning: generateColorPalette(28, 75),
warningAlpha: generateColorPalette(28, 75, false, true),
ok: generateColorPalette(113, 70),
okAlpha: generateColorPalette(113, 70, false, true),
error: generateColorPalette(0, 76),
errorAlpha: generateColorPalette(0, 76, false, true),
gridLineColor: 'rgba(150, 150, 180, 0.15)',
};

View File

@ -1,18 +0,0 @@
import { InvokeAIThemeColors } from 'theme/themeTypes';
import { generateColorPalette } from '../util/generateColorPalette';
export const lightThemeColors: InvokeAIThemeColors = {
base: generateColorPalette(223, 10, true),
baseAlpha: generateColorPalette(223, 10, true, true),
accent: generateColorPalette(40, 80, true),
accentAlpha: generateColorPalette(40, 80, true, true),
working: generateColorPalette(47, 68, true),
workingAlpha: generateColorPalette(47, 68, true, true),
warning: generateColorPalette(28, 75, true),
warningAlpha: generateColorPalette(28, 75, true, true),
ok: generateColorPalette(122, 49, true),
okAlpha: generateColorPalette(122, 49, true, true),
error: generateColorPalette(0, 50, true),
errorAlpha: generateColorPalette(0, 50, true, true),
gridLineColor: 'rgba(0, 0, 0, 0.15)',
};

View File

@ -1,18 +0,0 @@
import { InvokeAIThemeColors } from 'theme/themeTypes';
import { generateColorPalette } from '../util/generateColorPalette';
export const oceanBlueColors: InvokeAIThemeColors = {
base: generateColorPalette(220, 30),
baseAlpha: generateColorPalette(220, 30, false, true),
accent: generateColorPalette(210, 80),
accentAlpha: generateColorPalette(210, 80, false, true),
working: generateColorPalette(47, 68),
workingAlpha: generateColorPalette(47, 68, false, true),
warning: generateColorPalette(28, 75),
warningAlpha: generateColorPalette(28, 75, false, true),
ok: generateColorPalette(122, 49),
okAlpha: generateColorPalette(122, 49, false, true),
error: generateColorPalette(0, 100),
errorAlpha: generateColorPalette(0, 100, false, true),
gridLineColor: 'rgba(136, 148, 184, 0.15)',
};

View File

@ -3,6 +3,7 @@ import {
createMultiStyleConfigHelpers, createMultiStyleConfigHelpers,
defineStyle, defineStyle,
} from '@chakra-ui/styled-system'; } from '@chakra-ui/styled-system';
import { mode } from '@chakra-ui/theme-tools';
const { definePartsStyle, defineMultiStyleConfig } = const { definePartsStyle, defineMultiStyleConfig } =
createMultiStyleConfigHelpers(parts.keys); createMultiStyleConfigHelpers(parts.keys);
@ -18,16 +19,16 @@ const invokeAIButton = defineStyle((props) => {
fontSize: 'sm', fontSize: 'sm',
border: 'none', border: 'none',
borderRadius: 'base', borderRadius: 'base',
bg: `${c}.800`, bg: mode(`${c}.200`, `${c}.700`)(props),
color: 'base.100', color: mode(`${c}.900`, `${c}.100`)(props),
_hover: { _hover: {
bg: `${c}.700`, bg: mode(`${c}.250`, `${c}.650`)(props),
}, },
_expanded: { _expanded: {
bg: `${c}.750`, bg: mode(`${c}.250`, `${c}.650`)(props),
borderBottomRadius: 'none', borderBottomRadius: 'none',
_hover: { _hover: {
bg: `${c}.700`, bg: mode(`${c}.300`, `${c}.600`)(props),
}, },
}, },
}; };
@ -36,7 +37,7 @@ const invokeAIButton = defineStyle((props) => {
const invokeAIPanel = defineStyle((props) => { const invokeAIPanel = defineStyle((props) => {
const { colorScheme: c } = props; const { colorScheme: c } = props;
return { return {
bg: `${c}.800`, bg: mode(`${c}.100`, `${c}.800`)(props),
borderRadius: 'base', borderRadius: 'base',
borderTopRadius: 'none', borderTopRadius: 'none',
}; };

View File

@ -1,44 +1,117 @@
import { defineStyle, defineStyleConfig } from '@chakra-ui/react'; import { defineStyle, defineStyleConfig } from '@chakra-ui/react';
import { mode } from '@chakra-ui/theme-tools';
const invokeAI = defineStyle((props) => { const invokeAI = defineStyle((props) => {
const { colorScheme: c } = props; const { colorScheme: c } = props;
// must specify `_disabled` colors if we override `_hover`, else hover on disabled has no styles // must specify `_disabled` colors if we override `_hover`, else hover on disabled has no styles
if (c === 'base') {
const _disabled = { const _disabled = {
bg: `${c}.600`, bg: mode('base.200', 'base.700')(props),
color: `${c}.100`, color: mode('base.500', 'base.150')(props),
svg: { svg: {
fill: `${c}.100`, fill: mode('base.500', 'base.150')(props),
}, },
opacity: 1,
}; };
return { return {
bg: `${c}.700`, bg: mode('base.200', 'base.600')(props),
color: `${c}.100`, color: mode('base.850', 'base.100')(props),
borderRadius: 'base', borderRadius: 'base',
textShadow: mode(
'0 0 0.3rem var(--invokeai-colors-base-50)',
'0 0 0.3rem var(--invokeai-colors-base-900)'
)(props),
svg: { svg: {
fill: `${c}.100`, fill: mode('base.850', 'base.100')(props),
filter: mode(
'drop-shadow(0px 0px 0.3rem var(--invokeai-colors-base-100))',
'drop-shadow(0px 0px 0.3rem var(--invokeai-colors-base-800))'
)(props),
}, },
_disabled, _disabled,
_hover: { _hover: {
bg: `${c}.650`, bg: mode('base.300', 'base.500')(props),
color: `${c}.50`, color: mode('base.900', 'base.50')(props),
svg: { svg: {
fill: `${c}.50`, fill: mode('base.900', 'base.50')(props),
}, },
_disabled, _disabled,
}, },
_checked: { _checked: {
bg: 'accent.700', bg: mode('accent.400', 'accent.600')(props),
color: 'accent.100', color: mode('base.50', 'base.100')(props),
svg: { svg: {
fill: 'accent.100', fill: mode(`${c}.50`, `${c}.100`)(props),
filter: mode(
`drop-shadow(0px 0px 0.3rem var(--invokeai-colors-${c}-600))`,
`drop-shadow(0px 0px 0.3rem var(--invokeai-colors-${c}-800))`
)(props),
}, },
_disabled, _disabled,
_hover: { _hover: {
bg: 'accent.600', bg: mode('accent.500', 'accent.500')(props),
color: 'accent.50', color: mode('white', 'base.50')(props),
svg: { svg: {
fill: 'accent.50', fill: mode('white', 'base.50')(props),
},
_disabled,
},
},
};
}
const _disabled = {
bg: mode(`${c}.200`, `${c}.700`)(props),
color: mode(`${c}.100`, `${c}.150`)(props),
svg: {
fill: mode(`${c}.100`, `${c}.150`)(props),
},
opacity: 1,
filter: mode(undefined, 'saturate(65%)')(props),
};
return {
bg: mode(`${c}.400`, `${c}.600`)(props),
color: mode(`base.50`, `base.100`)(props),
borderRadius: 'base',
textShadow: mode(
`0 0 0.3rem var(--invokeai-colors-${c}-600)`,
`0 0 0.3rem var(--invokeai-colors-${c}-900)`
)(props),
svg: {
fill: mode(`base.50`, `base.100`)(props),
filter: mode(
`drop-shadow(0px 0px 0.3rem var(--invokeai-colors-${c}-600))`,
`drop-shadow(0px 0px 0.3rem var(--invokeai-colors-${c}-800))`
)(props),
},
_disabled,
_hover: {
bg: mode(`${c}.500`, `${c}.500`)(props),
color: mode('white', `base.50`)(props),
svg: {
fill: mode('white', `base.50`)(props),
},
_disabled,
},
_checked: {
bg: mode('accent.400', 'accent.600')(props),
color: mode('base.50', 'base.100')(props),
svg: {
fill: mode(`base.50`, `base.100`)(props),
filter: mode(
`drop-shadow(0px 0px 0.3rem var(--invokeai-colors-${c}-600))`,
`drop-shadow(0px 0px 0.3rem var(--invokeai-colors-${c}-800))`
)(props),
},
_disabled,
_hover: {
bg: mode('accent.500', 'accent.500')(props),
color: mode('white', 'base.50')(props),
svg: {
fill: mode('white', 'base.50')(props),
}, },
_disabled, _disabled,
}, },

View File

@ -3,6 +3,7 @@ import {
createMultiStyleConfigHelpers, createMultiStyleConfigHelpers,
defineStyle, defineStyle,
} from '@chakra-ui/styled-system'; } from '@chakra-ui/styled-system';
import { mode } from '@chakra-ui/theme-tools';
const { definePartsStyle, defineMultiStyleConfig } = const { definePartsStyle, defineMultiStyleConfig } =
createMultiStyleConfigHelpers(parts.keys); createMultiStyleConfigHelpers(parts.keys);
@ -11,14 +12,18 @@ const invokeAIControl = defineStyle((props) => {
const { colorScheme: c } = props; const { colorScheme: c } = props;
return { return {
bg: mode('base.200', 'base.700')(props),
borderColor: mode('base.200', 'base.700')(props),
color: mode('base.900', 'base.100')(props),
_checked: { _checked: {
bg: `${c}.200`, bg: mode(`${c}.300`, `${c}.600`)(props),
borderColor: `${c}.200`, borderColor: mode(`${c}.300`, `${c}.600`)(props),
color: 'base.900', color: mode(`${c}.900`, `${c}.100`)(props),
_hover: { _hover: {
bg: `${c}.300`, bg: mode(`${c}.400`, `${c}.500`)(props),
borderColor: `${c}.300`, borderColor: mode(`${c}.400`, `${c}.500`)(props),
}, },
_disabled: { _disabled: {
@ -29,9 +34,9 @@ const invokeAIControl = defineStyle((props) => {
}, },
_indeterminate: { _indeterminate: {
bg: `${c}.200`, bg: mode(`${c}.300`, `${c}.600`)(props),
borderColor: `${c}.200`, borderColor: mode(`${c}.300`, `${c}.600`)(props),
color: 'base.900', color: mode(`${c}.900`, `${c}.100`)(props),
}, },
_disabled: { _disabled: {
@ -44,7 +49,7 @@ const invokeAIControl = defineStyle((props) => {
}, },
_invalid: { _invalid: {
borderColor: 'red.300', borderColor: mode('error.600', 'error.300')(props),
}, },
}; };
}); });

View File

@ -1,6 +1,7 @@
import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system'; import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system';
import { mode } from '@chakra-ui/theme-tools';
const invokeAI = defineStyle((_props) => { const invokeAI = defineStyle((props) => {
return { return {
fontSize: 'sm', fontSize: 'sm',
marginEnd: 0, marginEnd: 0,
@ -12,7 +13,7 @@ const invokeAI = defineStyle((_props) => {
_disabled: { _disabled: {
opacity: 0.4, opacity: 0.4,
}, },
color: 'base.300', color: mode('base.700', 'base.300')(props),
}; };
}); });

View File

@ -1,38 +1,40 @@
import { menuAnatomy } from '@chakra-ui/anatomy'; import { menuAnatomy } from '@chakra-ui/anatomy';
import { createMultiStyleConfigHelpers } from '@chakra-ui/react'; import { createMultiStyleConfigHelpers } from '@chakra-ui/react';
import { mode } from '@chakra-ui/theme-tools';
const { definePartsStyle, defineMultiStyleConfig } = const { definePartsStyle, defineMultiStyleConfig } =
createMultiStyleConfigHelpers(menuAnatomy.keys); createMultiStyleConfigHelpers(menuAnatomy.keys);
// define the base component styles // define the base component styles
const invokeAI = definePartsStyle({ const invokeAI = definePartsStyle((props) => ({
// define the part you're going to style // define the part you're going to style
button: { button: {
// this will style the MenuButton component // this will style the MenuButton component
fontWeight: '600', fontWeight: 500,
bg: 'base.500', bg: mode('base.300', 'base.500')(props),
color: 'base.200', color: mode('base.900', 'base.100')(props),
_hover: { _hover: {
bg: 'base.600', bg: mode('base.400', 'base.600')(props),
color: 'white', color: mode('base.900', 'base.50')(props),
fontWeight: 600,
}, },
}, },
list: { list: {
zIndex: 9999, zIndex: 9999,
bg: 'base.800', bg: mode('base.200', 'base.800')(props),
}, },
item: { item: {
// this will style the MenuItem and MenuItemOption components // this will style the MenuItem and MenuItemOption components
fontSize: 'sm', fontSize: 'sm',
bg: 'base.800', bg: mode('base.200', 'base.800')(props),
_hover: { _hover: {
bg: 'base.750', bg: mode('base.300', 'base.700')(props),
}, },
_focus: { _focus: {
bg: 'base.700', bg: mode('base.400', 'base.600')(props),
}, },
}, },
}); }));
export const menuTheme = defineMultiStyleConfig({ export const menuTheme = defineMultiStyleConfig({
variants: { variants: {

View File

@ -3,28 +3,31 @@ import {
createMultiStyleConfigHelpers, createMultiStyleConfigHelpers,
defineStyle, defineStyle,
} from '@chakra-ui/styled-system'; } from '@chakra-ui/styled-system';
import { mode } from '@chakra-ui/theme-tools';
const { defineMultiStyleConfig, definePartsStyle } = const { defineMultiStyleConfig, definePartsStyle } =
createMultiStyleConfigHelpers(parts.keys); createMultiStyleConfigHelpers(parts.keys);
const invokeAIOverlay = defineStyle({ const invokeAIOverlay = defineStyle((props) => ({
bg: 'blackAlpha.600', bg: mode('blackAlpha.700', 'blackAlpha.700')(props),
}); }));
const invokeAIDialogContainer = defineStyle({}); const invokeAIDialogContainer = defineStyle({});
const invokeAIDialog = defineStyle((_props) => { const invokeAIDialog = defineStyle((props) => {
return { return {
bg: 'base.850', layerStyle: 'first',
maxH: '80vh', maxH: '80vh',
}; };
}); });
const invokeAIHeader = defineStyle((_props) => { const invokeAIHeader = defineStyle((props) => {
return { return {
fontWeight: '600', fontWeight: '600',
fontSize: 'lg', fontSize: 'lg',
color: 'base.200', layerStyle: 'first',
borderTopRadius: 'base',
borderInlineEndRadius: 'base',
}; };
}); });
@ -37,7 +40,7 @@ const invokeAIBody = defineStyle({
const invokeAIFooter = defineStyle({}); const invokeAIFooter = defineStyle({});
export const invokeAI = definePartsStyle((props) => ({ export const invokeAI = definePartsStyle((props) => ({
overlay: invokeAIOverlay, overlay: invokeAIOverlay(props),
dialogContainer: invokeAIDialogContainer, dialogContainer: invokeAIDialogContainer,
dialog: invokeAIDialog(props), dialog: invokeAIDialog(props),
header: invokeAIHeader(props), header: invokeAIHeader(props),

View File

@ -5,6 +5,7 @@ import {
} from '@chakra-ui/styled-system'; } from '@chakra-ui/styled-system';
import { getInputOutlineStyles } from '../util/getInputOutlineStyles'; import { getInputOutlineStyles } from '../util/getInputOutlineStyles';
import { mode } from '@chakra-ui/theme-tools';
const { defineMultiStyleConfig, definePartsStyle } = const { defineMultiStyleConfig, definePartsStyle } =
createMultiStyleConfigHelpers(parts.keys); createMultiStyleConfigHelpers(parts.keys);
@ -33,7 +34,7 @@ const invokeAIStepperGroup = defineStyle((_props) => {
}; };
}); });
const invokeAIStepper = defineStyle((_props) => { const invokeAIStepper = defineStyle((props) => {
return { return {
border: 'none', border: 'none',
// expand arrow hitbox // expand arrow hitbox
@ -43,11 +44,11 @@ const invokeAIStepper = defineStyle((_props) => {
my: 0, my: 0,
svg: { svg: {
color: 'base.300', color: mode('base.700', 'base.300')(props),
width: 2.5, width: 2.5,
height: 2.5, height: 2.5,
_hover: { _hover: {
color: 'base.50', color: mode('base.900', 'base.100')(props),
}, },
}, },
}; };

View File

@ -3,7 +3,7 @@ import {
createMultiStyleConfigHelpers, createMultiStyleConfigHelpers,
defineStyle, defineStyle,
} from '@chakra-ui/styled-system'; } from '@chakra-ui/styled-system';
import { cssVar } from '@chakra-ui/theme-tools'; import { cssVar, mode } from '@chakra-ui/theme-tools';
const { defineMultiStyleConfig, definePartsStyle } = const { defineMultiStyleConfig, definePartsStyle } =
createMultiStyleConfigHelpers(parts.keys); createMultiStyleConfigHelpers(parts.keys);
@ -12,15 +12,20 @@ const $popperBg = cssVar('popper-bg');
const $arrowBg = cssVar('popper-arrow-bg'); const $arrowBg = cssVar('popper-arrow-bg');
const $arrowShadowColor = cssVar('popper-arrow-shadow-color'); const $arrowShadowColor = cssVar('popper-arrow-shadow-color');
const invokeAIContent = defineStyle((_props) => { const invokeAIContent = defineStyle((props) => {
return { return {
[$arrowBg.variable]: `colors.base.800`, [$arrowBg.variable]: mode('colors.base.100', 'colors.base.800')(props),
[$popperBg.variable]: `colors.base.800`, [$popperBg.variable]: mode('colors.base.100', 'colors.base.800')(props),
[$arrowShadowColor.variable]: `colors.base.600`, [$arrowShadowColor.variable]: mode(
'colors.base.400',
'colors.base.600'
)(props),
minW: 'unset', minW: 'unset',
width: 'unset', width: 'unset',
p: 4, p: 4,
bg: 'base.800', bg: mode('base.100', 'base.800')(props),
border: 'none',
shadow: 'dark-lg',
}; };
}); });

View File

@ -1,13 +1,14 @@
import { selectAnatomy as parts } from '@chakra-ui/anatomy'; import { selectAnatomy as parts } from '@chakra-ui/anatomy';
import { createMultiStyleConfigHelpers, defineStyle } from '@chakra-ui/react'; import { createMultiStyleConfigHelpers, defineStyle } from '@chakra-ui/react';
import { getInputOutlineStyles } from '../util/getInputOutlineStyles'; import { getInputOutlineStyles } from '../util/getInputOutlineStyles';
import { mode } from '@chakra-ui/theme-tools';
const { definePartsStyle, defineMultiStyleConfig } = const { definePartsStyle, defineMultiStyleConfig } =
createMultiStyleConfigHelpers(parts.keys); createMultiStyleConfigHelpers(parts.keys);
const invokeAIIcon = defineStyle((_props) => { const invokeAIIcon = defineStyle((props) => {
return { return {
color: 'base.300', color: mode('base.200', 'base.300')(props),
}; };
}); });

View File

@ -1,12 +1,13 @@
import { sliderAnatomy as parts } from '@chakra-ui/anatomy'; import { sliderAnatomy as parts } from '@chakra-ui/anatomy';
import { createMultiStyleConfigHelpers, defineStyle } from '@chakra-ui/react'; import { createMultiStyleConfigHelpers, defineStyle } from '@chakra-ui/react';
import { mode } from '@chakra-ui/theme-tools';
const { definePartsStyle, defineMultiStyleConfig } = const { definePartsStyle, defineMultiStyleConfig } =
createMultiStyleConfigHelpers(parts.keys); createMultiStyleConfigHelpers(parts.keys);
const invokeAITrack = defineStyle((_props) => { const invokeAITrack = defineStyle((props) => {
return { return {
bg: 'base.400', bg: mode('base.400', 'base.600')(props),
h: 1.5, h: 1.5,
}; };
}); });
@ -14,23 +15,24 @@ const invokeAITrack = defineStyle((_props) => {
const invokeAIFilledTrack = defineStyle((props) => { const invokeAIFilledTrack = defineStyle((props) => {
const { colorScheme: c } = props; const { colorScheme: c } = props;
return { return {
bg: `${c}.600`, bg: mode(`${c}.400`, `${c}.600`)(props),
h: 1.5, h: 1.5,
}; };
}); });
const invokeAIThumb = defineStyle((_props) => { const invokeAIThumb = defineStyle((props) => {
return { return {
w: 2, w: 2,
h: 4, h: 4,
bg: mode('base.50', 'base.100')(props),
}; };
}); });
const invokeAIMark = defineStyle((_props) => { const invokeAIMark = defineStyle((props) => {
return { return {
fontSize: 'xs', fontSize: 'xs',
fontWeight: '500', fontWeight: '500',
color: 'base.400', color: mode('base.700', 'base.400')(props),
mt: 2, mt: 2,
insetInlineStart: 'unset', insetInlineStart: 'unset',
}; };

View File

@ -3,6 +3,7 @@ import {
createMultiStyleConfigHelpers, createMultiStyleConfigHelpers,
defineStyle, defineStyle,
} from '@chakra-ui/styled-system'; } from '@chakra-ui/styled-system';
import { mode } from '@chakra-ui/theme-tools';
const { defineMultiStyleConfig, definePartsStyle } = const { defineMultiStyleConfig, definePartsStyle } =
createMultiStyleConfigHelpers(parts.keys); createMultiStyleConfigHelpers(parts.keys);
@ -11,13 +12,13 @@ const invokeAITrack = defineStyle((props) => {
const { colorScheme: c } = props; const { colorScheme: c } = props;
return { return {
bg: 'base.600', bg: mode('base.300', 'base.600')(props),
_focusVisible: { _focusVisible: {
boxShadow: 'none', boxShadow: 'none',
}, },
_checked: { _checked: {
bg: `${c}.600`, bg: mode(`${c}.400`, `${c}.500`)(props),
}, },
}; };
}); });
@ -26,7 +27,7 @@ const invokeAIThumb = defineStyle((props) => {
const { colorScheme: c } = props; const { colorScheme: c } = props;
return { return {
bg: `${c}.50`, bg: mode(`${c}.50`, `${c}.50`)(props),
}; };
}); });

View File

@ -3,6 +3,7 @@ import {
createMultiStyleConfigHelpers, createMultiStyleConfigHelpers,
defineStyle, defineStyle,
} from '@chakra-ui/styled-system'; } from '@chakra-ui/styled-system';
import { mode } from '@chakra-ui/theme-tools';
const { defineMultiStyleConfig, definePartsStyle } = const { defineMultiStyleConfig, definePartsStyle } =
createMultiStyleConfigHelpers(parts.keys); createMultiStyleConfigHelpers(parts.keys);
@ -16,30 +17,53 @@ const invokeAIRoot = defineStyle((_props) => {
const invokeAITab = defineStyle((_props) => ({})); const invokeAITab = defineStyle((_props) => ({}));
const invokeAITablist = defineStyle((_props) => ({ const invokeAITablist = defineStyle((props) => {
const { colorScheme: c } = props;
return {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
gap: 1, gap: 1,
color: 'base.700', color: mode('base.700', 'base.400')(props),
button: { button: {
fontSize: 'sm', fontSize: 'sm',
padding: 2, padding: 2,
borderRadius: 'base', borderRadius: 'base',
textShadow: mode(
`0 0 0.3rem var(--invokeai-colors-accent-100)`,
`0 0 0.3rem var(--invokeai-colors-accent-900)`
)(props),
svg: {
fill: mode('base.700', 'base.300')(props),
},
_selected: { _selected: {
borderBottomColor: 'base.800', bg: mode('accent.400', 'accent.600')(props),
bg: 'accent.700', color: mode('base.50', 'base.100')(props),
color: 'accent.100', svg: {
fill: mode(`base.50`, `base.100`)(props),
filter: mode(
`drop-shadow(0px 0px 0.3rem var(--invokeai-colors-${c}-600))`,
`drop-shadow(0px 0px 0.3rem var(--invokeai-colors-${c}-800))`
)(props),
},
_hover: { _hover: {
bg: 'accent.600', bg: mode('accent.500', 'accent.500')(props),
color: 'accent.50', color: mode('white', 'base.50')(props),
svg: {
fill: mode('white', 'base.50')(props),
},
}, },
}, },
_hover: { _hover: {
bg: 'base.600', bg: mode('base.100', 'base.800')(props),
color: 'base.50', color: mode('base.900', 'base.50')(props),
svg: {
fill: mode(`base.800`, `base.100`)(props),
}, },
}, },
})); },
};
});
const invokeAITabpanel = defineStyle((_props) => ({ const invokeAITabpanel = defineStyle((_props) => ({
padding: 0, padding: 0,
@ -59,5 +83,6 @@ export const tabsTheme = defineMultiStyleConfig({
}, },
defaultProps: { defaultProps: {
variant: 'invokeAI', variant: 'invokeAI',
colorScheme: 'accent',
}, },
}); });

View File

@ -1,7 +1,8 @@
import { defineStyle, defineStyleConfig } from '@chakra-ui/react'; import { defineStyle, defineStyleConfig } from '@chakra-ui/react';
import { mode } from '@chakra-ui/theme-tools';
const subtext = defineStyle((_props) => ({ const subtext = defineStyle((props) => ({
color: 'base.400', color: mode('colors.base.500', 'colors.base.400')(props),
})); }));
export const textTheme = defineStyleConfig({ export const textTheme = defineStyleConfig({

View File

@ -0,0 +1,17 @@
import { defineStyle, defineStyleConfig } from '@chakra-ui/react';
import { mode } from '@chakra-ui/theme-tools';
import { cssVar } from '@chakra-ui/theme-tools';
const $arrowBg = cssVar('popper-arrow-bg');
// define the base component styles
const baseStyle = defineStyle((props) => ({
borderRadius: 'base',
shadow: 'dark-lg',
bg: mode('base.700', 'base.200')(props),
[$arrowBg.variable]: mode('colors.base.700', 'colors.base.200')(props),
pb: 1.5,
}));
// export the component theme
export const tooltipTheme = defineStyleConfig({ baseStyle });

View File

@ -1,7 +1,6 @@
import { ThemeOverride } from '@chakra-ui/react'; import { ThemeOverride } from '@chakra-ui/react';
import type { StyleFunctionProps } from '@chakra-ui/styled-system';
import { invokeAIThemeColors } from 'theme/colors/invokeAI'; import { InvokeAIColors } from './colors/colors';
import { accordionTheme } from './components/accordion'; import { accordionTheme } from './components/accordion';
import { buttonTheme } from './components/button'; import { buttonTheme } from './components/button';
import { checkboxTheme } from './components/checkbox'; import { checkboxTheme } from './components/checkbox';
@ -12,13 +11,14 @@ import { modalTheme } from './components/modal';
import { numberInputTheme } from './components/numberInput'; import { numberInputTheme } from './components/numberInput';
import { popoverTheme } from './components/popover'; import { popoverTheme } from './components/popover';
import { progressTheme } from './components/progress'; import { progressTheme } from './components/progress';
import { no_scrollbar, scrollbar as _scrollbar } from './components/scrollbar'; import { no_scrollbar } from './components/scrollbar';
import { selectTheme } from './components/select'; import { selectTheme } from './components/select';
import { sliderTheme } from './components/slider'; import { sliderTheme } from './components/slider';
import { switchTheme } from './components/switch'; import { switchTheme } from './components/switch';
import { tabsTheme } from './components/tabs'; import { tabsTheme } from './components/tabs';
import { textTheme } from './components/text'; import { textTheme } from './components/text';
import { textareaTheme } from './components/textarea'; import { textareaTheme } from './components/textarea';
import { tooltipTheme } from './components/tooltip';
export const theme: ThemeOverride = { export const theme: ThemeOverride = {
config: { config: {
@ -26,30 +26,32 @@ export const theme: ThemeOverride = {
initialColorMode: 'dark', initialColorMode: 'dark',
useSystemColorMode: false, useSystemColorMode: false,
}, },
styles: { layerStyles: {
global: (_props: StyleFunctionProps) => ({
body: { body: {
bg: 'base.900', bg: 'base.50',
color: 'base.50', color: 'base.900',
overflow: { '.chakra-ui-dark &': { bg: 'base.900', color: 'base.50' },
base: 'scroll', },
xl: 'hidden', first: {
bg: 'base.100',
color: 'base.900',
'.chakra-ui-dark &': { bg: 'base.850', color: 'base.100' },
},
second: {
bg: 'base.200',
color: 'base.900',
'.chakra-ui-dark &': { bg: 'base.800', color: 'base.100' },
}, },
}, },
styles: {
global: () => ({
layerStyle: 'body',
'*': { ...no_scrollbar }, '*': { ...no_scrollbar },
}), }),
}, },
direction: 'ltr', direction: 'ltr',
fonts: { fonts: {
body: `'InterVariable', sans-serif`, body: `'Inter Variable', sans-serif`,
},
breakpoints: {
base: '0em', // 0px and onwards
sm: '30em', // 480px and onwards
md: '48em', // 768px and onwards
lg: '62em', // 992px and onwards
xl: '80em', // 1280px and onwards
'2xl': '96em', // 1536px and onwards
}, },
shadows: { shadows: {
light: { light: {
@ -68,9 +70,7 @@ export const theme: ThemeOverride = {
}, },
nodeSelectedOutline: `0 0 0 2px var(--invokeai-colors-base-500)`, nodeSelectedOutline: `0 0 0 2px var(--invokeai-colors-base-500)`,
}, },
colors: { colors: InvokeAIColors,
...invokeAIThemeColors,
},
components: { components: {
Button: buttonTheme, // Button and IconButton Button: buttonTheme, // Button and IconButton
Input: inputTheme, Input: inputTheme,
@ -88,5 +88,6 @@ export const theme: ThemeOverride = {
Checkbox: checkboxTheme, Checkbox: checkboxTheme,
Menu: menuTheme, Menu: menuTheme,
Text: textTheme, Text: textTheme,
Tooltip: tooltipTheme,
}, },
}; };

View File

@ -11,7 +11,6 @@ export type InvokeAIThemeColors = {
okAlpha: Partial<InvokeAIPaletteSteps>; okAlpha: Partial<InvokeAIPaletteSteps>;
error: Partial<InvokeAIPaletteSteps>; error: Partial<InvokeAIPaletteSteps>;
errorAlpha: Partial<InvokeAIPaletteSteps>; errorAlpha: Partial<InvokeAIPaletteSteps>;
gridLineColor: string;
}; };
export type InvokeAIPaletteSteps = { export type InvokeAIPaletteSteps = {

View File

@ -2,46 +2,35 @@ import { InvokeAIPaletteSteps } from 'theme/themeTypes';
/** /**
* Add two numbers together * Add two numbers together
* @param {String | Number} hue Hue of the color (0-360) - Reds 0, Greens 120, Blues 240 * @param {String | Number} H Hue of the color (0-360) - Reds 0, Greens 120, Blues 240
* @param {String | Number} saturation Saturation of the color (0-100) * @param {String | Number} L Saturation of the color (0-100)
* @param {boolean} light True to generate light color palette * @param {Boolean} alpha Whether or not to generate this palette as a transparency palette
*/ */
export function generateColorPalette( export function generateColorPalette(
hue: string | number, H: string | number,
saturation: string | number, S: string | number,
light = false,
alpha = false alpha = false
) { ) {
hue = String(hue); H = String(H);
saturation = String(saturation); S = String(S);
const colorSteps = Array.from({ length: 21 }, (_, i) => i * 50); const colorSteps = Array.from({ length: 21 }, (_, i) => i * 50);
const lightnessSteps = [ const lightnessSteps = [
0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 59, 64, 68, 73, 77, 82, 86, 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 59, 64, 68, 73, 77, 82, 86,
95, 100, 95, 100,
]; ];
const darkPalette: Partial<InvokeAIPaletteSteps> = {}; const p = colorSteps.reduce((palette, step, index) => {
const lightPalette: Partial<InvokeAIPaletteSteps> = {};
colorSteps.forEach((colorStep, index) => {
const A = alpha ? lightnessSteps[index] / 100 : 1; const A = alpha ? lightnessSteps[index] / 100 : 1;
// Lightness should be 50% for alpha colors // Lightness should be 50% for alpha colors
const darkPaletteLightness = alpha const L = alpha ? 50 : lightnessSteps[colorSteps.length - 1 - index];
? 50
: lightnessSteps[colorSteps.length - 1 - index];
darkPalette[ palette[step as keyof typeof palette] = `hsl(${H} ${S}% ${L}% / ${A})`;
colorStep as keyof typeof darkPalette
] = `hsl(${hue} ${saturation}% ${darkPaletteLightness}% / ${A})`;
const lightPaletteLightness = alpha ? 50 : lightnessSteps[index]; return palette;
}, {} as InvokeAIPaletteSteps);
lightPalette[ return p;
colorStep as keyof typeof lightPalette
] = `hsl(${hue} ${saturation}% ${lightPaletteLightness}% / ${A})`;
});
return light ? lightPalette : darkPalette;
} }

View File

@ -1,40 +1,40 @@
import { StyleFunctionProps } from '@chakra-ui/theme-tools'; import { StyleFunctionProps, mode } from '@chakra-ui/theme-tools';
export const getInputOutlineStyles = (_props?: StyleFunctionProps) => ({ export const getInputOutlineStyles = (props: StyleFunctionProps) => ({
outline: 'none', outline: 'none',
borderWidth: 2, borderWidth: 2,
borderStyle: 'solid', borderStyle: 'solid',
borderColor: 'base.800', borderColor: mode('base.200', 'base.800')(props),
bg: 'base.900', bg: mode('base.50', 'base.900')(props),
borderRadius: 'base', borderRadius: 'base',
color: 'base.100', color: mode('base.900', 'base.100')(props),
boxShadow: 'none', boxShadow: 'none',
_hover: { _hover: {
borderColor: 'base.600', borderColor: mode('base.300', 'base.600')(props),
}, },
_focus: { _focus: {
borderColor: 'accent.700', borderColor: mode('accent.200', 'accent.600')(props),
boxShadow: 'none', boxShadow: 'none',
_hover: { _hover: {
borderColor: 'accent.600', borderColor: mode('accent.300', 'accent.500')(props),
}, },
}, },
_invalid: { _invalid: {
borderColor: 'error.700', borderColor: mode('error.300', 'error.600')(props),
boxShadow: 'none', boxShadow: 'none',
_hover: { _hover: {
borderColor: 'error.600', borderColor: mode('error.400', 'error.500')(props),
}, },
}, },
_disabled: { _disabled: {
borderColor: 'base.700', borderColor: mode('base.300', 'base.700')(props),
bg: 'base.700', bg: mode('base.300', 'base.700')(props),
color: 'base.400', color: mode('base.600', 'base.400')(props),
_hover: { _hover: {
borderColor: 'base.700', borderColor: mode('base.300', 'base.700')(props),
}, },
}, },
_placeholder: { _placeholder: {
color: 'base.500', color: mode('base.700', 'base.400')(props),
}, },
}); });

View File

@ -0,0 +1,3 @@
export const mode =
(light: string, dark: string) => (colorMode: 'light' | 'dark') =>
colorMode === 'light' ? light : dark;

View File

@ -11,7 +11,8 @@
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Node", "moduleResolution": "Node",
"resolveJsonModule": true, // TODO: Disabled for IDE performance issues with our translation JSON
// "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",

File diff suppressed because it is too large Load Diff