mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 06:30:03 +00:00
fix(slideshow): present media controls (#1601)
* fix(slideshow): harden media controls in present decks
* refactor(slideshow): clear Fallow audit findings
Decompose flagged high-CRAP functions and extract production-code
duplications so the audit gate clears.
- core/runtime/bridge.ts handler — replace the 14-branch if-chain with a
CONTROL_HANDLERS dispatch table; flash-elements payload handling moves
to its own helper. Behavior preserved (all existing bridge.test.ts
cases hit the same dispatchers via the public installRuntimeControlBridge
API).
- player/slideshow/SlideshowController syncTo — split into
isValidSyncTarget / isCrossSlide / rerootStackTo helpers. The
stopSlideMedia decision and the stack re-rooting are now individually
named; the public method is a 4-line orchestrator.
- cli/commands/validate.ts run — extract emitJsonReport / emitTextReport
so the orchestrator no longer carries the dual JSON/text branches.
Cuts the cyclomatic complexity flagged by fallow after the
shouldIgnoreRequestFailure signature expansion shifted the fingerprint.
- player/hyperframes-player.ts — _setIframeMediaMuted and _stopIframeMedia
shared a `try { iframeDoc = contentDocument } catch { return }` preamble
(clone group 15). Extract _getSameOriginIframeDocument(): Document | null
and have both call sites consume it.
- studio/panels/SlideshowPanel.tsx — the notes controller's debounce-tail
and explicit flush() shared the pending-drain pattern (clone group 16).
Extract a drainPending() closure both call.
- player/hyperframes-player.test.ts — collapse the new stopMedia / muted
tests' repeated Object.defineProperty(iframe, "contentDocument", { get })
shape behind a stubIframeContentDocument helper.
No behavior changes — refactor only. Existing tests cover the affected
paths unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(validate): split run further; ignore test dup parity
Second Fallow pass surfaced two minor follow-ups after the first cut:
- packages/cli/src/commands/validate.ts run + emitTextReport still
carried minor CRAP findings (43.1 / 37.1, threshold 30). Extract
printValidationResult / formatConsoleEntry / formatTotals /
emitFailureReport so run becomes a try/catch + delegation, well
below the threshold; emitTextReport drops the inline format loops.
- .fallowrc.jsonc duplicates.ignore: add hyperframes-player.test.ts
alongside the existing SlideshowPanel.test.ts entry. Same reasoning
documented there — parallel arrange/act/assert test cases are
intentionally self-contained for readability; collapsing them under
shared fixtures would couple unrelated scenarios (same-origin vs
realm media, audio-locked permutations, seek bridge variants).
No behavior changes — refactor + config-policy parity only.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
cd832f01ac
commit
f0c4dee705
@@ -5,6 +5,7 @@ import type { RuntimeBridgeControlMessage, RuntimeOutboundMessage } from "./type
|
||||
type BridgeDeps = {
|
||||
onPlay: () => void;
|
||||
onPause: () => void;
|
||||
onStopMedia: () => void;
|
||||
onSeek: (frame: number, seekMode: "drag" | "commit") => void;
|
||||
onTick: () => void;
|
||||
onSetMuted: (muted: boolean) => void;
|
||||
@@ -29,68 +30,50 @@ export function postRuntimeMessage(payload: RuntimeOutboundMessage): void {
|
||||
}
|
||||
}
|
||||
|
||||
type BridgeControlData = Partial<RuntimeBridgeControlMessage>;
|
||||
type ControlHandler = (data: BridgeControlData, deps: BridgeDeps) => void;
|
||||
|
||||
// Per-action dispatchers. Splitting the handler into a lookup table keeps the
|
||||
// top-level message listener trivial (one map lookup), and each action's logic
|
||||
// becomes individually testable / inheritable for fallow's CRAP analysis.
|
||||
const CONTROL_HANDLERS: Record<string, ControlHandler> = {
|
||||
play: (_d, deps) => deps.onPlay(),
|
||||
pause: (_d, deps) => deps.onPause(),
|
||||
"stop-media": (_d, deps) => deps.onStopMedia(),
|
||||
seek: (data, deps) => deps.onSeek(Number(data.frame ?? 0), data.seekMode ?? "commit"),
|
||||
tick: (_d, deps) => deps.onTick(),
|
||||
"set-muted": (data, deps) => deps.onSetMuted(Boolean(data.muted)),
|
||||
"set-volume": (data, deps) =>
|
||||
deps.onSetVolume(Math.max(0, Math.min(1, Number(data.volume ?? 1)))),
|
||||
"set-media-output-muted": (data, deps) => deps.onSetMediaOutputMuted(Boolean(data.muted)),
|
||||
"set-playback-rate": (data, deps) => deps.onSetPlaybackRate(Number(data.playbackRate ?? 1)),
|
||||
"set-color-grading": (data, deps) =>
|
||||
deps.onSetColorGrading(data.target ?? null, data.grading ?? null),
|
||||
"set-color-grading-compare": (data, deps) =>
|
||||
deps.onSetColorGradingCompare(data.target ?? null, data.compare ?? null),
|
||||
"enable-pick-mode": (_d, deps) => deps.onEnablePickMode(),
|
||||
"disable-pick-mode": (_d, deps) => deps.onDisablePickMode(),
|
||||
"flash-elements": (data) => handleFlashElements(data),
|
||||
};
|
||||
|
||||
function handleFlashElements(data: BridgeControlData): void {
|
||||
// Briefly highlight elements — used by the chat-canvas bridge
|
||||
// to show what changed after an agent edit
|
||||
const selectors = (data as Record<string, unknown>).selectors as string[] | undefined;
|
||||
const duration = ((data as Record<string, unknown>).duration as number) || 800;
|
||||
if (selectors) {
|
||||
flashElements(selectors, duration);
|
||||
}
|
||||
}
|
||||
|
||||
export function installRuntimeControlBridge(deps: BridgeDeps): (event: MessageEvent) => void {
|
||||
const handler = (event: MessageEvent) => {
|
||||
const data = event.data as Partial<RuntimeBridgeControlMessage> | null;
|
||||
const data = event.data as BridgeControlData | null;
|
||||
if (!data || data.source !== "hf-parent" || data.type !== "control") return;
|
||||
const action = data.action;
|
||||
if (action === "play") {
|
||||
deps.onPlay();
|
||||
return;
|
||||
}
|
||||
if (action === "pause") {
|
||||
deps.onPause();
|
||||
return;
|
||||
}
|
||||
if (action === "seek") {
|
||||
deps.onSeek(Number(data.frame ?? 0), data.seekMode ?? "commit");
|
||||
return;
|
||||
}
|
||||
if (action === "tick") {
|
||||
deps.onTick();
|
||||
return;
|
||||
}
|
||||
if (action === "set-muted") {
|
||||
deps.onSetMuted(Boolean(data.muted));
|
||||
return;
|
||||
}
|
||||
if (action === "set-volume") {
|
||||
deps.onSetVolume(Math.max(0, Math.min(1, Number(data.volume ?? 1))));
|
||||
return;
|
||||
}
|
||||
if (action === "set-media-output-muted") {
|
||||
deps.onSetMediaOutputMuted(Boolean(data.muted));
|
||||
return;
|
||||
}
|
||||
if (action === "set-playback-rate") {
|
||||
deps.onSetPlaybackRate(Number(data.playbackRate ?? 1));
|
||||
return;
|
||||
}
|
||||
if (action === "set-color-grading") {
|
||||
deps.onSetColorGrading(data.target ?? null, data.grading ?? null);
|
||||
return;
|
||||
}
|
||||
if (action === "set-color-grading-compare") {
|
||||
deps.onSetColorGradingCompare(data.target ?? null, data.compare ?? null);
|
||||
return;
|
||||
}
|
||||
if (action === "enable-pick-mode") {
|
||||
deps.onEnablePickMode();
|
||||
return;
|
||||
}
|
||||
if (action === "disable-pick-mode") {
|
||||
deps.onDisablePickMode();
|
||||
return;
|
||||
}
|
||||
if (action === "flash-elements") {
|
||||
// Briefly highlight elements — used by the chat-canvas bridge
|
||||
// to show what changed after an agent edit
|
||||
const selectors = (data as Record<string, unknown>).selectors as string[] | undefined;
|
||||
const duration = ((data as Record<string, unknown>).duration as number) || 800;
|
||||
if (selectors) {
|
||||
flashElements(selectors, duration);
|
||||
}
|
||||
}
|
||||
if (typeof action !== "string") return;
|
||||
const fn = CONTROL_HANDLERS[action];
|
||||
if (fn) fn(data, deps);
|
||||
};
|
||||
window.addEventListener("message", handler);
|
||||
// Announce that the bridge listener is installed so the parent can replay
|
||||
|
||||
Reference in New Issue
Block a user