mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-14 13:27:13 +00:00
feat(studio): open rendered video in new tab on row click
Clicking a completed render row now opens the video inline in the browser instead of forcing a download. Added /render/:jobId/view endpoint with Content-Disposition: inline so the browser plays the video natively. Download button is preserved on hover for explicit saves. stopPropagation on action buttons prevents row click conflicts.
This commit is contained in:
@@ -126,6 +126,27 @@ export function registerRenderRoutes(api: Hono, adapter: StudioApiAdapter): void
|
||||
});
|
||||
});
|
||||
|
||||
// Serve render inline (for in-browser playback — opens in a new tab)
|
||||
api.get("/render/:jobId/view", (c) => {
|
||||
const { jobId } = c.req.param();
|
||||
const job = renderJobs.get(jobId);
|
||||
if (!job?.outputPath || !existsSync(job.outputPath)) {
|
||||
return c.json({ error: "not found" }, 404);
|
||||
}
|
||||
const isWebm = job.outputPath.endsWith(".webm");
|
||||
const contentType = isWebm ? "video/webm" : "video/mp4";
|
||||
const filename = job.outputPath.split("/").pop() ?? `render.mp4`;
|
||||
const content = readFileSync(job.outputPath);
|
||||
return new Response(content, {
|
||||
headers: {
|
||||
"Content-Type": contentType,
|
||||
"Content-Disposition": `inline; filename="${filename}"`,
|
||||
"Accept-Ranges": "bytes",
|
||||
"Content-Length": String(content.length),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Download render
|
||||
api.get("/render/:jobId/download", (c) => {
|
||||
const { jobId } = c.req.param();
|
||||
|
||||
@@ -25,18 +25,34 @@ export const RenderQueueItem = memo(function RenderQueueItem({
|
||||
}: RenderQueueItemProps) {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
const handleDownload = useCallback(() => {
|
||||
const a = document.createElement("a");
|
||||
a.href = `/api/render/${job.id}/download`;
|
||||
a.download = job.filename;
|
||||
a.click();
|
||||
}, [job.id, job.filename]);
|
||||
const handleOpen = useCallback(() => {
|
||||
window.open(`/api/render/${job.id}/view`, "_blank");
|
||||
}, [job.id]);
|
||||
|
||||
const handleDownload = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const a = document.createElement("a");
|
||||
a.href = `/api/render/${job.id}/download`;
|
||||
a.download = job.filename;
|
||||
a.click();
|
||||
},
|
||||
[job.id, job.filename],
|
||||
);
|
||||
|
||||
const isClickable = job.status === "complete";
|
||||
|
||||
return (
|
||||
<div
|
||||
onPointerEnter={() => setHovered(true)}
|
||||
onPointerLeave={() => setHovered(false)}
|
||||
className="px-3 py-2.5 border-b border-neutral-800/30 last:border-0"
|
||||
onClick={isClickable ? handleOpen : undefined}
|
||||
className={[
|
||||
"px-3 py-2.5 border-b border-neutral-800/30 last:border-0 transition-colors duration-150",
|
||||
isClickable ? "cursor-pointer hover:bg-neutral-800/30" : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Status indicator */}
|
||||
@@ -113,7 +129,10 @@ export const RenderQueueItem = memo(function RenderQueueItem({
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={onDelete}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
}}
|
||||
className="p-1 rounded text-neutral-500 hover:text-red-400 transition-colors"
|
||||
title="Remove"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user