From 9e7c7c0c1f93992157ae936a4ce246186430fdd3 Mon Sep 17 00:00:00 2001
From: Marc Chen <570171025@qq.com>
Date: Wed, 10 Jul 2024 16:52:46 +0800
Subject: [PATCH] support video preview
---
.../context-menu-item-save-images.tsx | 10 +---
.../reactflow-gallery/gallery.tsx | 45 +++++++-------
.../input-video-player-async.tsx | 54 ++++++++++++++++-
.../reactflow-input/input-video-player.tsx | 5 +-
.../reactflow-input/input-video-upload.tsx | 9 ++-
.../reactflow-node-imagepreviews.tsx | 60 +++++++++++++++----
.../websocket-controller.tsx | 26 ++++++++
apps/electron-frontend/src/styles/global.scss | 41 +++++++++++++
8 files changed, 201 insertions(+), 49 deletions(-)
diff --git a/apps/electron-frontend/src/components/workflow-editor/reactflow-context-menu/context-menu-item-save-images.tsx b/apps/electron-frontend/src/components/workflow-editor/reactflow-context-menu/context-menu-item-save-images.tsx
index 65cd5768..bc5a4a8a 100644
--- a/apps/electron-frontend/src/components/workflow-editor/reactflow-context-menu/context-menu-item-save-images.tsx
+++ b/apps/electron-frontend/src/components/workflow-editor/reactflow-context-menu/context-menu-item-save-images.tsx
@@ -5,6 +5,7 @@ import { useCallback } from "react";
import { message } from "antd";
import {downloadFile, downloadImagesAsZip} from "@comflowy/common/utils/download-helper";
import { getImagePreviewUrl } from "@comflowy/common/comfyui-bridge/bridge";
+import { usePreviewImages } from "../reactflow-node/reactflow-node-imagepreviews";
export function SaveImageMenuItem(props: NodeMenuProps) {
const {id, node} = props;
@@ -13,14 +14,7 @@ export function SaveImageMenuItem(props: NodeMenuProps) {
if (images.length === 0) {
return null;
}
- const imageWithPreview = images.map(image => {
- const imageSrc = getImagePreviewUrl(image.filename, image.type, image.subfolder)
- return {
- src: imageSrc,
- filename: image.filename,
- image
- }
- });;
+ const {mixed: imageWithPreview} = usePreviewImages(images)
const doDownload = useCallback(async () => {
try {
if (imageWithPreview.length === 1) {
diff --git a/apps/electron-frontend/src/components/workflow-editor/reactflow-gallery/gallery.tsx b/apps/electron-frontend/src/components/workflow-editor/reactflow-gallery/gallery.tsx
index b43a6541..a10337a3 100644
--- a/apps/electron-frontend/src/components/workflow-editor/reactflow-gallery/gallery.tsx
+++ b/apps/electron-frontend/src/components/workflow-editor/reactflow-gallery/gallery.tsx
@@ -9,6 +9,8 @@ import { KEYS, t } from "@comflowy/common/i18n";
import { ImageWithDownload, PreviewGroupWithDownload } from './image-with-download';
import { PreviewImage } from '@comflowy/common/types';
import { downloadFile, downloadImagesAsZip } from '@comflowy/common/utils/download-helper';
+import { usePreviewImages } from '../reactflow-node/reactflow-node-imagepreviews';
+import { VideoPreview } from '../reactflow-input/input-video-player-async';
const Gallery = (props: {
editing?: boolean;
@@ -16,24 +18,30 @@ const Gallery = (props: {
setSelectedImages?: (images: PreviewImage[]) => void;
}) => {
let images = useAppStore(st => st.persistedWorkflow.gallery || []);
- const imagesWithSrc = images.map(image => {
- const imageSrc = getImagePreviewUrl(image.filename, image.type, image.subfolder)
- return {
- src: imageSrc,
- filename: image.filename,
- image
- }
- });
+ const {mixed: imagesWithSrc} = usePreviewImages(images)
let $content = (
{imagesWithSrc.map((image, index) => {
- return (
-
- )
+ if (image.isImage) {
+ return (
+
+ )
+ }
+ if (image.isVideo) {
+ return (
+
+
+
+ )
+ }
+ return null
})}
)
@@ -107,14 +115,7 @@ export const GalleryEntry = React.memo(() => {
const downloadImages = useCallback(async () => {
try {
- const selectImagesWithSrc = selectedImages.map(image => {
- const imageSrc = getImagePreviewUrl(image.filename, image.type, image.subfolder)
- return {
- src: imageSrc,
- filename: image.filename,
- image
- }
- });;
+ const {mixed: selectImagesWithSrc} = usePreviewImages(selectedImages)
if (selectImagesWithSrc.length === 1) {
await downloadFile(selectImagesWithSrc[0].src, selectImagesWithSrc[0].filename)
diff --git a/apps/electron-frontend/src/components/workflow-editor/reactflow-input/input-video-player-async.tsx b/apps/electron-frontend/src/components/workflow-editor/reactflow-input/input-video-player-async.tsx
index 60449d85..f98935c3 100644
--- a/apps/electron-frontend/src/components/workflow-editor/reactflow-input/input-video-player-async.tsx
+++ b/apps/electron-frontend/src/components/workflow-editor/reactflow-input/input-video-player-async.tsx
@@ -1,3 +1,5 @@
+import { EyeOutlined } from "@ant-design/icons";
+import { Button, Modal, Space } from "antd";
import { Suspense, lazy, useEffect, useState } from "react";
import { isWindow } from "ui/utils/is-window";
@@ -5,7 +7,7 @@ const AsyncCO = lazy(async () => {
return await import("./input-video-player");
});
-export function AsyncVideoPlayer(props: {url: string}) {
+export function AsyncVideoPlayer(props: { url: string, controls?: boolean }) {
const [showFrontEndCode, setShowFrontEndCode] = useState(false);
useEffect(() => {
if (isWindow) {
@@ -18,7 +20,55 @@ export function AsyncVideoPlayer(props: {url: string}) {
return (
Loading...}>
-
+
);
+}
+
+export function VideoPreview(props: { url: string, controls?: boolean }) {
+ const [isModalVisible, setIsModalVisible] = useState(false);
+
+ const showModal = () => {
+ setIsModalVisible(true);
+ };
+
+ const handleCancel = () => {
+ setIsModalVisible(false);
+ };
+
+ const handleDownload = () => {
+ window.open(props.url, '_blank');
+ };
+
+ return (
+ <>
+
+
+
+ Download
+ ,
+ ,
+ ]}
+ width={800}
+ >
+
+
+ >
+ );
}
\ No newline at end of file
diff --git a/apps/electron-frontend/src/components/workflow-editor/reactflow-input/input-video-player.tsx b/apps/electron-frontend/src/components/workflow-editor/reactflow-input/input-video-player.tsx
index 391b478a..cd88ca91 100644
--- a/apps/electron-frontend/src/components/workflow-editor/reactflow-input/input-video-player.tsx
+++ b/apps/electron-frontend/src/components/workflow-editor/reactflow-input/input-video-player.tsx
@@ -3,9 +3,10 @@ import ReactPlayer from 'react-player';
interface InputVideoPlayerProps {
url: string;
+ controls?: boolean;
}
-const InputVideoPlayer: React.FC = ({ url }) => {
+const InputVideoPlayer: React.FC = ({ url, controls }) => {
const playerRef = useRef(null);
useEffect(() => {
@@ -16,7 +17,7 @@ const InputVideoPlayer: React.FC = ({ url }) => {
}, []);
return (
-
+
);
};
diff --git a/apps/electron-frontend/src/components/workflow-editor/reactflow-input/input-video-upload.tsx b/apps/electron-frontend/src/components/workflow-editor/reactflow-input/input-video-upload.tsx
index e4367447..fc78fcbd 100644
--- a/apps/electron-frontend/src/components/workflow-editor/reactflow-input/input-video-upload.tsx
+++ b/apps/electron-frontend/src/components/workflow-editor/reactflow-input/input-video-upload.tsx
@@ -7,7 +7,7 @@ import { useAppStore } from '@comflowy/common/store';
import { RcFile } from 'antd/es/upload';
import { getImagePreviewUrl, getUploadImageUrl } from '@comflowy/common/comfyui-bridge/bridge';
import { ImageWithDownload } from '../reactflow-gallery/image-with-download';
-import { AsyncVideoPlayer } from './input-video-player-async';
+import { AsyncVideoPlayer, VideoPreview } from './input-video-player-async';
export function InputUploadVideo({widget, node, id}: {
widget: Widget,
@@ -112,7 +112,12 @@ export function InputUploadVideo({widget, node, id}: {
}}
/>}
{!isGif && previewImage && (
-
+
+
+
)}
diff --git a/apps/electron-frontend/src/components/workflow-editor/reactflow-node/reactflow-node-imagepreviews.tsx b/apps/electron-frontend/src/components/workflow-editor/reactflow-node/reactflow-node-imagepreviews.tsx
index 70bb2705..7f298ca1 100644
--- a/apps/electron-frontend/src/components/workflow-editor/reactflow-node/reactflow-node-imagepreviews.tsx
+++ b/apps/electron-frontend/src/components/workflow-editor/reactflow-node/reactflow-node-imagepreviews.tsx
@@ -3,23 +3,12 @@ import { PreviewImage } from "@comflowy/common/types";
import { PreviewGroupWithDownload } from "../reactflow-gallery/image-with-download";
import { Image } from 'antd';
import {memo} from "react";
+import { VideoPreview } from "../reactflow-input/input-video-player-async";
export const NodeImagePreviews = memo(({ imagePreviews }: {
imagePreviews: PreviewImage[]
}) => {
- const imagePreviewsWithSrc = (imagePreviews || []).map((image, index) => {
- if (image.blobUrl) {
- return {
- src: image.blobUrl,
- filename: image.filename || "Untitled"
- }
- }
- const imageSrc = getImagePreviewUrl(image.filename, image.type, image.subfolder)
- return {
- src: imageSrc,
- filename: image.filename
- }
- });
+ const {images: imagePreviewsWithSrc, videos} = usePreviewImages(imagePreviews)
return (
1 ? "multiple" : "single"}`} >
@@ -37,8 +26,53 @@ export const NodeImagePreviews = memo(({ imagePreviews }: {
})
}
+ {videos.map((video, index) => {
+ return (
+
+
+
+ )
+ })}
)
});
+
+export function usePreviewImages(imagePreviews: PreviewImage[]) {
+ const imagePreviewsWithSrc = (imagePreviews || []).map((image, index) => {
+ if (image.blobUrl) {
+ return {
+ src: image.blobUrl,
+ filename: image.filename || "Untitled",
+ image,
+ isImage: true,
+ isVideo: false
+ }
+ }
+ const imageSrc = getImagePreviewUrl(image.filename, image.type, image.subfolder)
+ const isVideo = image.filename.endsWith("mp4") || image.filename.endsWith("mov") || image.filename.endsWith("avi") || image.filename.endsWith("webm")
+ const isImage = image.filename.endsWith("png") || image.filename.endsWith("jpg") || image.filename.endsWith("jpeg") || image.filename.endsWith("gif")
+ return {
+ src: imageSrc,
+ filename: image.filename,
+ image,
+ isVideo,
+ isImage
+ }
+ });
+
+ const videos = imagePreviewsWithSrc.filter(img => {
+ return img.isVideo;
+ });
+
+ const images = imagePreviewsWithSrc.filter(img => {
+ return img.isImage;
+ });
+
+ return { mixed: imagePreviewsWithSrc, images, videos };
+
+}
diff --git a/apps/electron-frontend/src/components/workflow-editor/websocket-controller/websocket-controller.tsx b/apps/electron-frontend/src/components/workflow-editor/websocket-controller/websocket-controller.tsx
index 5f9355ef..019b1aae 100644
--- a/apps/electron-frontend/src/components/workflow-editor/websocket-controller/websocket-controller.tsx
+++ b/apps/electron-frontend/src/components/workflow-editor/websocket-controller/websocket-controller.tsx
@@ -103,6 +103,32 @@ export function WsController(props: {clientId: string}): JSX.Element {
images.push(...a_images);
images.push(...b_images);
}
+
+ const gifs = msg.data.output?.gifs;
+ if (gifs) {
+ images.push(...gifs);
+ }
+
+ const videos = msg.data.output?.videos;
+ if (videos) {
+ images.push(...videos);
+ }
+
+ const audios = msg.data.output?.audios;
+ if (audios) {
+ images.push(...audios);
+ }
+
+ const files = msg.data.output?.files;
+ if (files) {
+ images.push(...files);
+ }
+
+ const texts = msg.data.output?.texts;
+ if (texts) {
+ images.push(...texts);
+ }
+
if (Array.isArray(images) && images.length > 0) {
onImageSave(msg.data.node, images)
}
diff --git a/apps/electron-frontend/src/styles/global.scss b/apps/electron-frontend/src/styles/global.scss
index 17e94555..b4472be1 100644
--- a/apps/electron-frontend/src/styles/global.scss
+++ b/apps/electron-frontend/src/styles/global.scss
@@ -171,4 +171,45 @@ span.meta, .ant-tag {
&:hover, &.active {
background-color: var(--backgroundColorL3);
}
+}
+
+
+.video-preview {
+ height: 100%;
+ width: 100%;
+ position: relative;
+ overflow: hidden;
+ border-radius: 6px;
+ .inner {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100%;
+ }
+ .preview-notication {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: rgba(0, 0, 0, 0.5);
+ color: white;
+ font-size: 14px;
+ font-weight: 500;
+ padding: 10px;
+ border-radius: 10px;
+ opacity: 0;
+ visibility: hidden;
+ transition: var(--transition);
+ cursor: pointer;
+ }
+ &:hover {
+ .preview-notication {
+ opacity: 1;
+ visibility: visible;
+ }
+ }
}
\ No newline at end of file