20260429_232813-fix: video frame display pipeline — default project seed, presigned URLs, Canvas/FrameTimeline real frames, upload-parse flow

This commit is contained in:
2026-04-29 23:42:18 +08:00
parent 51f1a60216
commit 35d6e1503c
16 changed files with 454 additions and 56 deletions

View File

@@ -1,15 +1,22 @@
import React, { useState } from 'react';
import { Play, Pause } from 'lucide-react';
import { cn } from '../lib/utils';
import { useStore } from '../store/useStore';
export function FrameTimeline() {
const [currentFrame, setCurrentFrame] = useState(142);
const totalFrames = 2400;
const frames = useStore((state) => state.frames);
const currentFrameIndex = useStore((state) => state.currentFrameIndex);
const setCurrentFrame = useStore((state) => state.setCurrentFrame);
const [isPlaying, setIsPlaying] = useState(false);
const totalFrames = frames.length;
const currentFrame = totalFrames > 0 ? currentFrameIndex + 1 : 0;
// show frames around current frame
const frameWindow = 20;
const frames = Array.from({ length: 41 }, (_, i) => currentFrame - frameWindow + i);
const frameWindow = 20;
const displayIndices = totalFrames > 0
? Array.from({ length: 41 }, (_, i) => currentFrameIndex - frameWindow + i)
: [];
return (
<div className="h-32 bg-[#111] border-t border-white/5 flex flex-col shrink-0 z-20">
@@ -17,19 +24,20 @@ export function FrameTimeline() {
<input
type="range"
min="1"
max={totalFrames}
max={Math.max(totalFrames, 1)}
value={currentFrame}
onChange={(e) => setCurrentFrame(parseInt(e.target.value))}
onChange={(e) => setCurrentFrame(parseInt(e.target.value) - 1)}
className="w-full absolute inset-0 opacity-0 cursor-ew-resize z-20"
disabled={totalFrames === 0}
/>
<div className="h-1 bg-white/10 w-full relative group-hover:h-2 transition-all">
<div
className="h-full bg-cyan-500 absolute left-0"
style={{ width: `${(currentFrame / totalFrames) * 100}%` }}
style={{ width: `${totalFrames > 0 ? (currentFrame / totalFrames) * 100 : 0}%` }}
/>
<div
className="w-3 h-3 bg-white rounded-full absolute top-1/2 -translate-y-1/2 -ml-1.5 shadow-sm transform scale-0 group-hover:scale-100 transition-transform shadow-cyan-500/50"
style={{ left: `${(currentFrame / totalFrames) * 100}%` }}
style={{ left: `${totalFrames > 0 ? (currentFrame / totalFrames) * 100 : 0}%` }}
/>
</div>
</div>
@@ -49,27 +57,42 @@ export function FrameTimeline() {
className="flex-1 flex gap-px flex-nowrap items-center overflow-hidden justify-center"
style={{ WebkitMaskImage: 'linear-gradient(to right, transparent, black 10%, black 90%, transparent)' }}
>
{frames.map((frameIdx) => {
if (frameIdx < 1 || frameIdx > totalFrames) {
return <div key={`empty-${frameIdx}`} className="w-16 h-12 opacity-0 shrink-0" />
}
const isCurrent = frameIdx === currentFrame;
return (
<div
key={frameIdx}
onClick={() => setCurrentFrame(frameIdx)}
className={cn(
"relative shrink-0 rounded-sm transition-all cursor-pointer flex items-center justify-center overflow-hidden group mx-0.5",
isCurrent ? "w-28 h-16 border-2 border-cyan-500 bg-gray-700 shadow-[0_0_15px_rgba(6,182,212,0.3)] z-10" : "w-16 h-12 border border-white/5 bg-gray-800/50 opacity-40 hover:opacity-100"
)}
>
<div className="absolute inset-0 bg-gradient-to-br from-gray-800 to-gray-900 opacity-20" />
<span className={cn("text-[9px] font-mono text-white text-center z-10", isCurrent ? "text-[10px] font-bold" : "")}>
{frameIdx.toString().padStart(4, '0')}
</span>
</div>
);
})}
{totalFrames === 0 ? (
<div className="text-xs text-gray-600 font-mono"></div>
) : (
displayIndices.map((idx) => {
if (idx < 0 || idx >= totalFrames) {
return <div key={`empty-${idx}`} className="w-16 h-12 opacity-0 shrink-0" />
}
const frame = frames[idx];
const isCurrent = idx === currentFrameIndex;
return (
<div
key={frame.id}
onClick={() => setCurrentFrame(idx)}
className={cn(
"relative shrink-0 rounded-sm transition-all cursor-pointer flex items-center justify-center overflow-hidden group mx-0.5",
isCurrent ? "w-28 h-16 border-2 border-cyan-500 bg-gray-700 shadow-[0_0_15px_rgba(6,182,212,0.3)] z-10" : "w-16 h-12 border border-white/5 bg-gray-800/50 opacity-40 hover:opacity-100"
)}
>
{frame.url ? (
<img
src={frame.url}
alt={`frame-${idx}`}
className="absolute inset-0 w-full h-full object-cover"
loading="lazy"
draggable={false}
/>
) : (
<div className="absolute inset-0 bg-gradient-to-br from-gray-800 to-gray-900 opacity-20" />
)}
<span className={cn("text-[9px] font-mono text-white text-center z-10 drop-shadow", isCurrent ? "text-[10px] font-bold" : "")}>
{(idx + 1).toString().padStart(4, '0')}
</span>
</div>
);
})
)}
</div>
<div className="w-48 text-right shrink-0">