diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index e859bd7dd7..b044be82cd 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1219,6 +1219,10 @@ "height": "Height", "imageFit": "Fit Initial Image To Output Size", "images": "Images", + "images_withCount_one": "Image", + "images_withCount_other": "Images", + "videos_withCount_one": "Video", + "videos_withCount_other": "Videos", "infillMethod": "Infill Method", "infillColorValue": "Fill Color", "info": "Info", diff --git a/invokeai/frontend/web/src/features/dnd/DndDragPreviewMultipleImage.tsx b/invokeai/frontend/web/src/features/dnd/DndDragPreviewMultipleImage.tsx index 2633d9e508..056ce11411 100644 --- a/invokeai/frontend/web/src/features/dnd/DndDragPreviewMultipleImage.tsx +++ b/invokeai/frontend/web/src/features/dnd/DndDragPreviewMultipleImage.tsx @@ -21,7 +21,7 @@ const DndDragPreviewMultipleImage = memo(({ image_names }: { image_names: string borderRadius="base" > {image_names.length} - {t('parameters.images')} + {t('parameters.images_withCount', { count: image_names.length })} ); }); diff --git a/invokeai/frontend/web/src/features/dnd/DndDragPreviewMultipleVideo.tsx b/invokeai/frontend/web/src/features/dnd/DndDragPreviewMultipleVideo.tsx index 4c600e88f7..6ccb7e4850 100644 --- a/invokeai/frontend/web/src/features/dnd/DndDragPreviewMultipleVideo.tsx +++ b/invokeai/frontend/web/src/features/dnd/DndDragPreviewMultipleVideo.tsx @@ -8,7 +8,7 @@ import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import type { Param0 } from 'tsafe'; -const DndDragPreviewMultipleVideo = memo(({ ids }: { ids: string[] }) => { +const DndDragPreviewMultipleVideo = memo(({ video_ids }: { video_ids: string[] }) => { const { t } = useTranslation(); return ( { bg="base.900" borderRadius="base" > - {ids.length} - {t('parameters.videos')} + {video_ids.length} + {t('parameters.videos_withCount', { count: video_ids.length })} ); }); @@ -31,11 +31,11 @@ DndDragPreviewMultipleVideo.displayName = 'DndDragPreviewMultipleVideo'; export type DndDragPreviewMultipleVideoState = { type: 'multiple-video'; container: HTMLElement; - ids: string[]; + video_ids: string[]; }; export const createMultipleVideoDragPreview = (arg: DndDragPreviewMultipleVideoState) => - createPortal(, arg.container); + createPortal(, arg.container); type SetMultipleDragPreviewArg = { multipleVideoDndData: MultipleVideoDndSourceData; @@ -51,7 +51,7 @@ export const setMultipleVideoDragPreview = ({ const { nativeSetDragImage, source, location } = onGenerateDragPreviewArgs; setCustomNativeDragPreview({ render({ container }) { - setDragPreviewState({ type: 'multiple-video', container, ids: multipleVideoDndData.payload.ids }); + setDragPreviewState({ type: 'multiple-video', container, video_ids: multipleVideoDndData.payload.video_ids }); return () => setDragPreviewState(null); }, nativeSetDragImage, diff --git a/invokeai/frontend/web/src/features/dnd/DndDragPreviewSingleVideo.tsx b/invokeai/frontend/web/src/features/dnd/DndDragPreviewSingleVideo.tsx index 4bd8c60f06..ddf2a909c7 100644 --- a/invokeai/frontend/web/src/features/dnd/DndDragPreviewSingleVideo.tsx +++ b/invokeai/frontend/web/src/features/dnd/DndDragPreviewSingleVideo.tsx @@ -3,6 +3,7 @@ import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/el import { chakra, Flex, Text } from '@invoke-ai/ui-library'; import type { SingleVideoDndSourceData } from 'features/dnd/dnd'; import { DND_IMAGE_DRAG_PREVIEW_SIZE, preserveOffsetOnSourceFallbackCentered } from 'features/dnd/util'; +import { GalleryVideoPlaceholder } from 'features/gallery/components/ImageGrid/GalleryVideo'; import { memo } from 'react'; import { createPortal } from 'react-dom'; import type { VideoDTO } from 'services/api/types'; @@ -12,14 +13,19 @@ const ChakraImg = chakra('img'); const DndDragPreviewSingleVideo = memo(({ videoDTO }: { videoDTO: VideoDTO }) => { return ( - - I AM A VIDEO + + diff --git a/invokeai/frontend/web/src/features/dnd/dnd.ts b/invokeai/frontend/web/src/features/dnd/dnd.ts index bd85542985..e092e4fb80 100644 --- a/invokeai/frontend/web/src/features/dnd/dnd.ts +++ b/invokeai/frontend/web/src/features/dnd/dnd.ts @@ -9,9 +9,11 @@ import { selectComparisonImages } from 'features/gallery/components/ImageViewer/ import type { BoardId } from 'features/gallery/store/types'; import { addImagesToBoard, + addVideosToBoard, createNewCanvasEntityFromImage, newCanvasFromImage, removeImagesFromBoard, + removeVideosFromBoard, replaceCanvasEntityObjectsWithImage, setComparisonImage, setGlobalReferenceImage, @@ -91,7 +93,7 @@ const _multipleVideo = buildTypeAndKey('multiple-video'); export type MultipleVideoDndSourceData = DndData< typeof _multipleVideo.type, typeof _multipleVideo.key, - { ids: string[]; board_id: BoardId } + { video_ids: string[]; board_id: BoardId } >; export const multipleVideoDndSource: DndSource = { ..._multipleVideo, @@ -473,12 +475,22 @@ export type AddImageToBoardDndTargetData = DndData< >; export const addImageToBoardDndTarget: DndTarget< AddImageToBoardDndTargetData, - SingleImageDndSourceData | MultipleImageDndSourceData + SingleImageDndSourceData | MultipleImageDndSourceData | SingleVideoDndSourceData | MultipleVideoDndSourceData > = { ..._addToBoard, typeGuard: buildTypeGuard(_addToBoard.key), getData: buildGetData(_addToBoard.key, _addToBoard.type), isValid: ({ sourceData, targetData }) => { + if (singleVideoDndSource.typeGuard(sourceData)) { + const currentBoard = sourceData.payload.videoDTO.board_id ?? 'none'; + const destinationBoard = targetData.payload.boardId; + return currentBoard !== destinationBoard; + } + if (multipleVideoDndSource.typeGuard(sourceData)) { + const currentBoard = sourceData.payload.board_id; + const destinationBoard = targetData.payload.boardId; + return currentBoard !== destinationBoard; + } if (singleImageDndSource.typeGuard(sourceData)) { const currentBoard = sourceData.payload.imageDTO.board_id ?? 'none'; const destinationBoard = targetData.payload.boardId; @@ -492,6 +504,18 @@ export const addImageToBoardDndTarget: DndTarget< return false; }, handler: ({ sourceData, targetData, dispatch }) => { + if (singleVideoDndSource.typeGuard(sourceData)) { + const { videoDTO } = sourceData.payload; + const { boardId } = targetData.payload; + addVideosToBoard({ video_ids: [videoDTO.video_id], boardId, dispatch }); + } + + if (multipleVideoDndSource.typeGuard(sourceData)) { + const { video_ids } = sourceData.payload; + const { boardId } = targetData.payload; + addVideosToBoard({ video_ids, boardId, dispatch }); + } + if (singleImageDndSource.typeGuard(sourceData)) { const { imageDTO } = sourceData.payload; const { boardId } = targetData.payload; @@ -517,7 +541,7 @@ export type RemoveImageFromBoardDndTargetData = DndData< >; export const removeImageFromBoardDndTarget: DndTarget< RemoveImageFromBoardDndTargetData, - SingleImageDndSourceData | MultipleImageDndSourceData + SingleImageDndSourceData | MultipleImageDndSourceData | SingleVideoDndSourceData | MultipleVideoDndSourceData > = { ..._removeFromBoard, typeGuard: buildTypeGuard(_removeFromBoard.key), @@ -533,6 +557,16 @@ export const removeImageFromBoardDndTarget: DndTarget< return currentBoard !== 'none'; } + if (singleVideoDndSource.typeGuard(sourceData)) { + const currentBoard = sourceData.payload.videoDTO.board_id ?? 'none'; + return currentBoard !== 'none'; + } + + if (multipleVideoDndSource.typeGuard(sourceData)) { + const currentBoard = sourceData.payload.board_id; + return currentBoard !== 'none'; + } + return false; }, handler: ({ sourceData, dispatch }) => { @@ -545,6 +579,16 @@ export const removeImageFromBoardDndTarget: DndTarget< const { image_names } = sourceData.payload; removeImagesFromBoard({ image_names, dispatch }); } + + if (singleVideoDndSource.typeGuard(sourceData)) { + const { videoDTO } = sourceData.payload; + removeVideosFromBoard({ video_ids: [videoDTO.video_id], dispatch }); + } + + if (multipleVideoDndSource.typeGuard(sourceData)) { + const { video_ids } = sourceData.payload; + removeVideosFromBoard({ video_ids, dispatch }); + } }, }; diff --git a/invokeai/frontend/web/src/features/dnd/useDndMonitor.ts b/invokeai/frontend/web/src/features/dnd/useDndMonitor.ts index 24d6bea168..8d2aeb30e7 100644 --- a/invokeai/frontend/web/src/features/dnd/useDndMonitor.ts +++ b/invokeai/frontend/web/src/features/dnd/useDndMonitor.ts @@ -4,7 +4,13 @@ import { logger } from 'app/logging/logger'; import { getStore } from 'app/store/nanostores/store'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; import { parseify } from 'common/util/serialize'; -import { dndTargets, multipleImageDndSource, singleImageDndSource } from 'features/dnd/dnd'; +import { + dndTargets, + multipleImageDndSource, + multipleVideoDndSource, + singleImageDndSource, + singleVideoDndSource, +} from 'features/dnd/dnd'; import { useEffect } from 'react'; const log = logger('dnd'); @@ -19,7 +25,12 @@ export const useDndMonitor = () => { const sourceData = source.data; // Check for allowed sources - if (!singleImageDndSource.typeGuard(sourceData) && !multipleImageDndSource.typeGuard(sourceData)) { + if ( + !singleImageDndSource.typeGuard(sourceData) && + !multipleImageDndSource.typeGuard(sourceData) && + !singleVideoDndSource.typeGuard(sourceData) && + !multipleVideoDndSource.typeGuard(sourceData) + ) { return false; } diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryVideo.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryVideo.tsx index c4ba374ad1..ade4d8b5ed 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryVideo.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryVideo.tsx @@ -112,7 +112,7 @@ export const GalleryVideo = memo(({ videoDTO }: Props) => { // multi-image drag. if (selection.length > 1 && selection.some((s) => s.id === videoDTO.video_id)) { return multipleVideoDndSource.getData({ - ids: selection.map((s) => s.id), + video_ids: selection.map((s) => s.id), board_id: boardId, }); } // Otherwise, initiate a single-image drag @@ -149,7 +149,10 @@ export const GalleryVideo = memo(({ videoDTO }: Props) => { onDragStart: ({ source }) => { // When we start dragging multiple images, set the dragging state to true if the dragged image is part of the // selection. This is called for all drag events. - if (multipleVideoDndSource.typeGuard(source.data) && source.data.payload.ids.includes(videoDTO.video_id)) { + if ( + multipleVideoDndSource.typeGuard(source.data) && + source.data.payload.video_ids.includes(videoDTO.video_id) + ) { setIsDragging(true); } }, diff --git a/invokeai/frontend/web/src/features/imageActions/actions.ts b/invokeai/frontend/web/src/features/imageActions/actions.ts index c53d6dab23..c27f415da6 100644 --- a/invokeai/frontend/web/src/features/imageActions/actions.ts +++ b/invokeai/frontend/web/src/features/imageActions/actions.ts @@ -37,6 +37,7 @@ import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; import { navigationApi } from 'features/ui/layouts/navigation-api'; import { WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared'; import { imageDTOToFile, imagesApi, uploadImage } from 'services/api/endpoints/images'; +import { videosApi } from 'services/api/endpoints/videos'; import type { ImageDTO } from 'services/api/types'; import type { Equals } from 'tsafe'; import { assert } from 'tsafe'; @@ -318,3 +319,15 @@ export const removeImagesFromBoard = (arg: { image_names: string[]; dispatch: Ap dispatch(imagesApi.endpoints.removeImagesFromBoard.initiate({ image_names }, { track: false })); dispatch(selectionChanged([])); }; + +export const addVideosToBoard = (arg: { video_ids: string[]; boardId: BoardId; dispatch: AppDispatch }) => { + const { video_ids, boardId, dispatch } = arg; + dispatch(videosApi.endpoints.addVideosToBoard.initiate({ video_ids, board_id: boardId }, { track: false })); + dispatch(selectionChanged([])); +}; + +export const removeVideosFromBoard = (arg: { video_ids: string[]; dispatch: AppDispatch }) => { + const { video_ids, dispatch } = arg; + dispatch(videosApi.endpoints.removeVideosFromBoard.initiate({ video_ids }, { track: false })); + dispatch(selectionChanged([])); +};