mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
ui: enhance intermediates clear, enhance board auto-add (#3851)
* feat(ui): enhance clear intermediates feature - retrieve the # of intermediates using a new query (just uses list images endpoint w/ limit of 0) - display the count in the UI - add types for clearIntermediates mutation - minor styling and verbiage changes * feat(ui): remove unused settings option for guides * feat(ui): use solid badge variant consistent with the rest of the usage of badges * feat(ui): update board ctx menu, add board auto-add - add context menu to system boards - only open is select board. did this so that you dont think its broken when you click it - add auto-add board. you can right click a user board to enable it for auto-add, or use the gallery settings popover to select it. the invoke button has a tooltip on a short delay to remind you that you have auto-add enabled - made useBoardName hook, provide it a board id and it gets your the board name - removed `boardIdToAdTo` state & logic, updated workflows to auto-switch and auto-add on image generation * fix(ui): clear controlnet when clearing intermediates * feat: Make Add Board icon a button * feat(db, api): clear intermediates now clears all of them * feat(ui): make reset webui text subtext style * feat(ui): board name change submits on blur --------- Co-authored-by: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com>
This commit is contained in:
parent
82554b25fe
commit
187cf906fa
@ -1,8 +1,7 @@
|
||||
import io
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import (Body, HTTPException, Path, Query, Request, Response,
|
||||
UploadFile)
|
||||
from fastapi import Body, HTTPException, Path, Query, Request, Response, UploadFile
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.routing import APIRouter
|
||||
from PIL import Image
|
||||
@ -11,9 +10,11 @@ from invokeai.app.invocations.metadata import ImageMetadata
|
||||
from invokeai.app.models.image import ImageCategory, ResourceOrigin
|
||||
from invokeai.app.services.image_record_storage import OffsetPaginatedResults
|
||||
from invokeai.app.services.item_storage import PaginatedResults
|
||||
from invokeai.app.services.models.image_record import (ImageDTO,
|
||||
ImageRecordChanges,
|
||||
ImageUrlsDTO)
|
||||
from invokeai.app.services.models.image_record import (
|
||||
ImageDTO,
|
||||
ImageRecordChanges,
|
||||
ImageUrlsDTO,
|
||||
)
|
||||
|
||||
from ..dependencies import ApiDependencies
|
||||
|
||||
@ -84,15 +85,16 @@ async def delete_image(
|
||||
# TODO: Does this need any exception handling at all?
|
||||
pass
|
||||
|
||||
|
||||
@images_router.post("/clear-intermediates", operation_id="clear_intermediates")
|
||||
async def clear_intermediates() -> int:
|
||||
"""Clears first 100 intermediates"""
|
||||
"""Clears all intermediates"""
|
||||
|
||||
try:
|
||||
count_deleted = ApiDependencies.invoker.services.images.delete_many(is_intermediate=True)
|
||||
count_deleted = ApiDependencies.invoker.services.images.delete_intermediates()
|
||||
return count_deleted
|
||||
except Exception as e:
|
||||
# TODO: Does this need any exception handling at all?
|
||||
raise HTTPException(status_code=500, detail="Failed to clear intermediates")
|
||||
pass
|
||||
|
||||
|
||||
@ -130,6 +132,7 @@ async def get_image_dto(
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
|
||||
@images_router.get(
|
||||
"/{image_name}/metadata",
|
||||
operation_id="get_image_metadata",
|
||||
@ -254,7 +257,8 @@ async def list_image_dtos(
|
||||
default=None, description="Whether to list intermediate images."
|
||||
),
|
||||
board_id: Optional[str] = Query(
|
||||
default=None, description="The board id to filter by. Use 'none' to find images without a board."
|
||||
default=None,
|
||||
description="The board id to filter by. Use 'none' to find images without a board.",
|
||||
),
|
||||
offset: int = Query(default=0, description="The page offset"),
|
||||
limit: int = Query(default=10, description="The number of images per page"),
|
||||
|
@ -122,6 +122,11 @@ class ImageRecordStorageBase(ABC):
|
||||
"""Deletes many image records."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete_intermediates(self) -> list[str]:
|
||||
"""Deletes all intermediate image records, returning a list of deleted image names."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def save(
|
||||
self,
|
||||
@ -461,6 +466,32 @@ class SqliteImageRecordStorage(ImageRecordStorageBase):
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
|
||||
def delete_intermediates(self) -> list[str]:
|
||||
try:
|
||||
self._lock.acquire()
|
||||
self._cursor.execute(
|
||||
"""--sql
|
||||
SELECT image_name FROM images
|
||||
WHERE is_intermediate = TRUE;
|
||||
"""
|
||||
)
|
||||
result = cast(list[sqlite3.Row], self._cursor.fetchall())
|
||||
image_names = list(map(lambda r: r[0], result))
|
||||
self._cursor.execute(
|
||||
"""--sql
|
||||
DELETE FROM images
|
||||
WHERE is_intermediate = TRUE;
|
||||
"""
|
||||
)
|
||||
self._conn.commit()
|
||||
return image_names
|
||||
except sqlite3.Error as e:
|
||||
self._conn.rollback()
|
||||
raise ImageRecordDeleteException from e
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
def save(
|
||||
self,
|
||||
image_name: str,
|
||||
|
@ -6,21 +6,33 @@ from typing import TYPE_CHECKING, Optional
|
||||
from PIL.Image import Image as PILImageType
|
||||
|
||||
from invokeai.app.invocations.metadata import ImageMetadata
|
||||
from invokeai.app.models.image import (ImageCategory,
|
||||
InvalidImageCategoryException,
|
||||
InvalidOriginException, ResourceOrigin)
|
||||
from invokeai.app.services.board_image_record_storage import \
|
||||
BoardImageRecordStorageBase
|
||||
from invokeai.app.models.image import (
|
||||
ImageCategory,
|
||||
InvalidImageCategoryException,
|
||||
InvalidOriginException,
|
||||
ResourceOrigin,
|
||||
)
|
||||
from invokeai.app.services.board_image_record_storage import BoardImageRecordStorageBase
|
||||
from invokeai.app.services.image_file_storage import (
|
||||
ImageFileDeleteException, ImageFileNotFoundException,
|
||||
ImageFileSaveException, ImageFileStorageBase)
|
||||
ImageFileDeleteException,
|
||||
ImageFileNotFoundException,
|
||||
ImageFileSaveException,
|
||||
ImageFileStorageBase,
|
||||
)
|
||||
from invokeai.app.services.image_record_storage import (
|
||||
ImageRecordDeleteException, ImageRecordNotFoundException,
|
||||
ImageRecordSaveException, ImageRecordStorageBase, OffsetPaginatedResults)
|
||||
ImageRecordDeleteException,
|
||||
ImageRecordNotFoundException,
|
||||
ImageRecordSaveException,
|
||||
ImageRecordStorageBase,
|
||||
OffsetPaginatedResults,
|
||||
)
|
||||
from invokeai.app.services.item_storage import ItemStorageABC
|
||||
from invokeai.app.services.models.image_record import (ImageDTO, ImageRecord,
|
||||
ImageRecordChanges,
|
||||
image_record_to_dto)
|
||||
from invokeai.app.services.models.image_record import (
|
||||
ImageDTO,
|
||||
ImageRecord,
|
||||
ImageRecordChanges,
|
||||
image_record_to_dto,
|
||||
)
|
||||
from invokeai.app.services.resource_name import NameServiceBase
|
||||
from invokeai.app.services.urls import UrlServiceBase
|
||||
from invokeai.app.util.metadata import get_metadata_graph_from_raw_session
|
||||
@ -109,12 +121,10 @@ class ImageServiceABC(ABC):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete_many(self, is_intermediate: bool) -> int:
|
||||
"""Deletes many images."""
|
||||
def delete_intermediates(self) -> int:
|
||||
"""Deletes all intermediate images."""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def delete_images_on_board(self, board_id: str):
|
||||
"""Deletes all images on a board."""
|
||||
@ -401,21 +411,13 @@ class ImageService(ImageServiceABC):
|
||||
except Exception as e:
|
||||
self._services.logger.error("Problem deleting image records and files")
|
||||
raise e
|
||||
|
||||
def delete_many(self, is_intermediate: bool):
|
||||
|
||||
def delete_intermediates(self) -> int:
|
||||
try:
|
||||
# only clears 100 at a time
|
||||
images = self._services.image_records.get_many(offset=0, limit=100, is_intermediate=is_intermediate,)
|
||||
count = len(images.items)
|
||||
image_name_list = list(
|
||||
map(
|
||||
lambda r: r.image_name,
|
||||
images.items,
|
||||
)
|
||||
)
|
||||
for image_name in image_name_list:
|
||||
image_names = self._services.image_records.delete_intermediates()
|
||||
count = len(image_names)
|
||||
for image_name in image_names:
|
||||
self._services.image_files.delete(image_name)
|
||||
self._services.image_records.delete_many(image_name_list)
|
||||
return count
|
||||
except ImageRecordDeleteException:
|
||||
self._services.logger.error(f"Failed to delete image records")
|
||||
|
@ -6,11 +6,7 @@ import {
|
||||
imageSelected,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { progressImageSet } from 'features/system/store/systemSlice';
|
||||
import {
|
||||
SYSTEM_BOARDS,
|
||||
imagesAdapter,
|
||||
imagesApi,
|
||||
} from 'services/api/endpoints/images';
|
||||
import { imagesAdapter, imagesApi } from 'services/api/endpoints/images';
|
||||
import { isImageOutput } from 'services/api/guards';
|
||||
import { sessionCanceled } from 'services/api/thunks/session';
|
||||
import {
|
||||
@ -32,8 +28,7 @@ export const addInvocationCompleteEventListener = () => {
|
||||
);
|
||||
const session_id = action.payload.data.graph_execution_state_id;
|
||||
|
||||
const { cancelType, isCancelScheduled, boardIdToAddTo } =
|
||||
getState().system;
|
||||
const { cancelType, isCancelScheduled } = getState().system;
|
||||
|
||||
// Handle scheduled cancelation
|
||||
if (cancelType === 'scheduled' && isCancelScheduled) {
|
||||
@ -88,26 +83,28 @@ export const addInvocationCompleteEventListener = () => {
|
||||
)
|
||||
);
|
||||
|
||||
// add image to the board if we had one selected
|
||||
if (boardIdToAddTo && !SYSTEM_BOARDS.includes(boardIdToAddTo)) {
|
||||
const { autoAddBoardId } = gallery;
|
||||
|
||||
// add image to the board if auto-add is enabled
|
||||
if (autoAddBoardId) {
|
||||
dispatch(
|
||||
imagesApi.endpoints.addImageToBoard.initiate({
|
||||
board_id: boardIdToAddTo,
|
||||
board_id: autoAddBoardId,
|
||||
imageDTO,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const { selectedBoardId } = gallery;
|
||||
|
||||
if (boardIdToAddTo && boardIdToAddTo !== selectedBoardId) {
|
||||
dispatch(boardIdSelected(boardIdToAddTo));
|
||||
} else if (!boardIdToAddTo) {
|
||||
dispatch(boardIdSelected('all'));
|
||||
}
|
||||
const { selectedBoardId, shouldAutoSwitch } = gallery;
|
||||
|
||||
// If auto-switch is enabled, select the new image
|
||||
if (getState().gallery.shouldAutoSwitch) {
|
||||
if (shouldAutoSwitch) {
|
||||
// if auto-add is enabled, switch the board as the image comes in
|
||||
if (autoAddBoardId && autoAddBoardId !== selectedBoardId) {
|
||||
dispatch(boardIdSelected(autoAddBoardId));
|
||||
} else if (!autoAddBoardId) {
|
||||
dispatch(boardIdSelected('images'));
|
||||
}
|
||||
dispatch(imageSelected(imageDTO.image_name));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,80 @@
|
||||
import { SelectItem } from '@mantine/core';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
|
||||
import IAIMantineSelectItemWithTooltip from 'common/components/IAIMantineSelectItemWithTooltip';
|
||||
import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
||||
|
||||
const selector = createSelector(
|
||||
[stateSelector],
|
||||
({ gallery }) => {
|
||||
const { autoAddBoardId } = gallery;
|
||||
|
||||
return {
|
||||
autoAddBoardId,
|
||||
};
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
const BoardAutoAddSelect = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { autoAddBoardId } = useAppSelector(selector);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const { boards, hasBoards } = useListAllBoardsQuery(undefined, {
|
||||
selectFromResult: ({ data }) => {
|
||||
const boards: SelectItem[] = [
|
||||
{
|
||||
label: 'None',
|
||||
value: 'none',
|
||||
},
|
||||
];
|
||||
data?.forEach(({ board_id, board_name }) => {
|
||||
boards.push({
|
||||
label: board_name,
|
||||
value: board_id,
|
||||
});
|
||||
});
|
||||
return {
|
||||
boards,
|
||||
hasBoards: boards.length > 1,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const handleChange = useCallback(
|
||||
(v: string | null) => {
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(autoAddBoardIdChanged(v === 'none' ? null : v));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<IAIMantineSearchableSelect
|
||||
label="Auto-Add Board"
|
||||
inputRef={inputRef}
|
||||
autoFocus
|
||||
placeholder={'Select a Board'}
|
||||
value={autoAddBoardId}
|
||||
data={boards}
|
||||
nothingFound="No matching Boards"
|
||||
itemComponent={IAIMantineSelectItemWithTooltip}
|
||||
disabled={!hasBoards}
|
||||
filter={(value, item: SelectItem) =>
|
||||
item.label?.toLowerCase().includes(value.toLowerCase().trim()) ||
|
||||
item.value.toLowerCase().includes(value.toLowerCase().trim())
|
||||
}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoardAutoAddSelect;
|
@ -0,0 +1,60 @@
|
||||
import { Box, MenuItem, MenuList } from '@chakra-ui/react';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu';
|
||||
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { FaFolder } from 'react-icons/fa';
|
||||
import { BoardDTO } from 'services/api/types';
|
||||
import { menuListMotionProps } from 'theme/components/menu';
|
||||
import GalleryBoardContextMenuItems from './GalleryBoardContextMenuItems';
|
||||
import SystemBoardContextMenuItems from './SystemBoardContextMenuItems';
|
||||
|
||||
type Props = {
|
||||
board?: BoardDTO;
|
||||
board_id: string;
|
||||
children: ContextMenuProps<HTMLDivElement>['children'];
|
||||
setBoardToDelete?: (board?: BoardDTO) => void;
|
||||
};
|
||||
|
||||
const BoardContextMenu = memo(
|
||||
({ board, board_id, setBoardToDelete, children }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const handleSelectBoard = useCallback(() => {
|
||||
dispatch(boardIdSelected(board?.board_id ?? board_id));
|
||||
}, [board?.board_id, board_id, dispatch]);
|
||||
return (
|
||||
<Box sx={{ touchAction: 'none', height: 'full' }}>
|
||||
<ContextMenu<HTMLDivElement>
|
||||
menuProps={{ size: 'sm', isLazy: true }}
|
||||
menuButtonProps={{
|
||||
bg: 'transparent',
|
||||
_hover: { bg: 'transparent' },
|
||||
}}
|
||||
renderMenu={() => (
|
||||
<MenuList
|
||||
sx={{ visibility: 'visible !important' }}
|
||||
motionProps={menuListMotionProps}
|
||||
>
|
||||
<MenuItem icon={<FaFolder />} onClickCapture={handleSelectBoard}>
|
||||
Select Board
|
||||
</MenuItem>
|
||||
{!board && <SystemBoardContextMenuItems board_id={board_id} />}
|
||||
{board && (
|
||||
<GalleryBoardContextMenuItems
|
||||
board={board}
|
||||
setBoardToDelete={setBoardToDelete}
|
||||
/>
|
||||
)}
|
||||
</MenuList>
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</ContextMenu>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
BoardContextMenu.displayName = 'HoverableBoard';
|
||||
|
||||
export default BoardContextMenu;
|
@ -1,5 +1,6 @@
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { useCallback } from 'react';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
import { useCreateBoardMutation } from 'services/api/endpoints/boards';
|
||||
|
||||
const DEFAULT_BOARD_NAME = 'My Board';
|
||||
@ -12,15 +13,14 @@ const AddBoardButton = () => {
|
||||
}, [createBoard]);
|
||||
|
||||
return (
|
||||
<IAIButton
|
||||
<IAIIconButton
|
||||
icon={<FaPlus />}
|
||||
isLoading={isLoading}
|
||||
tooltip="Add Board"
|
||||
aria-label="Add Board"
|
||||
onClick={handleCreateBoard}
|
||||
size="sm"
|
||||
sx={{ px: 4 }}
|
||||
>
|
||||
Add Board
|
||||
</IAIButton>
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -38,6 +38,7 @@ const AllAssetsBoard = ({ isSelected }: { isSelected: boolean }) => {
|
||||
|
||||
return (
|
||||
<GenericBoard
|
||||
board_id="assets"
|
||||
onClick={handleClick}
|
||||
isSelected={isSelected}
|
||||
icon={FaFileImage}
|
||||
|
@ -38,6 +38,7 @@ const AllImagesBoard = ({ isSelected }: { isSelected: boolean }) => {
|
||||
|
||||
return (
|
||||
<GenericBoard
|
||||
board_id="images"
|
||||
onClick={handleClick}
|
||||
isSelected={isSelected}
|
||||
icon={FaImages}
|
||||
|
@ -29,6 +29,7 @@ const BatchBoard = ({ isSelected }: { isSelected: boolean }) => {
|
||||
|
||||
return (
|
||||
<GenericBoard
|
||||
board_id="batch"
|
||||
droppableData={droppableData}
|
||||
onClick={handleBatchBoardClick}
|
||||
isSelected={isSelected}
|
||||
|
@ -1,31 +1,41 @@
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
ChakraProps,
|
||||
Editable,
|
||||
EditableInput,
|
||||
EditablePreview,
|
||||
Flex,
|
||||
Image,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Text,
|
||||
useColorMode,
|
||||
} from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||
import { MoveBoardDropData } from 'app/components/ImageDnd/typesafeDnd';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { ContextMenu } from 'chakra-ui-contextmenu';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import IAIDroppable from 'common/components/IAIDroppable';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { FaTrash, FaUser } from 'react-icons/fa';
|
||||
import { FaUser } from 'react-icons/fa';
|
||||
import { useUpdateBoardMutation } from 'services/api/endpoints/boards';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import { BoardDTO } from 'services/api/types';
|
||||
import { menuListMotionProps } from 'theme/components/menu';
|
||||
import { mode } from 'theme/util/mode';
|
||||
import BoardContextMenu from '../BoardContextMenu';
|
||||
|
||||
const AUTO_ADD_BADGE_STYLES: ChakraProps['sx'] = {
|
||||
bg: 'accent.200',
|
||||
color: 'blackAlpha.900',
|
||||
};
|
||||
|
||||
const BASE_BADGE_STYLES: ChakraProps['sx'] = {
|
||||
bg: 'base.500',
|
||||
color: 'whiteAlpha.900',
|
||||
};
|
||||
interface GalleryBoardProps {
|
||||
board: BoardDTO;
|
||||
isSelected: boolean;
|
||||
@ -35,6 +45,22 @@ interface GalleryBoardProps {
|
||||
const GalleryBoard = memo(
|
||||
({ board, isSelected, setBoardToDelete }: GalleryBoardProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
stateSelector,
|
||||
({ gallery }) => {
|
||||
const isSelectedForAutoAdd =
|
||||
board.board_id === gallery.autoAddBoardId;
|
||||
|
||||
return { isSelectedForAutoAdd };
|
||||
},
|
||||
defaultSelectorOptions
|
||||
),
|
||||
[board.board_id]
|
||||
);
|
||||
|
||||
const { isSelectedForAutoAdd } = useAppSelector(selector);
|
||||
|
||||
const { currentData: coverImage } = useGetImageDTOQuery(
|
||||
board.cover_image_name ?? skipToken
|
||||
@ -53,10 +79,6 @@ const GalleryBoard = memo(
|
||||
updateBoard({ board_id, changes: { board_name: newBoardName } });
|
||||
};
|
||||
|
||||
const handleDeleteBoard = useCallback(() => {
|
||||
setBoardToDelete(board);
|
||||
}, [board, setBoardToDelete]);
|
||||
|
||||
const droppableData: MoveBoardDropData = useMemo(
|
||||
() => ({
|
||||
id: board_id,
|
||||
@ -68,37 +90,10 @@ const GalleryBoard = memo(
|
||||
|
||||
return (
|
||||
<Box sx={{ touchAction: 'none', height: 'full' }}>
|
||||
<ContextMenu<HTMLDivElement>
|
||||
menuProps={{ size: 'sm', isLazy: true }}
|
||||
menuButtonProps={{
|
||||
bg: 'transparent',
|
||||
_hover: { bg: 'transparent' },
|
||||
}}
|
||||
renderMenu={() => (
|
||||
<MenuList
|
||||
sx={{ visibility: 'visible !important' }}
|
||||
motionProps={menuListMotionProps}
|
||||
>
|
||||
{board.image_count > 0 && (
|
||||
<>
|
||||
{/* <MenuItem
|
||||
isDisabled={!board.image_count}
|
||||
icon={<FaImages />}
|
||||
onClickCapture={handleAddBoardToBatch}
|
||||
>
|
||||
Add Board to Batch
|
||||
</MenuItem> */}
|
||||
</>
|
||||
)}
|
||||
<MenuItem
|
||||
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
||||
icon={<FaTrash />}
|
||||
onClickCapture={handleDeleteBoard}
|
||||
>
|
||||
Delete Board
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
)}
|
||||
<BoardContextMenu
|
||||
board={board}
|
||||
board_id={board_id}
|
||||
setBoardToDelete={setBoardToDelete}
|
||||
>
|
||||
{(ref) => (
|
||||
<Flex
|
||||
@ -154,7 +149,16 @@ const GalleryBoard = memo(
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
<Badge variant="solid">{board.image_count}</Badge>
|
||||
<Badge
|
||||
variant="solid"
|
||||
sx={
|
||||
isSelectedForAutoAdd
|
||||
? AUTO_ADD_BADGE_STYLES
|
||||
: BASE_BADGE_STYLES
|
||||
}
|
||||
>
|
||||
{board.image_count}
|
||||
</Badge>
|
||||
</Flex>
|
||||
<IAIDroppable
|
||||
data={droppableData}
|
||||
@ -172,7 +176,7 @@ const GalleryBoard = memo(
|
||||
>
|
||||
<Editable
|
||||
defaultValue={board_name}
|
||||
submitOnBlur={false}
|
||||
submitOnBlur={true}
|
||||
onSubmit={(nextValue) => {
|
||||
handleUpdateBoardName(nextValue);
|
||||
}}
|
||||
@ -205,7 +209,7 @@ const GalleryBoard = memo(
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
</ContextMenu>
|
||||
</BoardContextMenu>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
@ -2,9 +2,12 @@ import { As, Badge, Flex } from '@chakra-ui/react';
|
||||
import { TypesafeDroppableData } from 'app/components/ImageDnd/typesafeDnd';
|
||||
import IAIDroppable from 'common/components/IAIDroppable';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import { BoardId } from 'features/gallery/store/gallerySlice';
|
||||
import { ReactNode } from 'react';
|
||||
import BoardContextMenu from '../BoardContextMenu';
|
||||
|
||||
type GenericBoardProps = {
|
||||
board_id: BoardId;
|
||||
droppableData?: TypesafeDroppableData;
|
||||
onClick: () => void;
|
||||
isSelected: boolean;
|
||||
@ -22,6 +25,7 @@ const formatBadgeCount = (count: number) =>
|
||||
|
||||
const GenericBoard = (props: GenericBoardProps) => {
|
||||
const {
|
||||
board_id,
|
||||
droppableData,
|
||||
onClick,
|
||||
isSelected,
|
||||
@ -32,67 +36,72 @@ const GenericBoard = (props: GenericBoardProps) => {
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
flexDir: 'column',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
borderRadius: 'base',
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: 'base',
|
||||
w: 'full',
|
||||
aspectRatio: '1/1',
|
||||
overflow: 'hidden',
|
||||
shadow: isSelected ? 'selected.light' : undefined,
|
||||
_dark: { shadow: isSelected ? 'selected.dark' : undefined },
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<IAINoContentFallback
|
||||
boxSize={8}
|
||||
icon={icon}
|
||||
sx={{
|
||||
border: '2px solid var(--invokeai-colors-base-200)',
|
||||
_dark: { border: '2px solid var(--invokeai-colors-base-800)' },
|
||||
}}
|
||||
/>
|
||||
<BoardContextMenu board_id={board_id}>
|
||||
{(ref) => (
|
||||
<Flex
|
||||
ref={ref}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
insetInlineEnd: 0,
|
||||
top: 0,
|
||||
p: 1,
|
||||
flexDir: 'column',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
borderRadius: 'base',
|
||||
}}
|
||||
>
|
||||
{badgeCount !== undefined && (
|
||||
<Badge variant="solid">{formatBadgeCount(badgeCount)}</Badge>
|
||||
)}
|
||||
<Flex
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: 'base',
|
||||
w: 'full',
|
||||
aspectRatio: '1/1',
|
||||
overflow: 'hidden',
|
||||
shadow: isSelected ? 'selected.light' : undefined,
|
||||
_dark: { shadow: isSelected ? 'selected.dark' : undefined },
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<IAINoContentFallback
|
||||
boxSize={8}
|
||||
icon={icon}
|
||||
sx={{
|
||||
border: '2px solid var(--invokeai-colors-base-200)',
|
||||
_dark: { border: '2px solid var(--invokeai-colors-base-800)' },
|
||||
}}
|
||||
/>
|
||||
<Flex
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
insetInlineEnd: 0,
|
||||
top: 0,
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
{badgeCount !== undefined && (
|
||||
<Badge variant="solid">{formatBadgeCount(badgeCount)}</Badge>
|
||||
)}
|
||||
</Flex>
|
||||
<IAIDroppable data={droppableData} dropLabel={dropLabel} />
|
||||
</Flex>
|
||||
<Flex
|
||||
sx={{
|
||||
h: 'full',
|
||||
alignItems: 'center',
|
||||
fontWeight: isSelected ? 600 : undefined,
|
||||
fontSize: 'xs',
|
||||
color: isSelected ? 'base.900' : 'base.700',
|
||||
_dark: { color: isSelected ? 'base.50' : 'base.200' },
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Flex>
|
||||
</Flex>
|
||||
<IAIDroppable data={droppableData} dropLabel={dropLabel} />
|
||||
</Flex>
|
||||
<Flex
|
||||
sx={{
|
||||
h: 'full',
|
||||
alignItems: 'center',
|
||||
fontWeight: isSelected ? 600 : undefined,
|
||||
fontSize: 'xs',
|
||||
color: isSelected ? 'base.900' : 'base.700',
|
||||
_dark: { color: isSelected ? 'base.50' : 'base.200' },
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
</BoardContextMenu>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -39,6 +39,7 @@ const NoBoardBoard = ({ isSelected }: { isSelected: boolean }) => {
|
||||
|
||||
return (
|
||||
<GenericBoard
|
||||
board_id="no_board"
|
||||
droppableData={droppableData}
|
||||
dropLabel={<Text fontSize="md">Move</Text>}
|
||||
onClick={handleClick}
|
||||
|
@ -0,0 +1,79 @@
|
||||
import { MenuItem } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { FaMinus, FaPlus, FaTrash } from 'react-icons/fa';
|
||||
import { BoardDTO } from 'services/api/types';
|
||||
|
||||
type Props = {
|
||||
board: BoardDTO;
|
||||
setBoardToDelete?: (board?: BoardDTO) => void;
|
||||
};
|
||||
|
||||
const GalleryBoardContextMenuItems = ({ board, setBoardToDelete }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
stateSelector,
|
||||
({ gallery }) => {
|
||||
const isSelectedForAutoAdd =
|
||||
board.board_id === gallery.autoAddBoardId;
|
||||
|
||||
return { isSelectedForAutoAdd };
|
||||
},
|
||||
defaultSelectorOptions
|
||||
),
|
||||
[board.board_id]
|
||||
);
|
||||
|
||||
const { isSelectedForAutoAdd } = useAppSelector(selector);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
if (!setBoardToDelete) {
|
||||
return;
|
||||
}
|
||||
setBoardToDelete(board);
|
||||
}, [board, setBoardToDelete]);
|
||||
|
||||
const handleToggleAutoAdd = useCallback(() => {
|
||||
dispatch(
|
||||
autoAddBoardIdChanged(isSelectedForAutoAdd ? null : board.board_id)
|
||||
);
|
||||
}, [board.board_id, dispatch, isSelectedForAutoAdd]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{board.image_count > 0 && (
|
||||
<>
|
||||
{/* <MenuItem
|
||||
isDisabled={!board.image_count}
|
||||
icon={<FaImages />}
|
||||
onClickCapture={handleAddBoardToBatch}
|
||||
>
|
||||
Add Board to Batch
|
||||
</MenuItem> */}
|
||||
</>
|
||||
)}
|
||||
<MenuItem
|
||||
icon={isSelectedForAutoAdd ? <FaMinus /> : <FaPlus />}
|
||||
onClickCapture={handleToggleAutoAdd}
|
||||
>
|
||||
{isSelectedForAutoAdd ? 'Disable Auto-Add' : 'Auto-Add to this Board'}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
||||
icon={<FaTrash />}
|
||||
onClickCapture={handleDelete}
|
||||
>
|
||||
Delete Board
|
||||
</MenuItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(GalleryBoardContextMenuItems);
|
@ -0,0 +1,12 @@
|
||||
import { BoardId } from 'features/gallery/store/gallerySlice';
|
||||
import { memo } from 'react';
|
||||
|
||||
type Props = {
|
||||
board_id: BoardId;
|
||||
};
|
||||
|
||||
const SystemBoardContextMenuItems = ({ board_id }: Props) => {
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export default memo(SystemBoardContextMenuItems);
|
@ -1,20 +1,18 @@
|
||||
import { ChevronUpIcon } from '@chakra-ui/icons';
|
||||
import { Button, Flex, Text } from '@chakra-ui/react';
|
||||
import { Box, Button, Flex, Spacer, Text } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { memo } from 'react';
|
||||
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
||||
import { useBoardName } from 'services/api/hooks/useBoardName';
|
||||
|
||||
const selector = createSelector(
|
||||
[stateSelector],
|
||||
(state) => {
|
||||
const { selectedBoardId } = state.gallery;
|
||||
|
||||
return {
|
||||
selectedBoardId,
|
||||
};
|
||||
return { selectedBoardId };
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
@ -27,25 +25,7 @@ type Props = {
|
||||
const GalleryBoardName = (props: Props) => {
|
||||
const { isOpen, onToggle } = props;
|
||||
const { selectedBoardId } = useAppSelector(selector);
|
||||
const { selectedBoardName } = useListAllBoardsQuery(undefined, {
|
||||
selectFromResult: ({ data }) => {
|
||||
let selectedBoardName = '';
|
||||
if (selectedBoardId === 'images') {
|
||||
selectedBoardName = 'All Images';
|
||||
} else if (selectedBoardId === 'assets') {
|
||||
selectedBoardName = 'All Assets';
|
||||
} else if (selectedBoardId === 'no_board') {
|
||||
selectedBoardName = 'No Board';
|
||||
} else if (selectedBoardId === 'batch') {
|
||||
selectedBoardName = 'Batch';
|
||||
} else {
|
||||
const selectedBoard = data?.find((b) => b.board_id === selectedBoardId);
|
||||
selectedBoardName = selectedBoard?.board_name || 'Unknown Board';
|
||||
}
|
||||
|
||||
return { selectedBoardName };
|
||||
},
|
||||
});
|
||||
const boardName = useBoardName(selectedBoardId);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@ -54,6 +34,8 @@ const GalleryBoardName = (props: Props) => {
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
sx={{
|
||||
position: 'relative',
|
||||
gap: 2,
|
||||
w: 'full',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
@ -64,19 +46,22 @@ const GalleryBoardName = (props: Props) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
noOfLines={1}
|
||||
sx={{
|
||||
w: 'full',
|
||||
fontWeight: 600,
|
||||
color: 'base.800',
|
||||
_dark: {
|
||||
color: 'base.200',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{selectedBoardName}
|
||||
</Text>
|
||||
<Spacer />
|
||||
<Box position="relative">
|
||||
<Text
|
||||
noOfLines={1}
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
color: 'base.800',
|
||||
_dark: {
|
||||
color: 'base.200',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{boardName}
|
||||
</Text>
|
||||
</Box>
|
||||
<Spacer />
|
||||
<ChevronUpIcon
|
||||
sx={{
|
||||
transform: isOpen ? 'rotate(0deg)' : 'rotate(180deg)',
|
||||
|
@ -1,19 +1,20 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import IAIPopover from 'common/components/IAIPopover';
|
||||
import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox';
|
||||
import IAISlider from 'common/components/IAISlider';
|
||||
import { setGalleryImageMinimumWidth } from 'features/gallery/store/gallerySlice';
|
||||
|
||||
import {
|
||||
setGalleryImageMinimumWidth,
|
||||
shouldAutoSwitchChanged,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { ChangeEvent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaWrench } from 'react-icons/fa';
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { shouldAutoSwitchChanged } from 'features/gallery/store/gallerySlice';
|
||||
import BoardAutoAddSelect from './Boards/BoardAutoAddSelect';
|
||||
|
||||
const selector = createSelector(
|
||||
[stateSelector],
|
||||
@ -50,7 +51,7 @@ const GallerySettingsPopover = () => {
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Flex direction="column" gap={2}>
|
||||
<Flex direction="column" gap={4}>
|
||||
<IAISlider
|
||||
value={galleryImageMinimumWidth}
|
||||
onChange={handleChangeGalleryImageMinimumWidth}
|
||||
@ -68,6 +69,7 @@ const GallerySettingsPopover = () => {
|
||||
dispatch(shouldAutoSwitchChanged(e.target.checked))
|
||||
}
|
||||
/>
|
||||
<BoardAutoAddSelect />
|
||||
</Flex>
|
||||
</IAIPopover>
|
||||
);
|
||||
|
@ -25,6 +25,7 @@ export type BoardId =
|
||||
type GalleryState = {
|
||||
selection: string[];
|
||||
shouldAutoSwitch: boolean;
|
||||
autoAddBoardId: string | null;
|
||||
galleryImageMinimumWidth: number;
|
||||
selectedBoardId: BoardId;
|
||||
batchImageNames: string[];
|
||||
@ -34,6 +35,7 @@ type GalleryState = {
|
||||
export const initialGalleryState: GalleryState = {
|
||||
selection: [],
|
||||
shouldAutoSwitch: true,
|
||||
autoAddBoardId: null,
|
||||
galleryImageMinimumWidth: 96,
|
||||
selectedBoardId: 'images',
|
||||
batchImageNames: [],
|
||||
@ -123,14 +125,34 @@ export const gallerySlice = createSlice({
|
||||
state.batchImageNames = [];
|
||||
state.selection = [];
|
||||
},
|
||||
autoAddBoardIdChanged: (state, action: PayloadAction<string | null>) => {
|
||||
state.autoAddBoardId = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addMatcher(
|
||||
boardsApi.endpoints.deleteBoard.matchFulfilled,
|
||||
(state, action) => {
|
||||
if (action.meta.arg.originalArgs === state.selectedBoardId) {
|
||||
const deletedBoardId = action.meta.arg.originalArgs;
|
||||
if (deletedBoardId === state.selectedBoardId) {
|
||||
state.selectedBoardId = 'images';
|
||||
}
|
||||
if (deletedBoardId === state.autoAddBoardId) {
|
||||
state.autoAddBoardId = null;
|
||||
}
|
||||
}
|
||||
);
|
||||
builder.addMatcher(
|
||||
boardsApi.endpoints.listAllBoards.matchFulfilled,
|
||||
(state, action) => {
|
||||
const boards = action.payload;
|
||||
if (!state.autoAddBoardId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!boards.map((b) => b.board_id).includes(state.autoAddBoardId)) {
|
||||
state.autoAddBoardId = null;
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
@ -147,6 +169,7 @@ export const {
|
||||
isBatchEnabledChanged,
|
||||
imagesAddedToBatch,
|
||||
imagesRemovedFromBatch,
|
||||
autoAddBoardIdChanged,
|
||||
} = gallerySlice.actions;
|
||||
|
||||
export default gallerySlice.reducer;
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { Box, ChakraProps } from '@chakra-ui/react';
|
||||
import { Box, ChakraProps, Tooltip } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { userInvoked } from 'app/store/actions';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import IAIButton, { IAIButtonProps } from 'common/components/IAIButton';
|
||||
import IAIIconButton, {
|
||||
IAIIconButtonProps,
|
||||
@ -8,11 +11,13 @@ import IAIIconButton, {
|
||||
import { useIsReadyToInvoke } from 'common/hooks/useIsReadyToInvoke';
|
||||
import { clampSymmetrySteps } from 'features/parameters/store/generationSlice';
|
||||
import ProgressBar from 'features/system/components/ProgressBar';
|
||||
import { selectIsBusy } from 'features/system/store/systemSelectors';
|
||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||
import { useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaPlay } from 'react-icons/fa';
|
||||
import { useBoardName } from 'services/api/hooks/useBoardName';
|
||||
|
||||
const IN_PROGRESS_STYLES: ChakraProps['sx'] = {
|
||||
_disabled: {
|
||||
@ -26,6 +31,20 @@ const IN_PROGRESS_STYLES: ChakraProps['sx'] = {
|
||||
},
|
||||
};
|
||||
|
||||
const selector = createSelector(
|
||||
[stateSelector, activeTabNameSelector, selectIsBusy],
|
||||
({ gallery }, activeTabName, isBusy) => {
|
||||
const { autoAddBoardId } = gallery;
|
||||
|
||||
return {
|
||||
isBusy,
|
||||
autoAddBoardId,
|
||||
activeTabName,
|
||||
};
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
interface InvokeButton
|
||||
extends Omit<IAIButtonProps | IAIIconButtonProps, 'aria-label'> {
|
||||
iconButton?: boolean;
|
||||
@ -35,8 +54,8 @@ export default function InvokeButton(props: InvokeButton) {
|
||||
const { iconButton = false, ...rest } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
const isReady = useIsReadyToInvoke();
|
||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||
const isProcessing = useAppSelector((state) => state.system.isProcessing);
|
||||
const { isBusy, autoAddBoardId, activeTabName } = useAppSelector(selector);
|
||||
const autoAddBoardName = useBoardName(autoAddBoardId);
|
||||
|
||||
const handleInvoke = useCallback(() => {
|
||||
dispatch(clampSymmetrySteps());
|
||||
@ -75,43 +94,52 @@ export default function InvokeButton(props: InvokeButton) {
|
||||
<ProgressBar />
|
||||
</Box>
|
||||
)}
|
||||
{iconButton ? (
|
||||
<IAIIconButton
|
||||
aria-label={t('parameters.invoke')}
|
||||
type="submit"
|
||||
icon={<FaPlay />}
|
||||
isDisabled={!isReady || isProcessing}
|
||||
onClick={handleInvoke}
|
||||
tooltip={t('parameters.invoke')}
|
||||
tooltipProps={{ placement: 'top' }}
|
||||
colorScheme="accent"
|
||||
id="invoke-button"
|
||||
{...rest}
|
||||
sx={{
|
||||
w: 'full',
|
||||
flexGrow: 1,
|
||||
...(isProcessing ? IN_PROGRESS_STYLES : {}),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<IAIButton
|
||||
aria-label={t('parameters.invoke')}
|
||||
type="submit"
|
||||
isDisabled={!isReady || isProcessing}
|
||||
onClick={handleInvoke}
|
||||
colorScheme="accent"
|
||||
id="invoke-button"
|
||||
{...rest}
|
||||
sx={{
|
||||
w: 'full',
|
||||
flexGrow: 1,
|
||||
fontWeight: 700,
|
||||
...(isProcessing ? IN_PROGRESS_STYLES : {}),
|
||||
}}
|
||||
>
|
||||
Invoke
|
||||
</IAIButton>
|
||||
)}
|
||||
<Tooltip
|
||||
placement="top"
|
||||
hasArrow
|
||||
openDelay={500}
|
||||
label={
|
||||
autoAddBoardId ? `Auto-Adding to ${autoAddBoardName}` : undefined
|
||||
}
|
||||
>
|
||||
{iconButton ? (
|
||||
<IAIIconButton
|
||||
aria-label={t('parameters.invoke')}
|
||||
type="submit"
|
||||
icon={<FaPlay />}
|
||||
isDisabled={!isReady || isBusy}
|
||||
onClick={handleInvoke}
|
||||
tooltip={t('parameters.invoke')}
|
||||
tooltipProps={{ placement: 'top' }}
|
||||
colorScheme="accent"
|
||||
id="invoke-button"
|
||||
{...rest}
|
||||
sx={{
|
||||
w: 'full',
|
||||
flexGrow: 1,
|
||||
...(isBusy ? IN_PROGRESS_STYLES : {}),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<IAIButton
|
||||
aria-label={t('parameters.invoke')}
|
||||
type="submit"
|
||||
isDisabled={!isReady || isBusy}
|
||||
onClick={handleInvoke}
|
||||
colorScheme="accent"
|
||||
id="invoke-button"
|
||||
{...rest}
|
||||
sx={{
|
||||
w: 'full',
|
||||
flexGrow: 1,
|
||||
fontWeight: 700,
|
||||
...(isBusy ? IN_PROGRESS_STYLES : {}),
|
||||
}}
|
||||
>
|
||||
Invoke
|
||||
</IAIButton>
|
||||
)}
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
@ -1,33 +0,0 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { postprocessingSelector } from 'features/parameters/store/postprocessingSelectors';
|
||||
import { setShouldLoopback } from 'features/parameters/store/postprocessingSlice';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaRecycle } from 'react-icons/fa';
|
||||
|
||||
const loopbackSelector = createSelector(
|
||||
postprocessingSelector,
|
||||
({ shouldLoopback }) => shouldLoopback
|
||||
);
|
||||
|
||||
const LoopbackButton = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const shouldLoopback = useAppSelector(loopbackSelector);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
aria-label={t('parameters.toggleLoopback')}
|
||||
tooltip={t('parameters.toggleLoopback')}
|
||||
isChecked={shouldLoopback}
|
||||
icon={<FaRecycle />}
|
||||
onClick={() => {
|
||||
dispatch(setShouldLoopback(!shouldLoopback));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoopbackButton;
|
@ -9,7 +9,6 @@ const ProcessButtons = () => {
|
||||
return (
|
||||
<Flex gap={2}>
|
||||
<InvokeButton />
|
||||
{/* {activeTabName === 'img2img' && <LoopbackButton />} */}
|
||||
<CancelButton />
|
||||
</Flex>
|
||||
);
|
||||
|
@ -1,60 +1,71 @@
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { StyledFlex } from './SettingsModal';
|
||||
import { Heading, Text } from '@chakra-ui/react';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import IAIButton from '../../../../common/components/IAIButton';
|
||||
import { useClearIntermediatesMutation } from '../../../../services/api/endpoints/images';
|
||||
import { addToast } from '../../store/systemSlice';
|
||||
import {
|
||||
useClearIntermediatesMutation,
|
||||
useGetIntermediatesCountQuery,
|
||||
} from '../../../../services/api/endpoints/images';
|
||||
import { resetCanvas } from '../../../canvas/store/canvasSlice';
|
||||
import { addToast } from '../../store/systemSlice';
|
||||
import { StyledFlex } from './SettingsModal';
|
||||
import { controlNetReset } from 'features/controlNet/store/controlNetSlice';
|
||||
|
||||
export default function SettingsClearIntermediates() {
|
||||
const dispatch = useAppDispatch();
|
||||
const [isDisabled, setIsDisabled] = useState(false);
|
||||
|
||||
const { data: intermediatesCount, refetch: updateIntermediatesCount } =
|
||||
useGetIntermediatesCountQuery();
|
||||
|
||||
const [clearIntermediates, { isLoading: isLoadingClearIntermediates }] =
|
||||
useClearIntermediatesMutation();
|
||||
|
||||
const handleClickClearIntermediates = useCallback(() => {
|
||||
clearIntermediates({})
|
||||
clearIntermediates()
|
||||
.unwrap()
|
||||
.then((response) => {
|
||||
dispatch(controlNetReset());
|
||||
dispatch(resetCanvas());
|
||||
dispatch(
|
||||
addToast({
|
||||
title:
|
||||
response === 0
|
||||
? `No intermediates to clear`
|
||||
: `Successfully cleared ${response} intermediates`,
|
||||
title: `Cleared ${response} intermediates`,
|
||||
status: 'info',
|
||||
})
|
||||
);
|
||||
if (response < 100) {
|
||||
setIsDisabled(true);
|
||||
}
|
||||
});
|
||||
}, [clearIntermediates, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
// update the count on mount
|
||||
updateIntermediatesCount();
|
||||
}, [updateIntermediatesCount]);
|
||||
|
||||
const buttonText = intermediatesCount
|
||||
? `Clear ${intermediatesCount} Intermediate${
|
||||
intermediatesCount > 1 ? 's' : ''
|
||||
}`
|
||||
: 'No Intermediates to Clear';
|
||||
|
||||
return (
|
||||
<StyledFlex>
|
||||
<Heading size="sm">Clear Intermediates</Heading>
|
||||
<IAIButton
|
||||
colorScheme="error"
|
||||
colorScheme="warning"
|
||||
onClick={handleClickClearIntermediates}
|
||||
isLoading={isLoadingClearIntermediates}
|
||||
isDisabled={isDisabled}
|
||||
isDisabled={!intermediatesCount}
|
||||
>
|
||||
{isDisabled ? 'Intermediates Cleared' : 'Clear 100 Intermediates'}
|
||||
{buttonText}
|
||||
</IAIButton>
|
||||
<Text>
|
||||
Will permanently delete first 100 intermediates found on disk and in
|
||||
database
|
||||
<Text fontWeight="bold">
|
||||
Clearing intermediates will reset your Canvas and ControlNet state.
|
||||
</Text>
|
||||
<Text fontWeight="bold">This will also clear your canvas state.</Text>
|
||||
<Text>
|
||||
<Text variant="subtext">
|
||||
Intermediate images are byproducts of generation, different from the
|
||||
result images in the gallery. Purging intermediates will free disk
|
||||
space. Your gallery images will not be deleted.
|
||||
result images in the gallery. Clearing intermediates will free disk
|
||||
space.
|
||||
</Text>
|
||||
<Text variant="subtext">Your gallery images will not be deleted.</Text>
|
||||
</StyledFlex>
|
||||
);
|
||||
}
|
||||
|
@ -24,7 +24,6 @@ import {
|
||||
setEnableImageDebugging,
|
||||
setIsNodesEnabled,
|
||||
setShouldConfirmOnDelete,
|
||||
setShouldDisplayGuides,
|
||||
shouldAntialiasProgressImageChanged,
|
||||
shouldLogToConsoleChanged,
|
||||
} from 'features/system/store/systemSlice';
|
||||
@ -56,7 +55,6 @@ const selector = createSelector(
|
||||
(system: SystemState, ui: UIState) => {
|
||||
const {
|
||||
shouldConfirmOnDelete,
|
||||
shouldDisplayGuides,
|
||||
enableImageDebugging,
|
||||
consoleLogLevel,
|
||||
shouldLogToConsole,
|
||||
@ -73,7 +71,6 @@ const selector = createSelector(
|
||||
|
||||
return {
|
||||
shouldConfirmOnDelete,
|
||||
shouldDisplayGuides,
|
||||
enableImageDebugging,
|
||||
shouldUseCanvasBetaLayout,
|
||||
shouldUseSliders,
|
||||
@ -139,7 +136,6 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
||||
|
||||
const {
|
||||
shouldConfirmOnDelete,
|
||||
shouldDisplayGuides,
|
||||
enableImageDebugging,
|
||||
shouldUseCanvasBetaLayout,
|
||||
shouldUseSliders,
|
||||
@ -195,7 +191,7 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
||||
<Modal
|
||||
isOpen={isSettingsModalOpen}
|
||||
onClose={onSettingsModalClose}
|
||||
size="xl"
|
||||
size="2xl"
|
||||
isCentered
|
||||
>
|
||||
<ModalOverlay />
|
||||
@ -231,14 +227,6 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
||||
|
||||
<StyledFlex>
|
||||
<Heading size="sm">{t('settings.ui')}</Heading>
|
||||
<SettingSwitch
|
||||
label={t('settings.displayHelpIcons')}
|
||||
isChecked={shouldDisplayGuides}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldDisplayGuides(e.target.checked))
|
||||
}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
label={t('settings.useSlidersForAll')}
|
||||
isChecked={shouldUseSliders}
|
||||
@ -317,8 +305,12 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
||||
</IAIButton>
|
||||
{shouldShowResetWebUiText && (
|
||||
<>
|
||||
<Text>{t('settings.resetWebUIDesc1')}</Text>
|
||||
<Text>{t('settings.resetWebUIDesc2')}</Text>
|
||||
<Text variant="subtext">
|
||||
{t('settings.resetWebUIDesc1')}
|
||||
</Text>
|
||||
<Text variant="subtext">
|
||||
{t('settings.resetWebUIDesc2')}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</StyledFlex>
|
||||
|
@ -38,7 +38,6 @@ export interface SystemState {
|
||||
currentIteration: number;
|
||||
totalIterations: number;
|
||||
currentStatusHasSteps: boolean;
|
||||
shouldDisplayGuides: boolean;
|
||||
isCancelable: boolean;
|
||||
enableImageDebugging: boolean;
|
||||
toastQueue: UseToastOptions[];
|
||||
@ -84,14 +83,12 @@ export interface SystemState {
|
||||
shouldAntialiasProgressImage: boolean;
|
||||
language: keyof typeof LANGUAGES;
|
||||
isUploading: boolean;
|
||||
boardIdToAddTo?: string;
|
||||
isNodesEnabled: boolean;
|
||||
}
|
||||
|
||||
export const initialSystemState: SystemState = {
|
||||
isConnected: false,
|
||||
isProcessing: false,
|
||||
shouldDisplayGuides: true,
|
||||
isGFPGANAvailable: true,
|
||||
isESRGANAvailable: true,
|
||||
shouldConfirmOnDelete: true,
|
||||
@ -134,9 +131,6 @@ export const systemSlice = createSlice({
|
||||
setShouldConfirmOnDelete: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldConfirmOnDelete = action.payload;
|
||||
},
|
||||
setShouldDisplayGuides: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldDisplayGuides = action.payload;
|
||||
},
|
||||
setIsCancelable: (state, action: PayloadAction<boolean>) => {
|
||||
state.isCancelable = action.payload;
|
||||
},
|
||||
@ -204,7 +198,6 @@ export const systemSlice = createSlice({
|
||||
*/
|
||||
builder.addCase(appSocketSubscribed, (state, action) => {
|
||||
state.sessionId = action.payload.sessionId;
|
||||
state.boardIdToAddTo = action.payload.boardId;
|
||||
state.canceledSession = '';
|
||||
});
|
||||
|
||||
@ -213,7 +206,6 @@ export const systemSlice = createSlice({
|
||||
*/
|
||||
builder.addCase(appSocketUnsubscribed, (state) => {
|
||||
state.sessionId = null;
|
||||
state.boardIdToAddTo = undefined;
|
||||
});
|
||||
|
||||
/**
|
||||
@ -390,7 +382,6 @@ export const {
|
||||
setIsProcessing,
|
||||
setShouldConfirmOnDelete,
|
||||
setCurrentStatus,
|
||||
setShouldDisplayGuides,
|
||||
setIsCancelable,
|
||||
setEnableImageDebugging,
|
||||
addToast,
|
||||
|
@ -98,16 +98,7 @@ export default function ModelListItem(props: ModelListItemProps) {
|
||||
onClick={handleSelectModel}
|
||||
>
|
||||
<Flex gap={4} alignItems="center">
|
||||
<Badge
|
||||
minWidth={14}
|
||||
p={1}
|
||||
fontSize="sm"
|
||||
sx={{
|
||||
bg: 'base.350',
|
||||
color: 'base.900',
|
||||
_dark: { bg: 'base.500' },
|
||||
}}
|
||||
>
|
||||
<Badge minWidth={14} p={0.5} fontSize="sm" variant="solid">
|
||||
{
|
||||
modelBaseTypeMap[
|
||||
model.base_model as keyof typeof modelBaseTypeMap
|
||||
|
@ -127,6 +127,13 @@ export const imagesApi = api.injectEndpoints({
|
||||
// 24 hours - reducing this to a few minutes would reduce memory usage.
|
||||
keepUnusedDataFor: 86400,
|
||||
}),
|
||||
getIntermediatesCount: build.query<number, void>({
|
||||
query: () => ({ url: getListImagesUrl({ is_intermediate: true }) }),
|
||||
providesTags: ['IntermediatesCount'],
|
||||
transformResponse: (response: OffsetPaginatedResults_ImageDTO_) => {
|
||||
return response.total;
|
||||
},
|
||||
}),
|
||||
getImageDTO: build.query<ImageDTO, string>({
|
||||
query: (image_name) => ({ url: `images/${image_name}` }),
|
||||
providesTags: (result, error, arg) => {
|
||||
@ -148,8 +155,9 @@ export const imagesApi = api.injectEndpoints({
|
||||
},
|
||||
keepUnusedDataFor: 86400, // 24 hours
|
||||
}),
|
||||
clearIntermediates: build.mutation({
|
||||
clearIntermediates: build.mutation<number, void>({
|
||||
query: () => ({ url: `images/clear-intermediates`, method: 'POST' }),
|
||||
invalidatesTags: ['IntermediatesCount'],
|
||||
}),
|
||||
deleteImage: build.mutation<void, ImageDTO>({
|
||||
query: ({ image_name }) => ({
|
||||
@ -617,6 +625,7 @@ export const imagesApi = api.injectEndpoints({
|
||||
});
|
||||
|
||||
export const {
|
||||
useGetIntermediatesCountQuery,
|
||||
useListImagesQuery,
|
||||
useLazyListImagesQuery,
|
||||
useGetImageDTOQuery,
|
||||
|
26
invokeai/frontend/web/src/services/api/hooks/useBoardName.ts
Normal file
26
invokeai/frontend/web/src/services/api/hooks/useBoardName.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { BoardId } from 'features/gallery/store/gallerySlice';
|
||||
import { useListAllBoardsQuery } from '../endpoints/boards';
|
||||
|
||||
export const useBoardName = (board_id: BoardId | null | undefined) => {
|
||||
const { boardName } = useListAllBoardsQuery(undefined, {
|
||||
selectFromResult: ({ data }) => {
|
||||
let boardName = '';
|
||||
if (board_id === 'images') {
|
||||
boardName = 'All Images';
|
||||
} else if (board_id === 'assets') {
|
||||
boardName = 'All Assets';
|
||||
} else if (board_id === 'no_board') {
|
||||
boardName = 'No Board';
|
||||
} else if (board_id === 'batch') {
|
||||
boardName = 'Batch';
|
||||
} else {
|
||||
const selectedBoard = data?.find((b) => b.board_id === board_id);
|
||||
boardName = selectedBoard?.board_name || 'Unknown Board';
|
||||
}
|
||||
|
||||
return { boardName };
|
||||
},
|
||||
});
|
||||
|
||||
return boardName;
|
||||
};
|
Loading…
Reference in New Issue
Block a user