mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
remove rest of cache, add bulk select UI
This commit is contained in:
parent
f01df49128
commit
3a531a3c88
@ -385,6 +385,8 @@
|
||||
"viewerImage": "Viewer Image",
|
||||
"compareImage": "Compare Image",
|
||||
"openInViewer": "Open in Viewer",
|
||||
"selectAllOnPage": "Select All On Page",
|
||||
"selectAllOnBoard": "Select All On Board",
|
||||
"selectForCompare": "Select for Compare",
|
||||
"selectAnImageToCompare": "Select an Image to Compare",
|
||||
"slider": "Slider",
|
||||
|
@ -2,8 +2,7 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import type { ImageCache } from 'services/api/types';
|
||||
import { getListImagesUrl, imagesSelectors } from 'services/api/util';
|
||||
import { getListImagesUrl } from 'services/api/util';
|
||||
|
||||
export const addFirstListImagesListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
@ -18,13 +17,10 @@ export const addFirstListImagesListener = (startAppListening: AppStartListening)
|
||||
cancelActiveListeners();
|
||||
unsubscribe();
|
||||
|
||||
// TODO: figure out how to type the predicate
|
||||
const data = action.payload as ImageCache;
|
||||
const data = action.payload;
|
||||
|
||||
if (data.ids.length > 0) {
|
||||
// Select the first image
|
||||
const firstImage = imagesSelectors.selectAll(data)[0];
|
||||
dispatch(imageSelected(firstImage ?? null));
|
||||
if (data.items.length > 0) {
|
||||
dispatch(imageSelected(data.items[0] ?? null));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -3,7 +3,6 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
|
||||
import { boardIdSelected, galleryViewChanged, imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import { imagesSelectors } from 'services/api/util';
|
||||
|
||||
export const addBoardIdSelectedListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
@ -35,11 +34,12 @@ export const addBoardIdSelectedListener = (startAppListening: AppStartListening)
|
||||
const { data: boardImagesData } = imagesApi.endpoints.listImages.select(queryArgs)(getState());
|
||||
|
||||
if (boardImagesData && boardIdSelected.match(action) && action.payload.selectedImageName) {
|
||||
const selectedImage = imagesSelectors.selectById(boardImagesData, action.payload.selectedImageName);
|
||||
const selectedImage = boardImagesData.items.find(
|
||||
(item) => item.image_name === action.payload.selectedImageName
|
||||
);
|
||||
dispatch(imageSelected(selectedImage || null));
|
||||
} else if (boardImagesData) {
|
||||
const firstImage = imagesSelectors.selectAll(boardImagesData)[0];
|
||||
dispatch(imageSelected(firstImage || null));
|
||||
dispatch(imageSelected(boardImagesData.items[0] || null));
|
||||
} else {
|
||||
// board has no images - deselect
|
||||
dispatch(imageSelected(null));
|
||||
|
@ -22,11 +22,10 @@ import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { isImageFieldInputInstance } from 'features/nodes/types/field';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { clamp, forEach } from 'lodash-es';
|
||||
import { forEach } from 'lodash-es';
|
||||
import { api } from 'services/api';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import { imagesSelectors } from 'services/api/util';
|
||||
|
||||
const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
||||
state.nodes.present.nodes.forEach((node) => {
|
||||
@ -123,23 +122,11 @@ export const addRequestedSingleImageDeletionListener = (startAppListening: AppSt
|
||||
const lastSelectedImage = state.gallery.selection[state.gallery.selection.length - 1]?.image_name;
|
||||
|
||||
if (imageDTO && imageDTO?.image_name === lastSelectedImage) {
|
||||
const { image_name } = imageDTO;
|
||||
|
||||
const baseQueryArgs = selectListImagesQueryArgs(state);
|
||||
const { data } = imagesApi.endpoints.listImages.select(baseQueryArgs)(state);
|
||||
|
||||
const cachedImageDTOs = data ? imagesSelectors.selectAll(data) : [];
|
||||
|
||||
const deletedImageIndex = cachedImageDTOs.findIndex((i) => i.image_name === image_name);
|
||||
|
||||
const filteredImageDTOs = cachedImageDTOs.filter((i) => i.image_name !== image_name);
|
||||
|
||||
const newSelectedImageIndex = clamp(deletedImageIndex, 0, filteredImageDTOs.length - 1);
|
||||
|
||||
const newSelectedImageDTO = filteredImageDTOs[newSelectedImageIndex];
|
||||
|
||||
if (newSelectedImageDTO) {
|
||||
dispatch(imageSelected(newSelectedImageDTO));
|
||||
if (data && data.items[0]) {
|
||||
dispatch(imageSelected(data.items[0]));
|
||||
} else {
|
||||
dispatch(imageSelected(null));
|
||||
}
|
||||
@ -188,10 +175,8 @@ export const addRequestedSingleImageDeletionListener = (startAppListening: AppSt
|
||||
const queryArgs = selectListImagesQueryArgs(state);
|
||||
const { data } = imagesApi.endpoints.listImages.select(queryArgs)(state);
|
||||
|
||||
const newSelectedImageDTO = data ? imagesSelectors.selectAll(data)[0] : undefined;
|
||||
|
||||
if (newSelectedImageDTO) {
|
||||
dispatch(imageSelected(newSelectedImageDTO));
|
||||
if (data && data.items[0]) {
|
||||
dispatch(imageSelected(data.items[0]));
|
||||
} else {
|
||||
dispatch(imageSelected(null));
|
||||
}
|
||||
|
@ -15,7 +15,12 @@ import {
|
||||
} from 'features/controlLayers/store/controlLayersSlice';
|
||||
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
|
||||
import { isValidDrop } from 'features/dnd/util/isValidDrop';
|
||||
import { imageSelected, imageToCompareChanged, isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
||||
import {
|
||||
imageSelected,
|
||||
imageToCompareChanged,
|
||||
isImageViewerOpenChanged,
|
||||
selectionChanged,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
@ -216,6 +221,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
|
||||
board_id: boardId,
|
||||
})
|
||||
);
|
||||
dispatch(selectionChanged([]));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -233,6 +239,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
|
||||
imageDTO,
|
||||
})
|
||||
);
|
||||
dispatch(selectionChanged([]));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -248,6 +255,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
|
||||
board_id: boardId,
|
||||
})
|
||||
);
|
||||
dispatch(selectionChanged([]));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -261,6 +269,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
|
||||
imageDTOs,
|
||||
})
|
||||
);
|
||||
dispatch(selectionChanged([]));
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
@ -8,14 +8,14 @@ import {
|
||||
galleryViewChanged,
|
||||
imageSelected,
|
||||
isImageViewerOpenChanged,
|
||||
offsetChanged,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { IMAGE_CATEGORIES, IMAGE_LIMIT } from 'features/gallery/store/types';
|
||||
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
|
||||
import { zNodeStatus } from 'features/nodes/types/invocation';
|
||||
import { CANVAS_OUTPUT } from 'features/nodes/util/graph/constants';
|
||||
import { boardsApi } from 'services/api/endpoints/boards';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import { imageListDefaultSort } from 'services/api/util';
|
||||
import { getCategories, getListImagesUrl } from 'services/api/util';
|
||||
import { socketInvocationComplete } from 'services/events/actions';
|
||||
|
||||
// These nodes output an image, but do not actually *save* an image, so we don't want to handle the gallery logic on them
|
||||
@ -52,32 +52,6 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi
|
||||
}
|
||||
|
||||
if (!imageDTO.is_intermediate) {
|
||||
/**
|
||||
* Cache updates for when an image result is received
|
||||
* - add it to the no_board/images
|
||||
*/
|
||||
|
||||
dispatch(
|
||||
imagesApi.util.updateQueryData(
|
||||
'listImages',
|
||||
{
|
||||
board_id: imageDTO.board_id ?? 'none',
|
||||
categories: IMAGE_CATEGORIES,
|
||||
offset: gallery.offset,
|
||||
limit: gallery.limit,
|
||||
is_intermediate: false,
|
||||
},
|
||||
(draft) => {
|
||||
const updatedListLength = draft.items.unshift(imageDTO);
|
||||
draft.items.sort(imageListDefaultSort());
|
||||
if (updatedListLength > IMAGE_LIMIT) {
|
||||
draft.items.pop();
|
||||
}
|
||||
draft.total += 1;
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// update the total images for the board
|
||||
dispatch(
|
||||
boardsApi.util.updateQueryData('getBoardImagesTotal', imageDTO.board_id ?? 'none', (draft) => {
|
||||
@ -86,7 +60,18 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(imagesApi.util.invalidateTags([{ type: 'Board', id: imageDTO.board_id ?? 'none' }]));
|
||||
dispatch(
|
||||
imagesApi.util.invalidateTags([
|
||||
{ type: 'Board', id: imageDTO.board_id ?? 'none' },
|
||||
{
|
||||
type: 'ImageList',
|
||||
id: getListImagesUrl({
|
||||
board_id: imageDTO.board_id ?? 'none',
|
||||
categories: getCategories(imageDTO),
|
||||
}),
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
const { shouldAutoSwitch } = gallery;
|
||||
|
||||
@ -106,6 +91,8 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi
|
||||
);
|
||||
}
|
||||
|
||||
dispatch(offsetChanged(0));
|
||||
|
||||
if (!imageDTO.board_id && gallery.selectedBoardId !== 'none') {
|
||||
dispatch(
|
||||
boardIdSelected({
|
||||
|
@ -0,0 +1,47 @@
|
||||
import { Flex, IconButton, Tag, TagCloseButton, TagLabel, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages';
|
||||
import { selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BiSelectMultiple } from 'react-icons/bi';
|
||||
|
||||
export const GalleryBulkSelect = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { selection } = useAppSelector((s) => s.gallery);
|
||||
const { t } = useTranslation();
|
||||
const { imageDTOs } = useGalleryImages();
|
||||
|
||||
const onClickClearSelection = useCallback(() => {
|
||||
dispatch(selectionChanged([]));
|
||||
}, [dispatch]);
|
||||
|
||||
const onClickSelectAllPage = useCallback(() => {
|
||||
dispatch(selectionChanged(selection.concat(imageDTOs)));
|
||||
}, [dispatch, imageDTOs, selection]);
|
||||
|
||||
return (
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<Tag>
|
||||
<TagLabel>
|
||||
{selection.length} {t('common.selected')}
|
||||
</TagLabel>
|
||||
{selection.length > 0 && (
|
||||
<Tooltip label="Clear selection">
|
||||
<TagCloseButton onClick={onClickClearSelection} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Tag>
|
||||
|
||||
<Tooltip label={t('gallery.selectAllOnPage')}>
|
||||
<IconButton
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon={<BiSelectMultiple />}
|
||||
aria-label="Bulk select"
|
||||
onClick={onClickSelectAllPage}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
);
|
||||
};
|
@ -10,6 +10,7 @@ import { RiServerLine } from 'react-icons/ri';
|
||||
|
||||
import BoardsList from './Boards/BoardsList/BoardsList';
|
||||
import GalleryBoardName from './GalleryBoardName';
|
||||
import { GalleryBulkSelect } from './GalleryBulkSelect';
|
||||
import GallerySettingsPopover from './GallerySettingsPopover';
|
||||
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
|
||||
import { GalleryPagination } from './ImageGrid/GalleryPagination';
|
||||
@ -71,6 +72,8 @@ const ImageGalleryContent = () => {
|
||||
</TabList>
|
||||
</Tabs>
|
||||
</Flex>
|
||||
<GalleryBulkSelect />
|
||||
|
||||
<GalleryImageGrid />
|
||||
<GalleryPagination />
|
||||
</Flex>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { offsetChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useListImagesQuery } from 'services/api/endpoints/images';
|
||||
|
||||
export const useGalleryPagination = (pageButtonsPerSide: number = 2) => {
|
||||
@ -50,6 +50,13 @@ export const useGalleryPagination = (pageButtonsPerSide: number = 2) => {
|
||||
dispatch(offsetChanged((pages - 1) * (limit || 0)));
|
||||
}, [dispatch, pages, limit]);
|
||||
|
||||
// handle when total/pages decrease and user is on high page number (ie bulk removing or deleting)
|
||||
useEffect(() => {
|
||||
if (currentPage + 1 > pages) {
|
||||
goToLast();
|
||||
}
|
||||
}, [currentPage, pages, goToLast]);
|
||||
|
||||
// calculate the page buttons to display - current page with 3 around it
|
||||
const pageButtons = useMemo(() => {
|
||||
const buttons = [];
|
||||
@ -77,6 +84,10 @@ export const useGalleryPagination = (pageButtonsPerSide: number = 2) => {
|
||||
return `${startItem}-${endItem} of ${total}`;
|
||||
}, [total, currentPage, limit]);
|
||||
|
||||
const numberOnPage = useMemo(() => {
|
||||
return Math.min((currentPage + 1) * (limit || 0), total);
|
||||
}, [currentPage, limit, total]);
|
||||
|
||||
const api = useMemo(
|
||||
() => ({
|
||||
count,
|
||||
@ -94,6 +105,7 @@ export const useGalleryPagination = (pageButtonsPerSide: number = 2) => {
|
||||
isFirstEnabled,
|
||||
isLastEnabled,
|
||||
rangeDisplay,
|
||||
numberOnPage,
|
||||
}),
|
||||
[
|
||||
count,
|
||||
@ -111,6 +123,7 @@ export const useGalleryPagination = (pageButtonsPerSide: number = 2) => {
|
||||
isFirstEnabled,
|
||||
isLastEnabled,
|
||||
rangeDisplay,
|
||||
numberOnPage,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -107,7 +107,7 @@ export const gallerySlice = createSlice({
|
||||
offsetChanged: (state, action: PayloadAction<number>) => {
|
||||
state.offset = action.payload;
|
||||
},
|
||||
limitChanged: (state, action: PayloadAction<number | undefined>) => {
|
||||
limitChanged: (state, action: PayloadAction<number>) => {
|
||||
state.limit = action.payload;
|
||||
},
|
||||
},
|
||||
|
@ -1,56 +1,9 @@
|
||||
import { createEntityAdapter } from '@reduxjs/toolkit';
|
||||
import { getSelectorsOptions } from 'app/store/createMemoizedSelector';
|
||||
import { dateComparator } from 'common/util/dateComparator';
|
||||
import { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
||||
import queryString from 'query-string';
|
||||
import { buildV1Url } from 'services/api';
|
||||
|
||||
import type { ImageCache, ImageDTO, ListImagesArgs } from './types';
|
||||
|
||||
export const getIsImageInDateRange = (data: ImageCache | undefined, imageDTO: ImageDTO) => {
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const totalCachedImageDtos = imagesSelectors.selectAll(data);
|
||||
|
||||
if (totalCachedImageDtos.length <= 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const cachedStarredImages = [];
|
||||
const cachedUnstarredImages = [];
|
||||
|
||||
for (let index = 0; index < totalCachedImageDtos.length; index++) {
|
||||
const image = totalCachedImageDtos[index];
|
||||
if (image?.starred) {
|
||||
cachedStarredImages.push(image);
|
||||
}
|
||||
if (!image?.starred) {
|
||||
cachedUnstarredImages.push(image);
|
||||
}
|
||||
}
|
||||
|
||||
if (imageDTO.starred) {
|
||||
const lastStarredImage = cachedStarredImages[cachedStarredImages.length - 1];
|
||||
// if starring or already starred, want to look in list of starred images
|
||||
if (!lastStarredImage) {
|
||||
return true;
|
||||
} // no starred images showing, so always show this one
|
||||
const createdDate = new Date(imageDTO.created_at);
|
||||
const oldestDate = new Date(lastStarredImage.created_at);
|
||||
return createdDate >= oldestDate;
|
||||
} else {
|
||||
const lastUnstarredImage = cachedUnstarredImages[cachedUnstarredImages.length - 1];
|
||||
// if unstarring or already unstarred, want to look in list of unstarred images
|
||||
if (!lastUnstarredImage) {
|
||||
return false;
|
||||
} // no unstarred images showing, so don't show this one
|
||||
const createdDate = new Date(imageDTO.created_at);
|
||||
const oldestDate = new Date(lastUnstarredImage.created_at);
|
||||
return createdDate >= oldestDate;
|
||||
}
|
||||
};
|
||||
import type { ImageDTO, ListImagesArgs } from './types';
|
||||
|
||||
export const getCategories = (imageDTO: ImageDTO) => {
|
||||
if (IMAGE_CATEGORIES.includes(imageDTO.image_category)) {
|
||||
@ -59,22 +12,6 @@ export const getCategories = (imageDTO: ImageDTO) => {
|
||||
return ASSETS_CATEGORIES;
|
||||
};
|
||||
|
||||
// The adapter is not actually the data store - it just provides helper functions to interact
|
||||
// with some other store of data. We will use the RTK Query cache as that store.
|
||||
export const imagesAdapter = createEntityAdapter<ImageDTO, string>({
|
||||
selectId: (image) => image.image_name,
|
||||
sortComparer: (a, b) => {
|
||||
// Compare starred images first
|
||||
if (a.starred && !b.starred) {
|
||||
return -1;
|
||||
}
|
||||
if (!a.starred && b.starred) {
|
||||
return 1;
|
||||
}
|
||||
return dateComparator(b.created_at, a.created_at);
|
||||
},
|
||||
});
|
||||
|
||||
export const imageListDefaultSort = () => {
|
||||
return (a: ImageDTO, b: ImageDTO) => {
|
||||
if (a.starred && !b.starred) {
|
||||
@ -87,9 +24,6 @@ export const imageListDefaultSort = () => {
|
||||
};
|
||||
};
|
||||
|
||||
// Create selectors for the adapter.
|
||||
export const imagesSelectors = imagesAdapter.getSelectors(undefined, getSelectorsOptions);
|
||||
|
||||
// Helper to create the url for the listImages endpoint. Also we use it to create the cache key.
|
||||
export const getListImagesUrl = (queryArgs: ListImagesArgs) =>
|
||||
buildV1Url(`images/?${queryString.stringify(queryArgs, { arrayFormat: 'none' })}`);
|
||||
|
Loading…
Reference in New Issue
Block a user