forked from 77media/video-flow
角色替换联动
This commit is contained in:
parent
200bddb031
commit
14a61b9dec
@ -9,9 +9,10 @@ type FloatingGlassPanelProps = {
|
||||
children: ReactNode;
|
||||
width?: string;
|
||||
r_key?: string | number;
|
||||
panel_style?: React.CSSProperties;
|
||||
};
|
||||
|
||||
export default function FloatingGlassPanel({ open, onClose, children, width = '320px', r_key }: FloatingGlassPanelProps) {
|
||||
export default function FloatingGlassPanel({ open, onClose, children, width = '320px', r_key, panel_style }: FloatingGlassPanelProps) {
|
||||
// 定义弹出动画
|
||||
const bounceAnimation = {
|
||||
scale: [0.95, 1.02, 0.98, 1],
|
||||
@ -48,7 +49,7 @@ export default function FloatingGlassPanel({ open, onClose, children, width = '3
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ width }}
|
||||
style={{ width, ...panel_style }}
|
||||
className="rounded-xl backdrop-blur-md bg-white/10 border border-white/20 shadow-xl text-white p-4"
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { GlassIconButton } from '@/components/ui/glass-icon-button';
|
||||
import { Check } from 'lucide-react';
|
||||
|
||||
interface ImageWaveProps {
|
||||
// 图片列表数据
|
||||
@ -152,8 +154,6 @@ export const ImageWave: React.FC<ImageWaveProps> = ({
|
||||
setCurrentExpandedItem(null);
|
||||
} else {
|
||||
setCurrentExpandedItem(index);
|
||||
setCurrentSelectedIndex(index);
|
||||
onClick?.(index);
|
||||
}
|
||||
};
|
||||
|
||||
@ -188,6 +188,11 @@ export const ImageWave: React.FC<ImageWaveProps> = ({
|
||||
};
|
||||
}, [autoAnimate]);
|
||||
|
||||
const handleSelectImage = (index: number) => {
|
||||
setCurrentSelectedIndex(index);
|
||||
onClick?.(index);
|
||||
};
|
||||
|
||||
return (
|
||||
<Wrapper width={containerWidth} height={containerHeight}>
|
||||
<Items ref={itemsRef} gap={gap} className={currentExpandedItem !== null ? 'has-expanded' : ''}>
|
||||
@ -196,11 +201,19 @@ export const ImageWave: React.FC<ImageWaveProps> = ({
|
||||
key={index}
|
||||
width={itemWidth}
|
||||
height={itemHeight}
|
||||
className={`item ${currentExpandedItem === index ? 'expanded' : ''} ${currentSelectedIndex === index ? 'selected' : ''}`}
|
||||
className={`group relative item ${currentExpandedItem === index ? 'expanded' : ''} ${currentSelectedIndex === index ? 'selected' : ''}`}
|
||||
style={{ backgroundImage: `url(${image})` }}
|
||||
onClick={() => handleItemClick(index)}
|
||||
tabIndex={0}
|
||||
/>
|
||||
>
|
||||
{/* 添加一个玻璃按钮 勾选当前图片 移入/选中改变图标颜色 */}
|
||||
<GlassIconButton
|
||||
icon={Check}
|
||||
size='sm'
|
||||
onClick={() => handleSelectImage(index)}
|
||||
className="absolute top-1 right-1 z-[999] cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity duration-300 group-hover:bg-blue-500/50"
|
||||
/>
|
||||
</Item>
|
||||
))}
|
||||
</Items>
|
||||
</Wrapper>
|
||||
|
||||
57
components/ui/character-library-selector.tsx
Normal file
57
components/ui/character-library-selector.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import FloatingGlassPanel from './FloatingGlassPanel';
|
||||
import { ImageWave } from '@/components/ui/ImageWave';
|
||||
|
||||
const imageUrls = [
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg',
|
||||
];
|
||||
|
||||
export function CharacterLibrarySelector({
|
||||
isReplaceLibraryOpen,
|
||||
setIsReplaceLibraryOpen,
|
||||
onSelect,
|
||||
}: {
|
||||
isReplaceLibraryOpen: boolean;
|
||||
setIsReplaceLibraryOpen: (open: boolean) => void;
|
||||
onSelect: (index: number) => void;
|
||||
}) {
|
||||
return (
|
||||
<FloatingGlassPanel
|
||||
open={isReplaceLibraryOpen}
|
||||
width='90vw'
|
||||
panel_style={{ background: 'unset', border: 'unset', backdropFilter: 'unset', boxShadow: 'none' }}
|
||||
onClose={() => setIsReplaceLibraryOpen(false)}
|
||||
>
|
||||
{/* 内容 */}
|
||||
<ImageWave
|
||||
images={imageUrls}
|
||||
containerWidth="90vw"
|
||||
containerHeight="calc(var(--index) * 15)"
|
||||
itemWidth="calc(var(--index) * 2)"
|
||||
itemHeight="calc(var(--index) * 12)"
|
||||
gap="0.1rem"
|
||||
autoAnimate={true}
|
||||
autoAnimateInterval={100}
|
||||
onClick={(index) => {
|
||||
onSelect(index);
|
||||
}}
|
||||
/>
|
||||
</FloatingGlassPanel>
|
||||
);
|
||||
}
|
||||
@ -2,14 +2,11 @@ import React, { useState, useRef } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Upload, Library, Play, Pause, RefreshCw, Wand2, Users, Check, ReplaceAll, X } from 'lucide-react';
|
||||
import { cn } from '@/public/lib/utils';
|
||||
import { GlassIconButton } from './glass-icon-button';
|
||||
import { ReplaceCharacterModal } from './replace-character-modal';
|
||||
import { Slider } from './slider';
|
||||
import CharacterEditor from './character-editor';
|
||||
import ImageBlurTransition from './ImageBlurTransition';
|
||||
import FloatingGlassPanel from './FloatingGlassPanel';
|
||||
import { ReplaceCharacterPanel, mockShots, mockCharacter } from './replace-character-panel';
|
||||
import { ImageWave } from '@/components/ui/ImageWave';
|
||||
import { CharacterLibrarySelector } from './character-library-selector';
|
||||
|
||||
interface Appearance {
|
||||
hairStyle: string;
|
||||
@ -58,45 +55,19 @@ interface CharacterTabContentProps {
|
||||
roles: Role[];
|
||||
}
|
||||
|
||||
const imageUrls = [
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg',
|
||||
];
|
||||
|
||||
export function CharacterTabContent({
|
||||
taskSketch,
|
||||
currentRoleIndex,
|
||||
onSketchSelect,
|
||||
roles = [mockRole]
|
||||
}: CharacterTabContentProps) {
|
||||
const [isReplaceModalOpen, setIsReplaceModalOpen] = useState(false);
|
||||
const [activeReplaceMethod, setActiveReplaceMethod] = useState('upload');
|
||||
const [newTag, setNewTag] = useState('');
|
||||
const [localRole, setLocalRole] = useState(mockRole);
|
||||
const [currentRole, setCurrentRole] = useState(roles[currentRoleIndex]);
|
||||
const [isReplacePanelOpen, setIsReplacePanelOpen] = useState(false);
|
||||
const [replacePanelKey, setReplacePanelKey] = useState(0);
|
||||
const [ignoreReplace, setIgnoreReplace] = useState(false);
|
||||
const [isReplaceLibraryOpen, setIsReplaceLibraryOpen] = useState(false);
|
||||
const [replaceLibraryKey, setReplaceLibraryKey] = useState(0);
|
||||
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const handleReplaceCharacter = (url: string) => {
|
||||
setCurrentRole({
|
||||
@ -133,6 +104,13 @@ export function CharacterTabContent({
|
||||
setCurrentRole(roles[index]);
|
||||
};
|
||||
|
||||
// 从角色库中选择角色
|
||||
const handleSelectCharacter = (index: number) => {
|
||||
console.log('index', index);
|
||||
setIsReplaceLibraryOpen(false);
|
||||
handleReplaceCharacter('https://c.huiying.video/images/5740cb7c-6e08-478f-9e7c-bca7f78a2bf6.jpg');
|
||||
};
|
||||
|
||||
// 如果没有角色数据,显示占位内容
|
||||
if (!roles || roles.length === 0) {
|
||||
return (
|
||||
@ -260,7 +238,7 @@ export function CharacterTabContent({
|
||||
{/* 重新生成按钮、替换形象按钮 */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<motion.button
|
||||
onClick={() => console.log('Replace')}
|
||||
onClick={() => handleReplaceCharacter('https://c.huiying.video/images/5740cb7c-6e08-478f-9e7c-bca7f78a2bf6.jpg')}
|
||||
className="flex items-center justify-center gap-2 px-4 py-3 bg-pink-500/10 hover:bg-pink-500/20
|
||||
text-pink-500 rounded-lg transition-colors"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
@ -287,7 +265,12 @@ export function CharacterTabContent({
|
||||
</motion.div>
|
||||
|
||||
|
||||
<FloatingGlassPanel open={isReplacePanelOpen} width='500px' r_key={replacePanelKey}>
|
||||
<FloatingGlassPanel
|
||||
open={isReplacePanelOpen}
|
||||
width='500px'
|
||||
r_key={replacePanelKey}
|
||||
onClose={() => handleCloseReplacePanel()}
|
||||
>
|
||||
<ReplaceCharacterPanel
|
||||
shots={mockShots}
|
||||
character={mockCharacter}
|
||||
@ -297,41 +280,11 @@ export function CharacterTabContent({
|
||||
</FloatingGlassPanel>
|
||||
|
||||
{/* 从角色库中选择角色 */}
|
||||
<FloatingGlassPanel open={isReplaceLibraryOpen} width='90vw' r_key={replaceLibraryKey}>
|
||||
{/* 标题 从角色库中选择角色 */}
|
||||
<div className="text-2xl font-semibold text-white text-center">Role Library</div>
|
||||
{/* 内容 */}
|
||||
<ImageWave
|
||||
images={imageUrls}
|
||||
containerWidth="90vw"
|
||||
containerHeight="calc(var(--index) * 15)"
|
||||
itemWidth="calc(var(--index) * 2)"
|
||||
itemHeight="calc(var(--index) * 12)"
|
||||
gap="0.1rem"
|
||||
autoAnimate={true}
|
||||
autoAnimateInterval={100}
|
||||
onClick={(index) => {
|
||||
console.log('index', index);
|
||||
}}
|
||||
/>
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex justify-end gap-4">
|
||||
<button
|
||||
onClick={() => setIsReplaceLibraryOpen(false)}
|
||||
className="px-4 py-2 rounded-lg bg-white/10 text-white hover:bg-white/20 transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
console.log('replace');
|
||||
}}
|
||||
className="px-4 py-2 rounded-lg bg-blue-500 text-white hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
替换
|
||||
</button>
|
||||
</div>
|
||||
</FloatingGlassPanel>
|
||||
<CharacterLibrarySelector
|
||||
isReplaceLibraryOpen={isReplaceLibraryOpen}
|
||||
setIsReplaceLibraryOpen={setIsReplaceLibraryOpen}
|
||||
onSelect={handleSelectCharacter}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -51,6 +51,7 @@ type Props = {
|
||||
scanTimeout?: number;
|
||||
isScanFailed?: boolean; // 外部传入的失败状态
|
||||
onDetectionsChange?: (detections: PersonDetection[]) => void;
|
||||
onPersonClick?: (person: PersonDetection) => void;
|
||||
};
|
||||
|
||||
export const PersonDetectionScene: React.FC<Props> = ({
|
||||
@ -63,7 +64,8 @@ export const PersonDetectionScene: React.FC<Props> = ({
|
||||
onScanExit,
|
||||
scanTimeout = 10000,
|
||||
isScanFailed = false,
|
||||
onDetectionsChange
|
||||
onDetectionsChange,
|
||||
onPersonClick
|
||||
}) => {
|
||||
const scanControls = useAnimation();
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
@ -374,7 +376,9 @@ export const PersonDetectionScene: React.FC<Props> = ({
|
||||
{detections.map((person, index) => {
|
||||
|
||||
return (
|
||||
<React.Fragment key={person.id}>
|
||||
<div key={person.id} className="cursor-pointer" onClick={() => {
|
||||
onPersonClick?.(person);
|
||||
}}>
|
||||
<PersonBox person={person} />
|
||||
<motion.div
|
||||
className="absolute z-50 px-3 py-1 text-white text-xs bg-cyan-500/20 border border-cyan-400/30 rounded-md backdrop-blur-md whitespace-nowrap"
|
||||
@ -388,7 +392,7 @@ export const PersonDetectionScene: React.FC<Props> = ({
|
||||
>
|
||||
{person.name}
|
||||
</motion.div>
|
||||
</React.Fragment>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</AnimatePresence>
|
||||
|
||||
@ -4,6 +4,7 @@ import { Shot, Character } from '@/app/model/types';
|
||||
interface ReplaceCharacterPanelProps {
|
||||
shots: Shot[];
|
||||
character: Character;
|
||||
showAddToLibrary?: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: (selectedShots: string[], addToLibrary: boolean) => void;
|
||||
}
|
||||
@ -47,6 +48,7 @@ export const mockCharacter: Character = {
|
||||
export function ReplaceCharacterPanel({
|
||||
shots = mockShots,
|
||||
character = mockCharacter,
|
||||
showAddToLibrary = true,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}: ReplaceCharacterPanelProps) {
|
||||
@ -55,7 +57,7 @@ export function ReplaceCharacterPanel({
|
||||
title="替换新形象"
|
||||
shots={shots}
|
||||
item={character}
|
||||
showAddToLibrary={true}
|
||||
showAddToLibrary={showAddToLibrary}
|
||||
addToLibraryText="新形象同步添加至角色库"
|
||||
onClose={onClose}
|
||||
onConfirm={onConfirm}
|
||||
|
||||
@ -100,14 +100,14 @@ export function ReplacePanel({
|
||||
{/* 分镜展示区 */}
|
||||
<div className="space-y-2">
|
||||
<div className="text-white/80 text-sm">选择需要替换的分镜:</div>
|
||||
<div className="flex gap-4 overflow-x-auto pb-4 hide-scrollbar">
|
||||
<div className="flex gap-4 overflow-x-auto pb-4 hide-scrollbar h-64">
|
||||
{shots.map((shot) => (
|
||||
<motion.div
|
||||
key={shot.id}
|
||||
className={cn(
|
||||
'relative flex-shrink-0 rounded-lg overflow-hidden cursor-pointer',
|
||||
'aspect-video border-2',
|
||||
hoveredVideoId === shot.id ? 'w-64' : 'w-32',
|
||||
hoveredVideoId === shot.id ? 'w-auto' : 'w-32',
|
||||
selectedShots.includes(shot.id)
|
||||
? 'border-blue-500'
|
||||
: 'border-transparent hover:border-blue-500/50'
|
||||
|
||||
@ -330,7 +330,7 @@ export function SceneTabContent({
|
||||
{/* 重新生成按钮、替换形象按钮 */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<motion.button
|
||||
onClick={() => console.log('Replace')}
|
||||
onClick={() => handleReplaceScene('https://c.huiying.video/images/5740cb7c-6e08-478f-9e7c-bca7f78a2bf6.jpg')}
|
||||
className="flex items-center justify-center gap-2 px-4 py-3 bg-pink-500/10 hover:bg-pink-500/20
|
||||
text-pink-500 rounded-lg transition-colors"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
|
||||
@ -11,13 +11,18 @@ interface CharacterAttributes {
|
||||
age: string;
|
||||
}
|
||||
|
||||
// interface CharacterTokenProps extends ReactNodeViewProps {
|
||||
// onClick?: (attrs: CharacterAttributes) => void
|
||||
// }
|
||||
|
||||
export function CharacterToken(props: ReactNodeViewProps) {
|
||||
const [showCard, setShowCard] = useState(false)
|
||||
const { name, avatar, gender, age } = props.node.attrs as CharacterAttributes
|
||||
|
||||
const handleClick = () => {
|
||||
console.log('点击角色:', name)
|
||||
alert(`点击角色:${name}`)
|
||||
const { editor } = props;
|
||||
editor?.emit('character-clicked', props.node.attrs);
|
||||
}
|
||||
|
||||
return (
|
||||
@ -81,6 +86,12 @@ export const CharacterTokenExtension = Node.create({
|
||||
return ['character-token', mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
|
||||
// addStorage() {
|
||||
// return {
|
||||
// onClickCharacter: null as null | ((character: CharacterAttributes) => void),
|
||||
// }
|
||||
// },
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(CharacterToken);
|
||||
},
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { EditorContent, useEditor } from '@tiptap/react';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import { motion } from "framer-motion";
|
||||
@ -36,9 +36,10 @@ const initialContent = {
|
||||
|
||||
interface ShotEditorProps {
|
||||
onAddSegment?: () => void;
|
||||
onCharacterClick?: (attrs: any) => void;
|
||||
}
|
||||
|
||||
const ShotEditor = React.forwardRef<{ addSegment: () => void }, ShotEditorProps>(function ShotEditor({ onAddSegment }, ref) {
|
||||
const ShotEditor = React.forwardRef<{ addSegment: () => void, onCharacterClick: (attrs: any) => void }, ShotEditorProps>(function ShotEditor({ onAddSegment, onCharacterClick }, ref) {
|
||||
const [segments, setSegments] = useState(initialContent.content);
|
||||
|
||||
const editor = useEditor({
|
||||
@ -60,6 +61,20 @@ const ShotEditor = React.forwardRef<{ addSegment: () => void }, ShotEditorProps>
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const handleCharacterClick = (attrs: any) => {
|
||||
console.log('SceneEditor 收到角色点击事件:', attrs)
|
||||
// 你可以这里 setState 打开一个弹窗 / 面板等
|
||||
onCharacterClick?.(attrs);
|
||||
};
|
||||
|
||||
editor?.on('character-clicked', handleCharacterClick as any);
|
||||
|
||||
return () => {
|
||||
editor?.off('character-clicked', handleCharacterClick as any);
|
||||
};
|
||||
}, [editor]);
|
||||
|
||||
const addSegment = () => {
|
||||
if (!editor) return;
|
||||
|
||||
|
||||
@ -10,6 +10,9 @@ import { MediaPropertiesModal } from './media-properties-modal';
|
||||
import { DramaLineChart } from './drama-line-chart';
|
||||
import { PersonDetection, PersonDetectionScene } from './person-detection';
|
||||
import ShotEditor from './shot-editor/ShotEditor';
|
||||
import { CharacterLibrarySelector } from './character-library-selector';
|
||||
import FloatingGlassPanel from './FloatingGlassPanel';
|
||||
import { ReplaceCharacterPanel, mockShots, mockCharacter } from './replace-character-panel';
|
||||
|
||||
interface ShotTabContentProps {
|
||||
taskSketch: any[];
|
||||
@ -29,16 +32,16 @@ export function ShotTabContent({
|
||||
const videosRef = useRef<HTMLDivElement>(null);
|
||||
const videoPlayerRef = useRef<HTMLVideoElement>(null);
|
||||
const [isPlaying, setIsPlaying] = React.useState(externalIsPlaying);
|
||||
const [isMuted, setIsMuted] = React.useState(false);
|
||||
const [progress, setProgress] = React.useState(0);
|
||||
const [isReplaceModalOpen, setIsReplaceModalOpen] = React.useState(false);
|
||||
const [activeReplaceMethod, setActiveReplaceMethod] = React.useState<'upload' | 'library' | 'generate'>('upload');
|
||||
const [isMediaPropertiesModalOpen, setIsMediaPropertiesModalOpen] = React.useState(false);
|
||||
|
||||
const [triggerScan, setTriggerScan] = useState(false);
|
||||
const [detections, setDetections] = useState<PersonDetection[]>([]);
|
||||
const [scanState, setScanState] = useState<'idle' | 'scanning' | 'detected'>('idle');
|
||||
|
||||
const [isReplaceLibraryOpen, setIsReplaceLibraryOpen] = useState(false);
|
||||
const [isReplacePanelOpen, setIsReplacePanelOpen] = useState(false);
|
||||
|
||||
const [shots, setShots] = useState<any[]>([]);
|
||||
|
||||
|
||||
// 监听外部播放状态变化
|
||||
useEffect(() => {
|
||||
@ -87,13 +90,6 @@ export function ShotTabContent({
|
||||
}
|
||||
}, [isPlaying, currentSketchIndex]);
|
||||
|
||||
// 更新进度条
|
||||
const handleTimeUpdate = () => {
|
||||
if (videoPlayerRef.current) {
|
||||
const progress = (videoPlayerRef.current.currentTime / videoPlayerRef.current.duration) * 100;
|
||||
setProgress(progress);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理扫描开始
|
||||
const handleScan = () => {
|
||||
@ -130,6 +126,27 @@ export function ShotTabContent({
|
||||
}
|
||||
};
|
||||
|
||||
// 处理人物点击 打开角色库
|
||||
const handlePersonClick = (person: PersonDetection) => {
|
||||
console.log('person', person);
|
||||
setIsReplaceLibraryOpen(true);
|
||||
};
|
||||
|
||||
// 从角色库中选择角色
|
||||
const handleSelectCharacter = (index: number) => {
|
||||
console.log('index', index);
|
||||
setIsReplaceLibraryOpen(false);
|
||||
// 模拟打开替换面板
|
||||
setTimeout(() => {
|
||||
setIsReplacePanelOpen(true);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// 确认替换角色
|
||||
const handleConfirmReplace = (selectedShots: string[], addToLibrary: boolean) => {
|
||||
|
||||
};
|
||||
|
||||
// 如果没有数据,显示空状态
|
||||
if (sketches.length === 0) {
|
||||
return (
|
||||
@ -176,14 +193,14 @@ export function ShotTabContent({
|
||||
<span className="text-xs text-white/90">Shot {index + 1}</span>
|
||||
</div>
|
||||
{/* 鼠标悬浮/移出 显示/隐藏 删除图标 */}
|
||||
<div className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
{/* <div className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<button
|
||||
onClick={() => console.log('Delete sketch')}
|
||||
className="text-red-500"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div> */}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
@ -251,6 +268,7 @@ export function ShotTabContent({
|
||||
onScanTimeout={handleScanTimeout}
|
||||
onScanExit={handleScanTimeout}
|
||||
onDetectionsChange={handleDetectionsChange}
|
||||
onPersonClick={handlePersonClick}
|
||||
/>
|
||||
|
||||
{/* <video
|
||||
@ -324,10 +342,17 @@ export function ShotTabContent({
|
||||
|
||||
{/* 基础配置 */}
|
||||
<div className='space-y-4 col-span-1'>
|
||||
<ShotEditor ref={editorRef} onAddSegment={() => {
|
||||
// 可以在这里添加其他逻辑
|
||||
console.log('分镜添加成功');
|
||||
}} />
|
||||
<ShotEditor
|
||||
ref={editorRef}
|
||||
onAddSegment={() => {
|
||||
// 可以在这里添加其他逻辑
|
||||
console.log('分镜添加成功');
|
||||
}}
|
||||
onCharacterClick={(attrs) => {
|
||||
console.log('attrs', attrs);
|
||||
setIsReplaceLibraryOpen(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 重新生成按钮、新增分镜按钮 */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
@ -357,17 +382,6 @@ export function ShotTabContent({
|
||||
|
||||
</motion.div>
|
||||
|
||||
{/* 替换视频弹窗 */}
|
||||
<ReplaceVideoModal
|
||||
isOpen={isReplaceModalOpen}
|
||||
activeReplaceMethod={activeReplaceMethod}
|
||||
onClose={() => setIsReplaceModalOpen(false)}
|
||||
onVideoSelect={(video) => {
|
||||
console.log('Selected video:', video);
|
||||
setIsReplaceModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Media Properties 弹窗 */}
|
||||
<MediaPropertiesModal
|
||||
isOpen={isMediaPropertiesModalOpen}
|
||||
@ -377,7 +391,25 @@ export function ShotTabContent({
|
||||
onSketchSelect={onSketchSelect}
|
||||
/>
|
||||
|
||||
<FloatingGlassPanel
|
||||
open={isReplacePanelOpen}
|
||||
width='66vw'
|
||||
onClose={() => setIsReplacePanelOpen(false)}
|
||||
>
|
||||
<ReplaceCharacterPanel
|
||||
shots={mockShots}
|
||||
character={mockCharacter}
|
||||
showAddToLibrary={false}
|
||||
onClose={() => setIsReplacePanelOpen(false)}
|
||||
onConfirm={handleConfirmReplace}
|
||||
/>
|
||||
</FloatingGlassPanel>
|
||||
|
||||
<CharacterLibrarySelector
|
||||
isReplaceLibraryOpen={isReplaceLibraryOpen}
|
||||
setIsReplaceLibraryOpen={setIsReplaceLibraryOpen}
|
||||
onSelect={handleSelectCharacter}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user