diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx index b24146c798..e97affb322 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx @@ -36,7 +36,7 @@ import { import { ImageDTO } from 'services/api/types'; import { useDebounce } from 'use-debounce'; import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions'; -import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs'; +import { BsBookmarkStar, BsFillBookmarkStarFill } from 'react-icons/bs'; type SingleSelectionMenuItemsProps = { imageDTO: ImageDTO; @@ -211,11 +211,14 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { Change Board {imageDTO.pinned ? ( - } onClickCapture={handleUnpinImage}> + } onClickCapture={handleUnpinImage}> Unpin Image ) : ( - } onClickCapture={handlePinImage}> + } + onClickCapture={handlePinImage} + > Pin Image )} diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx index 97f8199aed..aba5181882 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx @@ -2,16 +2,19 @@ import { Box, Flex } from '@chakra-ui/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIDndImage from 'common/components/IAIDndImage'; import IAIFillSkeleton from 'common/components/IAIFillSkeleton'; -import { useMultiselect } from 'features/gallery/hooks/useMultiselect.ts'; import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice'; -import { MouseEvent, memo, useCallback, useMemo } from 'react'; -import { FaTrash } from 'react-icons/fa'; -import { useGetImageDTOQuery } from 'services/api/endpoints/images'; import { ImageDTOsDraggableData, ImageDraggableData, TypesafeDraggableData, } from 'features/dnd/types'; +import { useMultiselect } from 'features/gallery/hooks/useMultiselect.ts'; +import { MouseEvent, memo, useCallback, useMemo } from 'react'; +import { BsBookmarkStar, BsFillBookmarkStarFill } from 'react-icons/bs'; +import { + useChangeImagePinnedMutation, + useGetImageDTOQuery, +} from 'services/api/endpoints/images'; interface HoverableImageProps { imageName: string; @@ -59,6 +62,14 @@ const GalleryImage = (props: HoverableImageProps) => { } }, [imageDTO, selection, selectionCount]); + const [togglePin] = useChangeImagePinnedMutation(); + + const togglePinnedState = useCallback(() => { + if (imageDTO) { + togglePin({ imageDTO, pinned: !imageDTO.pinned }); + } + }, [togglePin, imageDTO]); + if (!imageDTO) { return ; } @@ -80,15 +91,17 @@ const GalleryImage = (props: HoverableImageProps) => { draggableData={draggableData} isSelected={isSelected} minSize={0} - onClickReset={handleDelete} + onClickReset={togglePinnedState} imageSx={{ w: 'full', h: 'full' }} isDropDisabled={true} isUploadDisabled={true} thumbnail={true} withHoverOverlay - resetIcon={} - resetTooltip="Delete image" - withResetIcon={shouldShowDeleteButton} // removed bc it's too easy to accidentally delete images + resetIcon={ + imageDTO.pinned ? : + } + resetTooltip="Pin image" + withResetIcon={true} /> diff --git a/invokeai/frontend/web/src/services/api/endpoints/images.ts b/invokeai/frontend/web/src/services/api/endpoints/images.ts index 9320e7a137..ef99d735c2 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/images.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/images.ts @@ -399,15 +399,18 @@ export const imagesApi = api.injectEndpoints({ method: 'PATCH', body: { pinned }, }), - invalidatesTags: (result, error, { imageDTO }) => [ - { - type: 'ImageList', - id: getListImagesUrl({ - board_id: imageDTO.board_id, - categories: IMAGE_CATEGORIES, - }), - }, - ], + invalidatesTags: (result, error, { imageDTO }) => { + const categories = getCategories(imageDTO); + return [ + { + type: 'ImageList', + id: getListImagesUrl({ + board_id: imageDTO.board_id, + categories, + }), + }, + ] + }, async onQueryStarted( { imageDTO, pinned }, { dispatch, queryFulfilled, getState } @@ -465,27 +468,14 @@ export const imagesApi = api.injectEndpoints({ const isCacheFullyPopulated = currentCache.data && currentCache.data.ids.length >= (total ?? 0); - const isInDateRangeForPinnedState = getIsImageInDateRange( + const isInDateRange = getIsImageInDateRange( currentCache.data, updatedImage ); - if (!isInDateRangeForPinnedState) { - // if newly pinned or unpinned image is not in date range for its new state, remove from cache - patches.push( - dispatch( - imagesApi.util.updateQueryData( - 'listImages', - queryArgs, - (draft) => { - imagesAdapter.removeOne(draft, updatedImage.image_name); - } - ) - ) - ); - } + // should we remove images from cache if _not_ in date range? ie you are showing 100 of 101 pinned images and you unpin one. technically it should disappear from list. - if (isCacheFullyPopulated || isInDateRangeForPinnedState) { + if (isCacheFullyPopulated || isInDateRange) { // *upsert* to $cache patches.push( dispatch( diff --git a/invokeai/frontend/web/src/services/api/util.ts b/invokeai/frontend/web/src/services/api/util.ts index 10c4557eec..a97e96f312 100644 --- a/invokeai/frontend/web/src/services/api/util.ts +++ b/invokeai/frontend/web/src/services/api/util.ts @@ -21,21 +21,35 @@ export const getIsImageInDateRange = ( return true; } - const cacheImageDTOSForPinnedState = totalCachedImageDtos.filter((image) => image.pinned === imageDTO.pinned); + const cachedPinnedImages = []; + const cachedUnpinnedImages = []; - if (cacheImageDTOSForPinnedState.length > 1) { - // Images are sorted by `pinned` DESC and then `created_at` DESC - // check if the image is newer than the oldest image in the cache for either the pinned group or unpinned group + for (let index = 0; index < totalCachedImageDtos.length; index++) { + const image = totalCachedImageDtos[index]; + if (image?.pinned) cachedPinnedImages.push(image) + if (!image?.pinned) cachedUnpinnedImages.push(image) + } + + const lastPinnedImage = cachedPinnedImages[cachedPinnedImages.length - 1]; + const lastUnpinnedImage = cachedUnpinnedImages[cachedUnpinnedImages.length - 1]; + + if (!lastPinnedImage || !lastUnpinnedImage) { + // satisfy TS gods, we already confirmed the array has more than one image + return false; + } + + if (imageDTO.pinned) { + // if pinning or already pinned, want to look in list of pinned images const createdDate = new Date(imageDTO.created_at); - const oldestImage = cacheImageDTOSForPinnedState[cacheImageDTOSForPinnedState.length - 1]; - if (!oldestImage) { - // satisfy TS gods, we already confirmed the array has more than one image - return false; - } - const oldestDate = new Date(oldestImage.created_at); + const oldestDate = new Date(lastPinnedImage.created_at); + return createdDate >= oldestDate; + } else { + // if unpinning or already unpinned, want to look in list of unpinned images + const createdDate = new Date(imageDTO.created_at); + const oldestDate = new Date(lastUnpinnedImage.created_at); return createdDate >= oldestDate; } - return false; + }; export const getCategories = (imageDTO: ImageDTO) => {