forked from 77media/video-flow
同步主界面数据到编辑界面
This commit is contained in:
parent
2c88b8201b
commit
0ef11c4e89
@ -621,7 +621,7 @@ export interface RoleResponse {
|
||||
}
|
||||
|
||||
|
||||
interface Role {
|
||||
export interface Role {
|
||||
name: string;
|
||||
url: string;
|
||||
status: number;
|
||||
@ -636,7 +636,7 @@ interface ShotSketch {
|
||||
script: string;
|
||||
status: number;
|
||||
}
|
||||
interface Video {
|
||||
export interface ShotVideo {
|
||||
video_id: string;
|
||||
urls: string[];
|
||||
video_status: number;
|
||||
@ -663,7 +663,7 @@ export interface TaskObject {
|
||||
total_count: number;
|
||||
}; // 分镜草图
|
||||
videos: {
|
||||
data: Video[];
|
||||
data: ShotVideo[];
|
||||
total_count: number;
|
||||
}; // 视频
|
||||
final: {
|
||||
|
||||
@ -179,6 +179,7 @@ export const useRoleShotServiceHook = (projectId: string,selectRole?:RoleEntity,
|
||||
return role.fromDraft
|
||||
});
|
||||
console.log('newDraftRoleList', newDraftRoleList)
|
||||
console.log('应用角色到分镜', shotSelectionList)
|
||||
// 循环调用接口,为每个选中的分镜单独调用
|
||||
const res = await Promise.all( shotSelectionList.map(async (shot) => {
|
||||
// 调用应用角色到分镜接口(不等待完成)
|
||||
@ -191,6 +192,8 @@ export const useRoleShotServiceHook = (projectId: string,selectRole?:RoleEntity,
|
||||
})
|
||||
}))
|
||||
|
||||
console.log('应用角色到分镜', res);
|
||||
|
||||
SaveEditUseCase.setVideoTasks([
|
||||
...SaveEditUseCase.videoTasks,
|
||||
...res.map(item=>{
|
||||
|
||||
@ -228,6 +228,7 @@ const WorkFlow = React.memo(function WorkFlow() {
|
||||
SaveEditUseCase.clearData();
|
||||
setIsEditModalOpen(false)
|
||||
}}
|
||||
taskObject={taskObject}
|
||||
currentSketchIndex={currentSketchIndex}
|
||||
roles={taskObject.roles.data}
|
||||
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
||||
|
||||
@ -5,13 +5,14 @@ import { useSearchParams } from 'next/navigation';
|
||||
import { useRoleServiceHook } from "@/app/service/Interaction/RoleService";
|
||||
import { useRoleShotServiceHook } from "@/app/service/Interaction/RoleShotService";
|
||||
import { useScriptService } from "@/app/service/Interaction/ScriptService";
|
||||
import { VideoSegmentEntity } from "@/app/service/domain/Entities";
|
||||
|
||||
export const useEditData = (tabType: string, originalText?: string) => {
|
||||
const searchParams = useSearchParams();
|
||||
const projectId = searchParams.get('episodeId') || '';
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [scriptData, setScriptData] = useState<any[]>([]);
|
||||
const [shotData, setShotData] = useState<any[]>([]);
|
||||
const [shotData, setShotData] = useState<VideoSegmentEntity[]>([]);
|
||||
|
||||
const [roleData, setRoleData] = useState<any[]>([]);
|
||||
|
||||
@ -24,6 +25,7 @@ export const useEditData = (tabType: string, originalText?: string) => {
|
||||
|
||||
const {
|
||||
videoSegments,
|
||||
selectedSegment,
|
||||
scriptRoles,
|
||||
getVideoSegmentList,
|
||||
setSelectedSegment,
|
||||
@ -123,6 +125,7 @@ export const useEditData = (tabType: string, originalText?: string) => {
|
||||
applyScript,
|
||||
// shot
|
||||
shotData,
|
||||
selectedSegment,
|
||||
scriptRoles,
|
||||
setSelectedSegment,
|
||||
regenerateVideoSegment,
|
||||
|
||||
@ -11,30 +11,10 @@ import HorizontalScroller from './HorizontalScroller';
|
||||
import { useEditData } from '@/components/pages/work-flow/use-edit-data';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { RoleEntity } from '@/app/service/domain/Entities';
|
||||
|
||||
interface Appearance {
|
||||
hairStyle: string;
|
||||
skinTone: string;
|
||||
facialFeatures: string;
|
||||
bodyType: string;
|
||||
}
|
||||
|
||||
interface Role {
|
||||
name: string;
|
||||
url: string;
|
||||
sound: string;
|
||||
soundDescription: string;
|
||||
roleDescription: string;
|
||||
age: number;
|
||||
gender: 'male' | 'female' | 'other';
|
||||
ethnicity: string;
|
||||
appearance: Appearance;
|
||||
// 新增标签数组
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
import { Role } from '@/api/DTO/movieEdit';
|
||||
|
||||
interface CharacterTabContentProps {
|
||||
originalRoles: Role[];
|
||||
onClose: () => void;
|
||||
onApply: () => void;
|
||||
setActiveTab: (tabId: string) => void;
|
||||
@ -42,10 +22,10 @@ interface CharacterTabContentProps {
|
||||
|
||||
|
||||
export const CharacterTabContent = forwardRef<
|
||||
{ switchBefore: (tabId: string) => boolean, saveBefore: () => void },
|
||||
{ switchBefore: (tabId: string) => boolean, saveOrCloseBefore: () => void },
|
||||
CharacterTabContentProps
|
||||
>((props, ref) => {
|
||||
const { onClose, onApply, setActiveTab } = props;
|
||||
const { onClose, onApply, setActiveTab, originalRoles } = props;
|
||||
const [isReplacePanelOpen, setIsReplacePanelOpen] = useState(false);
|
||||
const [replacePanelKey, setReplacePanelKey] = useState(0);
|
||||
const [ignoreReplace, setIgnoreReplace] = useState(false);
|
||||
@ -76,7 +56,6 @@ CharacterTabContentProps
|
||||
regenerateRole,
|
||||
fetchUserRoleLibrary,
|
||||
uploadImageAndUpdateRole,
|
||||
changeTabCallback,
|
||||
// role shot
|
||||
shotSelectionList,
|
||||
fetchRoleShots,
|
||||
@ -91,7 +70,8 @@ CharacterTabContentProps
|
||||
switchBefore: (tabId: string) => {
|
||||
setNextToTabId(tabId);
|
||||
// 判断 角色是否修改
|
||||
const isChange = selectedRole!.isChangeRole
|
||||
const currentIndex = getCurrentIndex();
|
||||
const isChange = currentIndex !== -1 && isRoleChange(originalRoles[currentIndex]);
|
||||
console.log('switchBefore', isChange);
|
||||
if (isChange) {
|
||||
setTriggerType('tab');
|
||||
@ -99,15 +79,16 @@ CharacterTabContentProps
|
||||
}
|
||||
return isChange;
|
||||
},
|
||||
saveBefore: () => {
|
||||
console.log('saveBefore');
|
||||
saveOrCloseBefore: () => {
|
||||
console.log('saveOrCloseBefore');
|
||||
// 判断 角色是否修改
|
||||
changeTabCallback((isChange: Boolean) => {
|
||||
if (isChange) {
|
||||
setTriggerType('apply');
|
||||
handleStartReplaceCharacter();
|
||||
}
|
||||
});
|
||||
const currentIndex = getCurrentIndex();
|
||||
if (currentIndex !== -1 && isRoleChange(originalRoles[currentIndex])) {
|
||||
setTriggerType('apply');
|
||||
handleStartReplaceCharacter();
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
@ -179,24 +160,33 @@ CharacterTabContentProps
|
||||
setIsReplacePanelOpen(false);
|
||||
};
|
||||
|
||||
// 对比角色是否修改
|
||||
const isRoleChange = (role: Role) => {
|
||||
console.log('对比角色是否修改', role, selectedRole);
|
||||
return role.name !== selectedRole?.name || role.url !== selectedRole?.imageUrl;
|
||||
};
|
||||
// 获取当前选中下标
|
||||
const getCurrentIndex = () => {
|
||||
return originalRoles.findIndex(role => role.name === selectedRole?.name);
|
||||
};
|
||||
|
||||
const handleChangeRole = (index: number) => {
|
||||
const oldRole = roleData.find(role => role.id === selectedRole?.id);
|
||||
console.log('切换角色前对比');
|
||||
changeTabCallback((isChange: Boolean) => {
|
||||
if (isChange) {
|
||||
setTriggerType('user');
|
||||
setIsRemindReplacePanelOpen(true);
|
||||
setNextToUserIndex(index);
|
||||
return;
|
||||
}
|
||||
const currentIndex = getCurrentIndex();
|
||||
if (currentIndex === index) return;
|
||||
if (currentIndex !== -1 && isRoleChange(originalRoles[currentIndex])) {
|
||||
setTriggerType('user');
|
||||
setIsRemindReplacePanelOpen(true);
|
||||
setNextToUserIndex(index);
|
||||
return;
|
||||
}
|
||||
|
||||
// 重置替换规则
|
||||
setEnableAnimation(false);
|
||||
setIgnoreReplace(false);
|
||||
setIsRegenerate(false);
|
||||
// 重置替换规则
|
||||
setEnableAnimation(false);
|
||||
setIgnoreReplace(false);
|
||||
setIsRegenerate(false);
|
||||
|
||||
selectRole(roleData[index]);
|
||||
});
|
||||
selectRole(roleData[index]);
|
||||
};
|
||||
|
||||
// 从角色库中选择角色
|
||||
@ -266,18 +256,8 @@ CharacterTabContentProps
|
||||
});
|
||||
};
|
||||
|
||||
// 如果loading 显示loading状态
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
|
||||
<div className="w-12 h-12 mb-4 animate-spin rounded-full border-b-2 border-blue-600" />
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 如果没有角色数据,显示占位内容
|
||||
if (roleData.length === 0) {
|
||||
if (originalRoles.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
|
||||
<Users className="w-16 h-16 mb-4" />
|
||||
@ -306,22 +286,22 @@ CharacterTabContentProps
|
||||
<HorizontalScroller
|
||||
itemWidth={96}
|
||||
gap={0}
|
||||
selectedIndex={roleData.findIndex(role => role.id === selectedRole?.id)}
|
||||
selectedIndex={originalRoles?.findIndex(role => role.name === selectedRole?.name)}
|
||||
onItemClick={(i: number) => handleChangeRole(i)}
|
||||
>
|
||||
{roleData.map((role, index) => (
|
||||
{originalRoles.map((role, index) => (
|
||||
<motion.div
|
||||
key={`role-${index}`}
|
||||
className={cn(
|
||||
'relative flex-shrink-0 w-24 rounded-lg overflow-hidden cursor-pointer',
|
||||
'aspect-[9/16]',
|
||||
role.id === selectedRole?.id ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
|
||||
role.name === selectedRole?.name ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
|
||||
)}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<img
|
||||
src={role.imageUrl}
|
||||
src={role.url}
|
||||
alt={role.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
@ -335,78 +315,83 @@ CharacterTabContentProps
|
||||
</motion.div>
|
||||
|
||||
{/* 下部分:角色详情 */}
|
||||
<motion.div
|
||||
className="grid grid-cols-2 gap-6"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
{ loading ? (
|
||||
<div className="flex flex-col items-center justify-center min-h-[300px] text-white/50">
|
||||
<Loader2 className="w-12 h-12 mb-4 animate-spin" />
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
) : (
|
||||
<motion.div
|
||||
className="grid grid-cols-2 gap-6"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
|
||||
{/* 左列:角色预览 */}
|
||||
<div className="space-y-4">
|
||||
{/* 角色预览图 */}
|
||||
<div className="w-full h-full mx-auto rounded-lg relative group">
|
||||
<ImageBlurTransition
|
||||
src={selectedRole?.imageUrl || ''}
|
||||
alt={selectedRole?.name || ''}
|
||||
width='100%'
|
||||
height='100%'
|
||||
enableAnimation={enableAnimation}
|
||||
{/* 左列:角色预览 */}
|
||||
<div className="space-y-4">
|
||||
{/* 角色预览图 */}
|
||||
<div className="w-full h-full mx-auto rounded-lg relative group">
|
||||
<ImageBlurTransition
|
||||
src={selectedRole?.imageUrl || ''}
|
||||
alt={selectedRole?.name || ''}
|
||||
width='100%'
|
||||
height='100%'
|
||||
enableAnimation={enableAnimation}
|
||||
/>
|
||||
{/* 应用角色按钮 */}
|
||||
<div className='absolute top-3 right-3 flex gap-2'>
|
||||
<motion.button
|
||||
className="p-2 bg-black/50 hover:bg-black/70
|
||||
text-white rounded-full backdrop-blur-sm transition-colors z-10"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={handleUploadClick}
|
||||
disabled={isUploading}
|
||||
>
|
||||
{isUploading ? <Loader2 className="w-4 h-4 animate-spin" /> : <ImageUp className="w-4 h-4" />}
|
||||
</motion.button>
|
||||
<motion.button
|
||||
className="p-2 bg-black/50 hover:bg-black/70
|
||||
text-white rounded-full backdrop-blur-sm transition-colors z-10"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => handleOpenReplaceLibrary()}
|
||||
>
|
||||
<Library className="w-4 h-4" />
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{/* 右列:角色信息 */}
|
||||
<div className="space-y-4">
|
||||
<CharacterEditor
|
||||
ref={characterEditorRef}
|
||||
className="min-h-[calc(100%-4rem)]"
|
||||
description={selectedRole?.generateText || ''}
|
||||
highlight={selectedRole?.tags || []}
|
||||
onSmartPolish={handleSmartPolish}
|
||||
onUpdateText={(text: string) => updateRoleText(text)}
|
||||
/>
|
||||
{/* 应用角色按钮 */}
|
||||
<div className='absolute top-3 right-3 flex gap-2'>
|
||||
{/* 重新生成按钮、替换形象按钮 */}
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
<motion.button
|
||||
className="p-2 bg-black/50 hover:bg-black/70
|
||||
text-white rounded-full backdrop-blur-sm transition-colors z-10"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={handleUploadClick}
|
||||
disabled={isUploading}
|
||||
onClick={() => handleRegenerate()}
|
||||
className="flex items-center justify-center gap-2 px-4 py-3 bg-blue-500/10 hover:bg-blue-500/20
|
||||
text-blue-500 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
disabled={isRegenerate}
|
||||
>
|
||||
{isUploading ? <Loader2 className="w-4 h-4 animate-spin" /> : <ImageUp className="w-4 h-4" />}
|
||||
</motion.button>
|
||||
<motion.button
|
||||
className="p-2 bg-black/50 hover:bg-black/70
|
||||
text-white rounded-full backdrop-blur-sm transition-colors z-10"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => handleOpenReplaceLibrary()}
|
||||
>
|
||||
<Library className="w-4 h-4" />
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
<span>{isRegenerate ? 'Regenerating...' : 'Regenerate'}</span>
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{/* 右列:角色信息 */}
|
||||
<div className="space-y-4">
|
||||
<CharacterEditor
|
||||
ref={characterEditorRef}
|
||||
className="min-h-[calc(100%-4rem)]"
|
||||
description={selectedRole?.generateText || ''}
|
||||
highlight={selectedRole?.tags || []}
|
||||
onSmartPolish={handleSmartPolish}
|
||||
onUpdateText={(text: string) => updateRoleText(text)}
|
||||
/>
|
||||
{/* 重新生成按钮、替换形象按钮 */}
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
<motion.button
|
||||
onClick={() => handleRegenerate()}
|
||||
className="flex items-center justify-center gap-2 px-4 py-3 bg-blue-500/10 hover:bg-blue-500/20
|
||||
text-blue-500 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
disabled={isRegenerate}
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
<span>{isRegenerate ? 'Regenerating...' : 'Regenerate'}</span>
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
|
||||
<FloatingGlassPanel
|
||||
open={isReplacePanelOpen}
|
||||
|
||||
@ -12,6 +12,7 @@ import { CharacterTabContent } from './character-tab-content';
|
||||
import { MusicTabContent } from './music-tab-content';
|
||||
import FloatingGlassPanel from './FloatingGlassPanel';
|
||||
import { SaveEditUseCase } from '@/app/service/usecase/SaveEditUseCase';
|
||||
import { TaskObject } from '@/api/DTO/movieEdit';
|
||||
|
||||
interface EditModalProps {
|
||||
isOpen: boolean;
|
||||
@ -23,6 +24,7 @@ interface EditModalProps {
|
||||
isPauseWorkFlow: boolean;
|
||||
fallbackToStep: any;
|
||||
originalText?: string;
|
||||
taskObject?: TaskObject;
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
@ -44,7 +46,8 @@ export function EditModal({
|
||||
setIsPauseWorkFlow,
|
||||
isPauseWorkFlow,
|
||||
fallbackToStep,
|
||||
originalText
|
||||
originalText,
|
||||
taskObject
|
||||
}: EditModalProps) {
|
||||
const [activeTab, setActiveTab] = useState(activeEditTab);
|
||||
const [currentIndex, setCurrentIndex] = useState(currentSketchIndex);
|
||||
@ -55,6 +58,7 @@ export function EditModal({
|
||||
const [remindFallbackText, setRemindFallbackText] = useState('The task will be regenerated and edited. Do you want to continue?');
|
||||
const scriptTabContentRef = useRef<any>(null);
|
||||
const characterTabContentRef = useRef<any>(null);
|
||||
const shotTabContentRef = useRef<any>(null);
|
||||
// 添加一个状态来标记是否是从切换tab触发的提醒
|
||||
const [pendingSwitchTabId, setPendingSwitchTabId] = useState<string | null>(null);
|
||||
const [disabledBtn, setDisabledBtn] = useState(false);
|
||||
@ -72,9 +76,9 @@ export function EditModal({
|
||||
const isTabDisabled = (tabId: string) => {
|
||||
if (tabId === 'settings') return false;
|
||||
// 换成 如果对应标签下 数据存在 就不禁用
|
||||
// if (tabId === '1') return roles.length === 0;
|
||||
if (tabId === '1') return taskObject?.roles.data.length === 0;
|
||||
// if (tabId === '2') return taskScenes.length === 0;
|
||||
// if (tabId === '3') return sketchVideo.length === 0;
|
||||
if (tabId === '3') return taskObject?.videos.data.length === 0;
|
||||
if (tabId === '4') return false;
|
||||
return false;
|
||||
};
|
||||
@ -98,6 +102,11 @@ export function EditModal({
|
||||
if (characterTabContent) {
|
||||
return characterTabContent.switchBefore(tabId);
|
||||
}
|
||||
} else if (activeTab === '3') {
|
||||
const shotTabContent = shotTabContentRef.current;
|
||||
if (shotTabContent) {
|
||||
return shotTabContent.switchBefore(tabId);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -117,11 +126,11 @@ export function EditModal({
|
||||
console.log('handleSave');
|
||||
// setIsRemindFallbackOpen(true);
|
||||
if (activeTab === '0') {
|
||||
scriptTabContentRef.current.saveBefore();
|
||||
scriptTabContentRef.current.saveOrCloseBefore();
|
||||
} else if (activeTab === '1') {
|
||||
characterTabContentRef.current.saveBefore();
|
||||
characterTabContentRef.current.saveOrCloseBefore();
|
||||
} else if (activeTab === '3') {
|
||||
handleConfirmGotoFallback();
|
||||
shotTabContentRef.current.saveOrCloseBefore('apply');
|
||||
}
|
||||
}
|
||||
|
||||
@ -170,7 +179,13 @@ export function EditModal({
|
||||
const handleClickClose = () => {
|
||||
// TODO 关闭前 检查 当前tab 下是否有更新 如果有更新 则提醒用户 是否确认应用
|
||||
// 暂时 默认弹出提醒
|
||||
setIsRemindCloseOpen(true);
|
||||
if (activeTab === '0') {
|
||||
scriptTabContentRef.current.saveOrCloseBefore();
|
||||
} else if (activeTab === '1') {
|
||||
characterTabContentRef.current.saveOrCloseBefore();
|
||||
} else if (activeTab === '3') {
|
||||
shotTabContentRef.current.saveOrCloseBefore('close');
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirmApply = () => {
|
||||
@ -195,6 +210,7 @@ export function EditModal({
|
||||
originalText={originalText}
|
||||
onApply={handleApply}
|
||||
setActiveTab={setActiveTab}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
case '1':
|
||||
@ -204,6 +220,7 @@ export function EditModal({
|
||||
onClose={onClose}
|
||||
onApply={handleApply}
|
||||
setActiveTab={setActiveTab}
|
||||
originalRoles={taskObject?.roles.data || []}
|
||||
/>
|
||||
);
|
||||
case '2':
|
||||
@ -215,9 +232,12 @@ export function EditModal({
|
||||
case '3':
|
||||
return (
|
||||
<ShotTabContent
|
||||
ref={shotTabContentRef}
|
||||
originalVideos={taskObject?.videos.data || []}
|
||||
currentSketchIndex={currentIndex}
|
||||
roles={roles}
|
||||
onApply={handleApply}
|
||||
onClose={onClose}
|
||||
setActiveTab={setActiveTab}
|
||||
/>
|
||||
);
|
||||
case '4':
|
||||
|
||||
@ -12,13 +12,14 @@ interface ScriptTabContentProps {
|
||||
originalText?: string;
|
||||
onApply: () => void;
|
||||
setActiveTab: (tabId: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const ScriptTabContent = forwardRef<
|
||||
{ switchBefore: (tabId: string) => boolean, saveBefore: () => void },
|
||||
{ switchBefore: (tabId: string) => boolean, saveOrCloseBefore: () => void },
|
||||
ScriptTabContentProps
|
||||
>((props, ref) => {
|
||||
const { setIsPauseWorkFlow, isPauseWorkFlow, originalText, onApply, setActiveTab } = props;
|
||||
const { setIsPauseWorkFlow, isPauseWorkFlow, originalText, onApply, setActiveTab, onClose } = props;
|
||||
const { loading, scriptData, setAnyAttribute, applyScript } = useEditData('script', originalText);
|
||||
|
||||
const [isUpdate, setIsUpdate] = useState(false);
|
||||
@ -39,10 +40,12 @@ export const ScriptTabContent = forwardRef<
|
||||
}
|
||||
return isUpdate;
|
||||
},
|
||||
saveBefore: () => {
|
||||
console.log('saveBefore');
|
||||
saveOrCloseBefore: () => {
|
||||
console.log('saveOrCloseBefore');
|
||||
if (isUpdate) {
|
||||
onApply();
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import React, { useRef, useEffect, useState, forwardRef } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { RefreshCw, User, Loader2, X, Plus, Video, CircleX } from 'lucide-react';
|
||||
import { cn } from '@/public/lib/utils';
|
||||
@ -11,19 +11,26 @@ import FloatingGlassPanel from './FloatingGlassPanel';
|
||||
import { ReplaceCharacterPanel } from './replace-character-panel';
|
||||
import HorizontalScroller from './HorizontalScroller';
|
||||
import { useEditData } from '@/components/pages/work-flow/use-edit-data';
|
||||
import { RoleEntity } from '@/app/service/domain/Entities';
|
||||
import { RoleEntity, VideoSegmentEntity } from '@/app/service/domain/Entities';
|
||||
import { ShotVideo } from '@/api/DTO/movieEdit';
|
||||
|
||||
interface ShotTabContentProps {
|
||||
currentSketchIndex: number;
|
||||
roles?: any[];
|
||||
originalVideos: ShotVideo[];
|
||||
onApply: () => void;
|
||||
onClose: () => void;
|
||||
setActiveTab: (tabId: string) => void;
|
||||
}
|
||||
|
||||
export const ShotTabContent = (props: ShotTabContentProps) => {
|
||||
const { currentSketchIndex = 0, roles = [], onApply } = props;
|
||||
export const ShotTabContent = forwardRef<
|
||||
{ switchBefore: (tabId: string) => boolean, saveOrCloseBefore: (type: 'apply' | 'close') => void },
|
||||
ShotTabContentProps
|
||||
>((props, ref) => {
|
||||
const { currentSketchIndex = 0, onApply, onClose, originalVideos, setActiveTab } = props;
|
||||
const {
|
||||
loading,
|
||||
shotData,
|
||||
selectedSegment,
|
||||
scriptRoles,
|
||||
setSelectedSegment,
|
||||
regenerateVideoSegment,
|
||||
@ -36,8 +43,6 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
||||
calculateRecognitionBoxes,
|
||||
setSelectedRole
|
||||
} = useEditData('shot');
|
||||
const [selectedIndex, setSelectedIndex] = useState(currentSketchIndex);
|
||||
|
||||
const [detections, setDetections] = useState<PersonDetection[]>([]);
|
||||
const [scanState, setScanState] = useState<'idle' | 'scanning' | 'detected' | 'failed' | 'timeout'>('idle');
|
||||
const [isScanFailed, setIsScanFailed] = useState(false);
|
||||
@ -51,13 +56,54 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
||||
const [isRegenerate, setIsRegenerate] = useState(false);
|
||||
const [pendingRegeneration, setPendingRegeneration] = useState(false);
|
||||
|
||||
const [isInitialized, setIsInitialized] = useState(true);
|
||||
const [triggerType, setTriggerType] = useState<'tab' | 'apply' | 'close'>('tab');
|
||||
const [nextToTabId, setNextToTabId] = useState<string>('');
|
||||
const [isRemindApplyUpdate, setIsRemindApplyUpdate] = useState(false);
|
||||
const [updateData, setUpdateData] = useState<VideoSegmentEntity[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('shotTabContent-----shotData', shotData);
|
||||
}, [shotData]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('-==========shotData===========-', shotData);
|
||||
// 只在初始化且有角色数据时执行
|
||||
if (isInitialized && shotData.length > 0) {
|
||||
setIsInitialized(false);
|
||||
setSelectedSegment(shotData[0]);
|
||||
}
|
||||
}, [shotData, isInitialized]);
|
||||
|
||||
// 暴露方法给父组件
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
switchBefore: (tabId: string) => {
|
||||
setNextToTabId(tabId);
|
||||
// 判断 是否修改数据
|
||||
const isChange = handleGetUpdateData().length > 0;
|
||||
console.log('switchBefore', isChange);
|
||||
if (isChange) {
|
||||
setTriggerType('tab');
|
||||
setIsRemindApplyUpdate(true);
|
||||
}
|
||||
return isChange;
|
||||
},
|
||||
saveOrCloseBefore: (type: 'apply' | 'close') => {
|
||||
console.log('saveOrCloseBefore');
|
||||
// 判断 是否修改数据
|
||||
const isChange = handleGetUpdateData().length > 0;
|
||||
if (isChange) {
|
||||
setTriggerType(type);
|
||||
setIsRemindApplyUpdate(true);
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (pendingRegeneration) {
|
||||
console.log('pendingRegeneration', pendingRegeneration, shotData[selectedIndex]?.lens);
|
||||
console.log('pendingRegeneration', pendingRegeneration, selectedSegment?.lens);
|
||||
regenerateVideoSegment().then(() => {
|
||||
setPendingRegeneration(false);
|
||||
setIsRegenerate(false);
|
||||
@ -65,16 +111,35 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
||||
}
|
||||
}, [pendingRegeneration]);
|
||||
|
||||
// 监听当前选中index变化
|
||||
// 监听当前选中segment变化
|
||||
useEffect(() => {
|
||||
console.log('shotTabContent-----shotData', shotData);
|
||||
if (shotData.length > 0) {
|
||||
if (shotData.length > 0 && !selectedSegment) {
|
||||
// 清空检测状态 和 检测结果
|
||||
setScanState('idle');
|
||||
setDetections([]);
|
||||
setSelectedSegment(shotData[selectedIndex]);
|
||||
setSelectedSegment(shotData[0]);
|
||||
}
|
||||
}, [selectedIndex, shotData]);
|
||||
}, [shotData, selectedSegment]);
|
||||
|
||||
// 获取修改的数据
|
||||
const handleGetUpdateData = () => {
|
||||
console.log('handleGetUpdateData', shotData, originalVideos);
|
||||
const updateData: VideoSegmentEntity[] = [];
|
||||
shotData.forEach((shot, index) => {
|
||||
const a = shot.videoUrl.map((url) => url.video_url).join(',');
|
||||
const b = originalVideos[index].urls.join(',');
|
||||
if (a !== b) {
|
||||
updateData.push({
|
||||
...shot,
|
||||
name: 'Segment ' + (index + 1)
|
||||
});
|
||||
}
|
||||
});
|
||||
console.log('updateData', updateData);
|
||||
setUpdateData(updateData);
|
||||
return updateData;
|
||||
}
|
||||
|
||||
// 处理扫描开始
|
||||
const handleScan = async () => {
|
||||
@ -174,16 +239,34 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
||||
onApply();
|
||||
};
|
||||
|
||||
// 应用修改
|
||||
const handleApplyUpdate = () => {
|
||||
console.log('apply update');
|
||||
onApply();
|
||||
}
|
||||
|
||||
// 忽略修改
|
||||
const handleIgnoreUpdate = () => {
|
||||
console.log('ignore update');
|
||||
if (triggerType === 'apply') {
|
||||
onClose();
|
||||
} else if (triggerType === 'tab') {
|
||||
setActiveTab(nextToTabId);
|
||||
}
|
||||
}
|
||||
|
||||
// 点击按钮重新生成
|
||||
const handleRegenerate = async () => {
|
||||
console.log('regenerate');
|
||||
setIsRegenerate(true);
|
||||
const shotInfo = shotsEditorRef.current.getShotInfo();
|
||||
console.log('shotInfo', shotInfo);
|
||||
setSelectedSegment({
|
||||
...shotData[selectedIndex],
|
||||
lens: shotInfo
|
||||
});
|
||||
if (selectedSegment) {
|
||||
setSelectedSegment({
|
||||
...selectedSegment,
|
||||
lens: shotInfo
|
||||
});
|
||||
}
|
||||
setTimeout(() => {
|
||||
setPendingRegeneration(true);
|
||||
}, 1000);
|
||||
@ -197,22 +280,20 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
||||
|
||||
// 切换选择分镜
|
||||
const handleSelectShot = (index: number) => {
|
||||
// 切换前 判断数据是否发生变化
|
||||
setSelectedIndex(index);
|
||||
// 通过 video_id 找到对应的分镜
|
||||
const selectedVideo = originalVideos[index];
|
||||
const targetSegment = shotData.find(shot =>
|
||||
shot.videoUrl.some(url => url.video_id === selectedVideo.video_id)
|
||||
);
|
||||
if (targetSegment) {
|
||||
setSelectedSegment(targetSegment);
|
||||
}
|
||||
};
|
||||
|
||||
// 如果loading 显示loading状态
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
|
||||
<div className="w-12 h-12 mb-4 animate-spin rounded-full border-b-2 border-blue-600" />
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// 如果没有数据,显示空状态
|
||||
if (shotData.length === 0) {
|
||||
if (originalVideos.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
|
||||
<Video className="w-16 h-16 mb-4" />
|
||||
@ -233,28 +314,23 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
||||
<HorizontalScroller
|
||||
itemWidth={128}
|
||||
gap={0}
|
||||
selectedIndex={selectedIndex}
|
||||
selectedIndex={originalVideos.findIndex(shot => selectedSegment?.videoUrl.some(url => url.video_id === shot.video_id))}
|
||||
onItemClick={(i: number) => handleSelectShot(i)}
|
||||
>
|
||||
{shotData.map((shot, index) => (
|
||||
{originalVideos.map((shot, index) => (
|
||||
<motion.div
|
||||
key={shot.id || index}
|
||||
key={shot.video_id || index}
|
||||
className={cn(
|
||||
'relative flex-shrink-0 w-32 aspect-video rounded-lg overflow-hidden cursor-pointer group',
|
||||
selectedIndex === index ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
|
||||
selectedSegment?.videoUrl.some(url => url.video_id === shot.video_id) ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
|
||||
)}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
{(shot.status === 0 || shot.videoUrl.length === 0) && (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
|
||||
</div>
|
||||
)}
|
||||
{shot.status === 1 && shot.videoUrl[0] && (
|
||||
{shot.urls.length > 0 && (
|
||||
<video
|
||||
src={shot.videoUrl[0].video_url}
|
||||
key={shot.videoUrl[0].video_url}
|
||||
src={shot.urls[0]}
|
||||
key={shot.urls[0]}
|
||||
className="w-full h-full object-cover"
|
||||
muted
|
||||
loop
|
||||
@ -263,12 +339,6 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
||||
onMouseLeave={(e) => e.currentTarget.pause()}
|
||||
/>
|
||||
)}
|
||||
{/* 任务失败 */}
|
||||
{shot.status === 2 && (
|
||||
<div className="w-full h-full flex items-center justify-center bg-red-500/10">
|
||||
<CircleX className="w-4 h-4 text-red-500" />
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute bottom-0 left-0 right-0 p-1 bg-gradient-to-t from-black/60 to-transparent">
|
||||
<span className="text-xs text-white/90">{index + 1}</span>
|
||||
</div>
|
||||
@ -291,14 +361,14 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
||||
<HorizontalScroller
|
||||
itemWidth={'auto'}
|
||||
gap={0}
|
||||
selectedIndex={selectedIndex}
|
||||
selectedIndex={originalVideos.findIndex(shot => selectedSegment?.videoUrl.some(url => url.video_id === shot.video_id))}
|
||||
onItemClick={(i: number) => handleSelectShot(i)}
|
||||
>
|
||||
{shotData.map((shot, index) => {
|
||||
const isActive = selectedIndex === index;
|
||||
{originalVideos.map((shot, index) => {
|
||||
const isActive = selectedSegment?.videoUrl.some(url => url.video_id === shot.video_id);
|
||||
return (
|
||||
<motion.div
|
||||
key={shot.id || index}
|
||||
key={shot.video_id || index}
|
||||
className={cn(
|
||||
'flex-shrink-0 cursor-pointer transition-all duration-300',
|
||||
isActive ? 'text-white' : 'text-white/50 hover:text-white/80'
|
||||
@ -311,14 +381,8 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm whitespace-nowrap flex items-center gap-1">
|
||||
<span>Segment {index + 1}</span>
|
||||
{shot.status === 0 && (
|
||||
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
|
||||
)}
|
||||
{shot.status === 2 && (
|
||||
<CircleX className="w-4 h-4 text-red-500" />
|
||||
)}
|
||||
</span>
|
||||
{index < shotData.length - 1 && (
|
||||
{index < originalVideos.length - 1 && (
|
||||
<span className="text-white/20">|</span>
|
||||
)}
|
||||
</div>
|
||||
@ -335,7 +399,12 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
||||
|
||||
|
||||
{/* 下部分 */}
|
||||
{shotData[selectedIndex] && (
|
||||
{loading ? (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
|
||||
<div className="w-12 h-12 mb-4 animate-spin rounded-full border-b-2 border-blue-600" />
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
) : selectedSegment && (
|
||||
<motion.div
|
||||
className="grid grid-cols-2 gap-4 w-full"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
@ -346,23 +415,23 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
||||
<div className="space-y-4 col-span-1">
|
||||
{/* 选中的视频预览 */}
|
||||
<>
|
||||
{(shotData[selectedIndex]?.status === 0) && (
|
||||
{(selectedSegment?.status === 0) && (
|
||||
<div className="w-full h-full flex items-center gap-1 justify-center rounded-lg bg-black/30">
|
||||
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
|
||||
<span className="text-white/50">Loading...</span>
|
||||
</div>
|
||||
)}
|
||||
<AnimatePresence mode="wait">
|
||||
{shotData[selectedIndex]?.status === 1 && shotData[selectedIndex]?.videoUrl.length && (
|
||||
{selectedSegment?.status === 1 && selectedSegment?.videoUrl.length && (
|
||||
<motion.div
|
||||
className="aspect-video rounded-lg overflow-hidden relative group"
|
||||
key={`video-preview-${selectedIndex}`}
|
||||
key={`video-preview-${selectedSegment?.id}`}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
<PersonDetectionScene
|
||||
videoSrc={shotData[selectedIndex]?.videoUrl[0].video_url}
|
||||
videoSrc={selectedSegment?.videoUrl[0].video_url}
|
||||
detections={detections}
|
||||
scanState={scanState}
|
||||
triggerScan={scanState === 'scanning'}
|
||||
@ -396,7 +465,7 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{(shotData[selectedIndex]?.status === 2) && (
|
||||
{(selectedSegment?.status === 2) && (
|
||||
<div className="w-full h-full flex gap-1 items-center justify-center rounded-lg bg-red-500/10">
|
||||
<CircleX className="w-4 h-4 text-red-500" />
|
||||
<span className="text-white/50">Failed, click to regenerate</span>
|
||||
@ -406,11 +475,11 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
||||
</div>
|
||||
|
||||
{/* 基础配置 */}
|
||||
<div className='space-y-4 col-span-1' key={selectedIndex}>
|
||||
<div className='space-y-4 col-span-1' key={selectedSegment?.id}>
|
||||
<ShotsEditor
|
||||
ref={shotsEditorRef}
|
||||
roles={scriptRoles}
|
||||
shotInfo={shotData[selectedIndex].lens}
|
||||
shotInfo={selectedSegment.lens}
|
||||
style={{height: 'calc(100% - 4rem)'}}
|
||||
/>
|
||||
|
||||
@ -466,6 +535,79 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
||||
setIsReplaceLibraryOpen={setIsReplaceLibraryOpen}
|
||||
onSelect={handleSelectCharacter}
|
||||
/>
|
||||
|
||||
<FloatingGlassPanel
|
||||
open={isRemindApplyUpdate}
|
||||
width='66vw'
|
||||
onClose={() => setIsRemindApplyUpdate(false)}
|
||||
>
|
||||
<div className='h-full w-full flex flex-col'>
|
||||
{/* 提示文字 有分镜被修改 是否需要应用 */}
|
||||
<div className='flex-1 flex items-center justify-center'>
|
||||
<span className='text-white/50'>Three are some segments have been modified, do you need to apply?</span>
|
||||
</div>
|
||||
{/* 已修改分镜列表 */}
|
||||
<HorizontalScroller
|
||||
itemWidth={128}
|
||||
gap={0}
|
||||
>
|
||||
{updateData.map((shot, index) => (
|
||||
<motion.div
|
||||
key={shot.id || index}
|
||||
className={cn(
|
||||
'relative flex-shrink-0 w-32 aspect-video rounded-lg overflow-hidden cursor-pointer group'
|
||||
)}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
{(shot.status === 0 || shot.videoUrl.length === 0) && (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
|
||||
</div>
|
||||
)}
|
||||
{shot.status === 1 && shot.videoUrl[0] && (
|
||||
<video
|
||||
src={shot.videoUrl[0].video_url}
|
||||
key={shot.videoUrl[0].video_url}
|
||||
className="w-full h-full object-cover"
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
onMouseEnter={(e) => e.currentTarget.play()}
|
||||
onMouseLeave={(e) => e.currentTarget.pause()}
|
||||
/>
|
||||
)}
|
||||
{/* 任务失败 */}
|
||||
{shot.status === 2 && (
|
||||
<div className="w-full h-full flex items-center justify-center bg-red-500/10">
|
||||
<CircleX className="w-4 h-4 text-red-500" />
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute bottom-0 left-0 right-0 p-1 bg-gradient-to-t from-black/60 to-transparent">
|
||||
<span className="text-xs text-white/90">{shot.name}</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</HorizontalScroller>
|
||||
{/* 按钮 应用 忽略 */}
|
||||
<div className='flex items-center justify-end gap-2'>
|
||||
<motion.button
|
||||
onClick={() => handleApplyUpdate()}
|
||||
className='px-4 py-2 bg-blue-500/10 hover:bg-blue-500/20
|
||||
text-blue-500 rounded-lg transition-colors'
|
||||
>
|
||||
<span>Apply</span>
|
||||
</motion.button>
|
||||
<motion.button
|
||||
onClick={() => handleIgnoreUpdate()}
|
||||
className='px-4 py-2 bg-gray-500/10 hover:bg-gray-500/20
|
||||
text-gray-500 rounded-lg transition-colors'
|
||||
>
|
||||
<span>Ignore</span>
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
</FloatingGlassPanel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user