mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(studio): add volume/rate/media-start sliders to FlatMediaSection
This commit is contained in:
@@ -129,3 +129,118 @@ describe("FlatMediaSection — cutout", () => {
|
||||
act(() => root.unmount());
|
||||
});
|
||||
});
|
||||
|
||||
describe("FlatMediaSection — volume/rate/media-start", () => {
|
||||
it("renders volume at its stored percentage and commits a new value on drag", () => {
|
||||
const onSetAttribute = vi.fn();
|
||||
const element = makeVideoElement({ dataAttributes: { volume: "0.5" } });
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
const root = createRoot(host);
|
||||
act(() => {
|
||||
root.render(
|
||||
<FlatMediaSection
|
||||
projectDir={null}
|
||||
element={element}
|
||||
styles={{}}
|
||||
onSetStyle={vi.fn()}
|
||||
onSetAttribute={onSetAttribute}
|
||||
onSetHtmlAttribute={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
expect(host.textContent).toContain("50%");
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("commits a new volume value on slider track pointerdown", () => {
|
||||
const onSetAttribute = vi.fn();
|
||||
const element = makeVideoElement({ dataAttributes: { volume: "0.5" } });
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
const root = createRoot(host);
|
||||
act(() => {
|
||||
root.render(
|
||||
<FlatMediaSection
|
||||
projectDir={null}
|
||||
element={element}
|
||||
styles={{}}
|
||||
onSetStyle={vi.fn()}
|
||||
onSetAttribute={onSetAttribute}
|
||||
onSetHtmlAttribute={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
const volumeTrack = host.querySelectorAll('[data-flat-slider-track="true"]')[0];
|
||||
Object.defineProperty(volumeTrack, "getBoundingClientRect", {
|
||||
value: () => ({ left: 0, width: 100, top: 0, height: 2, right: 100, bottom: 2 }),
|
||||
});
|
||||
act(() => {
|
||||
volumeTrack.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true, clientX: 50 }));
|
||||
});
|
||||
// min=0, max=100, ratio=0.5 -> raw=50 -> commit(50) -> 50/100=0.5 -> "0.5"
|
||||
expect(onSetAttribute).toHaveBeenCalledWith("volume", "0.5");
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("commits a new rate value on slider track pointerdown", () => {
|
||||
const onSetAttribute = vi.fn();
|
||||
const element = makeVideoElement();
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
const root = createRoot(host);
|
||||
act(() => {
|
||||
root.render(
|
||||
<FlatMediaSection
|
||||
projectDir={null}
|
||||
element={element}
|
||||
styles={{}}
|
||||
onSetStyle={vi.fn()}
|
||||
onSetAttribute={onSetAttribute}
|
||||
onSetHtmlAttribute={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
const rateTrack = host.querySelectorAll('[data-flat-slider-track="true"]')[1];
|
||||
Object.defineProperty(rateTrack, "getBoundingClientRect", {
|
||||
value: () => ({ left: 0, width: 100, top: 0, height: 2, right: 100, bottom: 2 }),
|
||||
});
|
||||
act(() => {
|
||||
rateTrack.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true, clientX: 100 }));
|
||||
});
|
||||
// min=25, max=300, ratio=1.0 -> raw=300 -> commit(300) -> 300/100=3 -> "3"
|
||||
expect(onSetAttribute).toHaveBeenCalledWith("playback-rate", "3");
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("commits a new media-start value on slider track pointerdown", () => {
|
||||
const onSetAttribute = vi.fn();
|
||||
const element = makeVideoElement();
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
const root = createRoot(host);
|
||||
act(() => {
|
||||
root.render(
|
||||
<FlatMediaSection
|
||||
projectDir={null}
|
||||
element={element}
|
||||
styles={{}}
|
||||
onSetStyle={vi.fn()}
|
||||
onSetAttribute={onSetAttribute}
|
||||
onSetHtmlAttribute={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
const mediaStartTrack = host.querySelectorAll('[data-flat-slider-track="true"]')[2];
|
||||
Object.defineProperty(mediaStartTrack, "getBoundingClientRect", {
|
||||
value: () => ({ left: 0, width: 100, top: 0, height: 2, right: 100, bottom: 2 }),
|
||||
});
|
||||
act(() => {
|
||||
mediaStartTrack.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true, clientX: 100 }));
|
||||
});
|
||||
// no source-duration set -> mediaStartMax=Math.max(30, Math.ceil(0+10))=30 -> max=3000
|
||||
// ratio=1.0 -> raw=3000 -> commit(3000) -> (3000/100).toFixed(2) = "30.00"
|
||||
expect(onSetAttribute).toHaveBeenCalledWith("media-start", "30.00");
|
||||
act(() => root.unmount());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,9 +4,12 @@ import type { DomEditSelection } from "./domEditing";
|
||||
import {
|
||||
type BackgroundRemovalProgress,
|
||||
type BackgroundRemovalResult,
|
||||
formatNumericValue,
|
||||
formatTimingValue,
|
||||
parseNumericValue,
|
||||
stripQueryAndHash,
|
||||
} from "./propertyPanelHelpers";
|
||||
import { FlatSelectRow, FlatToggle } from "./propertyPanelFlatPrimitives";
|
||||
import { FlatSelectRow, FlatSlider, FlatToggle } from "./propertyPanelFlatPrimitives";
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
export function FlatMediaSection({
|
||||
@@ -36,12 +39,24 @@ export function FlatMediaSection({
|
||||
) => Promise<BackgroundRemovalResult>;
|
||||
}) {
|
||||
const isVideo = element.tagName === "video";
|
||||
// oxlint-disable-next-line no-unused-vars -- wired into the Volume/Rate/Muted gate in Task 4
|
||||
const isAudio = element.tagName === "audio";
|
||||
const isImage = element.tagName === "img";
|
||||
const isVisualMedia = isVideo || isImage;
|
||||
const el = element.element;
|
||||
|
||||
const volume = parseNumericValue(element.dataAttributes.volume ?? "") ?? 1;
|
||||
const volumePercent = Math.round(volume * 100);
|
||||
const mediaStart =
|
||||
Number.parseFloat(
|
||||
element.dataAttributes["media-start"] ?? element.dataAttributes["playback-start"] ?? "0",
|
||||
) || 0;
|
||||
const playbackRate = Number.parseFloat(element.dataAttributes["playback-rate"] ?? "1") || 1;
|
||||
const sourceDuration =
|
||||
Number.parseFloat(element.dataAttributes["source-duration"] ?? "") ||
|
||||
(el as HTMLMediaElement).duration ||
|
||||
0;
|
||||
const mediaStartMax = Math.max(30, Math.ceil(sourceDuration || mediaStart + 10));
|
||||
|
||||
const srcAttr = el.getAttribute("src") ?? "";
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [removeBusy, setRemoveBusy] = useState(false);
|
||||
@@ -172,6 +187,39 @@ export function FlatMediaSection({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{(isVideo || isAudio) && (
|
||||
<>
|
||||
<FlatSlider
|
||||
label="Volume"
|
||||
value={volumePercent}
|
||||
min={0}
|
||||
max={100}
|
||||
tier={volumePercent === 100 ? "default" : "explicitCustom"}
|
||||
displayValue={`${volumePercent}%`}
|
||||
onCommit={(next) => void onSetAttribute("volume", formatNumericValue(next / 100))}
|
||||
/>
|
||||
<FlatSlider
|
||||
label="Rate"
|
||||
value={playbackRate * 100}
|
||||
min={25}
|
||||
max={300}
|
||||
tier={playbackRate === 1 ? "default" : "explicitCustom"}
|
||||
displayValue={`${formatNumericValue(playbackRate)}x`}
|
||||
onCommit={(next) =>
|
||||
void onSetAttribute("playback-rate", formatNumericValue(next / 100))
|
||||
}
|
||||
/>
|
||||
<FlatSlider
|
||||
label="Media start"
|
||||
value={Math.round(mediaStart * 100)}
|
||||
min={0}
|
||||
max={mediaStartMax * 100}
|
||||
tier={mediaStart === 0 ? "default" : "explicitCustom"}
|
||||
displayValue={formatTimingValue(mediaStart)}
|
||||
onCommit={(next) => void onSetAttribute("media-start", (next / 100).toFixed(2))}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user