forked from 77media/video-flow
Merge branch 'dev' of https://git.qikongjian.com/77media/video-flow into dev
This commit is contained in:
commit
7960573a12
@ -9,9 +9,10 @@ type FloatingGlassPanelProps = {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
width?: string;
|
width?: string;
|
||||||
r_key?: string | number;
|
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 = {
|
const bounceAnimation = {
|
||||||
scale: [0.95, 1.02, 0.98, 1],
|
scale: [0.95, 1.02, 0.98, 1],
|
||||||
@ -48,7 +49,7 @@ export default function FloatingGlassPanel({ open, onClose, children, width = '3
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<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"
|
className="rounded-xl backdrop-blur-md bg-white/10 border border-white/20 shadow-xl text-white p-4"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
172
components/ui/HorizontalScroller.tsx
Normal file
172
components/ui/HorizontalScroller.tsx
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import React, {
|
||||||
|
useRef,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useCallback,
|
||||||
|
forwardRef,
|
||||||
|
ReactNode,
|
||||||
|
Ref,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
import gsap from 'gsap'
|
||||||
|
import { Draggable } from 'gsap/Draggable'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
|
||||||
|
gsap.registerPlugin(Draggable)
|
||||||
|
|
||||||
|
export interface HorizontalScrollerProps {
|
||||||
|
children: ReactNode[]
|
||||||
|
itemWidth?: number | 'auto'
|
||||||
|
gap?: number
|
||||||
|
selectedIndex?: number
|
||||||
|
onItemClick?: (index: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HorizontalScrollerRef {
|
||||||
|
scrollToIndex: (index: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const HorizontalScroller = forwardRef(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
children,
|
||||||
|
itemWidth = 300,
|
||||||
|
gap = 16,
|
||||||
|
selectedIndex,
|
||||||
|
onItemClick,
|
||||||
|
}: HorizontalScrollerProps,
|
||||||
|
ref: Ref<HorizontalScrollerRef>
|
||||||
|
) => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const wrapperRef = useRef<HTMLDivElement>(null)
|
||||||
|
const dragInstance = useRef<Draggable | null>(null)
|
||||||
|
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
|
||||||
|
const [currentIndex, setCurrentIndex] = useState(selectedIndex || 0)
|
||||||
|
|
||||||
|
const scrollToIndex = useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
if (!containerRef.current || !wrapperRef.current || index < 0 || index >= children.length) return
|
||||||
|
|
||||||
|
const containerWidth = containerRef.current.offsetWidth
|
||||||
|
let targetX = 0
|
||||||
|
|
||||||
|
if (itemWidth === 'auto') {
|
||||||
|
const itemElement = itemRefs.current[index]
|
||||||
|
if (!itemElement) return
|
||||||
|
|
||||||
|
const itemRect = itemElement.getBoundingClientRect()
|
||||||
|
const wrapperRect = wrapperRef.current.getBoundingClientRect()
|
||||||
|
targetX = -(itemElement.offsetLeft - (containerWidth - itemRect.width) / 2)
|
||||||
|
} else {
|
||||||
|
targetX = -(index * (itemWidth + gap) - (containerWidth - itemWidth) / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxScroll = wrapperRef.current.scrollWidth - containerWidth
|
||||||
|
targetX = Math.max(-maxScroll, Math.min(targetX, 0))
|
||||||
|
|
||||||
|
gsap.to(wrapperRef.current, {
|
||||||
|
x: targetX,
|
||||||
|
duration: 0.6,
|
||||||
|
ease: 'power3.out',
|
||||||
|
onUpdate: () => {
|
||||||
|
if (dragInstance.current?.update) {
|
||||||
|
dragInstance.current.update()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
setCurrentIndex(index)
|
||||||
|
},
|
||||||
|
[itemWidth, gap, children.length]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!wrapperRef.current || !containerRef.current) return
|
||||||
|
|
||||||
|
const maxScroll =
|
||||||
|
wrapperRef.current.scrollWidth - containerRef.current.offsetWidth
|
||||||
|
|
||||||
|
dragInstance.current = Draggable.create(wrapperRef.current, {
|
||||||
|
type: 'x',
|
||||||
|
edgeResistance: 0.85,
|
||||||
|
inertia: true,
|
||||||
|
bounds: {
|
||||||
|
minX: -maxScroll,
|
||||||
|
maxX: 0,
|
||||||
|
},
|
||||||
|
onDrag: function () {
|
||||||
|
gsap.to(wrapperRef.current!, {
|
||||||
|
x: this.x,
|
||||||
|
duration: 0.3,
|
||||||
|
ease: 'power3.out',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})[0]
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof selectedIndex === 'number') {
|
||||||
|
scrollToIndex(selectedIndex)
|
||||||
|
}
|
||||||
|
}, [selectedIndex, scrollToIndex])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'ArrowLeft') {
|
||||||
|
e.preventDefault()
|
||||||
|
let newIndex = Math.max(0, currentIndex - 1)
|
||||||
|
if (newIndex === 0 && currentIndex - 1 < 0) {
|
||||||
|
newIndex = children.length - 1
|
||||||
|
}
|
||||||
|
onItemClick?.(newIndex)
|
||||||
|
} else if (e.key === 'ArrowRight') {
|
||||||
|
e.preventDefault()
|
||||||
|
let newIndex = Math.min(children.length - 1, currentIndex + 1)
|
||||||
|
if (newIndex === children.length - 1 && currentIndex + 1 > children.length - 1) {
|
||||||
|
newIndex = 0
|
||||||
|
}
|
||||||
|
onItemClick?.(newIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown)
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||||
|
}, [currentIndex, children.length, scrollToIndex, onItemClick])
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
scrollToIndex,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="relative w-full overflow-auto touch-none cursor-grab active:cursor-grabbing"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={wrapperRef}
|
||||||
|
className="flex select-none"
|
||||||
|
style={{ gap: `${gap}px` }}
|
||||||
|
>
|
||||||
|
{React.Children.map(children, (child, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
onClick={() => {
|
||||||
|
onItemClick?.(index)
|
||||||
|
}}
|
||||||
|
ref={(el) => (itemRefs.current[index] = el)}
|
||||||
|
style={{
|
||||||
|
minWidth: itemWidth === 'auto' ? 'auto' : itemWidth,
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
className={`p-2 rounded-lg overflow-hidden cursor-pointer hover:scale-[1.03] transition-transform duration-300`}
|
||||||
|
>
|
||||||
|
{child}
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default HorizontalScroller
|
||||||
@ -10,9 +10,17 @@ type ImageBlurTransitionProps = {
|
|||||||
alt?: string;
|
alt?: string;
|
||||||
width?: number | string;
|
width?: number | string;
|
||||||
height?: number | string;
|
height?: number | string;
|
||||||
|
enableAnimation?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ImageBlurTransition({ src, alt = '', width = 480, height = 300, className }: ImageBlurTransitionProps) {
|
export default function ImageBlurTransition({
|
||||||
|
src,
|
||||||
|
alt = '',
|
||||||
|
width = 480,
|
||||||
|
height = 300,
|
||||||
|
className,
|
||||||
|
enableAnimation = true
|
||||||
|
}: ImageBlurTransitionProps) {
|
||||||
const [current, setCurrent] = useState(src);
|
const [current, setCurrent] = useState(src);
|
||||||
const [isFlipping, setIsFlipping] = useState(false);
|
const [isFlipping, setIsFlipping] = useState(false);
|
||||||
|
|
||||||
@ -33,36 +41,44 @@ export default function ImageBlurTransition({ src, alt = '', width = 480, height
|
|||||||
style={{
|
style={{
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
perspective: 1000, // 关键:提供 3D 深度
|
perspective: enableAnimation ? 1000 : 'none', // 只在启用动画时提供 3D 深度
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AnimatePresence mode="wait">
|
{enableAnimation ? (
|
||||||
<motion.img
|
<AnimatePresence mode="wait">
|
||||||
key={current}
|
<motion.img
|
||||||
src={current}
|
key={current}
|
||||||
|
src={current}
|
||||||
|
alt={alt}
|
||||||
|
className="absolute w-full h-auto object-cover rounded-xl"
|
||||||
|
initial={{
|
||||||
|
opacity: 0,
|
||||||
|
filter: 'blur(8px)',
|
||||||
|
scale: 1.02,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
opacity: 1,
|
||||||
|
filter: 'blur(0px)',
|
||||||
|
scale: 1,
|
||||||
|
}}
|
||||||
|
exit={{
|
||||||
|
opacity: 0,
|
||||||
|
filter: 'blur(4px)',
|
||||||
|
scale: 0.98,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 0.3,
|
||||||
|
ease: 'easeInOut',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</AnimatePresence>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
className="absolute w-full h-auto object-cover rounded-xl"
|
className="absolute w-full h-auto object-cover rounded-xl"
|
||||||
initial={{
|
|
||||||
opacity: 0,
|
|
||||||
filter: 'blur(8px)',
|
|
||||||
scale: 1.02,
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
opacity: 1,
|
|
||||||
filter: 'blur(0px)',
|
|
||||||
scale: 1,
|
|
||||||
}}
|
|
||||||
exit={{
|
|
||||||
opacity: 0,
|
|
||||||
filter: 'blur(4px)',
|
|
||||||
scale: 0.98,
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
duration: 0.3,
|
|
||||||
ease: 'easeInOut',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</AnimatePresence>
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { GlassIconButton } from '@/components/ui/glass-icon-button';
|
||||||
|
import { Check } from 'lucide-react';
|
||||||
|
|
||||||
interface ImageWaveProps {
|
interface ImageWaveProps {
|
||||||
// 图片列表数据
|
// 图片列表数据
|
||||||
@ -152,8 +154,6 @@ export const ImageWave: React.FC<ImageWaveProps> = ({
|
|||||||
setCurrentExpandedItem(null);
|
setCurrentExpandedItem(null);
|
||||||
} else {
|
} else {
|
||||||
setCurrentExpandedItem(index);
|
setCurrentExpandedItem(index);
|
||||||
setCurrentSelectedIndex(index);
|
|
||||||
onClick?.(index);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -188,6 +188,11 @@ export const ImageWave: React.FC<ImageWaveProps> = ({
|
|||||||
};
|
};
|
||||||
}, [autoAnimate]);
|
}, [autoAnimate]);
|
||||||
|
|
||||||
|
const handleSelectImage = (index: number) => {
|
||||||
|
setCurrentSelectedIndex(index);
|
||||||
|
onClick?.(index);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper width={containerWidth} height={containerHeight}>
|
<Wrapper width={containerWidth} height={containerHeight}>
|
||||||
<Items ref={itemsRef} gap={gap} className={currentExpandedItem !== null ? 'has-expanded' : ''}>
|
<Items ref={itemsRef} gap={gap} className={currentExpandedItem !== null ? 'has-expanded' : ''}>
|
||||||
@ -196,11 +201,19 @@ export const ImageWave: React.FC<ImageWaveProps> = ({
|
|||||||
key={index}
|
key={index}
|
||||||
width={itemWidth}
|
width={itemWidth}
|
||||||
height={itemHeight}
|
height={itemHeight}
|
||||||
className={`item ${currentExpandedItem === index ? 'expanded' : ''} ${currentSelectedIndex === index ? 'selected' : ''}`}
|
className={`group relative item ${currentExpandedItem === index ? 'expanded' : ''} ${currentSelectedIndex === index ? 'selected' : ''}`}
|
||||||
style={{ backgroundImage: `url(${image})` }}
|
style={{ backgroundImage: `url(${image})` }}
|
||||||
onClick={() => handleItemClick(index)}
|
onClick={() => handleItemClick(index)}
|
||||||
tabIndex={0}
|
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>
|
</Items>
|
||||||
</Wrapper>
|
</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,12 @@ import React, { useState, useRef } from 'react';
|
|||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { Upload, Library, Play, Pause, RefreshCw, Wand2, Users, Check, ReplaceAll, X } from 'lucide-react';
|
import { Upload, Library, Play, Pause, RefreshCw, Wand2, Users, Check, ReplaceAll, X } from 'lucide-react';
|
||||||
import { cn } from '@/public/lib/utils';
|
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 CharacterEditor from './character-editor';
|
||||||
import ImageBlurTransition from './ImageBlurTransition';
|
import ImageBlurTransition from './ImageBlurTransition';
|
||||||
import FloatingGlassPanel from './FloatingGlassPanel';
|
import FloatingGlassPanel from './FloatingGlassPanel';
|
||||||
import { ReplaceCharacterPanel, mockShots, mockCharacter } from './replace-character-panel';
|
import { ReplaceCharacterPanel, mockShots, mockCharacter } from './replace-character-panel';
|
||||||
import { ImageWave } from '@/components/ui/ImageWave';
|
import { CharacterLibrarySelector } from './character-library-selector';
|
||||||
|
import HorizontalScroller from './HorizontalScroller';
|
||||||
|
|
||||||
interface Appearance {
|
interface Appearance {
|
||||||
hairStyle: string;
|
hairStyle: string;
|
||||||
@ -58,47 +56,25 @@ interface CharacterTabContentProps {
|
|||||||
roles: Role[];
|
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({
|
export function CharacterTabContent({
|
||||||
taskSketch,
|
taskSketch,
|
||||||
currentRoleIndex,
|
currentRoleIndex,
|
||||||
onSketchSelect,
|
onSketchSelect,
|
||||||
roles = [mockRole]
|
roles = [mockRole]
|
||||||
}: CharacterTabContentProps) {
|
}: CharacterTabContentProps) {
|
||||||
const [isReplaceModalOpen, setIsReplaceModalOpen] = useState(false);
|
|
||||||
const [activeReplaceMethod, setActiveReplaceMethod] = useState('upload');
|
|
||||||
const [newTag, setNewTag] = useState('');
|
|
||||||
const [localRole, setLocalRole] = useState(mockRole);
|
const [localRole, setLocalRole] = useState(mockRole);
|
||||||
const [currentRole, setCurrentRole] = useState(roles[currentRoleIndex]);
|
const [currentRole, setCurrentRole] = useState(roles[currentRoleIndex]);
|
||||||
const [isReplacePanelOpen, setIsReplacePanelOpen] = useState(false);
|
const [isReplacePanelOpen, setIsReplacePanelOpen] = useState(false);
|
||||||
const [replacePanelKey, setReplacePanelKey] = useState(0);
|
const [replacePanelKey, setReplacePanelKey] = useState(0);
|
||||||
const [ignoreReplace, setIgnoreReplace] = useState(false);
|
const [ignoreReplace, setIgnoreReplace] = useState(false);
|
||||||
const [isReplaceLibraryOpen, setIsReplaceLibraryOpen] = useState(false);
|
const [isReplaceLibraryOpen, setIsReplaceLibraryOpen] = useState(false);
|
||||||
const [replaceLibraryKey, setReplaceLibraryKey] = useState(0);
|
|
||||||
|
const [selectRoleIndex, setSelectRoleIndex] = useState(0);
|
||||||
|
const [enableAnimation, setEnableAnimation] = useState(true);
|
||||||
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
||||||
|
|
||||||
const handleReplaceCharacter = (url: string) => {
|
const handleReplaceCharacter = (url: string) => {
|
||||||
|
setEnableAnimation(true);
|
||||||
setCurrentRole({
|
setCurrentRole({
|
||||||
...currentRole,
|
...currentRole,
|
||||||
url: url
|
url: url
|
||||||
@ -114,13 +90,14 @@ export function CharacterTabContent({
|
|||||||
setIsReplacePanelOpen(false);
|
setIsReplacePanelOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 取消替换
|
||||||
const handleCloseReplacePanel = () => {
|
const handleCloseReplacePanel = () => {
|
||||||
setIsReplacePanelOpen(false);
|
setIsReplacePanelOpen(false);
|
||||||
setIgnoreReplace(true);
|
setIgnoreReplace(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangeRole = (index: number) => {
|
const handleChangeRole = (index: number) => {
|
||||||
if (currentRole.url !== roles[currentRoleIndex].url && !ignoreReplace) {
|
if (currentRole.url !== roles[selectRoleIndex].url && !ignoreReplace) {
|
||||||
// 提示 角色已修改,弹出替换角色面板
|
// 提示 角色已修改,弹出替换角色面板
|
||||||
if (isReplacePanelOpen) {
|
if (isReplacePanelOpen) {
|
||||||
setReplacePanelKey(replacePanelKey + 1);
|
setReplacePanelKey(replacePanelKey + 1);
|
||||||
@ -129,10 +106,21 @@ export function CharacterTabContent({
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onSketchSelect(index);
|
// 重置替换规则
|
||||||
|
setEnableAnimation(false);
|
||||||
|
setIgnoreReplace(false);
|
||||||
|
|
||||||
|
setSelectRoleIndex(index);
|
||||||
setCurrentRole(roles[index]);
|
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) {
|
if (!roles || roles.length === 0) {
|
||||||
return (
|
return (
|
||||||
@ -152,16 +140,20 @@ export function CharacterTabContent({
|
|||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="flex gap-4 overflow-x-auto p-2 hide-scrollbar">
|
<HorizontalScroller
|
||||||
|
itemWidth={96}
|
||||||
|
gap={0}
|
||||||
|
selectedIndex={selectRoleIndex}
|
||||||
|
onItemClick={(i: number) => handleChangeRole(i)}
|
||||||
|
>
|
||||||
{roles.map((role, index) => (
|
{roles.map((role, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={`role-${index}`}
|
key={`role-${index}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex-shrink-0 w-24 rounded-lg overflow-hidden cursor-pointer',
|
'relative flex-shrink-0 w-24 rounded-lg overflow-hidden cursor-pointer',
|
||||||
'aspect-[9/16]',
|
'aspect-[9/16]',
|
||||||
currentRoleIndex === index ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
|
selectRoleIndex === index ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
|
||||||
)}
|
)}
|
||||||
onClick={() => handleChangeRole(index)}
|
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
>
|
>
|
||||||
@ -175,13 +167,13 @@ export function CharacterTabContent({
|
|||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</HorizontalScroller>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* 下部分:角色详情 */}
|
{/* 下部分:角色详情 */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="grid grid-cols-2 gap-6 p-4"
|
className="grid grid-cols-2 gap-6"
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.2 }}
|
transition={{ delay: 0.2 }}
|
||||||
@ -196,6 +188,7 @@ export function CharacterTabContent({
|
|||||||
alt={currentRole.name}
|
alt={currentRole.name}
|
||||||
width='100%'
|
width='100%'
|
||||||
height='auto'
|
height='auto'
|
||||||
|
enableAnimation={enableAnimation}
|
||||||
/>
|
/>
|
||||||
{/* 应用角色按钮 */}
|
{/* 应用角色按钮 */}
|
||||||
<div className='absolute top-3 right-3 flex gap-2'>
|
<div className='absolute top-3 right-3 flex gap-2'>
|
||||||
@ -260,7 +253,7 @@ export function CharacterTabContent({
|
|||||||
{/* 重新生成按钮、替换形象按钮 */}
|
{/* 重新生成按钮、替换形象按钮 */}
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<motion.button
|
<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
|
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"
|
text-pink-500 rounded-lg transition-colors"
|
||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
@ -287,7 +280,12 @@ export function CharacterTabContent({
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
|
||||||
<FloatingGlassPanel open={isReplacePanelOpen} width='500px' r_key={replacePanelKey}>
|
<FloatingGlassPanel
|
||||||
|
open={isReplacePanelOpen}
|
||||||
|
width='500px'
|
||||||
|
r_key={replacePanelKey}
|
||||||
|
onClose={() => handleCloseReplacePanel()}
|
||||||
|
>
|
||||||
<ReplaceCharacterPanel
|
<ReplaceCharacterPanel
|
||||||
shots={mockShots}
|
shots={mockShots}
|
||||||
character={mockCharacter}
|
character={mockCharacter}
|
||||||
@ -297,41 +295,11 @@ export function CharacterTabContent({
|
|||||||
</FloatingGlassPanel>
|
</FloatingGlassPanel>
|
||||||
|
|
||||||
{/* 从角色库中选择角色 */}
|
{/* 从角色库中选择角色 */}
|
||||||
<FloatingGlassPanel open={isReplaceLibraryOpen} width='90vw' r_key={replaceLibraryKey}>
|
<CharacterLibrarySelector
|
||||||
{/* 标题 从角色库中选择角色 */}
|
isReplaceLibraryOpen={isReplaceLibraryOpen}
|
||||||
<div className="text-2xl font-semibold text-white text-center">Role Library</div>
|
setIsReplaceLibraryOpen={setIsReplaceLibraryOpen}
|
||||||
{/* 内容 */}
|
onSelect={handleSelectCharacter}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -235,7 +235,7 @@ export function EditModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 底部操作栏 */}
|
{/* 底部操作栏 */}
|
||||||
{/* <div className="p-4 border-t border-white/10 bg-black/20">
|
<div className="p-4 border-t border-white/10 bg-black/20">
|
||||||
<div className="flex justify-end gap-3">
|
<div className="flex justify-end gap-3">
|
||||||
<motion.button
|
<motion.button
|
||||||
className="px-4 py-2 rounded-lg bg-white/10 text-white hover:bg-white/20 transition-colors"
|
className="px-4 py-2 rounded-lg bg-white/10 text-white hover:bg-white/20 transition-colors"
|
||||||
@ -253,7 +253,7 @@ export function EditModal({
|
|||||||
Save
|
Save
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
</div> */}
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -51,6 +51,7 @@ type Props = {
|
|||||||
scanTimeout?: number;
|
scanTimeout?: number;
|
||||||
isScanFailed?: boolean; // 外部传入的失败状态
|
isScanFailed?: boolean; // 外部传入的失败状态
|
||||||
onDetectionsChange?: (detections: PersonDetection[]) => void;
|
onDetectionsChange?: (detections: PersonDetection[]) => void;
|
||||||
|
onPersonClick?: (person: PersonDetection) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PersonDetectionScene: React.FC<Props> = ({
|
export const PersonDetectionScene: React.FC<Props> = ({
|
||||||
@ -63,7 +64,8 @@ export const PersonDetectionScene: React.FC<Props> = ({
|
|||||||
onScanExit,
|
onScanExit,
|
||||||
scanTimeout = 10000,
|
scanTimeout = 10000,
|
||||||
isScanFailed = false,
|
isScanFailed = false,
|
||||||
onDetectionsChange
|
onDetectionsChange,
|
||||||
|
onPersonClick
|
||||||
}) => {
|
}) => {
|
||||||
const scanControls = useAnimation();
|
const scanControls = useAnimation();
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
@ -374,7 +376,9 @@ export const PersonDetectionScene: React.FC<Props> = ({
|
|||||||
{detections.map((person, index) => {
|
{detections.map((person, index) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={person.id}>
|
<div key={person.id} className="cursor-pointer" onClick={() => {
|
||||||
|
onPersonClick?.(person);
|
||||||
|
}}>
|
||||||
<PersonBox person={person} />
|
<PersonBox person={person} />
|
||||||
<motion.div
|
<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"
|
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}
|
{person.name}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</React.Fragment>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { Shot, Character } from '@/app/model/types';
|
|||||||
interface ReplaceCharacterPanelProps {
|
interface ReplaceCharacterPanelProps {
|
||||||
shots: Shot[];
|
shots: Shot[];
|
||||||
character: Character;
|
character: Character;
|
||||||
|
showAddToLibrary?: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onConfirm: (selectedShots: string[], addToLibrary: boolean) => void;
|
onConfirm: (selectedShots: string[], addToLibrary: boolean) => void;
|
||||||
}
|
}
|
||||||
@ -47,6 +48,7 @@ export const mockCharacter: Character = {
|
|||||||
export function ReplaceCharacterPanel({
|
export function ReplaceCharacterPanel({
|
||||||
shots = mockShots,
|
shots = mockShots,
|
||||||
character = mockCharacter,
|
character = mockCharacter,
|
||||||
|
showAddToLibrary = true,
|
||||||
onClose,
|
onClose,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
}: ReplaceCharacterPanelProps) {
|
}: ReplaceCharacterPanelProps) {
|
||||||
@ -55,7 +57,7 @@ export function ReplaceCharacterPanel({
|
|||||||
title="替换新形象"
|
title="替换新形象"
|
||||||
shots={shots}
|
shots={shots}
|
||||||
item={character}
|
item={character}
|
||||||
showAddToLibrary={true}
|
showAddToLibrary={showAddToLibrary}
|
||||||
addToLibraryText="新形象同步添加至角色库"
|
addToLibraryText="新形象同步添加至角色库"
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onConfirm={onConfirm}
|
onConfirm={onConfirm}
|
||||||
|
|||||||
@ -100,14 +100,14 @@ export function ReplacePanel({
|
|||||||
{/* 分镜展示区 */}
|
{/* 分镜展示区 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="text-white/80 text-sm">选择需要替换的分镜:</div>
|
<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) => (
|
{shots.map((shot) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={shot.id}
|
key={shot.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex-shrink-0 rounded-lg overflow-hidden cursor-pointer',
|
'relative flex-shrink-0 rounded-lg overflow-hidden cursor-pointer',
|
||||||
'aspect-video border-2',
|
'aspect-video border-2',
|
||||||
hoveredVideoId === shot.id ? 'w-64' : 'w-32',
|
hoveredVideoId === shot.id ? 'w-auto' : 'w-32',
|
||||||
selectedShots.includes(shot.id)
|
selectedShots.includes(shot.id)
|
||||||
? 'border-blue-500'
|
? 'border-blue-500'
|
||||||
: 'border-transparent hover:border-blue-500/50'
|
: 'border-transparent hover:border-blue-500/50'
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { cn } from '@/public/lib/utils';
|
|||||||
import SceneEditor from './scene-editor';
|
import SceneEditor from './scene-editor';
|
||||||
import FloatingGlassPanel from './FloatingGlassPanel';
|
import FloatingGlassPanel from './FloatingGlassPanel';
|
||||||
import { ReplaceScenePanel, mockShots } from './replace-scene-panel';
|
import { ReplaceScenePanel, mockShots } from './replace-scene-panel';
|
||||||
|
import HorizontalScroller from './HorizontalScroller';
|
||||||
|
|
||||||
interface SceneEnvironment {
|
interface SceneEnvironment {
|
||||||
time: {
|
time: {
|
||||||
@ -195,45 +196,44 @@ export function SceneTabContent({
|
|||||||
>
|
>
|
||||||
{/* 分镜缩略图行 */}
|
{/* 分镜缩略图行 */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
<HorizontalScroller
|
||||||
ref={thumbnailsRef}
|
itemWidth={128}
|
||||||
className="relative flex gap-4 overflow-x-auto p-2 hide-scrollbar"
|
gap={0}
|
||||||
|
selectedIndex={currentSketchIndex}
|
||||||
|
onItemClick={(i: number) => handleChangeScene(i)}
|
||||||
>
|
>
|
||||||
<div className="flex gap-4 min-w-fit">
|
{sketches.map((sketch, index) => (
|
||||||
{sketches.map((sketch, index) => (
|
<motion.div
|
||||||
<motion.div
|
key={sketch.id || index}
|
||||||
key={sketch.id || index}
|
className={cn(
|
||||||
className={cn(
|
'relative flex-shrink-0 w-32 aspect-video rounded-lg overflow-hidden cursor-pointer group',
|
||||||
'relative flex-shrink-0 w-32 aspect-video rounded-lg overflow-hidden cursor-pointer group',
|
currentSketchIndex === index ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
|
||||||
currentSketchIndex === index ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
|
)}
|
||||||
)}
|
whileHover={{ scale: 1.05 }}
|
||||||
onClick={() => handleChangeScene(index)}
|
whileTap={{ scale: 0.95 }}
|
||||||
whileHover={{ scale: 1.05 }}
|
>
|
||||||
whileTap={{ scale: 0.95 }}
|
<img
|
||||||
>
|
src={sketch.url}
|
||||||
<img
|
alt={`Sketch ${index + 1}`}
|
||||||
src={sketch.url}
|
className="w-full h-full object-cover"
|
||||||
alt={`Sketch ${index + 1}`}
|
/>
|
||||||
className="w-full h-full object-cover"
|
<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">Scene {index + 1}</span>
|
||||||
<div className="absolute bottom-0 left-0 right-0 p-1 bg-gradient-to-t from-black/60 to-transparent">
|
</div>
|
||||||
<span className="text-xs text-white/90">Scene {index + 1}</span>
|
{/* 鼠标悬浮/移出 显示/隐藏 删除图标 */}
|
||||||
</div>
|
{/* <div className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||||
{/* 鼠标悬浮/移出 显示/隐藏 删除图标 */}
|
<button
|
||||||
<div className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
onClick={(e) => {
|
||||||
<button
|
e.stopPropagation();
|
||||||
onClick={(e) => {
|
console.log('Delete sketch');
|
||||||
e.stopPropagation();
|
}}
|
||||||
console.log('Delete sketch');
|
className="text-red-500"
|
||||||
}}
|
>
|
||||||
className="text-red-500"
|
<Trash2 className="w-4 h-4" />
|
||||||
>
|
</button>
|
||||||
<Trash2 className="w-4 h-4" />
|
</div> */}
|
||||||
</button>
|
</motion.div>
|
||||||
</div>
|
))}
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{/* 新增占位符 */}
|
{/* 新增占位符 */}
|
||||||
{/* <motion.div
|
{/* <motion.div
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -251,14 +251,16 @@ export function SceneTabContent({
|
|||||||
<span className="text-xs">添加场景</span>
|
<span className="text-xs">添加场景</span>
|
||||||
</div>
|
</div>
|
||||||
</motion.div> */}
|
</motion.div> */}
|
||||||
</div>
|
</HorizontalScroller>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 脚本预览行 - 单行滚动 */}
|
{/* 脚本预览行 - 单行滚动 */}
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<div
|
<HorizontalScroller
|
||||||
ref={scriptsRef}
|
itemWidth={'auto'}
|
||||||
className="flex overflow-x-auto hide-scrollbar py-2 gap-1"
|
gap={0}
|
||||||
|
selectedIndex={currentSketchIndex}
|
||||||
|
onItemClick={(i: number) => handleChangeScene(i)}
|
||||||
>
|
>
|
||||||
{sketches.map((script, index) => {
|
{sketches.map((script, index) => {
|
||||||
const isActive = currentSketchIndex === index;
|
const isActive = currentSketchIndex === index;
|
||||||
@ -269,7 +271,6 @@ export function SceneTabContent({
|
|||||||
'flex-shrink-0 cursor-pointer transition-all duration-300',
|
'flex-shrink-0 cursor-pointer transition-all duration-300',
|
||||||
isActive ? 'text-white' : 'text-white/50 hover:text-white/80'
|
isActive ? 'text-white' : 'text-white/50 hover:text-white/80'
|
||||||
)}
|
)}
|
||||||
onClick={() => handleChangeScene(index)}
|
|
||||||
initial={false}
|
initial={false}
|
||||||
animate={{
|
animate={{
|
||||||
scale: isActive ? 1.02 : 1,
|
scale: isActive ? 1.02 : 1,
|
||||||
@ -286,7 +287,7 @@ export function SceneTabContent({
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</HorizontalScroller>
|
||||||
|
|
||||||
{/* 渐变遮罩 */}
|
{/* 渐变遮罩 */}
|
||||||
<div className="absolute left-0 top-0 bottom-0 w-8 bg-gradient-to-r from-[#1a1b1e] to-transparent pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity" />
|
<div className="absolute left-0 top-0 bottom-0 w-8 bg-gradient-to-r from-[#1a1b1e] to-transparent pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||||
@ -330,7 +331,7 @@ export function SceneTabContent({
|
|||||||
{/* 重新生成按钮、替换形象按钮 */}
|
{/* 重新生成按钮、替换形象按钮 */}
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<motion.button
|
<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
|
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"
|
text-pink-500 rounded-lg transition-colors"
|
||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
|
|||||||
@ -11,13 +11,18 @@ interface CharacterAttributes {
|
|||||||
age: string;
|
age: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// interface CharacterTokenProps extends ReactNodeViewProps {
|
||||||
|
// onClick?: (attrs: CharacterAttributes) => void
|
||||||
|
// }
|
||||||
|
|
||||||
export function CharacterToken(props: ReactNodeViewProps) {
|
export function CharacterToken(props: ReactNodeViewProps) {
|
||||||
const [showCard, setShowCard] = useState(false)
|
const [showCard, setShowCard] = useState(false)
|
||||||
const { name, avatar, gender, age } = props.node.attrs as CharacterAttributes
|
const { name, avatar, gender, age } = props.node.attrs as CharacterAttributes
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
console.log('点击角色:', name)
|
console.log('点击角色:', name)
|
||||||
alert(`点击角色:${name}`)
|
const { editor } = props;
|
||||||
|
editor?.emit('character-clicked', props.node.attrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -38,7 +43,7 @@ export function CharacterToken(props: ReactNodeViewProps) {
|
|||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, y: 4 }}
|
exit={{ opacity: 0, y: 4 }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
className="absolute top-full left-0 mt-2 w-64 rounded-lg bg-gray-900 border border-gray-800 shadow-2xl p-4 z-50"
|
className="absolute top-full left-0 mt-2 w-64 rounded-lg backdrop-blur-md bg-white/10 border border-white/20 p-4 z-50"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<img
|
<img
|
||||||
@ -81,6 +86,12 @@ export const CharacterTokenExtension = Node.create({
|
|||||||
return ['character-token', mergeAttributes(HTMLAttributes)];
|
return ['character-token', mergeAttributes(HTMLAttributes)];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// addStorage() {
|
||||||
|
// return {
|
||||||
|
// onClickCharacter: null as null | ((character: CharacterAttributes) => void),
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
|
||||||
addNodeView() {
|
addNodeView() {
|
||||||
return ReactNodeViewRenderer(CharacterToken);
|
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 { EditorContent, useEditor } from '@tiptap/react';
|
||||||
import StarterKit from '@tiptap/starter-kit';
|
import StarterKit from '@tiptap/starter-kit';
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
@ -36,9 +36,10 @@ const initialContent = {
|
|||||||
|
|
||||||
interface ShotEditorProps {
|
interface ShotEditorProps {
|
||||||
onAddSegment?: () => void;
|
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 [segments, setSegments] = useState(initialContent.content);
|
||||||
|
|
||||||
const editor = useEditor({
|
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 = () => {
|
const addSegment = () => {
|
||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,10 @@ import { MediaPropertiesModal } from './media-properties-modal';
|
|||||||
import { DramaLineChart } from './drama-line-chart';
|
import { DramaLineChart } from './drama-line-chart';
|
||||||
import { PersonDetection, PersonDetectionScene } from './person-detection';
|
import { PersonDetection, PersonDetectionScene } from './person-detection';
|
||||||
import ShotEditor from './shot-editor/ShotEditor';
|
import ShotEditor from './shot-editor/ShotEditor';
|
||||||
|
import { CharacterLibrarySelector } from './character-library-selector';
|
||||||
|
import FloatingGlassPanel from './FloatingGlassPanel';
|
||||||
|
import { ReplaceCharacterPanel, mockShots, mockCharacter } from './replace-character-panel';
|
||||||
|
import HorizontalScroller from './HorizontalScroller';
|
||||||
|
|
||||||
interface ShotTabContentProps {
|
interface ShotTabContentProps {
|
||||||
taskSketch: any[];
|
taskSketch: any[];
|
||||||
@ -24,21 +28,19 @@ export function ShotTabContent({
|
|||||||
onSketchSelect,
|
onSketchSelect,
|
||||||
isPlaying: externalIsPlaying = true
|
isPlaying: externalIsPlaying = true
|
||||||
}: ShotTabContentProps) {
|
}: ShotTabContentProps) {
|
||||||
const thumbnailsRef = useRef<HTMLDivElement>(null);
|
|
||||||
const editorRef = useRef<any>(null);
|
const editorRef = useRef<any>(null);
|
||||||
const videosRef = useRef<HTMLDivElement>(null);
|
|
||||||
const videoPlayerRef = useRef<HTMLVideoElement>(null);
|
const videoPlayerRef = useRef<HTMLVideoElement>(null);
|
||||||
const [isPlaying, setIsPlaying] = React.useState(externalIsPlaying);
|
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 [isMediaPropertiesModalOpen, setIsMediaPropertiesModalOpen] = React.useState(false);
|
||||||
|
|
||||||
const [triggerScan, setTriggerScan] = useState(false);
|
|
||||||
const [detections, setDetections] = useState<PersonDetection[]>([]);
|
const [detections, setDetections] = useState<PersonDetection[]>([]);
|
||||||
const [scanState, setScanState] = useState<'idle' | 'scanning' | 'detected'>('idle');
|
const [scanState, setScanState] = useState<'idle' | 'scanning' | 'detected'>('idle');
|
||||||
|
|
||||||
|
const [isReplaceLibraryOpen, setIsReplaceLibraryOpen] = useState(false);
|
||||||
|
const [isReplacePanelOpen, setIsReplacePanelOpen] = useState(false);
|
||||||
|
|
||||||
|
const [shots, setShots] = useState<any[]>([]);
|
||||||
|
|
||||||
|
|
||||||
// 监听外部播放状态变化
|
// 监听外部播放状态变化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -48,31 +50,6 @@ export function ShotTabContent({
|
|||||||
// 确保 taskSketch 是数组
|
// 确保 taskSketch 是数组
|
||||||
const sketches = Array.isArray(taskSketch) ? taskSketch : [];
|
const sketches = Array.isArray(taskSketch) ? taskSketch : [];
|
||||||
|
|
||||||
// 自动滚动到选中项
|
|
||||||
useEffect(() => {
|
|
||||||
if (thumbnailsRef.current && videosRef.current) {
|
|
||||||
const thumbnailContainer = thumbnailsRef.current;
|
|
||||||
const videoContainer = videosRef.current;
|
|
||||||
|
|
||||||
const thumbnailWidth = thumbnailContainer.children[0]?.clientWidth ?? 0;
|
|
||||||
const thumbnailGap = 16; // gap-4 = 16px
|
|
||||||
const thumbnailScrollPosition = (thumbnailWidth + thumbnailGap) * currentSketchIndex;
|
|
||||||
|
|
||||||
const videoElement = videoContainer.children[currentSketchIndex] as HTMLElement;
|
|
||||||
const videoScrollPosition = videoElement?.offsetLeft ?? 0;
|
|
||||||
|
|
||||||
thumbnailContainer.scrollTo({
|
|
||||||
left: thumbnailScrollPosition - thumbnailContainer.clientWidth / 2 + thumbnailWidth / 2,
|
|
||||||
behavior: 'smooth'
|
|
||||||
});
|
|
||||||
|
|
||||||
videoContainer.scrollTo({
|
|
||||||
left: videoScrollPosition - videoContainer.clientWidth / 2 + videoElement?.clientWidth / 2,
|
|
||||||
behavior: 'smooth'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [currentSketchIndex]);
|
|
||||||
|
|
||||||
// 视频播放控制
|
// 视频播放控制
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (videoPlayerRef.current) {
|
if (videoPlayerRef.current) {
|
||||||
@ -87,13 +64,6 @@ export function ShotTabContent({
|
|||||||
}
|
}
|
||||||
}, [isPlaying, currentSketchIndex]);
|
}, [isPlaying, currentSketchIndex]);
|
||||||
|
|
||||||
// 更新进度条
|
|
||||||
const handleTimeUpdate = () => {
|
|
||||||
if (videoPlayerRef.current) {
|
|
||||||
const progress = (videoPlayerRef.current.currentTime / videoPlayerRef.current.duration) * 100;
|
|
||||||
setProgress(progress);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理扫描开始
|
// 处理扫描开始
|
||||||
const handleScan = () => {
|
const handleScan = () => {
|
||||||
@ -130,6 +100,33 @@ 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) => {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换选择分镜
|
||||||
|
const handleSelectShot = (index: number) => {
|
||||||
|
// 切换前 判断数据是否发生变化
|
||||||
|
onSketchSelect(index);
|
||||||
|
};
|
||||||
|
|
||||||
// 如果没有数据,显示空状态
|
// 如果没有数据,显示空状态
|
||||||
if (sketches.length === 0) {
|
if (sketches.length === 0) {
|
||||||
return (
|
return (
|
||||||
@ -148,9 +145,11 @@ export function ShotTabContent({
|
|||||||
>
|
>
|
||||||
{/* 分镜缩略图行 */}
|
{/* 分镜缩略图行 */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
<HorizontalScroller
|
||||||
ref={thumbnailsRef}
|
itemWidth={128}
|
||||||
className="flex gap-4 overflow-x-auto p-2 hide-scrollbar"
|
gap={0}
|
||||||
|
selectedIndex={currentSketchIndex}
|
||||||
|
onItemClick={(i: number) => handleSelectShot(i)}
|
||||||
>
|
>
|
||||||
{sketches.map((sketch, index) => (
|
{sketches.map((sketch, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
@ -159,9 +158,8 @@ export function ShotTabContent({
|
|||||||
'relative flex-shrink-0 w-32 aspect-video rounded-lg overflow-hidden cursor-pointer group',
|
'relative flex-shrink-0 w-32 aspect-video rounded-lg overflow-hidden cursor-pointer group',
|
||||||
currentSketchIndex === index ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
|
currentSketchIndex === index ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
|
||||||
)}
|
)}
|
||||||
onClick={() => onSketchSelect(index)}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileHover={{ scale: 1.05 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
>
|
>
|
||||||
<video
|
<video
|
||||||
src={sketch.url}
|
src={sketch.url}
|
||||||
@ -176,24 +174,26 @@ export function ShotTabContent({
|
|||||||
<span className="text-xs text-white/90">Shot {index + 1}</span>
|
<span className="text-xs text-white/90">Shot {index + 1}</span>
|
||||||
</div>
|
</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
|
<button
|
||||||
onClick={() => console.log('Delete sketch')}
|
onClick={() => console.log('Delete sketch')}
|
||||||
className="text-red-500"
|
className="text-red-500"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div> */}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</HorizontalScroller>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 视频描述行 - 单行滚动 */}
|
{/* 视频描述行 - 单行滚动 */}
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<div
|
<HorizontalScroller
|
||||||
ref={videosRef}
|
itemWidth={'auto'}
|
||||||
className="flex overflow-x-auto hide-scrollbar py-2 gap-1"
|
gap={0}
|
||||||
|
selectedIndex={currentSketchIndex}
|
||||||
|
onItemClick={(i: number) => handleSelectShot(i)}
|
||||||
>
|
>
|
||||||
{sketches.map((video, index) => {
|
{sketches.map((video, index) => {
|
||||||
const isActive = currentSketchIndex === index;
|
const isActive = currentSketchIndex === index;
|
||||||
@ -204,7 +204,6 @@ export function ShotTabContent({
|
|||||||
'flex-shrink-0 cursor-pointer transition-all duration-300',
|
'flex-shrink-0 cursor-pointer transition-all duration-300',
|
||||||
isActive ? 'text-white' : 'text-white/50 hover:text-white/80'
|
isActive ? 'text-white' : 'text-white/50 hover:text-white/80'
|
||||||
)}
|
)}
|
||||||
onClick={() => onSketchSelect(index)}
|
|
||||||
initial={false}
|
initial={false}
|
||||||
animate={{
|
animate={{
|
||||||
scale: isActive ? 1.02 : 1,
|
scale: isActive ? 1.02 : 1,
|
||||||
@ -221,7 +220,7 @@ export function ShotTabContent({
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</HorizontalScroller>
|
||||||
|
|
||||||
{/* 渐变遮罩 */}
|
{/* 渐变遮罩 */}
|
||||||
<div className="absolute left-0 top-0 bottom-0 w-8 bg-gradient-to-r from-[#1a1b1e] to-transparent pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity" />
|
<div className="absolute left-0 top-0 bottom-0 w-8 bg-gradient-to-r from-[#1a1b1e] to-transparent pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||||
@ -251,6 +250,7 @@ export function ShotTabContent({
|
|||||||
onScanTimeout={handleScanTimeout}
|
onScanTimeout={handleScanTimeout}
|
||||||
onScanExit={handleScanTimeout}
|
onScanExit={handleScanTimeout}
|
||||||
onDetectionsChange={handleDetectionsChange}
|
onDetectionsChange={handleDetectionsChange}
|
||||||
|
onPersonClick={handlePersonClick}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* <video
|
{/* <video
|
||||||
@ -324,10 +324,17 @@ export function ShotTabContent({
|
|||||||
|
|
||||||
{/* 基础配置 */}
|
{/* 基础配置 */}
|
||||||
<div className='space-y-4 col-span-1'>
|
<div className='space-y-4 col-span-1'>
|
||||||
<ShotEditor ref={editorRef} onAddSegment={() => {
|
<ShotEditor
|
||||||
// 可以在这里添加其他逻辑
|
ref={editorRef}
|
||||||
console.log('分镜添加成功');
|
onAddSegment={() => {
|
||||||
}} />
|
// 可以在这里添加其他逻辑
|
||||||
|
console.log('分镜添加成功');
|
||||||
|
}}
|
||||||
|
onCharacterClick={(attrs) => {
|
||||||
|
console.log('attrs', attrs);
|
||||||
|
setIsReplaceLibraryOpen(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 重新生成按钮、新增分镜按钮 */}
|
{/* 重新生成按钮、新增分镜按钮 */}
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
@ -357,17 +364,6 @@ export function ShotTabContent({
|
|||||||
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* 替换视频弹窗 */}
|
|
||||||
<ReplaceVideoModal
|
|
||||||
isOpen={isReplaceModalOpen}
|
|
||||||
activeReplaceMethod={activeReplaceMethod}
|
|
||||||
onClose={() => setIsReplaceModalOpen(false)}
|
|
||||||
onVideoSelect={(video) => {
|
|
||||||
console.log('Selected video:', video);
|
|
||||||
setIsReplaceModalOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Media Properties 弹窗 */}
|
{/* Media Properties 弹窗 */}
|
||||||
<MediaPropertiesModal
|
<MediaPropertiesModal
|
||||||
isOpen={isMediaPropertiesModalOpen}
|
isOpen={isMediaPropertiesModalOpen}
|
||||||
@ -377,7 +373,25 @@ export function ShotTabContent({
|
|||||||
onSketchSelect={onSketchSelect}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user