forked from 77media/video-flow
Merge branch 'dev' of https://git.qikongjian.com/77media/video-flow into dev
This commit is contained in:
commit
4ef38e0c73
@ -738,6 +738,8 @@ export const createMovieProjectV1 = async (request: {
|
|||||||
resolution: "720p" | "1080p" | "4k";
|
resolution: "720p" | "1080p" | "4k";
|
||||||
/** 语言 */
|
/** 语言 */
|
||||||
language: string;
|
language: string;
|
||||||
|
/** 视频时长 */
|
||||||
|
video_duration: string;
|
||||||
}) => {
|
}) => {
|
||||||
return post<ApiResponse<{
|
return post<ApiResponse<{
|
||||||
/** 项目ID */
|
/** 项目ID */
|
||||||
|
|||||||
@ -85,7 +85,8 @@ export interface UseScriptService {
|
|||||||
userId: string,
|
userId: string,
|
||||||
mode: "automatic" | "manual",
|
mode: "automatic" | "manual",
|
||||||
resolution: string,
|
resolution: string,
|
||||||
language: string
|
language: string,
|
||||||
|
video_duration: string
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
/** 设置任何属性 */
|
/** 设置任何属性 */
|
||||||
setAnyAttribute: any;
|
setAnyAttribute: any;
|
||||||
@ -158,7 +159,8 @@ export const useScriptService = (): UseScriptService => {
|
|||||||
userId: string,
|
userId: string,
|
||||||
mode: "automatic" | "manual",
|
mode: "automatic" | "manual",
|
||||||
resolution: string,
|
resolution: string,
|
||||||
language: string
|
language: string,
|
||||||
|
video_duration: string
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -169,7 +171,8 @@ export const useScriptService = (): UseScriptService => {
|
|||||||
userId,
|
userId,
|
||||||
mode as "automatic" | "manual",
|
mode as "automatic" | "manual",
|
||||||
resolution as "720p" | "1080p" | "4k",
|
resolution as "720p" | "1080p" | "4k",
|
||||||
language
|
language,
|
||||||
|
video_duration
|
||||||
);
|
);
|
||||||
|
|
||||||
setProjectId(projectData.project_id);
|
setProjectId(projectData.project_id);
|
||||||
|
|||||||
@ -60,10 +60,10 @@ export class TextToShotAdapter {
|
|||||||
let currentText = text;
|
let currentText = text;
|
||||||
|
|
||||||
// 按角色名称长度降序排序,避免短名称匹配到长名称的一部分
|
// 按角色名称长度降序排序,避免短名称匹配到长名称的一部分
|
||||||
// 既要兼容 首字母大写 其余小写、还要兼容 全部大写
|
// 既要兼容 每个单词 首字母大写 其余小写、还要兼容 全部大写
|
||||||
const sortedRoles = [...roles].sort((a, b) => b.name.length - a.name.length).map(role => ({
|
const sortedRoles = [...roles].sort((a, b) => b.name.length - a.name.length).map(role => ({
|
||||||
...role,
|
...role,
|
||||||
name: role.name.charAt(0).toUpperCase() + role.name.slice(1).toLowerCase()
|
name: role.name.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' ')
|
||||||
})).concat([...roles].map(role => ({
|
})).concat([...roles].map(role => ({
|
||||||
...role,
|
...role,
|
||||||
name: role.name.toUpperCase()
|
name: role.name.toUpperCase()
|
||||||
@ -257,6 +257,7 @@ export class TextToShotAdapter {
|
|||||||
if (shotData.shotDescContent.length > 0) {
|
if (shotData.shotDescContent.length > 0) {
|
||||||
// 合并所有描述段落的文本内容
|
// 合并所有描述段落的文本内容
|
||||||
shotData.shotDescContent.forEach(paragraph => {
|
shotData.shotDescContent.forEach(paragraph => {
|
||||||
|
if (paragraph.content) {
|
||||||
paragraph.content.forEach(node => {
|
paragraph.content.forEach(node => {
|
||||||
if (node.type === 'text') {
|
if (node.type === 'text') {
|
||||||
currentScript += node.text;
|
currentScript += node.text;
|
||||||
@ -265,6 +266,7 @@ export class TextToShotAdapter {
|
|||||||
currentScript += node.attrs.name;
|
currentScript += node.attrs.name;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -40,7 +40,8 @@ describe("ScriptService 业务逻辑测试", () => {
|
|||||||
"user123",
|
"user123",
|
||||||
"automatic",
|
"automatic",
|
||||||
"720p",
|
"720p",
|
||||||
"en"
|
"en",
|
||||||
|
"10"
|
||||||
);
|
);
|
||||||
expect(createRes.project_id).toBeDefined();
|
expect(createRes.project_id).toBeDefined();
|
||||||
projectId = createRes.project_id;
|
projectId = createRes.project_id;
|
||||||
|
|||||||
@ -126,7 +126,8 @@ export class ScriptEditUseCase {
|
|||||||
userId: string ,
|
userId: string ,
|
||||||
mode: "automatic" | "manual" = "automatic",
|
mode: "automatic" | "manual" = "automatic",
|
||||||
resolution: "720p" | "1080p" | "4k" = "720p",
|
resolution: "720p" | "1080p" | "4k" = "720p",
|
||||||
language: string
|
language: string,
|
||||||
|
video_duration: string
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
// 调用创建项目API
|
// 调用创建项目API
|
||||||
@ -136,6 +137,7 @@ export class ScriptEditUseCase {
|
|||||||
mode,
|
mode,
|
||||||
resolution,
|
resolution,
|
||||||
language,
|
language,
|
||||||
|
video_duration,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.successful) {
|
if (!response.successful) {
|
||||||
|
|||||||
@ -7,7 +7,7 @@ interface AISuggestionBarProps {
|
|||||||
suggestions: string[];
|
suggestions: string[];
|
||||||
onSuggestionClick: (suggestion: string) => void;
|
onSuggestionClick: (suggestion: string) => void;
|
||||||
onSubmit: (text: string) => void;
|
onSubmit: (text: string) => void;
|
||||||
onFocus: () => void;
|
onClick: () => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ export function AISuggestionBar({
|
|||||||
suggestions,
|
suggestions,
|
||||||
onSuggestionClick,
|
onSuggestionClick,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onFocus,
|
onClick,
|
||||||
placeholder = "输入你的想法,或点击预设词条获取 AI 建议..."
|
placeholder = "输入你的想法,或点击预设词条获取 AI 建议..."
|
||||||
}: AISuggestionBarProps) {
|
}: AISuggestionBarProps) {
|
||||||
const [inputText, setInputText] = useState('');
|
const [inputText, setInputText] = useState('');
|
||||||
@ -89,7 +89,7 @@ export function AISuggestionBar({
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-w-5xl mx-auto px-6">
|
<div className="max-w-5xl mx-auto px-6" onClick={onClick}>
|
||||||
{/* 智能预设词条 英文 */}
|
{/* 智能预设词条 英文 */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{showSuggestions && !isCollapsed && (
|
{showSuggestions && !isCollapsed && (
|
||||||
@ -179,7 +179,6 @@ export function AISuggestionBar({
|
|||||||
if (isCollapsed) {
|
if (isCollapsed) {
|
||||||
toggleCollapse();
|
toggleCollapse();
|
||||||
}
|
}
|
||||||
onFocus();
|
|
||||||
}}
|
}}
|
||||||
onBlur={() => setIsFocused(false)}
|
onBlur={() => setIsFocused(false)}
|
||||||
placeholder={isCollapsed ? "点击展开..." : placeholder}
|
placeholder={isCollapsed ? "点击展开..." : placeholder}
|
||||||
|
|||||||
@ -30,10 +30,15 @@ import { AudioRecorder } from "./AudioRecorder";
|
|||||||
import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService";
|
import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService";
|
||||||
import { createScriptEpisodeNew } from "@/api/script_episode";
|
import { createScriptEpisodeNew } from "@/api/script_episode";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
<<<<<<< HEAD
|
||||||
import { EditorContent, useEditor } from "@tiptap/react";
|
import { EditorContent, useEditor } from "@tiptap/react";
|
||||||
import StarterKit from "@tiptap/starter-kit";
|
import StarterKit from "@tiptap/starter-kit";
|
||||||
import { HighlightTextExtension } from "@/components/ui/main-editor/HighlightText";
|
import { HighlightTextExtension } from "@/components/ui/main-editor/HighlightText";
|
||||||
import Placeholder from "@tiptap/extension-placeholder";
|
import Placeholder from "@tiptap/extension-placeholder";
|
||||||
|
import { createMovieProjectV1 } from "@/api/video_flow";
|
||||||
|
=======
|
||||||
|
import { createMovieProjectV1 } from "@/api/video_flow";
|
||||||
|
>>>>>>> 3a713f988f2a1b9f322258e14c192470d879eee5
|
||||||
|
|
||||||
// 自定义音频播放器样式
|
// 自定义音频播放器样式
|
||||||
const customAudioPlayerStyles = `
|
const customAudioPlayerStyles = `
|
||||||
@ -548,8 +553,8 @@ export function ChatInputBox() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 调用创建剧集API
|
// 调用创建剧集API
|
||||||
const episodeResponse = await createScriptEpisodeNew(episodeData);
|
const episodeResponse = await createMovieProjectV1(episodeData);
|
||||||
console.log("episodeResponse", episodeResponse);
|
console.log('episodeResponse', episodeResponse);
|
||||||
if (episodeResponse.code !== 0) {
|
if (episodeResponse.code !== 0) {
|
||||||
console.error(`创建剧集失败: ${episodeResponse.message}`);
|
console.error(`创建剧集失败: ${episodeResponse.message}`);
|
||||||
alert(`创建剧集失败: ${episodeResponse.message}`);
|
alert(`创建剧集失败: ${episodeResponse.message}`);
|
||||||
|
|||||||
@ -159,6 +159,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-auto-rows: auto;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.videoContainer-qteKNi {
|
.videoContainer-qteKNi {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -166,13 +170,14 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
|
width: max-content;
|
||||||
}
|
}
|
||||||
.heroVideo-FIzuK1 {
|
.heroVideo-FIzuK1 {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
object-position: center;
|
object-position: center;
|
||||||
background-color: #0003;
|
background-color: #0003;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
.container-kIPoeH {
|
.container-kIPoeH {
|
||||||
|
|||||||
@ -21,7 +21,6 @@ const WorkFlow = React.memo(function WorkFlow() {
|
|||||||
console.log("init-WorkFlow");
|
console.log("init-WorkFlow");
|
||||||
return () => console.log("unmount-WorkFlow");
|
return () => console.log("unmount-WorkFlow");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [isEditModalOpen, setIsEditModalOpen] = React.useState(false);
|
const [isEditModalOpen, setIsEditModalOpen] = React.useState(false);
|
||||||
const [activeEditTab, setActiveEditTab] = React.useState('1');
|
const [activeEditTab, setActiveEditTab] = React.useState('1');
|
||||||
@ -92,8 +91,7 @@ const WorkFlow = React.memo(function WorkFlow() {
|
|||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<div className="w-full overflow-hidden h-[calc(100vh-6rem)] absolute top-[4rem] left-0 right-0 px-[1rem]">
|
<div className="w-full overflow-hidden h-[calc(100vh-6rem)] absolute top-[4rem] left-0 right-0 px-[1rem]">
|
||||||
<div className="flex h-full flex-col justify-start items-center">
|
<div className="w-full h-full">
|
||||||
<div className="container-H2sRZG">
|
|
||||||
<div className="splashContainer-otuV_A">
|
<div className="splashContainer-otuV_A">
|
||||||
<div className="content-vPGYx8">
|
<div className="content-vPGYx8">
|
||||||
<div className="info-UUGkPJ">
|
<div className="info-UUGkPJ">
|
||||||
@ -148,7 +146,7 @@ const WorkFlow = React.memo(function WorkFlow() {
|
|||||||
) : isLoading ? (
|
) : isLoading ? (
|
||||||
<Skeleton className="w-full aspect-video rounded-lg" />
|
<Skeleton className="w-full aspect-video rounded-lg" />
|
||||||
) : (
|
) : (
|
||||||
<div className="heroVideo-FIzuK1" style={{ aspectRatio: "16 / 9" }} key={currentSketchIndex}>
|
<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={currentSketchIndex}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<MediaViewer
|
<MediaViewer
|
||||||
taskObject={taskObject}
|
taskObject={taskObject}
|
||||||
@ -168,10 +166,9 @@ const WorkFlow = React.memo(function WorkFlow() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{taskObject.currentStage !== 'final_video' && taskObject.currentStage !== 'script' && (
|
{taskObject.currentStage !== 'final_video' && taskObject.currentStage !== 'script' && (
|
||||||
<div className="imageGrid-ymZV9z hide-scrollbar">
|
<div className="h-[112px] w-[calc((100vh-6rem-200px)/9*16)]">
|
||||||
<ErrorBoundary>
|
|
||||||
<ThumbnailGrid
|
<ThumbnailGrid
|
||||||
isEditModalOpen={isEditModalOpen}
|
isDisabledFocus={isEditModalOpen || isPauseWorkFlow}
|
||||||
taskObject={taskObject}
|
taskObject={taskObject}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
currentSketchIndex={currentSketchIndex}
|
currentSketchIndex={currentSketchIndex}
|
||||||
@ -183,14 +180,12 @@ const WorkFlow = React.memo(function WorkFlow() {
|
|||||||
totalSketchCount={totalSketchCount}
|
totalSketchCount={totalSketchCount}
|
||||||
onSketchSelect={setCurrentSketchIndex}
|
onSketchSelect={setCurrentSketchIndex}
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 暂停/播放按钮 */}
|
{/* 暂停/播放按钮 */}
|
||||||
{
|
{
|
||||||
@ -202,7 +197,7 @@ const WorkFlow = React.memo(function WorkFlow() {
|
|||||||
tooltip={isPauseWorkFlow ? "Play" : "Pause"}
|
tooltip={isPauseWorkFlow ? "Play" : "Pause"}
|
||||||
onClick={() => setIsPauseWorkFlow(!isPauseWorkFlow)}
|
onClick={() => setIsPauseWorkFlow(!isPauseWorkFlow)}
|
||||||
/>
|
/>
|
||||||
{ mode !== 'automatic' && (
|
{ !mode.includes('auto') && (
|
||||||
<GlassIconButton
|
<GlassIconButton
|
||||||
icon={ChevronLast}
|
icon={ChevronLast}
|
||||||
size='lg'
|
size='lg'
|
||||||
@ -220,7 +215,7 @@ const WorkFlow = React.memo(function WorkFlow() {
|
|||||||
suggestions={mockSuggestions}
|
suggestions={mockSuggestions}
|
||||||
onSuggestionClick={handleSuggestionClick}
|
onSuggestionClick={handleSuggestionClick}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onFocus={() => setIsPauseWorkFlow(true)}
|
onClick={() => setIsPauseWorkFlow(true)}
|
||||||
placeholder="Please input your ideas, or click the predefined tags to receive AI advice..."
|
placeholder="Please input your ideas, or click the predefined tags to receive AI advice..."
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|||||||
@ -137,7 +137,7 @@ export const MediaViewer = React.memo(function MediaViewer({
|
|||||||
return (
|
return (
|
||||||
<video
|
<video
|
||||||
ref={finalVideoRef}
|
ref={finalVideoRef}
|
||||||
className="w-[auto] h-full object-cover rounded-lg"
|
className="w-full h-full object-cover rounded-lg"
|
||||||
src={taskObject.final.url}
|
src={taskObject.final.url}
|
||||||
autoPlay={isFinalVideoPlaying}
|
autoPlay={isFinalVideoPlaying}
|
||||||
loop
|
loop
|
||||||
@ -305,7 +305,7 @@ export const MediaViewer = React.memo(function MediaViewer({
|
|||||||
transition={{ duration: 0.8, ease: "easeInOut" }}
|
transition={{ duration: 0.8, ease: "easeInOut" }}
|
||||||
>
|
>
|
||||||
<video
|
<video
|
||||||
className="w-[auto] h-full rounded-lg object-cover object-center"
|
className="w-full h-full rounded-lg object-cover object-center"
|
||||||
src={taskObject.final.url}
|
src={taskObject.final.url}
|
||||||
loop
|
loop
|
||||||
playsInline
|
playsInline
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { Loader2, X } from 'lucide-react';
|
|||||||
import { TaskObject } from '@/api/DTO/movieEdit';
|
import { TaskObject } from '@/api/DTO/movieEdit';
|
||||||
|
|
||||||
interface ThumbnailGridProps {
|
interface ThumbnailGridProps {
|
||||||
isEditModalOpen: boolean;
|
isDisabledFocus: boolean;
|
||||||
taskObject: TaskObject;
|
taskObject: TaskObject;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
currentSketchIndex: number;
|
currentSketchIndex: number;
|
||||||
@ -22,7 +22,7 @@ interface ThumbnailGridProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ThumbnailGrid({
|
export function ThumbnailGrid({
|
||||||
isEditModalOpen,
|
isDisabledFocus,
|
||||||
taskObject,
|
taskObject,
|
||||||
isLoading,
|
isLoading,
|
||||||
currentSketchIndex,
|
currentSketchIndex,
|
||||||
@ -124,7 +124,7 @@ export function ThumbnailGrid({
|
|||||||
// 监听键盘事件
|
// 监听键盘事件
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 组件挂载时自动聚焦
|
// 组件挂载时自动聚焦
|
||||||
if (thumbnailsRef.current && !isEditModalOpen) {
|
if (thumbnailsRef.current && !isDisabledFocus) {
|
||||||
thumbnailsRef.current.focus();
|
thumbnailsRef.current.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,7 +134,7 @@ export function ThumbnailGrid({
|
|||||||
|
|
||||||
// 确保在数据变化时保持焦点
|
// 确保在数据变化时保持焦点
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (thumbnailsRef.current && !isFocused && !isEditModalOpen) {
|
if (thumbnailsRef.current && !isFocused && !isDisabledFocus) {
|
||||||
thumbnailsRef.current.focus();
|
thumbnailsRef.current.focus();
|
||||||
}
|
}
|
||||||
}, [taskObject.currentStage, isFocused]);
|
}, [taskObject.currentStage, isFocused]);
|
||||||
@ -267,7 +267,6 @@ export function ThumbnailGrid({
|
|||||||
<div className="absolute inset-0 bg-black/10 flex items-center justify-center z-20">
|
<div className="absolute inset-0 bg-black/10 flex items-center justify-center z-20">
|
||||||
<div className="text-blue-500 text-xl font-bold flex items-center gap-2">
|
<div className="text-blue-500 text-xl font-bold flex items-center gap-2">
|
||||||
<Loader2 className="w-10 h-10 animate-spin" />
|
<Loader2 className="w-10 h-10 animate-spin" />
|
||||||
<span>Generating...</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -275,7 +274,6 @@ export function ThumbnailGrid({
|
|||||||
<div className="absolute inset-0 bg-red-500/10 flex items-center justify-center z-20">
|
<div className="absolute inset-0 bg-red-500/10 flex items-center justify-center z-20">
|
||||||
<div className="text-red-500 text-xl font-bold flex items-center gap-2">
|
<div className="text-red-500 text-xl font-bold flex items-center gap-2">
|
||||||
<X className="w-10 h-10" />
|
<X className="w-10 h-10" />
|
||||||
<span>Failed</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -329,7 +327,6 @@ export function ThumbnailGrid({
|
|||||||
<div className="absolute inset-0 bg-black/10 flex items-center justify-center">
|
<div className="absolute inset-0 bg-black/10 flex items-center justify-center">
|
||||||
<div className="text-blue-500 text-xl font-bold flex items-center gap-2">
|
<div className="text-blue-500 text-xl font-bold flex items-center gap-2">
|
||||||
<Loader2 className="w-10 h-10 animate-spin" />
|
<Loader2 className="w-10 h-10 animate-spin" />
|
||||||
<span>Generating...</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -337,7 +334,6 @@ export function ThumbnailGrid({
|
|||||||
<div className="absolute inset-0 bg-red-500/10 flex items-center justify-center">
|
<div className="absolute inset-0 bg-red-500/10 flex items-center justify-center">
|
||||||
<div className="text-red-500 text-xl font-bold flex items-center gap-2">
|
<div className="text-red-500 text-xl font-bold flex items-center gap-2">
|
||||||
<X className="w-10 h-10" />
|
<X className="w-10 h-10" />
|
||||||
<span>Failed</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -366,7 +362,7 @@ export function ThumbnailGrid({
|
|||||||
<div
|
<div
|
||||||
ref={thumbnailsRef}
|
ref={thumbnailsRef}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className="w-full grid grid-flow-col auto-cols-[20%] gap-4 overflow-x-auto hide-scrollbar px-1 py-1 cursor-grab active:cursor-grabbing focus:outline-none select-none"
|
className="w-full h-full grid grid-flow-col auto-cols-[20%] gap-4 overflow-x-auto hide-scrollbar px-1 py-1 cursor-grab active:cursor-grabbing focus:outline-none select-none"
|
||||||
autoFocus
|
autoFocus
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import React, { forwardRef, useEffect, useRef, useState } from "react";
|
import React, { forwardRef, useRef, useState } from "react";
|
||||||
|
import { useDeepCompareEffect } from "@/hooks/useDeepCompareEffect";
|
||||||
import { Plus, X, UserRoundPlus, MessageCirclePlus, MessageCircleMore, ClipboardType } from "lucide-react";
|
import { Plus, X, UserRoundPlus, MessageCirclePlus, MessageCircleMore, ClipboardType } from "lucide-react";
|
||||||
import ShotEditor from "./ShotEditor";
|
import ShotEditor from "./ShotEditor";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@ -51,16 +52,16 @@ export const ShotsEditor = forwardRef<any, ShotsEditorProps>(function ShotsEdito
|
|||||||
const descEditorRef = useRef<any>(null);
|
const descEditorRef = useRef<any>(null);
|
||||||
const dialogEditorRef = useRef<any>(null);
|
const dialogEditorRef = useRef<any>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useDeepCompareEffect(() => {
|
||||||
console.log('-==========shotInfo===========-', shotInfo);
|
|
||||||
if (shotInfo) {
|
if (shotInfo) {
|
||||||
|
console.log('-==========shotInfo===========-', shotInfo);
|
||||||
const shots = shotInfo.map((shot) => {
|
const shots = shotInfo.map((shot) => {
|
||||||
return TextToShotAdapter.fromLensType(shot, roles);
|
return TextToShotAdapter.fromLensType(shot, roles);
|
||||||
});
|
});
|
||||||
console.log('-==========shots===========-', shots);
|
console.log('-==========shots===========-', shots);
|
||||||
setShots(shots as Shot[]);
|
setShots(shots as Shot[]);
|
||||||
}
|
}
|
||||||
}, [shotInfo]);
|
}, [shotInfo, roles]);
|
||||||
|
|
||||||
const handleDescContentChange = (content: any) => {
|
const handleDescContentChange = (content: any) => {
|
||||||
const shot = shots[currentShotIndex];
|
const shot = shots[currentShotIndex];
|
||||||
|
|||||||
@ -47,11 +47,19 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
|||||||
const [isLoadingShots, setIsLoadingShots] = useState(false);
|
const [isLoadingShots, setIsLoadingShots] = useState(false);
|
||||||
const shotsEditorRef = useRef<any>(null);
|
const shotsEditorRef = useRef<any>(null);
|
||||||
const [isRegenerate, setIsRegenerate] = useState(false);
|
const [isRegenerate, setIsRegenerate] = useState(false);
|
||||||
|
const [pendingRegeneration, setPendingRegeneration] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('shotTabContent-----roles', roles);
|
console.log('shotTabContent-----roles', roles);
|
||||||
}, [roles]);
|
}, [roles]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pendingRegeneration) {
|
||||||
|
regenerateVideoSegment();
|
||||||
|
setPendingRegeneration(false);
|
||||||
|
}
|
||||||
|
}, [shotData[selectedIndex]?.lens]);
|
||||||
|
|
||||||
// 监听当前选中index变化
|
// 监听当前选中index变化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('shotTabContent-----shotData', shotData);
|
console.log('shotTabContent-----shotData', shotData);
|
||||||
@ -165,13 +173,12 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
|||||||
console.log('regenerate');
|
console.log('regenerate');
|
||||||
setIsRegenerate(true);
|
setIsRegenerate(true);
|
||||||
const shotInfo = shotsEditorRef.current.getShotInfo();
|
const shotInfo = shotsEditorRef.current.getShotInfo();
|
||||||
console.log('shotTabContent-----shotInfo', shotInfo);
|
console.log('shotInfo', shotInfo);
|
||||||
setSelectedSegment({
|
setSelectedSegment({
|
||||||
...shotData[selectedIndex],
|
...shotData[selectedIndex],
|
||||||
lens: shotInfo
|
lens: shotInfo
|
||||||
});
|
});
|
||||||
await regenerateVideoSegment();
|
setPendingRegeneration(true);
|
||||||
setIsRegenerate(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 新增分镜
|
// 新增分镜
|
||||||
@ -408,7 +415,7 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
|||||||
<motion.button
|
<motion.button
|
||||||
onClick={() => handleRegenerate()}
|
onClick={() => handleRegenerate()}
|
||||||
className="flex items-center justify-center gap-2 px-4 py-3 bg-blue-500/10 hover:bg-blue-500/20
|
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"
|
text-blue-500 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
disabled={isRegenerate}
|
disabled={isRegenerate}
|
||||||
|
|||||||
20
hooks/useDeepCompareEffect.ts
Normal file
20
hooks/useDeepCompareEffect.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import isEqual from 'lodash/isEqual';
|
||||||
|
|
||||||
|
function useDeepCompareMemoize<T>(value: T) {
|
||||||
|
const ref = useRef<T>();
|
||||||
|
|
||||||
|
if (!isEqual(value, ref.current)) {
|
||||||
|
ref.current = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ref.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeepCompareEffect(
|
||||||
|
callback: () => void | (() => void),
|
||||||
|
dependencies: any[]
|
||||||
|
) {
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
useEffect(callback, dependencies.map(useDeepCompareMemoize));
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user