forked from 77media/video-flow
- [x] 显示 原分镜,剪辑后
- [x] 支持批量下载分镜视频 Chatbox查找消息是否有任务 还在执行中
This commit is contained in:
parent
254b226c6e
commit
9365896bb6
@ -267,6 +267,15 @@ export function useMessages({ config, onMessagesUpdate }: UseMessagesProps): [Me
|
||||
}
|
||||
}, [updateMessages]);
|
||||
|
||||
// 监听 消息列表中是否存在 pending 状态的消息,有的话 将 loading 置为 true
|
||||
useEffect(() => {
|
||||
if (displayMessages.some(msg => msg.status === 'pending')) {
|
||||
setIsLoading(true);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [displayMessages]);
|
||||
|
||||
return [
|
||||
{
|
||||
messages: displayMessages,
|
||||
|
||||
@ -216,7 +216,7 @@ const WorkFlow = React.memo(function WorkFlow() {
|
||||
{isLoading ? (
|
||||
<Skeleton className="w-full aspect-video rounded-lg" />
|
||||
) : (
|
||||
<div className={`relative heroVideo-FIzuK1 ${['final_video', 'script'].includes(taskObject.currentStage) ? 'h-[calc(100vh-6rem)] w-[calc((100vh-6rem)/9*16)]' : 'h-[calc(100vh-6rem-200px)] w-[calc((100vh-6rem-200px)/9*16)]'}`} style={{ aspectRatio: "16 / 9" }} key={taskObject.currentStage+'_'+currentSketchIndex}>
|
||||
<div className={`relative heroVideo-FIzuK1 ${['script'].includes(taskObject.currentStage) ? 'h-[calc(100vh-6rem)] w-[calc((100vh-6rem)/9*16)]' : 'h-[calc(100vh-6rem-200px)] w-[calc((100vh-6rem-200px)/9*16)]'}`} style={{ aspectRatio: "16 / 9" }} key={taskObject.currentStage+'_'+currentSketchIndex}>
|
||||
<MediaViewer
|
||||
taskObject={taskObject}
|
||||
scriptData={scriptData}
|
||||
@ -242,7 +242,7 @@ const WorkFlow = React.memo(function WorkFlow() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{taskObject.currentStage !== 'final_video' && taskObject.currentStage !== 'script' && (
|
||||
{taskObject.currentStage !== 'script' && (
|
||||
<div className="h-[123px] w-[calc((100vh-6rem-200px)/9*16)]">
|
||||
<ThumbnailGrid
|
||||
isDisabledFocus={isEditModalOpen || isPauseWorkFlow || isFocusChatInput}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import React, { useRef, useEffect, useState, SetStateAction, useMemo } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Edit3, Play, Pause, Volume2, VolumeX, Maximize, Minimize, Loader2, X, Scissors, RotateCcw, MessageCircleMore, Download } from 'lucide-react';
|
||||
import { Edit3, Play, Pause, Volume2, VolumeX, Maximize, Minimize, Loader2, X, Scissors, RotateCcw, MessageCircleMore, Download, ArrowDownWideNarrow } from 'lucide-react';
|
||||
import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal';
|
||||
import { GlassIconButton } from '@/components/ui/glass-icon-button';
|
||||
import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer';
|
||||
@ -10,7 +10,7 @@ import { mockScriptData } from '@/components/script-renderer/mock';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { TaskObject } from '@/api/DTO/movieEdit';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { downloadVideo } from '@/utils/tools';
|
||||
import { downloadVideo, downloadAllVideos } from '@/utils/tools';
|
||||
|
||||
interface MediaViewerProps {
|
||||
taskObject: TaskObject;
|
||||
@ -65,6 +65,7 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
const [userHasInteracted, setUserHasInteracted] = useState(false);
|
||||
const [toosBtnRight, setToodsBtnRight] = useState('1rem');
|
||||
const [isLoadingDownloadBtn, setIsLoadingDownloadBtn] = useState(false);
|
||||
const [isLoadingDownloadAllVideosBtn, setIsLoadingDownloadAllVideosBtn] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSmartChatBoxOpen) {
|
||||
@ -364,6 +365,14 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
onClick={() => handleEditClick('3', 'final')}
|
||||
/>
|
||||
</Tooltip>
|
||||
{/* 下载所有视频按钮 */}
|
||||
<Tooltip placement="top" title="Download all videos">
|
||||
<GlassIconButton icon={ArrowDownWideNarrow} size='sm' loading={isLoadingDownloadAllVideosBtn} onClick={ async () => {
|
||||
setIsLoadingDownloadAllVideosBtn(true);
|
||||
await downloadAllVideos(taskObject.videos.data.flatMap((video: any) => video.urls));
|
||||
setIsLoadingDownloadAllVideosBtn(false);
|
||||
}} />
|
||||
</Tooltip>
|
||||
{/* 下载按钮 */}
|
||||
<Tooltip placement="top" title="Download video">
|
||||
<GlassIconButton icon={Download} loading={isLoadingDownloadBtn} size='sm' onClick={async () => {
|
||||
@ -493,6 +502,14 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
}
|
||||
}} />
|
||||
</Tooltip>
|
||||
{/* 下载所有视频按钮 */}
|
||||
<Tooltip placement="top" title="Download all videos">
|
||||
<GlassIconButton icon={ArrowDownWideNarrow} size='sm' loading={isLoadingDownloadAllVideosBtn} onClick={ async () => {
|
||||
setIsLoadingDownloadAllVideosBtn(true);
|
||||
await downloadAllVideos(taskObject.videos.data.flatMap((video: any) => video.urls));
|
||||
setIsLoadingDownloadAllVideosBtn(false);
|
||||
}} />
|
||||
</Tooltip>
|
||||
{/* 下载按钮 */}
|
||||
<Tooltip placement="top" title="Download video">
|
||||
<GlassIconButton icon={Download} loading={isLoadingDownloadBtn} size='sm' onClick={async () => {
|
||||
|
||||
@ -15,6 +15,9 @@ interface ThumbnailGridProps {
|
||||
onRetryVideo: (video_id: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 视频缩略图网格组件,支持hover时播放视频预览
|
||||
*/
|
||||
export function ThumbnailGrid({
|
||||
isDisabledFocus,
|
||||
taskObject,
|
||||
@ -22,6 +25,17 @@ export function ThumbnailGrid({
|
||||
onSketchSelect,
|
||||
onRetryVideo
|
||||
}: ThumbnailGridProps) {
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||
|
||||
/** 处理鼠标进入缩略图事件 */
|
||||
const handleMouseEnter = useCallback((index: number) => {
|
||||
setHoveredIndex(index);
|
||||
}, []);
|
||||
|
||||
/** 处理鼠标离开缩略图事件 */
|
||||
const handleMouseLeave = useCallback((index: number) => {
|
||||
setHoveredIndex(null);
|
||||
}, []);
|
||||
const thumbnailsRef = useRef<HTMLDivElement>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [startX, setStartX] = useState(0);
|
||||
@ -159,13 +173,8 @@ export function ThumbnailGrid({
|
||||
console.log('taskObject.currentStage_thumbnail-grid', taskObject.currentStage);
|
||||
}, [taskObject.currentStage]);
|
||||
|
||||
// 粗剪/精剪最终成片阶段不显示缩略图
|
||||
if (taskObject.currentStage === 'final_video') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 渲染视频阶段的缩略图
|
||||
const renderVideoThumbnails = () => (
|
||||
const renderVideoThumbnails = (disabled: boolean = false) => (
|
||||
taskObject.videos.data.map((video, index) => {
|
||||
const urls: string = video.urls ? video.urls.join(',') : '';
|
||||
|
||||
@ -173,8 +182,8 @@ export function ThumbnailGrid({
|
||||
<div
|
||||
key={`video-${urls}-${index}`}
|
||||
className={`relative aspect-video rounded-lg overflow-hidden
|
||||
${currentSketchIndex === index ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}`}
|
||||
onClick={() => !isDragging && onSketchSelect(index)}
|
||||
${(currentSketchIndex === index && !disabled) ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}`}
|
||||
onClick={() => !isDragging && !disabled && onSketchSelect(index)}
|
||||
>
|
||||
|
||||
{/* 视频层 */}
|
||||
@ -202,11 +211,28 @@ export function ThumbnailGrid({
|
||||
// loop
|
||||
// muted
|
||||
// />
|
||||
<img
|
||||
className="w-full h-full object-cover"
|
||||
src={`${taskObject.videos.data[index].urls[0]}?x-oss-process=video/snapshot,t_1000,f_jpg`}
|
||||
draggable="false"
|
||||
/>
|
||||
<div
|
||||
className="w-full h-full relative"
|
||||
onMouseEnter={() => handleMouseEnter(index)}
|
||||
onMouseLeave={() => handleMouseLeave(index)}
|
||||
>
|
||||
<img
|
||||
className="w-full h-full object-cover"
|
||||
src={`${taskObject.videos.data[index].urls[0]}?x-oss-process=video/snapshot,t_1000,f_jpg`}
|
||||
draggable="false"
|
||||
alt="video thumbnail"
|
||||
/>
|
||||
{hoveredIndex === index && (
|
||||
<video
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
src={taskObject.videos.data[index].urls[0]}
|
||||
autoPlay
|
||||
muted
|
||||
playsInline
|
||||
loop
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
|
||||
|
||||
@ -316,6 +342,7 @@ export function ThumbnailGrid({
|
||||
>
|
||||
{taskObject.currentStage === 'video' && renderVideoThumbnails()}
|
||||
{(taskObject.currentStage === 'scene' || taskObject.currentStage === 'character') && renderSketchThumbnails(getCurrentData())}
|
||||
{taskObject.currentStage === 'final_video' && renderVideoThumbnails(true)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -716,7 +716,6 @@ export function useWorkflowData({ onEditPlanGenerated }: UseWorkflowDataProps =
|
||||
applyScript,
|
||||
fallbackToStep,
|
||||
originalText: state.originalText,
|
||||
// showGotoCutButton: from && currentLoadingText.includes('Post-production') ? true : false,
|
||||
showGotoCutButton: (canGoToCut && (isGenerateEditPlan || taskObject.currentStage === 'final_video') || isShowError) ? true : false,
|
||||
generateEditPlan: openEditPlan,
|
||||
handleRetryVideo
|
||||
|
||||
@ -94,3 +94,13 @@ export const downloadVideo = async (url: string) => {
|
||||
console.error('下载视频失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 下载所有视频
|
||||
* @param urls 视频URL列表
|
||||
*/
|
||||
export const downloadAllVideos = async (urls: string[]) => {
|
||||
for (const url of urls) {
|
||||
await downloadVideo(url);
|
||||
}
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user