forked from 77media/video-flow
336 lines
11 KiB
TypeScript
336 lines
11 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect, SetStateAction } from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { X, Image, Users, Video, Music, Settings, FileText, Undo2, TriangleAlert } from 'lucide-react';
|
|
import { cn } from '@/public/lib/utils';
|
|
import { ScriptTabContent } from './script-tab-content';
|
|
import { SceneTabContent } from './scene-tab-content';
|
|
import { ShotTabContent } from './shot-tab-content';
|
|
import { SettingsTabContent } from './settings-tab-content';
|
|
import { CharacterTabContent } from './character-tab-content';
|
|
import { MusicTabContent } from './music-tab-content';
|
|
import FloatingGlassPanel from './FloatingGlassPanel';
|
|
|
|
interface EditModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
activeEditTab: string;
|
|
taskStatus: string;
|
|
taskSketch: any[];
|
|
sketchVideo: any[];
|
|
taskScenes: any[];
|
|
currentSketchIndex: number;
|
|
onSketchSelect: (index: number) => void;
|
|
roles?: any[];
|
|
music?: any;
|
|
setIsPauseWorkFlow: (isPauseWorkFlow: boolean) => void;
|
|
setAnyAttribute: any;
|
|
isPauseWorkFlow: boolean;
|
|
scriptData: any[] | null;
|
|
applyScript: any;
|
|
fallbackToStep: any;
|
|
}
|
|
|
|
const tabs = [
|
|
{ id: '0', label: 'Script', icon: FileText },
|
|
{ id: '1', label: 'Character', icon: Users },
|
|
{ id: '2', label: 'Scene', icon: Image },
|
|
{ id: '3', label: 'Video', icon: Video },
|
|
{ id: '4', label: 'Music', icon: Music },
|
|
// { id: '5', label: '剪辑', icon: Scissors },
|
|
{ id: 'settings', label: 'Settings', icon: Settings },
|
|
];
|
|
|
|
export function EditModal({
|
|
isOpen,
|
|
onClose,
|
|
activeEditTab,
|
|
taskStatus,
|
|
taskSketch,
|
|
sketchVideo,
|
|
taskScenes,
|
|
currentSketchIndex,
|
|
onSketchSelect,
|
|
roles = [],
|
|
music,
|
|
setIsPauseWorkFlow,
|
|
setAnyAttribute,
|
|
isPauseWorkFlow,
|
|
scriptData,
|
|
applyScript,
|
|
fallbackToStep
|
|
}: EditModalProps) {
|
|
const [activeTab, setActiveTab] = useState(activeEditTab);
|
|
const [currentIndex, setCurrentIndex] = useState(currentSketchIndex);
|
|
const [currentRoleIndex, setCurrentRoleIndex] = useState(0);
|
|
const [isRemindFallbackOpen, setIsRemindFallbackOpen] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setCurrentIndex(currentSketchIndex);
|
|
}, [isOpen]);
|
|
|
|
// 当 activeEditTab 改变时更新 activeTab
|
|
useEffect(() => {
|
|
setActiveTab(activeEditTab);
|
|
}, [activeEditTab]);
|
|
|
|
const isTabDisabled = (tabId: string) => {
|
|
if (tabId === 'settings') return false;
|
|
// 换成 如果对应标签下 数据存在 就不禁用
|
|
// if (tabId === '1') return roles.length === 0;
|
|
if (tabId === '2') return taskScenes.length === 0;
|
|
if (tabId === '3') return sketchVideo.length === 0;
|
|
if (tabId === '4') return false;
|
|
return false;
|
|
};
|
|
|
|
const hanldeChangeSelect = (index: number) => {
|
|
if (activeTab === '1') {
|
|
setCurrentRoleIndex(index);
|
|
} else {
|
|
setCurrentIndex(index);
|
|
}
|
|
}
|
|
|
|
const handleChangeTab = (tabId: string, disabled: boolean) => {
|
|
if (disabled) return;
|
|
setActiveTab(tabId);
|
|
setCurrentIndex(0);
|
|
}
|
|
|
|
const handleSave = () => {
|
|
console.log('handleSave');
|
|
setIsRemindFallbackOpen(true);
|
|
}
|
|
|
|
const handleConfirmGotoFallback = () => {
|
|
console.log('handleConfirmGotoFallback');
|
|
if (activeTab === '0') {
|
|
fallbackToStep('0');
|
|
// 应用剧本
|
|
applyScript();
|
|
} else {
|
|
fallbackToStep('1');
|
|
}
|
|
}
|
|
const handleCloseRemindFallbackPanel = () => {
|
|
setIsRemindFallbackOpen(false);
|
|
}
|
|
|
|
const renderTabContent = () => {
|
|
switch (activeTab) {
|
|
case '0':
|
|
return (
|
|
<ScriptTabContent
|
|
scriptData={scriptData}
|
|
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
|
setAnyAttribute={setAnyAttribute}
|
|
isPauseWorkFlow={isPauseWorkFlow}
|
|
applyScript={applyScript}
|
|
/>
|
|
);
|
|
case '1':
|
|
return (
|
|
<CharacterTabContent
|
|
taskSketch={taskSketch}
|
|
currentRoleIndex={currentRoleIndex}
|
|
onSketchSelect={hanldeChangeSelect}
|
|
roles={roles}
|
|
/>
|
|
);
|
|
case '2':
|
|
return (
|
|
<SceneTabContent
|
|
taskSketch={taskScenes}
|
|
currentSketchIndex={currentIndex}
|
|
onSketchSelect={hanldeChangeSelect}
|
|
/>
|
|
);
|
|
case '3':
|
|
return (
|
|
<ShotTabContent
|
|
currentSketchIndex={currentIndex}
|
|
roles={roles}
|
|
/>
|
|
);
|
|
case '4':
|
|
return (
|
|
<MusicTabContent
|
|
taskSketch={taskSketch}
|
|
currentSketchIndex={currentIndex}
|
|
onSketchSelect={onSketchSelect}
|
|
music={music}
|
|
/>
|
|
);
|
|
case 'settings':
|
|
return (
|
|
<SettingsTabContent
|
|
onSettingChange={(key, value) => {
|
|
console.log('Setting changed:', key, value);
|
|
// TODO: 实现设置更新逻辑
|
|
}}
|
|
/>
|
|
);
|
|
default:
|
|
return (
|
|
<div className="min-h-[400px] flex items-center justify-center text-white/50">
|
|
{tabs.find(tab => tab.id === activeTab)?.label} Content area
|
|
</div>
|
|
);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
{isOpen && (
|
|
<>
|
|
{/* 背景遮罩 */}
|
|
<motion.div
|
|
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
onClick={onClose}
|
|
/>
|
|
|
|
{/* 弹窗内容 */}
|
|
<div className="fixed inset-x-0 bottom-0 z-50 flex justify-center">
|
|
<motion.div
|
|
className={cn(
|
|
"w-[88%] min-w-[888px] h-[95vh] bg-[#1a1b1e] rounded-t-2xl overflow-hidden"
|
|
)}
|
|
initial={{ y: '100%' }}
|
|
animate={{ y: 0 }}
|
|
exit={{ y: '100%' }}
|
|
transition={{
|
|
type: 'spring',
|
|
damping: 25,
|
|
stiffness: 200,
|
|
}}
|
|
>
|
|
{/* 标签栏 */}
|
|
<div className="flex items-center gap-1 p-2 overflow-x-auto hide-scrollbar bg-white/5">
|
|
<div className="flex-1 flex gap-1">
|
|
{tabs.map((tab) => {
|
|
const Icon = tab.icon;
|
|
const disabled = isTabDisabled(tab.id);
|
|
return (
|
|
<motion.button
|
|
key={tab.id}
|
|
className={cn(
|
|
'flex items-center gap-2 px-4 py-2 rounded-lg transition-colors relative',
|
|
activeTab === tab.id ? 'text-white' : 'text-white/50',
|
|
disabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-white/10',
|
|
)}
|
|
onClick={() => handleChangeTab(tab.id, disabled)}
|
|
whileHover={disabled ? undefined : { scale: 1.02 }}
|
|
whileTap={disabled ? undefined : { scale: 0.98 }}
|
|
>
|
|
<Icon className="w-4 h-4" />
|
|
<span>{tab.label}</span>
|
|
{activeTab === tab.id && (
|
|
<motion.div
|
|
className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-500"
|
|
layoutId="activeTab"
|
|
transition={{ type: 'spring', damping: 20 }}
|
|
/>
|
|
)}
|
|
</motion.button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* 弹窗操作按钮 */}
|
|
<div className="pl-4 border-l border-white/10">
|
|
{/* 关闭按钮 */}
|
|
<motion.button
|
|
className="p-2 rounded-full hover:bg-white/10 transition-colors"
|
|
onClick={onClose}
|
|
whileHover={{ rotate: 90 }}
|
|
whileTap={{ scale: 0.9 }}
|
|
>
|
|
<X className="w-5 h-5 text-white/70" />
|
|
</motion.button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 内容区域 */}
|
|
<div className="overflow-y-auto p-4" style={{ height: 'calc(100% - 8rem)' }}>
|
|
<AnimatePresence mode="wait">
|
|
<motion.div
|
|
key={activeTab}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -20 }}
|
|
transition={{ duration: 0.2 }}
|
|
className="h-full"
|
|
>
|
|
{renderTabContent()}
|
|
</motion.div>
|
|
</AnimatePresence>
|
|
</div>
|
|
|
|
{/* 底部操作栏 */}
|
|
<div className="p-4 border-t border-white/10 bg-black/20">
|
|
<div className="flex justify-end gap-3">
|
|
<motion.button
|
|
className="px-4 py-2 rounded-lg bg-white/10 text-white hover:bg-white/20 transition-colors"
|
|
whileHover={{ scale: 1.02 }}
|
|
whileTap={{ scale: 0.98 }}
|
|
onClick={onClose}
|
|
>
|
|
Reset
|
|
</motion.button>
|
|
<motion.button
|
|
className="px-4 py-2 rounded-lg bg-blue-500 text-white hover:bg-blue-600 transition-colors"
|
|
whileHover={{ scale: 1.02 }}
|
|
whileTap={{ scale: 0.98 }}
|
|
onClick={() => {handleSave()}}
|
|
>
|
|
Save
|
|
</motion.button>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
|
|
{/* 提醒用户 点击保存 工作流将回退 并重新执行工作流 */}
|
|
<FloatingGlassPanel
|
|
open={isRemindFallbackOpen}
|
|
width='500px'
|
|
clickMaskClose={false}
|
|
>
|
|
<div className="flex flex-col items-center gap-4 text-white py-4">
|
|
<div className="flex items-center gap-3">
|
|
<TriangleAlert className="w-6 h-6 text-yellow-400" />
|
|
<p className="text-lg font-medium">The task will be regenerated and edited. Do you want to continue?</p>
|
|
</div>
|
|
|
|
<div className="flex gap-3 mt-2">
|
|
<button
|
|
onClick={() => handleConfirmGotoFallback()}
|
|
data-alt="confirm-replace-button"
|
|
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-md transition-colors duration-200 flex items-center gap-2"
|
|
>
|
|
<Undo2 className="w-4 h-4" />
|
|
Continue
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => handleCloseRemindFallbackPanel()}
|
|
data-alt="ignore-button"
|
|
className="px-4 py-2 bg-gray-600 hover:bg-gray-700 rounded-md transition-colors duration-200 flex items-center gap-2"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</FloatingGlassPanel>
|
|
</>
|
|
)}
|
|
</AnimatePresence>
|
|
);
|
|
}
|