|
|
|
@ -431,16 +431,16 @@ export function VideoStitchNode({ id, data, selected }: NodeProps<VideoStitchNod |
|
|
|
{renderHandles()} |
|
|
|
|
|
|
|
<div className="flex-1 flex flex-col min-h-0 gap-2"> |
|
|
|
{/* Filmstrip UI */} |
|
|
|
<div className="flex-1 flex flex-col min-h-0 gap-2"> |
|
|
|
{/* Filmstrip + controls area (shrink-0: only takes space it needs) */} |
|
|
|
<div className="shrink-0 flex flex-col gap-2"> |
|
|
|
{orderedClips.length === 0 ? ( |
|
|
|
<div className="flex-1 flex items-center justify-center border border-dashed border-neutral-600 rounded"> |
|
|
|
<div className="h-16 flex items-center justify-center border border-dashed border-neutral-600 rounded"> |
|
|
|
<span className="text-[10px] text-neutral-500">Connect videos to stitch</span> |
|
|
|
</div> |
|
|
|
) : ( |
|
|
|
<> |
|
|
|
{/* Filmstrip */} |
|
|
|
<div className="flex-1 overflow-y-auto nowheel grid grid-cols-4 content-start gap-2 p-2 bg-neutral-900/50 rounded"> |
|
|
|
<div className="overflow-y-auto nowheel grid grid-cols-4 content-start gap-2 p-2 bg-neutral-900/50 rounded"> |
|
|
|
{orderedClips.map((clip) => { |
|
|
|
const thumbnail = thumbnails.get(clip.edgeId); |
|
|
|
return ( |
|
|
|
@ -511,14 +511,6 @@ export function VideoStitchNode({ id, data, selected }: NodeProps<VideoStitchNod |
|
|
|
})} |
|
|
|
</div> |
|
|
|
|
|
|
|
{/* Stitch button */} |
|
|
|
<button |
|
|
|
onClick={handleStitch} |
|
|
|
disabled={orderedClips.length < 2 || nodeData.status === "loading" || isRunning} |
|
|
|
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 disabled:bg-neutral-700 disabled:text-neutral-500 disabled:cursor-not-allowed rounded text-white text-xs font-medium transition-colors" |
|
|
|
> |
|
|
|
{nodeData.status === "loading" ? "Processing..." : "Stitch"} |
|
|
|
</button> |
|
|
|
</> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
@ -549,16 +541,16 @@ export function VideoStitchNode({ id, data, selected }: NodeProps<VideoStitchNod |
|
|
|
</div> |
|
|
|
)} |
|
|
|
|
|
|
|
{/* Output preview */} |
|
|
|
{/* Output preview (flex-1: grows with node) */} |
|
|
|
{nodeData.outputVideo && nodeData.status !== "loading" && ( |
|
|
|
<div className="relative w-full"> |
|
|
|
<div className="relative flex-1 min-h-0"> |
|
|
|
<video |
|
|
|
src={nodeData.outputVideo} |
|
|
|
controls |
|
|
|
autoPlay |
|
|
|
loop |
|
|
|
muted |
|
|
|
className="w-full h-auto max-h-40 object-contain rounded" |
|
|
|
className="w-full h-full object-contain rounded" |
|
|
|
playsInline |
|
|
|
/> |
|
|
|
<button |
|
|
|
@ -572,6 +564,36 @@ export function VideoStitchNode({ id, data, selected }: NodeProps<VideoStitchNod |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
)} |
|
|
|
|
|
|
|
{/* Controls row: Loop selector + Stitch button (below video, right-aligned) */} |
|
|
|
{orderedClips.length > 0 && ( |
|
|
|
<div className="shrink-0 flex items-center justify-end gap-2"> |
|
|
|
<div className="flex items-center gap-1"> |
|
|
|
<span className="text-[10px] text-neutral-400">Loop</span> |
|
|
|
{([1, 2, 3] as const).map((count) => ( |
|
|
|
<button |
|
|
|
key={count} |
|
|
|
onClick={() => updateNodeData(id, { loopCount: count })} |
|
|
|
className={`nodrag px-1.5 py-0.5 rounded text-[10px] font-medium transition-colors ${ |
|
|
|
(nodeData.loopCount || 1) === count |
|
|
|
? "bg-blue-600 text-white" |
|
|
|
: "bg-neutral-700 text-neutral-400 hover:bg-neutral-600 hover:text-neutral-300" |
|
|
|
}`}
|
|
|
|
> |
|
|
|
{count}x |
|
|
|
</button> |
|
|
|
))} |
|
|
|
</div> |
|
|
|
|
|
|
|
<button |
|
|
|
onClick={handleStitch} |
|
|
|
disabled={orderedClips.length < 2 || nodeData.status === "loading" || isRunning} |
|
|
|
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 disabled:bg-neutral-700 disabled:text-neutral-500 disabled:cursor-not-allowed rounded text-white text-xs font-medium transition-colors" |
|
|
|
> |
|
|
|
{nodeData.status === "loading" ? "Processing..." : "Stitch"} |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
</BaseNode> |
|
|
|
); |
|
|
|
|