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
@@ -57,7 +57,6 @@ export function safeParseManifest(html: string): SlideshowManifest {
|
||||
import {
|
||||
toggleMainLineSlide,
|
||||
reorderMainLineSlide,
|
||||
reorderBranchSlide,
|
||||
setSlideNotes,
|
||||
addFragment,
|
||||
removeFragment,
|
||||
@@ -94,19 +93,24 @@ export function makeSlideshowNotesController(): NotesController {
|
||||
let pending: Pending | null = null;
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// Atomically swap the pending entry out and fire its persist (if any).
|
||||
// Used by both the debounce timer's tail and the explicit flush() path.
|
||||
const drainPending = (): void => {
|
||||
const p = pending;
|
||||
if (p === null) return;
|
||||
pending = null;
|
||||
p.persist(p.manifest).catch((err: unknown) => {
|
||||
console.error("[slideshow] notes persist failed:", err);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
schedule(manifest, persist, delayMs) {
|
||||
if (timer !== null) clearTimeout(timer);
|
||||
pending = { manifest, persist };
|
||||
timer = setTimeout(() => {
|
||||
timer = null;
|
||||
const p = pending;
|
||||
if (p !== null) {
|
||||
pending = null;
|
||||
p.persist(p.manifest).catch((err: unknown) => {
|
||||
console.error("[slideshow] notes persist failed:", err);
|
||||
});
|
||||
}
|
||||
drainPending();
|
||||
}, delayMs);
|
||||
return timer;
|
||||
},
|
||||
@@ -116,13 +120,7 @@ export function makeSlideshowNotesController(): NotesController {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
const p = pending;
|
||||
if (p !== null) {
|
||||
pending = null;
|
||||
p.persist(p.manifest).catch((err: unknown) => {
|
||||
console.error("[slideshow] notes persist failed:", err);
|
||||
});
|
||||
}
|
||||
drainPending();
|
||||
},
|
||||
|
||||
cancel() {
|
||||
@@ -293,15 +291,6 @@ export function SlideshowPanel({ scenes, onPersist, onPersistNotes }: SlideshowP
|
||||
[applyManifest],
|
||||
);
|
||||
|
||||
const handleReorderBranchSlide = useCallback(
|
||||
(sequenceId: string, sceneId: string, dir: "up" | "down") => {
|
||||
applyManifest(reorderBranchSlide(manifestRef.current, sequenceId, sceneId, dir)).catch(
|
||||
() => {},
|
||||
);
|
||||
},
|
||||
[applyManifest],
|
||||
);
|
||||
|
||||
const handleSetNotes = useCallback(
|
||||
(notes: string) => {
|
||||
if (!selectedSceneId) return;
|
||||
@@ -459,7 +448,6 @@ export function SlideshowPanel({ scenes, onPersist, onPersistNotes }: SlideshowP
|
||||
selectedSceneId={selectedSceneId}
|
||||
selectedSequenceId={selectedSequenceId}
|
||||
onSelectBranchSlide={handleSelectBranchSlide}
|
||||
onReorderBranchSlide={handleReorderBranchSlide}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -216,7 +216,6 @@ export interface BranchTreeProps {
|
||||
selectedSceneId: string | null;
|
||||
selectedSequenceId: string | null;
|
||||
onSelectBranchSlide: (sequenceId: string, sceneId: string) => void;
|
||||
onReorderBranchSlide: (sequenceId: string, sceneId: string, dir: "up" | "down") => void;
|
||||
}
|
||||
|
||||
export function BranchTree({
|
||||
@@ -229,7 +228,6 @@ export function BranchTree({
|
||||
selectedSceneId,
|
||||
selectedSequenceId,
|
||||
onSelectBranchSlide,
|
||||
onReorderBranchSlide,
|
||||
}: BranchTreeProps) {
|
||||
const [newLabel, setNewLabel] = useState("");
|
||||
const inputId = useId();
|
||||
@@ -280,7 +278,6 @@ export function BranchTree({
|
||||
selectedSceneId={selectedSceneId}
|
||||
selectedSequenceId={selectedSequenceId}
|
||||
onSelectBranchSlide={onSelectBranchSlide}
|
||||
onReorderBranchSlide={onReorderBranchSlide}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -298,7 +295,6 @@ interface BranchItemProps {
|
||||
selectedSceneId: string | null;
|
||||
selectedSequenceId: string | null;
|
||||
onSelectBranchSlide: (sequenceId: string, sceneId: string) => void;
|
||||
onReorderBranchSlide: (sequenceId: string, sceneId: string, dir: "up" | "down") => void;
|
||||
}
|
||||
|
||||
function BranchItem({
|
||||
@@ -310,7 +306,6 @@ function BranchItem({
|
||||
selectedSceneId,
|
||||
selectedSequenceId,
|
||||
onSelectBranchSlide,
|
||||
onReorderBranchSlide,
|
||||
}: BranchItemProps) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [draft, setDraft] = useState(seq.label);
|
||||
@@ -362,8 +357,7 @@ function BranchItem({
|
||||
</div>
|
||||
<div className="flex flex-col gap-px pl-2">
|
||||
{scenes.map((scene) => {
|
||||
const branchPos = seq.slides.findIndex((s) => s.sceneId === scene.id);
|
||||
const assigned = branchPos !== -1;
|
||||
const assigned = seq.slides.some((s) => s.sceneId === scene.id);
|
||||
const isSelected = selectedSequenceId === seq.id && selectedSceneId === scene.id;
|
||||
return (
|
||||
<div
|
||||
@@ -378,39 +372,16 @@ function BranchItem({
|
||||
className="accent-studio-accent flex-shrink-0"
|
||||
/>
|
||||
{assigned ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={isSelected}
|
||||
className={`flex-1 text-left truncate transition-colors hover:text-neutral-200 ${
|
||||
isSelected ? "text-white" : "text-neutral-400"
|
||||
}`}
|
||||
onClick={() => onSelectBranchSlide(seq.id, scene.id)}
|
||||
>
|
||||
{scene.label || scene.id}
|
||||
</button>
|
||||
<span className="text-[9px] text-neutral-600 tabular-nums flex-shrink-0">
|
||||
{branchPos + 1}/{seq.slides.length}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Move ${scene.label || scene.id} earlier in branch ${seq.label}`}
|
||||
disabled={branchPos <= 0}
|
||||
className="text-[10px] text-neutral-500 hover:text-neutral-200 disabled:opacity-30 disabled:cursor-default px-0.5"
|
||||
onClick={() => onReorderBranchSlide(seq.id, scene.id, "up")}
|
||||
>
|
||||
▲
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Move ${scene.label || scene.id} later in branch ${seq.label}`}
|
||||
disabled={branchPos >= seq.slides.length - 1}
|
||||
className="text-[10px] text-neutral-500 hover:text-neutral-200 disabled:opacity-30 disabled:cursor-default px-0.5"
|
||||
onClick={() => onReorderBranchSlide(seq.id, scene.id, "down")}
|
||||
>
|
||||
▼
|
||||
</button>
|
||||
</>
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={isSelected}
|
||||
className={`flex-1 text-left truncate transition-colors hover:text-neutral-200 ${
|
||||
isSelected ? "text-white" : "text-neutral-400"
|
||||
}`}
|
||||
onClick={() => onSelectBranchSlide(seq.id, scene.id)}
|
||||
>
|
||||
{scene.label || scene.id}
|
||||
</button>
|
||||
) : (
|
||||
<span className="flex-1 truncate">{scene.label || scene.id}</span>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user