forked from 77media/video-flow
视频首帧、下载loading等
This commit is contained in:
parent
eee3026699
commit
08ca156174
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { Loader2, Check, Pencil, Trash, Download } from 'lucide-react';
|
import { Loader2, Download } from 'lucide-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import './style/create-to-video2.css';
|
import './style/create-to-video2.css';
|
||||||
|
|
||||||
@ -28,6 +28,7 @@ export default function CreateToVideo2() {
|
|||||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const [userId, setUserId] = useState<number>(0);
|
const [userId, setUserId] = useState<number>(0);
|
||||||
|
const [isLoadingDownloadBtn, setIsLoadingDownloadBtn] = useState(false);
|
||||||
|
|
||||||
// 添加一个 ref 来跟踪当前正在加载的页码
|
// 添加一个 ref 来跟踪当前正在加载的页码
|
||||||
const loadingPageRef = useRef<number | null>(null);
|
const loadingPageRef = useRef<number | null>(null);
|
||||||
@ -193,23 +194,24 @@ export default function CreateToVideo2() {
|
|||||||
<div
|
<div
|
||||||
key={project.project_id}
|
key={project.project_id}
|
||||||
className="group flex flex-col bg-black/20 rounded-lg overflow-hidden cursor-pointer hover:bg-white/5 transition-all duration-300"
|
className="group flex flex-col bg-black/20 rounded-lg overflow-hidden cursor-pointer hover:bg-white/5 transition-all duration-300"
|
||||||
onClick={() => router.push(`/movies/work-flow?episodeId=${project.project_id}`)}
|
|
||||||
onMouseEnter={() => handleMouseEnter(project.project_id)}
|
onMouseEnter={() => handleMouseEnter(project.project_id)}
|
||||||
onMouseLeave={() => handleMouseLeave(project.project_id)}
|
onMouseLeave={() => handleMouseLeave(project.project_id)}
|
||||||
data-alt="project-card"
|
data-alt="project-card"
|
||||||
>
|
>
|
||||||
{/* 视频/图片区域 */}
|
{/* 视频/图片区域 */}
|
||||||
<div className="relative aspect-video">
|
<div className="relative aspect-video" onClick={() => router.push(`/movies/work-flow?episodeId=${project.project_id}`)}>
|
||||||
{(project.final_video_url || project.final_simple_video_url) ? (
|
{(project.final_video_url || project.final_simple_video_url || project.video_urls) ? (
|
||||||
<video
|
<video
|
||||||
ref={(el) => setVideoRef(project.project_id, el)}
|
ref={(el) => setVideoRef(project.project_id, el)}
|
||||||
src={project.final_video_url || project.final_simple_video_url}
|
src={project.final_video_url || project.final_simple_video_url || project.video_urls}
|
||||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||||
muted
|
muted
|
||||||
loop
|
loop
|
||||||
playsInline
|
playsInline
|
||||||
preload="none"
|
preload="none"
|
||||||
poster={`${project.final_video_url || project.final_simple_video_url}?vframe/jpg/offset/1`}
|
poster={
|
||||||
|
project.final_video_url || project.final_simple_video_url ? `${project.final_video_url || project.final_simple_video_url}?vframe/jpg/offset/1` : `${project.video_urls}?x-oss-process=video/snapshot,t_1000,f_jpg`
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
@ -224,9 +226,14 @@ export default function CreateToVideo2() {
|
|||||||
{(project.final_video_url || project.final_simple_video_url) && (
|
{(project.final_video_url || project.final_simple_video_url) && (
|
||||||
<div className="absolute top-1 right-1">
|
<div className="absolute top-1 right-1">
|
||||||
<Tooltip placement="top" title="Download">
|
<Tooltip placement="top" title="Download">
|
||||||
<Button size="small" type="text" className="w-[2.5rem] h-[2.5rem] rounded-full items-center justify-center p-0 hidden group-hover:flex transition-all duration-300 hover:bg-white/15" onClick={() => {
|
<Button size="small" type="text" disabled={isLoadingDownloadBtn} className="w-[2.5rem] h-[2.5rem] rounded-full items-center justify-center p-0 hidden group-hover:flex transition-all duration-300 hover:bg-white/15" onClick={async (e) => {
|
||||||
downloadVideo(project.final_video_url || project.final_simple_video_url);
|
e.stopPropagation(); // 阻止事件冒泡
|
||||||
}}><Download className="w-4 h-4 text-white" /></Button>
|
setIsLoadingDownloadBtn(true);
|
||||||
|
await downloadVideo(project.final_video_url || project.final_simple_video_url);
|
||||||
|
setIsLoadingDownloadBtn(false);
|
||||||
|
}}>
|
||||||
|
{isLoadingDownloadBtn ? <Loader2 className="w-4 h-4 animate-spin text-white" /> : <Download className="w-4 h-4 text-white" />}
|
||||||
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -50,18 +50,8 @@ const WorkFlow = React.memo(function WorkFlow() {
|
|||||||
aiEditingButtonRef.current?.handleAIEditing();
|
aiEditingButtonRef.current?.handleAIEditing();
|
||||||
editingNotificationKey.current = `editing-${Date.now()}`;
|
editingNotificationKey.current = `editing-${Date.now()}`;
|
||||||
showEditingNotification({
|
showEditingNotification({
|
||||||
title: '视频编辑中', // 可选
|
|
||||||
description: 'AI正在为您编辑视频...', // 可选
|
description: 'AI正在为您编辑视频...', // 可选
|
||||||
key: editingNotificationKey.current,
|
key: editingNotificationKey.current,
|
||||||
onComplete: () => {
|
|
||||||
console.log('编辑完成');
|
|
||||||
// 3秒后关闭通知
|
|
||||||
setTimeout(() => {
|
|
||||||
if (editingNotificationKey.current) {
|
|
||||||
notification.destroy(editingNotificationKey.current);
|
|
||||||
}
|
|
||||||
}, 3000);
|
|
||||||
},
|
|
||||||
onFail: () => {
|
onFail: () => {
|
||||||
console.log('编辑失败');
|
console.log('编辑失败');
|
||||||
// 清缓存 生成计划 视频重新分析
|
// 清缓存 生成计划 视频重新分析
|
||||||
@ -115,7 +105,16 @@ const WorkFlow = React.memo(function WorkFlow() {
|
|||||||
// 更新通知状态为完成
|
// 更新通知状态为完成
|
||||||
showEditingNotification({
|
showEditingNotification({
|
||||||
isCompleted: true,
|
isCompleted: true,
|
||||||
key: editingNotificationKey.current
|
key: editingNotificationKey.current,
|
||||||
|
onComplete: () => {
|
||||||
|
console.log('编辑完成');
|
||||||
|
// 3秒后关闭通知
|
||||||
|
setTimeout(() => {
|
||||||
|
if (editingNotificationKey.current) {
|
||||||
|
notification.destroy(editingNotificationKey.current);
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [taskObject.final, isHandleEdit]);
|
}, [taskObject.final, isHandleEdit]);
|
||||||
|
|||||||
@ -489,6 +489,7 @@ export const AIEditingIframeButton = React.forwardRef<
|
|||||||
// 暴露 handleAIEditing 方法
|
// 暴露 handleAIEditing 方法
|
||||||
React.useImperativeHandle(ref, () => ({
|
React.useImperativeHandle(ref, () => ({
|
||||||
handleAIEditing: async () => {
|
handleAIEditing: async () => {
|
||||||
|
console.log('handleAIEditing', iframeRef.current);
|
||||||
iframeRef.current?.startAIEditing();
|
iframeRef.current?.startAIEditing();
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|||||||
@ -34,8 +34,6 @@ const descriptionStyle = {
|
|||||||
interface EditingNotificationProps {
|
interface EditingNotificationProps {
|
||||||
/** 编辑是否完成 */
|
/** 编辑是否完成 */
|
||||||
isCompleted?: boolean;
|
isCompleted?: boolean;
|
||||||
/** 自定义标题 */
|
|
||||||
title?: string;
|
|
||||||
/** 自定义描述 */
|
/** 自定义描述 */
|
||||||
description?: string;
|
description?: string;
|
||||||
/** key */
|
/** key */
|
||||||
@ -53,7 +51,6 @@ interface EditingNotificationProps {
|
|||||||
export const showEditingNotification = (props: EditingNotificationProps) => {
|
export const showEditingNotification = (props: EditingNotificationProps) => {
|
||||||
const {
|
const {
|
||||||
isCompleted = false,
|
isCompleted = false,
|
||||||
title = 'AI Video Editing',
|
|
||||||
description = 'Your video is being edited by AI...',
|
description = 'Your video is being edited by AI...',
|
||||||
key,
|
key,
|
||||||
onComplete,
|
onComplete,
|
||||||
@ -125,15 +122,14 @@ export const showEditingNotification = (props: EditingNotificationProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-alt="editing-notification" style={{ minWidth: '300px' }}>
|
<div data-alt="editing-notification" style={{ minWidth: '300px' }}>
|
||||||
<h3 style={messageStyle}>
|
<h3 style={{
|
||||||
{scissorsIcon}
|
|
||||||
{title}
|
|
||||||
</h3>
|
|
||||||
<p style={{
|
|
||||||
...descriptionStyle,
|
...descriptionStyle,
|
||||||
color: status === 'exception' ? '#ff4d4f' :
|
color: status === 'exception' ? '#ff4d4f' :
|
||||||
'rgba(255, 255, 255, 0.65)',
|
'rgba(255, 255, 255, 0.65)',
|
||||||
}}>{currentDescription}</p>
|
}}>
|
||||||
|
{scissorsIcon}
|
||||||
|
{currentDescription}
|
||||||
|
</h3>
|
||||||
<Progress
|
<Progress
|
||||||
percent={Math.round(progress)}
|
percent={Math.round(progress)}
|
||||||
status={status}
|
status={status}
|
||||||
|
|||||||
@ -64,6 +64,7 @@ export const MediaViewer = React.memo(function MediaViewer({
|
|||||||
const [finalVideoReady, setFinalVideoReady] = useState(false);
|
const [finalVideoReady, setFinalVideoReady] = useState(false);
|
||||||
const [userHasInteracted, setUserHasInteracted] = useState(false);
|
const [userHasInteracted, setUserHasInteracted] = useState(false);
|
||||||
const [toosBtnRight, setToodsBtnRight] = useState('1rem');
|
const [toosBtnRight, setToodsBtnRight] = useState('1rem');
|
||||||
|
const [isLoadingDownloadBtn, setIsLoadingDownloadBtn] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSmartChatBoxOpen) {
|
if (isSmartChatBoxOpen) {
|
||||||
@ -365,8 +366,10 @@ export const MediaViewer = React.memo(function MediaViewer({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/* 下载按钮 */}
|
{/* 下载按钮 */}
|
||||||
<Tooltip placement="top" title="Download video">
|
<Tooltip placement="top" title="Download video">
|
||||||
<GlassIconButton icon={Download} size='sm' onClick={() => {
|
<GlassIconButton icon={Download} loading={isLoadingDownloadBtn} size='sm' onClick={async () => {
|
||||||
downloadVideo(taskObject.final.url);
|
setIsLoadingDownloadBtn(true);
|
||||||
|
await downloadVideo(taskObject.final.url);
|
||||||
|
setIsLoadingDownloadBtn(false);
|
||||||
}} />
|
}} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{showGotoCutButton && (
|
{showGotoCutButton && (
|
||||||
@ -492,10 +495,12 @@ export const MediaViewer = React.memo(function MediaViewer({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/* 下载按钮 */}
|
{/* 下载按钮 */}
|
||||||
<Tooltip placement="top" title="Download video">
|
<Tooltip placement="top" title="Download video">
|
||||||
<GlassIconButton icon={Download} size='sm' onClick={() => {
|
<GlassIconButton icon={Download} loading={isLoadingDownloadBtn} size='sm' onClick={async () => {
|
||||||
const currentVideo = taskObject.videos.data[currentSketchIndex];
|
const currentVideo = taskObject.videos.data[currentSketchIndex];
|
||||||
if (currentVideo && currentVideo.urls && currentVideo.urls.length > 0) {
|
if (currentVideo && currentVideo.urls && currentVideo.urls.length > 0) {
|
||||||
downloadVideo(currentVideo.urls[0]);
|
setIsLoadingDownloadBtn(true);
|
||||||
|
await downloadVideo(currentVideo.urls[0]);
|
||||||
|
setIsLoadingDownloadBtn(false);
|
||||||
}
|
}
|
||||||
}} />
|
}} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -648,7 +648,7 @@ export function useWorkflowData({ onEditPlanGenerated }: UseWorkflowDataProps =
|
|||||||
fallbackToStep,
|
fallbackToStep,
|
||||||
originalText: state.originalText,
|
originalText: state.originalText,
|
||||||
// showGotoCutButton: from && currentLoadingText.includes('Post-production') ? true : false,
|
// showGotoCutButton: from && currentLoadingText.includes('Post-production') ? true : false,
|
||||||
showGotoCutButton: canGoToCut && isGenerateEditPlan ? true : false,
|
showGotoCutButton: canGoToCut && (isGenerateEditPlan || taskObject.currentStage === 'final_video') ? true : false,
|
||||||
generateEditPlan: openEditPlan,
|
generateEditPlan: openEditPlan,
|
||||||
handleRetryVideo
|
handleRetryVideo
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import React, { forwardRef } from 'react';
|
import React, { forwardRef } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { LucideIcon } from 'lucide-react';
|
import { LucideIcon, Loader2 } from 'lucide-react';
|
||||||
import { cn } from '@/public/lib/utils';
|
import { cn } from '@/public/lib/utils';
|
||||||
|
|
||||||
// Define props without the ref
|
// Define props without the ref
|
||||||
@ -14,6 +14,7 @@ interface GlassIconButtonProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
[key: string]: any; // To allow spreading other props
|
[key: string]: any; // To allow spreading other props
|
||||||
text?: string;
|
text?: string;
|
||||||
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const variantStyles = {
|
const variantStyles = {
|
||||||
@ -38,12 +39,12 @@ const iconSizes = {
|
|||||||
const MotionButton = motion.button;
|
const MotionButton = motion.button;
|
||||||
|
|
||||||
export const GlassIconButton = forwardRef<HTMLButtonElement, GlassIconButtonProps>(
|
export const GlassIconButton = forwardRef<HTMLButtonElement, GlassIconButtonProps>(
|
||||||
({ icon: Icon, tooltip, variant = 'secondary', size = 'md', className, text, ...props }, ref) => {
|
({ icon: Icon, tooltip, variant = 'secondary', size = 'md', className, text, loading = false, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<MotionButton
|
<MotionButton
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative rounded-full backdrop-blur-md transition-colors shadow-lg border flex items-center gap-2',
|
'relative rounded-full backdrop-blur-md transition-colors shadow-lg border flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed',
|
||||||
variantStyles[variant],
|
variantStyles[variant],
|
||||||
sizeStyles[size],
|
sizeStyles[size],
|
||||||
className
|
className
|
||||||
@ -59,12 +60,13 @@ export const GlassIconButton = forwardRef<HTMLButtonElement, GlassIconButtonProp
|
|||||||
perspective: '1000px'
|
perspective: '1000px'
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<Icon className={cn('text-white', iconSizes[size])} />
|
{loading ? <Loader2 className={cn('text-white', iconSizes[size])} /> : <Icon className={cn('text-white', iconSizes[size])} />}
|
||||||
{text && (
|
{text && !loading && (
|
||||||
<span className="text-white text-xs">{text}</span>
|
<span className="text-white text-xs">{text}</span>
|
||||||
)}
|
)}
|
||||||
{tooltip && (
|
{tooltip && !loading && (
|
||||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 text-xs
|
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 text-xs
|
||||||
bg-black/80 text-white rounded-md opacity-0 group-hover:opacity-100 transition-opacity
|
bg-black/80 text-white rounded-md opacity-0 group-hover:opacity-100 transition-opacity
|
||||||
pointer-events-none whitespace-nowrap">
|
pointer-events-none whitespace-nowrap">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user