| import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'; |
| import { draggable, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; |
| import type { SystemStyleObject } from '@invoke-ai/ui-library'; |
| import { Box, Flex, Image } from '@invoke-ai/ui-library'; |
| import { createSelector } from '@reduxjs/toolkit'; |
| import { galleryImageClicked } from 'app/store/middleware/listenerMiddleware/listeners/galleryImageClicked'; |
| import { useAppStore } from 'app/store/nanostores/store'; |
| import { useAppSelector } from 'app/store/storeHooks'; |
| import { useBoolean } from 'common/hooks/useBoolean'; |
| import { multipleImageDndSource, singleImageDndSource } from 'features/dnd/dnd'; |
| import type { DndDragPreviewMultipleImageState } from 'features/dnd/DndDragPreviewMultipleImage'; |
| import { createMultipleImageDragPreview, setMultipleImageDragPreview } from 'features/dnd/DndDragPreviewMultipleImage'; |
| import type { DndDragPreviewSingleImageState } from 'features/dnd/DndDragPreviewSingleImage'; |
| import { createSingleImageDragPreview, setSingleImageDragPreview } from 'features/dnd/DndDragPreviewSingleImage'; |
| import { firefoxDndFix } from 'features/dnd/util'; |
| import { useImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu'; |
| import { GalleryImageHoverIcons } from 'features/gallery/components/ImageGrid/GalleryImageHoverIcons'; |
| import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId'; |
| import { SizedSkeletonLoader } from 'features/gallery/components/ImageGrid/SizedSkeletonLoader'; |
| import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer'; |
| import { imageToCompareChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice'; |
| import type { MouseEventHandler } from 'react'; |
| import { memo, useCallback, useEffect, useId, useMemo, useState } from 'react'; |
| import type { ImageDTO } from 'services/api/types'; |
|
|
| |
| export const GALLERY_IMAGE_CONTAINER_CLASS_NAME = 'gallery-image-container'; |
|
|
| const galleryImageContainerSX = { |
| containerType: 'inline-size', |
| w: 'full', |
| h: 'full', |
| '.gallery-image-size-badge': { |
| '@container (max-width: 80px)': { |
| '&': { display: 'none' }, |
| }, |
| }, |
| '&[data-is-dragging=true]': { |
| opacity: 0.3, |
| }, |
| '.gallery-image': { |
| touchAction: 'none', |
| userSelect: 'none', |
| webkitUserSelect: 'none', |
| position: 'relative', |
| justifyContent: 'center', |
| alignItems: 'center', |
| aspectRatio: '1/1', |
| '::before': { |
| content: '""', |
| display: 'inline-block', |
| position: 'absolute', |
| top: 0, |
| left: 0, |
| right: 0, |
| bottom: 0, |
| pointerEvents: 'none', |
| borderRadius: 'base', |
| }, |
| '&[data-selected=true]::before': { |
| boxShadow: |
| 'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-500), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)', |
| }, |
| '&[data-selected-for-compare=true]::before': { |
| boxShadow: |
| 'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-300), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)', |
| }, |
| '&:hover::before': { |
| boxShadow: |
| 'inset 0px 0px 0px 1px var(--invoke-colors-invokeBlue-300), inset 0px 0px 0px 2px var(--invoke-colors-invokeBlue-800)', |
| }, |
| '&:hover[data-selected=true]::before': { |
| boxShadow: |
| 'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-400), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)', |
| }, |
| '&:hover[data-selected-for-compare=true]::before': { |
| boxShadow: |
| 'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-200), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)', |
| }, |
| }, |
| } satisfies SystemStyleObject; |
|
|
| interface Props { |
| imageDTO: ImageDTO; |
| } |
|
|
| export const GalleryImage = memo(({ imageDTO }: Props) => { |
| const store = useAppStore(); |
| const [isDragging, setIsDragging] = useState(false); |
| const [dragPreviewState, setDragPreviewState] = useState< |
| DndDragPreviewSingleImageState | DndDragPreviewMultipleImageState | null |
| >(null); |
| const [element, ref] = useState<HTMLImageElement | null>(null); |
| const dndId = useId(); |
| const selectIsSelectedForCompare = useMemo( |
| () => createSelector(selectGallerySlice, (gallery) => gallery.imageToCompare?.image_name === imageDTO.image_name), |
| [imageDTO.image_name] |
| ); |
| const isSelectedForCompare = useAppSelector(selectIsSelectedForCompare); |
| const selectIsSelected = useMemo( |
| () => |
| createSelector(selectGallerySlice, (gallery) => { |
| for (const selectedImage of gallery.selection) { |
| if (selectedImage.image_name === imageDTO.image_name) { |
| return true; |
| } |
| } |
| return false; |
| }), |
| [imageDTO.image_name] |
| ); |
| const isSelected = useAppSelector(selectIsSelected); |
|
|
| useEffect(() => { |
| if (!element) { |
| return; |
| } |
| return combine( |
| firefoxDndFix(element), |
| draggable({ |
| element, |
| getInitialData: () => { |
| const { gallery } = store.getState(); |
| |
| |
| if ( |
| gallery.selection.length > 1 && |
| gallery.selection.find(({ image_name }) => image_name === imageDTO.image_name) !== undefined |
| ) { |
| return multipleImageDndSource.getData({ |
| imageDTOs: gallery.selection, |
| boardId: gallery.selectedBoardId, |
| }); |
| } |
|
|
| |
| return singleImageDndSource.getData({ imageDTO }, imageDTO.image_name); |
| }, |
| |
| onDragStart: ({ source }) => { |
| |
| |
| if (singleImageDndSource.typeGuard(source.data)) { |
| setIsDragging(true); |
| return; |
| } |
| }, |
| onGenerateDragPreview: (args) => { |
| if (multipleImageDndSource.typeGuard(args.source.data)) { |
| setMultipleImageDragPreview({ |
| multipleImageDndData: args.source.data, |
| onGenerateDragPreviewArgs: args, |
| setDragPreviewState, |
| }); |
| } else if (singleImageDndSource.typeGuard(args.source.data)) { |
| setSingleImageDragPreview({ |
| singleImageDndData: args.source.data, |
| onGenerateDragPreviewArgs: args, |
| setDragPreviewState, |
| }); |
| } |
| }, |
| }), |
| monitorForElements({ |
| |
| onDragStart: ({ source }) => { |
| |
| |
| if (multipleImageDndSource.typeGuard(source.data) && source.data.payload.imageDTOs.includes(imageDTO)) { |
| setIsDragging(true); |
| } |
| }, |
| onDrop: () => { |
| |
| setIsDragging(false); |
| }, |
| }) |
| ); |
| }, [imageDTO, element, store, dndId]); |
|
|
| const isHovered = useBoolean(false); |
|
|
| const onClick = useCallback<MouseEventHandler<HTMLDivElement>>( |
| (e) => { |
| store.dispatch( |
| galleryImageClicked({ |
| imageDTO, |
| shiftKey: e.shiftKey, |
| ctrlKey: e.ctrlKey, |
| metaKey: e.metaKey, |
| altKey: e.altKey, |
| }) |
| ); |
| }, |
| [imageDTO, store] |
| ); |
|
|
| const onDoubleClick = useCallback<MouseEventHandler<HTMLDivElement>>(() => { |
| |
| |
| $imageViewer.set(true); |
| store.dispatch(imageToCompareChanged(null)); |
| }, [store]); |
|
|
| const dataTestId = useMemo(() => getGalleryImageDataTestId(imageDTO.image_name), [imageDTO.image_name]); |
|
|
| useImageContextMenu(imageDTO, element); |
|
|
| return ( |
| <> |
| <Box |
| className={GALLERY_IMAGE_CONTAINER_CLASS_NAME} |
| sx={galleryImageContainerSX} |
| data-testid={dataTestId} |
| data-is-dragging={isDragging} |
| > |
| <Flex |
| role="button" |
| className="gallery-image" |
| onMouseOver={isHovered.setTrue} |
| onMouseOut={isHovered.setFalse} |
| onClick={onClick} |
| onDoubleClick={onDoubleClick} |
| data-selected={isSelected} |
| data-selected-for-compare={isSelectedForCompare} |
| > |
| <Image |
| ref={ref} |
| src={imageDTO.thumbnail_url} |
| fallback={<SizedSkeletonLoader width={imageDTO.width} height={imageDTO.height} />} |
| w={imageDTO.width} |
| objectFit="contain" |
| maxW="full" |
| maxH="full" |
| borderRadius="base" |
| /> |
| <GalleryImageHoverIcons imageDTO={imageDTO} isHovered={isHovered.isTrue} /> |
| </Flex> |
| </Box> |
| {dragPreviewState?.type === 'multiple-image' ? createMultipleImageDragPreview(dragPreviewState) : null} |
| {dragPreviewState?.type === 'single-image' ? createSingleImageDragPreview(dragPreviewState) : null} |
| </> |
| ); |
| }); |
|
|
| GalleryImage.displayName = 'GalleryImage'; |
|
|