Merge branch 'main' into fix/post-model-sync

This commit is contained in:
blessedcoolant
2023-07-20 20:16:14 +12:00
committed by GitHub
43 changed files with 1161 additions and 436 deletions

View File

@ -552,7 +552,8 @@
"saveSteps": "Save images every n steps",
"confirmOnDelete": "Confirm On Delete",
"displayHelpIcons": "Display Help Icons",
"useCanvasBeta": "Use Canvas Beta Layout",
"alternateCanvasLayout": "Alternate Canvas Layout",
"enableNodesEditor": "Enable Nodes Editor",
"enableImageDebugging": "Enable Image Debugging",
"useSlidersForAll": "Use Sliders For All Options",
"showProgressInViewer": "Show Progress Images in Viewer",
@ -569,7 +570,9 @@
"ui": "User Interface",
"favoriteSchedulers": "Favorite Schedulers",
"favoriteSchedulersPlaceholder": "No schedulers favorited",
"showAdvancedOptions": "Show Advanced Options"
"showAdvancedOptions": "Show Advanced Options",
"experimental": "Experimental",
"beta": "Beta"
},
"toast": {
"serverError": "Server Error",

View File

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

View File

@ -9,7 +9,7 @@ import {
} from '@chakra-ui/react';
import { memo } from 'react';
interface Props extends SwitchProps {
export interface IAISwitchProps extends SwitchProps {
label?: string;
width?: string | number;
formControlProps?: FormControlProps;
@ -20,7 +20,7 @@ interface Props extends SwitchProps {
/**
* Customized Chakra FormControl + Switch multi-part component.
*/
const IAISwitch = (props: Props) => {
const IAISwitch = (props: IAISwitchProps) => {
const {
label,
isDisabled = false,

View File

@ -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;

View File

@ -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;

View File

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

View File

@ -38,6 +38,7 @@ const AllAssetsBoard = ({ isSelected }: { isSelected: boolean }) => {
return (
<GenericBoard
board_id="assets"
onClick={handleClick}
isSelected={isSelected}
icon={FaFileImage}

View File

@ -38,6 +38,7 @@ const AllImagesBoard = ({ isSelected }: { isSelected: boolean }) => {
return (
<GenericBoard
board_id="images"
onClick={handleClick}
isSelected={isSelected}
icon={FaImages}

View File

@ -29,6 +29,7 @@ const BatchBoard = ({ isSelected }: { isSelected: boolean }) => {
return (
<GenericBoard
board_id="batch"
droppableData={droppableData}
onClick={handleBatchBoardClick}
isSelected={isSelected}

View File

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

View File

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

View File

@ -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}

View File

@ -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);

View File

@ -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);

View File

@ -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)',

View File

@ -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>
);

View File

@ -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;

View File

@ -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>
);

View File

@ -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;

View File

@ -9,7 +9,6 @@ const ProcessButtons = () => {
return (
<Flex gap={2}>
<InvokeButton />
{/* {activeTabName === 'img2img' && <LoopbackButton />} */}
<CancelButton />
</Flex>
);

View File

@ -0,0 +1,57 @@
import { Badge, BadgeProps, Flex, Text, TextProps } from '@chakra-ui/react';
import IAISwitch, { IAISwitchProps } from 'common/components/IAISwitch';
import { useTranslation } from 'react-i18next';
type SettingSwitchProps = IAISwitchProps & {
label: string;
useBadge?: boolean;
badgeLabel?: string;
textProps?: TextProps;
badgeProps?: BadgeProps;
};
export default function SettingSwitch(props: SettingSwitchProps) {
const { t } = useTranslation();
const {
label,
textProps,
useBadge = false,
badgeLabel = t('settings.experimental'),
badgeProps,
...rest
} = props;
return (
<Flex justifyContent="space-between" py={1}>
<Flex gap={2} alignItems="center">
<Text
sx={{
fontSize: 14,
_dark: {
color: 'base.300',
},
}}
{...textProps}
>
{label}
</Text>
{useBadge && (
<Badge
size="xs"
sx={{
px: 2,
color: 'base.700',
bg: 'accent.200',
_dark: { bg: 'accent.500', color: 'base.200' },
}}
{...badgeProps}
>
{badgeLabel}
</Badge>
)}
</Flex>
<IAISwitch {...rest} />
</Flex>
);
}

View File

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

View File

@ -11,13 +11,12 @@ import {
Text,
useDisclosure,
} from '@chakra-ui/react';
import { createSelector, current } from '@reduxjs/toolkit';
import { createSelector } from '@reduxjs/toolkit';
import { VALID_LOG_LEVELS } from 'app/logging/useLogger';
import { LOCALSTORAGE_KEYS, LOCALSTORAGE_PREFIX } from 'app/store/constants';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton';
import IAIMantineSelect from 'common/components/IAIMantineSelect';
import IAISwitch from 'common/components/IAISwitch';
import { systemSelector } from 'features/system/store/systemSelectors';
import {
SystemState,
@ -25,7 +24,6 @@ import {
setEnableImageDebugging,
setIsNodesEnabled,
setShouldConfirmOnDelete,
setShouldDisplayGuides,
shouldAntialiasProgressImageChanged,
shouldLogToConsoleChanged,
} from 'features/system/store/systemSlice';
@ -48,15 +46,15 @@ import {
} from 'react';
import { useTranslation } from 'react-i18next';
import { LogLevelName } from 'roarr';
import SettingsSchedulers from './SettingsSchedulers';
import SettingSwitch from './SettingSwitch';
import SettingsClearIntermediates from './SettingsClearIntermediates';
import SettingsSchedulers from './SettingsSchedulers';
const selector = createSelector(
[systemSelector, uiSelector],
(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 />
@ -206,7 +202,7 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
<Flex sx={{ gap: 4, flexDirection: 'column' }}>
<StyledFlex>
<Heading size="sm">{t('settings.general')}</Heading>
<IAISwitch
<SettingSwitch
label={t('settings.confirmOnDelete')}
isChecked={shouldConfirmOnDelete}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
@ -214,7 +210,7 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
}
/>
{shouldShowAdvancedOptionsSettings && (
<IAISwitch
<SettingSwitch
label={t('settings.showAdvancedOptions')}
isChecked={shouldShowAdvancedOptions}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
@ -231,37 +227,21 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
<StyledFlex>
<Heading size="sm">{t('settings.ui')}</Heading>
<IAISwitch
label={t('settings.displayHelpIcons')}
isChecked={shouldDisplayGuides}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldDisplayGuides(e.target.checked))
}
/>
{shouldShowBetaLayout && (
<IAISwitch
label={t('settings.useCanvasBeta')}
isChecked={shouldUseCanvasBetaLayout}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldUseCanvasBetaLayout(e.target.checked))
}
/>
)}
<IAISwitch
<SettingSwitch
label={t('settings.useSlidersForAll')}
isChecked={shouldUseSliders}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldUseSliders(e.target.checked))
}
/>
<IAISwitch
<SettingSwitch
label={t('settings.showProgressInViewer')}
isChecked={shouldShowProgressInViewer}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldShowProgressInViewer(e.target.checked))
}
/>
<IAISwitch
<SettingSwitch
label={t('settings.antialiasProgressImages')}
isChecked={shouldAntialiasProgressImage}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
@ -270,9 +250,21 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
)
}
/>
{shouldShowBetaLayout && (
<SettingSwitch
label={t('settings.alternateCanvasLayout')}
useBadge
badgeLabel={t('settings.beta')}
isChecked={shouldUseCanvasBetaLayout}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldUseCanvasBetaLayout(e.target.checked))
}
/>
)}
{shouldShowNodesToggle && (
<IAISwitch
label="Enable Nodes Editor (Experimental)"
<SettingSwitch
label={t('settings.enableNodesEditor')}
useBadge
isChecked={isNodesEnabled}
onChange={handleToggleNodes}
/>
@ -282,7 +274,7 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
{shouldShowDeveloperSettings && (
<StyledFlex>
<Heading size="sm">{t('settings.developer')}</Heading>
<IAISwitch
<SettingSwitch
label={t('settings.shouldLogToConsole')}
isChecked={shouldLogToConsole}
onChange={handleLogToConsoleChanged}
@ -294,7 +286,7 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
value={consoleLogLevel}
data={VALID_LOG_LEVELS.concat()}
/>
<IAISwitch
<SettingSwitch
label={t('settings.enableImageDebugging')}
isChecked={enableImageDebugging}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
@ -313,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>

View File

@ -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,

View File

@ -75,42 +75,49 @@ const ModelList = (props: ModelListProps) => {
labelPos="side"
/>
{['images', 'diffusers'].includes(modelFormatFilter) &&
filteredDiffusersModels.length > 0 && (
<StyledModelContainer>
<Flex sx={{ gap: 2, flexDir: 'column' }}>
<Text variant="subtext" fontSize="sm">
Diffusers
</Text>
{filteredDiffusersModels.map((model) => (
<ModelListItem
key={model.id}
model={model}
isSelected={selectedModelId === model.id}
setSelectedModelId={setSelectedModelId}
/>
))}
</Flex>
</StyledModelContainer>
)}
{['images', 'checkpoint'].includes(modelFormatFilter) &&
filteredCheckpointModels.length > 0 && (
<StyledModelContainer>
<Flex sx={{ gap: 2, flexDir: 'column' }}>
<Text variant="subtext" fontSize="sm">
Checkpoint
</Text>
{filteredCheckpointModels.map((model) => (
<ModelListItem
key={model.id}
model={model}
isSelected={selectedModelId === model.id}
setSelectedModelId={setSelectedModelId}
/>
))}
</Flex>
</StyledModelContainer>
)}
<Flex
flexDirection="column"
gap={4}
maxHeight={window.innerHeight - 280}
overflow="scroll"
>
{['images', 'diffusers'].includes(modelFormatFilter) &&
filteredDiffusersModels.length > 0 && (
<StyledModelContainer>
<Flex sx={{ gap: 2, flexDir: 'column' }}>
<Text variant="subtext" fontSize="sm">
Diffusers
</Text>
{filteredDiffusersModels.map((model) => (
<ModelListItem
key={model.id}
model={model}
isSelected={selectedModelId === model.id}
setSelectedModelId={setSelectedModelId}
/>
))}
</Flex>
</StyledModelContainer>
)}
{['images', 'checkpoint'].includes(modelFormatFilter) &&
filteredCheckpointModels.length > 0 && (
<StyledModelContainer>
<Flex sx={{ gap: 2, flexDir: 'column' }}>
<Text variant="subtext" fontSize="sm">
Checkpoints
</Text>
{filteredCheckpointModels.map((model) => (
<ModelListItem
key={model.id}
model={model}
isSelected={selectedModelId === model.id}
setSelectedModelId={setSelectedModelId}
/>
))}
</Flex>
</StyledModelContainer>
)}
</Flex>
</Flex>
</Flex>
);
@ -146,8 +153,6 @@ const StyledModelContainer = (props: PropsWithChildren) => {
return (
<Flex
flexDirection="column"
maxHeight={window.innerHeight - 280}
overflow="scroll"
gap={4}
borderRadius={4}
p={4}

View File

@ -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

View File

@ -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,

View 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;
};