|
|
|
@ -13,12 +13,6 @@ const MAX_FILE_SIZE = 200 * 1024 * 1024; // 200MB |
|
|
|
const ACCEPTED_FORMATS = "video/mp4,video/webm,video/quicktime"; |
|
|
|
const ACCEPTED_MIME_TYPES = ACCEPTED_FORMATS.split(","); |
|
|
|
|
|
|
|
function formatDuration(seconds: number): string { |
|
|
|
const mins = Math.floor(seconds / 60); |
|
|
|
const secs = Math.floor(seconds % 60); |
|
|
|
return `${mins}:${secs.toString().padStart(2, "0")}`; |
|
|
|
} |
|
|
|
|
|
|
|
export function VideoInputNode({ id, data, selected }: NodeProps<VideoInputNodeType>) { |
|
|
|
const nodeData = data; |
|
|
|
const updateNodeData = useWorkflowStore((state) => state.updateNodeData); |
|
|
|
@ -119,8 +113,9 @@ export function VideoInputNode({ id, data, selected }: NodeProps<VideoInputNodeT |
|
|
|
<BaseNode |
|
|
|
id={id} |
|
|
|
selected={selected} |
|
|
|
minWidth={250} |
|
|
|
minHeight={200} |
|
|
|
contentClassName="flex-1 min-h-0" |
|
|
|
aspectFitMedia={nodeData.video} |
|
|
|
fullBleed |
|
|
|
> |
|
|
|
<input |
|
|
|
ref={fileInputRef} |
|
|
|
@ -131,47 +126,24 @@ export function VideoInputNode({ id, data, selected }: NodeProps<VideoInputNodeT |
|
|
|
/> |
|
|
|
|
|
|
|
{nodeData.video ? ( |
|
|
|
<div className="relative group flex-1 flex flex-col min-h-0 gap-2"> |
|
|
|
<div className="relative group w-full h-full overflow-clip rounded-lg"> |
|
|
|
<video |
|
|
|
src={playbackUrl ?? undefined} |
|
|
|
controls |
|
|
|
className="w-full h-full object-cover rounded-lg" |
|
|
|
preload="metadata" |
|
|
|
/> |
|
|
|
{nodeData.isOptional && ( |
|
|
|
<span className="absolute top-1 left-1 z-10 text-[9px] font-medium text-neutral-300 bg-black/50 px-1.5 py-0.5 rounded"> |
|
|
|
<span className="absolute bottom-2 left-2 text-[9px] font-medium text-neutral-300 bg-black/50 px-1.5 py-0.5 rounded"> |
|
|
|
Optional |
|
|
|
</span> |
|
|
|
)} |
|
|
|
{/* Filename and duration */} |
|
|
|
<div className="flex items-center justify-between shrink-0"> |
|
|
|
<span className="text-[10px] text-neutral-400 truncate max-w-[150px]" title={nodeData.filename || ""}> |
|
|
|
{nodeData.filename} |
|
|
|
</span> |
|
|
|
<div className="flex items-center gap-1"> |
|
|
|
{nodeData.duration != null && ( |
|
|
|
<span className="text-[10px] text-neutral-500 bg-neutral-700/50 px-1.5 py-0.5 rounded"> |
|
|
|
{formatDuration(nodeData.duration)} |
|
|
|
</span> |
|
|
|
)} |
|
|
|
{nodeData.dimensions && ( |
|
|
|
<span className="text-[10px] text-neutral-500 bg-neutral-700/50 px-1.5 py-0.5 rounded"> |
|
|
|
{nodeData.dimensions.width}x{nodeData.dimensions.height} |
|
|
|
</span> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
{/* Video player */} |
|
|
|
<div className="flex-1 min-h-[120px] bg-neutral-900/50 rounded overflow-hidden"> |
|
|
|
<video |
|
|
|
src={playbackUrl ?? undefined} |
|
|
|
controls |
|
|
|
className="w-full h-full object-contain" |
|
|
|
preload="metadata" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
|
|
|
|
{/* Remove button */} |
|
|
|
<button |
|
|
|
onClick={handleRemove} |
|
|
|
className="absolute top-1 right-1 w-5 h-5 bg-black/60 text-white rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center" |
|
|
|
aria-label="Remove video" |
|
|
|
className="absolute top-2 right-2 w-6 h-6 bg-black/60 hover:bg-red-600/80 text-white rounded text-xs opacity-0 group-hover:opacity-100 focus:opacity-100 focus:ring-1 focus:ring-red-400 transition-all flex items-center justify-center" |
|
|
|
> |
|
|
|
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> |
|
|
|
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> |
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" /> |
|
|
|
</svg> |
|
|
|
</button> |
|
|
|
@ -185,14 +157,12 @@ export function VideoInputNode({ id, data, selected }: NodeProps<VideoInputNodeT |
|
|
|
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); fileInputRef.current?.click(); } }} |
|
|
|
onDrop={handleDrop} |
|
|
|
onDragOver={handleDragOver} |
|
|
|
className={`w-full h-full bg-neutral-900/40 flex flex-col items-center justify-center cursor-pointer hover:bg-neutral-800/60 transition-colors ${nodeData.isOptional ? "border-2 border-dashed border-neutral-600" : ""}`} |
|
|
|
className={`w-full h-full bg-neutral-900/40 flex flex-col items-center justify-center cursor-pointer hover:bg-neutral-900/60 transition-colors ${nodeData.isOptional ? "border-2 border-dashed border-neutral-600" : ""}`} |
|
|
|
> |
|
|
|
<svg className="w-8 h-8 text-neutral-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> |
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z" /> |
|
|
|
</svg> |
|
|
|
<span className="text-xs text-neutral-500 mt-2"> |
|
|
|
{nodeData.isOptional ? "Optional" : "Drop video or click"} |
|
|
|
</span> |
|
|
|
<span className="text-xs text-neutral-500 mt-2">{nodeData.isOptional ? "Optional" : "Drop video or click"}</span> |
|
|
|
</div> |
|
|
|
)} |
|
|
|
|
|
|
|
|