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([]));
+};