video-flow-b/components/ui/dialogue-item.tsx
2025-07-29 10:57:25 +08:00

167 lines
5.5 KiB
TypeScript

import React, { useState } from 'react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { motion } from 'framer-motion';
import { GripVertical, Trash2, Edit2, ChevronDown } from 'lucide-react';
import { DialogueItem as DialogueItemType, CharacterOption } from '@/app/model/enums';
import KeywordText from './keyword-text';
import * as Popover from '@radix-ui/react-popover';
import { mockCharacterOptions } from '@/app/model/enums';
import './style/dialogue-item.css';
interface DialogueItemProps {
dialogue: DialogueItemType;
onUpdate: (updates: Partial<DialogueItemType>) => void;
onDelete: () => void;
onCharacterChange?: (character: CharacterOption) => void;
}
const DialogueItem: React.FC<DialogueItemProps> = ({
dialogue,
onUpdate,
onDelete,
onCharacterChange,
}) => {
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(dialogue.text);
const [isCharacterSelectOpen, setIsCharacterSelectOpen] = useState(false);
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: dialogue.id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
const handleSave = () => {
onUpdate({ text: editText });
setIsEditing(false);
};
const handleCharacterSelect = (character: CharacterOption) => {
onUpdate({ speaker: character.name });
onCharacterChange?.(character);
setIsCharacterSelectOpen(false);
};
return (
<motion.div
ref={setNodeRef}
style={style}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className={`
group relative flex items-start gap-3 p-3 rounded-lg
${isDragging ? 'bg-white/10' : 'hover:bg-white/5'}
transition-colors duration-200 dialogue-item
`}
>
{/* Drag Handle */}
<div
{...attributes}
{...listeners}
className="mt-1 cursor-grab active:cursor-grabbing opacity-0 group-hover:opacity-50 hover:opacity-100 transition-opacity"
>
<GripVertical className="w-4 h-4" />
</div>
{/* Content */}
<div className="flex-grow">
{/* Character Selector */}
<Popover.Root open={isCharacterSelectOpen} onOpenChange={setIsCharacterSelectOpen}>
<Popover.Trigger asChild>
<button
className="mb-1 px-2 py-0.5 text-sm font-medium text-gray-400 rounded
hover:bg-white/5 flex items-center gap-1 transition-colors group/select"
>
{dialogue.speaker}
<ChevronDown className="w-3 h-3 opacity-50 group-hover/select:opacity-100 transition-opacity" />
</button>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
className="w-[200px] bg-black/90 backdrop-blur-xl rounded-lg border border-white/10
shadow-xl animate-in fade-in-80 z-50"
sideOffset={5}
>
<div className="p-1">
{mockCharacterOptions.map((char) => (
<button
key={char.characterId}
onClick={() => handleCharacterSelect(char)}
className="w-full px-3 py-2 text-sm text-left text-gray-200 rounded-md
hover:bg-white/5 transition-colors flex items-center gap-2"
>
<img
src={char.image}
alt={char.name}
className="w-6 h-6 rounded-full object-cover"
/>
{char.name}
</button>
))}
<div className="h-[1px] bg-white/10 my-1" />
<button
onClick={() => {
onUpdate({ speaker: '旁白' });
setIsCharacterSelectOpen(false);
}}
className="w-full px-3 py-2 text-sm text-left text-gray-400 rounded-md
hover:bg-white/5 transition-colors"
>
</button>
</div>
</Popover.Content>
</Popover.Portal>
</Popover.Root>
{isEditing ? (
<div className="flex flex-col gap-2">
<textarea
value={editText}
onChange={(e) => setEditText(e.target.value)}
className="w-full bg-black/20 rounded-lg p-2 text-sm resize-none focus:outline-none focus:ring-1 focus:ring-blue-500/50"
rows={3}
onBlur={() => {
handleSave();
setIsEditing(false);
}}
autoFocus
/>
</div>
) : (
<div className="text-sm text-gray-200" onClick={() => setIsEditing(true)}>
<KeywordText text={dialogue.text} />
</div>
)}
</div>
{/* Actions */}
<div className="flex gap-2 opacity-0 transition-opacity opt-btn-group">
<button
onClick={() => setIsEditing(true)}
className="p-1 rounded-md hover:bg-white/10 transition-colors"
>
<Edit2 className="w-4 h-4 text-gray-400" />
</button>
<button
onClick={onDelete}
className="p-1 rounded-md hover:bg-white/10 transition-colors"
>
<Trash2 className="w-4 h-4 text-red-400" />
</button>
</div>
</motion.div>
);
};
export default DialogueItem;