import { memo, useCallback, useState } from "react"; import { VideoFrameThumbnail } from "../ui/VideoFrameThumbnail"; import { Button } from "../ui/Button"; import type { RenderJob } from "./useRenderQueue"; interface RenderQueueItemProps { job: RenderJob; projectId: string; onDelete: () => void; onCancel: () => void; } function formatDuration(ms: number): string { if (ms < 1000) return `${ms}ms`; const s = Math.round(ms / 1000); return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`; } function formatTimeAgo(timestamp: number): string { const diff = Date.now() - timestamp; if (diff < 60000) return "just now"; if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`; return `${Math.floor(diff / 3600000)}h ago`; } /** Static frame extracted once via hidden video + canvas. */ export const RenderQueueItem = memo(function RenderQueueItem({ job, projectId, onDelete, onCancel, }: RenderQueueItemProps) { const [hovered, setHovered] = useState(false); const [videoReady, setVideoReady] = useState(false); const [confirmingDelete, setConfirmingDelete] = useState(false); // Direct file URL — serves from disk, survives server restarts const fileSrc = `/api/projects/${projectId}/renders/file/${job.filename}`; const handleOpen = useCallback(() => { window.open(fileSrc, "_blank"); }, [fileSrc]); const handleDownload = useCallback( (e: React.MouseEvent) => { e.stopPropagation(); const a = document.createElement("a"); a.href = fileSrc; a.download = job.filename; a.click(); }, [fileSrc, job.filename], ); const viewSrc = fileSrc; const isComplete = job.status === "complete"; const isRendering = job.status === "rendering"; return (
setHovered(true)} onPointerLeave={() => { setHovered(false); setVideoReady(false); setConfirmingDelete(false); }} className="px-3 py-2.5 border-b border-panel-border last:border-0 transition-colors duration-150 hover:bg-panel-hover/30" >
{/* Thumbnail — static frame; swaps to live video on hover. A real button so keyboard users can open the render too. */} {/* Info */}
{job.filename} {job.durationMs && ( {formatDuration(job.durationMs)} )}
{isRendering && (
{job.stage || "Rendering"} {job.progress}%
)} {job.status === "failed" && job.error && ( {job.error} )} {job.status === "cancelled" && ( Cancelled )} {!isRendering && ( {formatTimeAgo(job.createdAt)} )}
{/* Actions — always visible to prevent layout shifts */}
{isRendering ? ( ) : confirmingDelete ? ( <> ) : ( <> )}
); });