mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 15:20:13 +00:00
feat(studio): per-composition render button in compositions tab (#874)
* feat(studio): add per-composition render button in compositions tab Thread composition path through the full render pipeline so individual compositions can be rendered independently from the studio UI. - Add download icon button on each comp card (visible on hover) - Accept `composition` field in POST /projects/:id/render - Pass composition as `entryFile` to the producer's createRenderJob - Make the Export button in the Renders panel composition-aware (renders the active composition instead of always index.html) * fix(studio): make composition render buttons always visible The hover-only opacity made them undiscoverable. * fix(studio): address PR review — CLI adapter, path guard, a11y, tests, settings sync - Wire `composition` → `entryFile` in CLI studio adapter (studioServer.ts) so `hyperframes preview` renders the correct composition, not always index.html - Add path-traversal guard: reject composition paths that resolve outside projectDir - Add `aria-label` to the icon-only render button for screen readers - Add 4 tests: forwarding, empty/missing → undefined, path-traversal → 400 - Persist render settings (format/quality/fps) to localStorage so comp card buttons use the same settings as the Export panel * refactor(studio): extract render settings persistence to own module Move getPersistedRenderSettings/persistRenderSettings out of RenderQueue.tsx into renderSettings.ts so code-splitting the component doesn't drag along the helper.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { memo, useState, useRef, useEffect } from "react";
|
||||
import { RenderQueueItem } from "./RenderQueueItem";
|
||||
import type { RenderJob, ResolutionPreset } from "./useRenderQueue";
|
||||
import { getPersistedRenderSettings, persistRenderSettings } from "./renderSettings";
|
||||
|
||||
export interface CompositionDimensions {
|
||||
width: number;
|
||||
@@ -198,10 +199,11 @@ function FormatExportButton({
|
||||
isRendering: boolean;
|
||||
compositionDimensions?: CompositionDimensions | null;
|
||||
}) {
|
||||
const [format, setFormat] = useState<"mp4" | "webm" | "mov">("mp4");
|
||||
const [quality, setQuality] = useState<"draft" | "standard" | "high">("standard");
|
||||
const persisted = getPersistedRenderSettings();
|
||||
const [format, setFormat] = useState<"mp4" | "webm" | "mov">(persisted.format);
|
||||
const [quality, setQuality] = useState<"draft" | "standard" | "high">(persisted.quality);
|
||||
const [resolution, setResolution] = useState<ResolutionPreset | "auto">("auto");
|
||||
const [fps, setFps] = useState<24 | 30 | 60>(30);
|
||||
const [fps, setFps] = useState<24 | 30 | 60>(persisted.fps);
|
||||
|
||||
// MOV (ProRes) is a fixed-quality codec — quality selector has no effect.
|
||||
const showQuality = format !== "mov";
|
||||
@@ -228,7 +230,11 @@ function FormatExportButton({
|
||||
{showQuality && (
|
||||
<select
|
||||
value={quality}
|
||||
onChange={(e) => setQuality(e.target.value as "draft" | "standard" | "high")}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value as "draft" | "standard" | "high";
|
||||
setQuality(v);
|
||||
persistRenderSettings(format, v, fps);
|
||||
}}
|
||||
disabled={isRendering}
|
||||
title={QUALITY_OPTIONS.find((q) => q.value === quality)?.title}
|
||||
className="h-5 px-1 text-[10px] bg-neutral-800 border border-neutral-700 text-neutral-300 outline-none disabled:opacity-50"
|
||||
@@ -242,7 +248,11 @@ function FormatExportButton({
|
||||
)}
|
||||
<select
|
||||
value={fps}
|
||||
onChange={(e) => setFps(Number(e.target.value) as 24 | 30 | 60)}
|
||||
onChange={(e) => {
|
||||
const v = Number(e.target.value) as 24 | 30 | 60;
|
||||
setFps(v);
|
||||
persistRenderSettings(format, quality, v);
|
||||
}}
|
||||
disabled={isRendering}
|
||||
title="Frames per second"
|
||||
className="h-5 px-1 text-[10px] bg-neutral-800 border border-neutral-700 text-neutral-300 outline-none disabled:opacity-50"
|
||||
@@ -253,7 +263,11 @@ function FormatExportButton({
|
||||
</select>
|
||||
<select
|
||||
value={format}
|
||||
onChange={(e) => setFormat(e.target.value as "mp4" | "webm" | "mov")}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value as "mp4" | "webm" | "mov";
|
||||
setFormat(v);
|
||||
persistRenderSettings(v, quality, fps);
|
||||
}}
|
||||
disabled={isRendering}
|
||||
className="h-5 px-1 text-[10px] bg-neutral-800 border border-neutral-700 text-neutral-300 outline-none disabled:opacity-50"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user