forked from 77media/video-flow
调整按钮位置
This commit is contained in:
parent
cf79fae40e
commit
fef2df1643
@ -8,13 +8,13 @@ import { MediaViewer } from "./work-flow/media-viewer";
|
||||
import { ThumbnailGrid } from "./work-flow/thumbnail-grid";
|
||||
import { useWorkflowData } from "./work-flow/use-workflow-data";
|
||||
import { usePlaybackControls } from "./work-flow/use-playback-controls";
|
||||
import { AlertCircle, RefreshCw, Pause, Play, ChevronLast, MessageSquareText } from "lucide-react";
|
||||
import { AlertCircle, RefreshCw, Pause, Play, ChevronLast, ChevronsLeft, Bot, BriefcaseBusiness, Scissors } from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
import { GlassIconButton } from '@/components/ui/glass-icon-button';
|
||||
import { SaveEditUseCase } from "@/app/service/usecase/SaveEditUseCase";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import SmartChatBox from "@/components/SmartChatBox/SmartChatBox";
|
||||
import { Drawer } from 'antd';
|
||||
import { Drawer, Tooltip } from 'antd';
|
||||
|
||||
const WorkFlow = React.memo(function WorkFlow() {
|
||||
useEffect(() => {
|
||||
@ -28,6 +28,7 @@ const WorkFlow = React.memo(function WorkFlow() {
|
||||
const [previewVideoUrl, setPreviewVideoUrl] = React.useState<string | null>(null);
|
||||
const [previewVideoId, setPreviewVideoId] = React.useState<string | null>(null);
|
||||
const [isFocusChatInput, setIsFocusChatInput] = React.useState(false);
|
||||
const [isHovered, setIsHovered] = React.useState(false);
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const episodeId = searchParams.get('episodeId') || '';
|
||||
@ -92,6 +93,7 @@ const WorkFlow = React.memo(function WorkFlow() {
|
||||
isPauseWorkFlow={isPauseWorkFlow}
|
||||
showGotoCutButton={showGotoCutButton}
|
||||
onGotoCut={generateEditPlan}
|
||||
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -100,41 +102,10 @@ const WorkFlow = React.memo(function WorkFlow() {
|
||||
className="videoContainer-qteKNi"
|
||||
ref={containerRef}
|
||||
>
|
||||
{dataLoadError ? (
|
||||
<motion.div
|
||||
className="flex flex-col items-center justify-center w-full aspect-video rounded-lg bg-red-50 border-2 border-red-200"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<motion.div
|
||||
className="flex items-center gap-3 mb-4"
|
||||
initial={{ scale: 0.8 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ duration: 0.3, delay: 0.2 }}
|
||||
>
|
||||
<AlertCircle className="w-8 h-8 text-red-500" />
|
||||
<h3 className="text-lg font-medium text-red-800">数据加载失败</h3>
|
||||
</motion.div>
|
||||
|
||||
<p className="text-red-600 text-center mb-6 max-w-md px-4">
|
||||
{dataLoadError}
|
||||
</p>
|
||||
|
||||
<motion.button
|
||||
className="flex items-center gap-2 px-6 py-3 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
|
||||
onClick={() => retryLoadData?.()}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
重试加载
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
) : isLoading ? (
|
||||
{isLoading ? (
|
||||
<Skeleton className="w-full aspect-video rounded-lg" />
|
||||
) : (
|
||||
<div className={`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 ${['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}>
|
||||
<MediaViewer
|
||||
taskObject={taskObject}
|
||||
scriptData={scriptData}
|
||||
@ -152,6 +123,9 @@ const WorkFlow = React.memo(function WorkFlow() {
|
||||
setPreviewVideoUrl(url);
|
||||
setPreviewVideoId(id);
|
||||
}}
|
||||
showGotoCutButton={showGotoCutButton}
|
||||
onGotoCut={generateEditPlan}
|
||||
isSmartChatBoxOpen={isSmartChatBoxOpen}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -171,35 +145,18 @@ const WorkFlow = React.memo(function WorkFlow() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 暂停/播放按钮 */}
|
||||
{
|
||||
(taskObject.currentStage !== 'final_video') && (
|
||||
<div className="absolute right-12 bottom-16 z-[49] flex gap-4">
|
||||
<GlassIconButton
|
||||
icon={isPauseWorkFlow ? Play : Pause}
|
||||
size='md'
|
||||
tooltip={isPauseWorkFlow ? "Play" : "Pause"}
|
||||
onClick={() => setIsPauseWorkFlow(!isPauseWorkFlow)}
|
||||
/>
|
||||
{ !mode.includes('auto') && (
|
||||
<GlassIconButton
|
||||
icon={ChevronLast}
|
||||
size='md'
|
||||
tooltip="Next"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{/* 智能对话按钮 */}
|
||||
<div className="absolute right-12 bottom-32 z-[49] flex gap-4">
|
||||
<GlassIconButton
|
||||
icon={MessageSquareText}
|
||||
size='md'
|
||||
tooltip={"Chat"}
|
||||
onClick={() => setIsSmartChatBoxOpen(true)}
|
||||
/>
|
||||
<div
|
||||
className="fixed right-[2rem] top-[4rem] z-[49]"
|
||||
>
|
||||
<Tooltip title="Open chat" placement="left">
|
||||
<GlassIconButton
|
||||
icon={Bot}
|
||||
size='md'
|
||||
onClick={() => setIsSmartChatBoxOpen(true)}
|
||||
className="backdrop-blur-lg"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{/* 智能对话弹窗 */}
|
||||
|
||||
@ -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 } from 'lucide-react';
|
||||
import { Edit3, Play, Pause, Volume2, VolumeX, Maximize, Minimize, Loader2, X, Scissors } 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';
|
||||
@ -26,6 +26,9 @@ interface MediaViewerProps {
|
||||
mode: string;
|
||||
onOpenChat?: () => void;
|
||||
setVideoPreview?: (url: string, id: string) => void;
|
||||
showGotoCutButton?: boolean;
|
||||
onGotoCut: () => void;
|
||||
isSmartChatBoxOpen: boolean;
|
||||
}
|
||||
|
||||
export const MediaViewer = React.memo(function MediaViewer({
|
||||
@ -41,11 +44,14 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
applyScript,
|
||||
mode,
|
||||
onOpenChat,
|
||||
setVideoPreview
|
||||
setVideoPreview,
|
||||
showGotoCutButton,
|
||||
onGotoCut,
|
||||
isSmartChatBoxOpen
|
||||
}: MediaViewerProps) {
|
||||
const mainVideoRef = useRef<HTMLVideoElement>(null);
|
||||
const finalVideoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
const videoContentRef = useRef<HTMLDivElement>(null);
|
||||
// 音量控制状态
|
||||
const [isMuted, setIsMuted] = useState(false);
|
||||
const [volume, setVolume] = useState(0.8);
|
||||
@ -55,6 +61,17 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [finalVideoReady, setFinalVideoReady] = useState(false);
|
||||
const [userHasInteracted, setUserHasInteracted] = useState(false);
|
||||
const [toosBtnRight, setToodsBtnRight] = useState('1rem');
|
||||
|
||||
useEffect(() => {
|
||||
if (isSmartChatBoxOpen) {
|
||||
const videoContentWidth = videoContentRef.current?.clientWidth ?? 0;
|
||||
const right = (window.innerWidth * 0.25) - ((window.innerWidth - videoContentWidth) / 2) + 32;
|
||||
setToodsBtnRight(right + 'px');
|
||||
} else {
|
||||
setToodsBtnRight('1rem');
|
||||
}
|
||||
}, [isSmartChatBoxOpen])
|
||||
|
||||
// 音量控制函数
|
||||
const toggleMute = () => {
|
||||
@ -421,12 +438,13 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
};
|
||||
|
||||
// 渲染视频内容
|
||||
const renderVideoContent = () => {
|
||||
const renderVideoContent = (onGotoCut: () => void) => {
|
||||
const urls = taskObject.videos.data[currentSketchIndex].urls ? taskObject.videos.data[currentSketchIndex].urls.join(',') : '';
|
||||
return (
|
||||
<div
|
||||
className="relative w-full h-full rounded-lg group"
|
||||
key={`render-video-${urls}`}
|
||||
ref={videoContentRef}
|
||||
>
|
||||
{/* 背景模糊的图片 */}
|
||||
{taskObject.videos.data[currentSketchIndex].video_status !== 1 && (
|
||||
@ -479,22 +497,26 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* 添加到chat去编辑 按钮 */}
|
||||
<Tooltip title="Edit video with chat">
|
||||
<Button
|
||||
className="absolute top-4 left-4 z-[21] bg-white/10 backdrop-blur-sm border border-white/20 text-white"
|
||||
onClick={() => {
|
||||
{/* 跳转剪辑按钮 */}
|
||||
<div className="absolute top-4 right-4 z-[21] flex items-center gap-2 transition-right duration-100" style={{
|
||||
right: toosBtnRight
|
||||
}}>
|
||||
{/* 添加到chat去编辑 按钮 */}
|
||||
<Tooltip placement="top" title="Edit video with chat">
|
||||
<GlassIconButton icon={Video} size='sm' text="Edit with chat" onClick={() => {
|
||||
const currentVideo = taskObject.videos.data[currentSketchIndex];
|
||||
if (currentVideo && currentVideo.urls && currentVideo.urls.length > 0 && setVideoPreview) {
|
||||
setVideoPreview(currentVideo.urls[0], currentVideo.video_id);
|
||||
if (onOpenChat) onOpenChat();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Video className="w-4 h-4" />
|
||||
<span className="text-xs">Edit with chat</span>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}} />
|
||||
</Tooltip>
|
||||
{showGotoCutButton && (
|
||||
<Tooltip placement="top" title='Go to AI-powered editing platform'>
|
||||
<GlassIconButton icon={Scissors} size='sm' onClick={onGotoCut} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -672,7 +694,7 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
}
|
||||
|
||||
if (taskObject.currentStage === 'video') {
|
||||
return renderVideoContent();
|
||||
return renderVideoContent(onGotoCut);
|
||||
}
|
||||
|
||||
if (taskObject.currentStage === 'script') {
|
||||
|
||||
@ -8,7 +8,9 @@ import {
|
||||
Heart,
|
||||
Camera,
|
||||
Film,
|
||||
Scissors
|
||||
Scissors,
|
||||
Play,
|
||||
Pause
|
||||
} from 'lucide-react';
|
||||
import { TaskObject } from '@/api/DTO/movieEdit';
|
||||
import { GlassIconButton } from '@/components/ui/glass-icon-button';
|
||||
@ -21,6 +23,7 @@ interface TaskInfoProps {
|
||||
isPauseWorkFlow: boolean;
|
||||
showGotoCutButton: boolean;
|
||||
onGotoCut?: () => void;
|
||||
setIsPauseWorkFlow: (isPauseWorkFlow: boolean) => void;
|
||||
}
|
||||
|
||||
const stageIconMap = {
|
||||
@ -46,7 +49,7 @@ const TAG_COLORS = ['#924eadcc', '#4c90a0', '#3b4a5a', '#957558'];
|
||||
// const TAG_COLORS = ['#6bf5f9', '#92a6fc', '#ac71fd', '#c73dfe'];
|
||||
|
||||
// 阶段图标组件
|
||||
const StageIcons = ({ currentStage, isExpanded, isPauseWorkFlow }: { currentStage: number, isExpanded: boolean, isPauseWorkFlow: boolean }) => {
|
||||
const StageIcons = ({ currentStage, isExpanded, isPauseWorkFlow, setIsPauseWorkFlow }: { currentStage: number, isExpanded: boolean, isPauseWorkFlow: boolean, setIsPauseWorkFlow: (isPauseWorkFlow: boolean) => void }) => {
|
||||
// 根据当前阶段重新排序图标
|
||||
const orderedStages = useMemo(() => {
|
||||
const stages = Object.entries(stageIconMap).map(([stage, data]) => ({
|
||||
@ -58,69 +61,73 @@ const StageIcons = ({ currentStage, isExpanded, isPauseWorkFlow }: { currentStag
|
||||
}, [currentStage]);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="relative flex items-center"
|
||||
>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{orderedStages.map((stage, index) => {
|
||||
const isCurrentStage = stage.stage === currentStage;
|
||||
const Icon = stage.icon;
|
||||
|
||||
// 只显示当前阶段或展开状态
|
||||
if (!isExpanded && !isCurrentStage) return null;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={stage.stage}
|
||||
className="relative"
|
||||
initial={isExpanded ? {
|
||||
opacity: 0,
|
||||
x: -20,
|
||||
scale: 0.5
|
||||
} : {}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
scale: 1,
|
||||
transition: {
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 25,
|
||||
delay: index * 0.1
|
||||
}
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
x: 20,
|
||||
scale: 0.5,
|
||||
transition: { duration: 0.2 }
|
||||
}}
|
||||
style={{
|
||||
marginLeft: index > 0 ? '8px' : '0px',
|
||||
zIndex: isCurrentStage ? 2 : 1
|
||||
}}
|
||||
>
|
||||
<Tooltip title={isPauseWorkFlow ? "Click to Play" : "Click to Pause"} placement="bottom">
|
||||
<motion.div
|
||||
className="relative flex items-center cursor-pointer"
|
||||
onClick={() => setIsPauseWorkFlow(!isPauseWorkFlow)}
|
||||
>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{orderedStages.map((stage, index) => {
|
||||
const isCurrentStage = stage.stage === currentStage;
|
||||
const Icon = stage.icon;
|
||||
|
||||
// 只显示当前阶段或展开状态
|
||||
if (!isExpanded && !isCurrentStage) return null;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={`relative rounded-full p-1 ${isCurrentStage ? 'bg-opacity-20' : 'bg-opacity-10'}`}
|
||||
animate={(isCurrentStage && !isPauseWorkFlow) ? {
|
||||
rotate: [0, 360],
|
||||
scale: [1, 1.2, 1],
|
||||
transition: {
|
||||
rotate: { duration: 3, repeat: Infinity, ease: "linear" },
|
||||
scale: { duration: 1.5, repeat: Infinity, ease: "easeInOut" }
|
||||
}
|
||||
key={stage.stage}
|
||||
className="relative"
|
||||
initial={isExpanded ? {
|
||||
opacity: 0,
|
||||
x: -20,
|
||||
scale: 0.5
|
||||
} : {}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
scale: 1,
|
||||
transition: {
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 25,
|
||||
delay: index * 0.1
|
||||
}
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
x: 20,
|
||||
scale: 0.5,
|
||||
transition: { duration: 0.2 }
|
||||
}}
|
||||
style={{
|
||||
marginLeft: index > 0 ? '8px' : '0px',
|
||||
zIndex: isCurrentStage ? 2 : 1
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
className="w-5 h-5"
|
||||
style={{ color: stage.color }}
|
||||
/>
|
||||
<motion.div
|
||||
className={`relative rounded-full p-1 ${isCurrentStage ? 'bg-opacity-20 cursor-pointer' : 'bg-opacity-10'}`}
|
||||
animate={(isCurrentStage && !isPauseWorkFlow) ? {
|
||||
rotate: [0, 360],
|
||||
scale: [1, 1.2, 1],
|
||||
transition: {
|
||||
rotate: { duration: 3, repeat: Infinity, ease: "linear" },
|
||||
scale: { duration: 1.5, repeat: Infinity, ease: "easeInOut" }
|
||||
}
|
||||
} : {}}
|
||||
>
|
||||
<Icon
|
||||
className="w-5 h-5"
|
||||
style={{ color: stage.color }}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@ -130,7 +137,8 @@ export function TaskInfo({
|
||||
roles,
|
||||
isPauseWorkFlow,
|
||||
showGotoCutButton,
|
||||
onGotoCut
|
||||
onGotoCut,
|
||||
setIsPauseWorkFlow
|
||||
}: TaskInfoProps) {
|
||||
const [isScriptModalOpen, setIsScriptModalOpen] = useState(false);
|
||||
const [currentStage, setCurrentStage] = useState(0);
|
||||
@ -284,7 +292,7 @@ export function TaskInfo({
|
||||
onMouseEnter={() => setIsStageIconsExpanded(true)}
|
||||
onMouseLeave={() => setIsStageIconsExpanded(false)}
|
||||
>
|
||||
<StageIcons currentStage={currentStage} isExpanded={isStageIconsExpanded} isPauseWorkFlow={isPauseWorkFlow} />
|
||||
<StageIcons currentStage={currentStage} isExpanded={isStageIconsExpanded} isPauseWorkFlow={isPauseWorkFlow} setIsPauseWorkFlow={setIsPauseWorkFlow}/>
|
||||
|
||||
<motion.div
|
||||
className="relative"
|
||||
@ -402,12 +410,12 @@ export function TaskInfo({
|
||||
} : {}}
|
||||
/> */}
|
||||
|
||||
{/* 跳转剪辑按钮 */}
|
||||
{/* // 跳转剪辑按钮
|
||||
{showGotoCutButton && (
|
||||
<Tooltip placement="top" title='AI-powered editing platform'>
|
||||
<GlassIconButton icon={Scissors} size='sm' onClick={onGotoCut} />
|
||||
</Tooltip>
|
||||
)}
|
||||
)} */}
|
||||
</motion.div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -13,6 +13,7 @@ interface GlassIconButtonProps {
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
className?: string;
|
||||
[key: string]: any; // To allow spreading other props
|
||||
text?: string;
|
||||
}
|
||||
|
||||
const variantStyles = {
|
||||
@ -37,12 +38,12 @@ const iconSizes = {
|
||||
const MotionButton = motion.button;
|
||||
|
||||
export const GlassIconButton = forwardRef<HTMLButtonElement, GlassIconButtonProps>(
|
||||
({ icon: Icon, tooltip, variant = 'secondary', size = 'md', className, ...props }, ref) => {
|
||||
({ icon: Icon, tooltip, variant = 'secondary', size = 'md', className, text, ...props }, ref) => {
|
||||
return (
|
||||
<MotionButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative rounded-full backdrop-blur-md transition-colors shadow-lg border',
|
||||
'relative rounded-full backdrop-blur-md transition-colors shadow-lg border flex items-center gap-2',
|
||||
variantStyles[variant],
|
||||
sizeStyles[size],
|
||||
className
|
||||
@ -60,6 +61,9 @@ export const GlassIconButton = forwardRef<HTMLButtonElement, GlassIconButtonProp
|
||||
{...props}
|
||||
>
|
||||
<Icon className={cn('text-white', iconSizes[size])} />
|
||||
{text && (
|
||||
<span className="text-white text-xs">{text}</span>
|
||||
)}
|
||||
{tooltip && (
|
||||
<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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user