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 ( + <> +
+
+ +
+
+ Preview video +
+
+ + + 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