mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 23:00:03 +00:00
fix(ci): scope LOC check to studio, split useTimelinePlayer + hyperframes-player under 500 LOC (#750)
* refactor: split useTimelinePlayer.ts and hyperframes-player.ts into focused modules (<500 LOC each) * fix(ci): scope 500 LOC check to packages/studio, add allowlist for grandfathered files * feat(cli): Linux ARM64 support — auto-install Chromium on DGX Spark / GB10 / Jetson Chrome Headless Shell has no Linux ARM64 binary. On arm64 Linux: - Detects the platform automatically - Tries to auto-install system Chromium via apt-get (works on Ubuntu/Debian ARM) - Falls back to clear manual instructions with exact commands - 'hyperframes browser ensure' guides through the setup interactively - After setup, all render commands work without any flags * fix(ci): disable Windows Defender real-time monitoring to prevent EPERM builds Path exclusions are insufficient — Defender re-scans new files created during bun install before the exclusion takes effect. Disable real-time monitoring for the entire job duration instead (standard CI practice). * refactor(studio): split all files >500 LOC + extract useToast, delete allowlist All 11 large files split into focused modules under 500 LOC. App.tsx extracted toast logic into useToast hook (493 LOC now). .filesize-allowlist deleted — no longer needed. * fix: remove unused imports from split files, extract useToast from App.tsx App.tsx: 504 → 493 lines (toast logic extracted to useToast hook) timelineDOM.ts: remove unused imports from re-export pattern MotionPanel.tsx: remove unused clampStudioCustomEasePoints import studioMotionOps.ts: remove unused StudioGsapMotionDirection import * fix(ci): use Set-MpPreference to fully disable Windows Defender (both jobs) * fix(producer): use node --experimental-strip-types instead of tsx for build:fonts Eliminates the tsx binary dependency that Windows Defender locks during bun install, causing EPERM errors. Node 22.6+ strips TypeScript types natively with no external binary. * chore: remove .filesize-allowlist — App.tsx is now 493 lines (<500) * fix(ci): disable Windows Defender before checkout to prevent all EPERM races * fix(producer): skip build:fonts if fontData.generated.ts already exists The generated file is tracked in git, so CI doesn't need to regenerate it. This avoids @fontsource/inter node_modules access on Windows which triggers EPERM from Defender scanning during bun install.
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
import { useRef, useState, useCallback } from "react";
|
||||
import { buildClipRangeSelection, type TimelineRangeSelection } from "./timelineEditing";
|
||||
import type { TimelineElement } from "../store/playerStore";
|
||||
import { liveTime } from "../store/playerStore";
|
||||
import { GUTTER } from "./timelineLayout";
|
||||
|
||||
interface UseTimelineRangeSelectionInput {
|
||||
scrollRef: React.RefObject<HTMLDivElement | null>;
|
||||
ppsRef: React.RefObject<number>;
|
||||
effectiveDuration: number;
|
||||
pps: number;
|
||||
onSeek?: (time: number) => void;
|
||||
seekFromX: (clientX: number) => void;
|
||||
autoScrollDuringDrag: (clientX: number) => void;
|
||||
dragScrollRaf: React.RefObject<number>;
|
||||
isDragging: React.RefObject<boolean>;
|
||||
setShowPopover: (v: boolean) => void;
|
||||
}
|
||||
|
||||
export function useTimelineRangeSelection({
|
||||
scrollRef,
|
||||
ppsRef: _ppsRef,
|
||||
effectiveDuration: _effectiveDuration,
|
||||
pps,
|
||||
onSeek: _onSeek,
|
||||
seekFromX,
|
||||
autoScrollDuringDrag,
|
||||
dragScrollRaf,
|
||||
isDragging,
|
||||
setShowPopover,
|
||||
}: UseTimelineRangeSelectionInput) {
|
||||
const isRangeSelecting = useRef(false);
|
||||
const rangeAnchorTime = useRef(0);
|
||||
const [rangeSelection, setRangeSelection] = useState<TimelineRangeSelection | null>(null);
|
||||
const shiftClickClipRef = useRef<{
|
||||
element: TimelineElement;
|
||||
anchorX: number;
|
||||
anchorY: number;
|
||||
} | null>(null);
|
||||
|
||||
const handlePointerDown = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
if (e.button !== 0) return;
|
||||
if (e.shiftKey) {
|
||||
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||||
isRangeSelecting.current = true;
|
||||
setShowPopover(false);
|
||||
const rect = scrollRef.current?.getBoundingClientRect();
|
||||
if (rect) {
|
||||
const x = e.clientX - rect.left + (scrollRef.current?.scrollLeft ?? 0) - GUTTER;
|
||||
const time = Math.max(0, x / pps);
|
||||
rangeAnchorTime.current = time;
|
||||
setRangeSelection({ start: time, end: time, anchorX: e.clientX, anchorY: e.clientY });
|
||||
}
|
||||
return;
|
||||
}
|
||||
shiftClickClipRef.current = null;
|
||||
if ((e.target as HTMLElement).closest("[data-clip]")) return;
|
||||
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||||
isDragging.current = true;
|
||||
setRangeSelection(null);
|
||||
setShowPopover(false);
|
||||
seekFromX(e.clientX);
|
||||
},
|
||||
[seekFromX, pps, scrollRef, isDragging, setShowPopover],
|
||||
);
|
||||
|
||||
const handlePointerMove = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
if (isRangeSelecting.current) {
|
||||
const rect = scrollRef.current?.getBoundingClientRect();
|
||||
if (rect) {
|
||||
const x = e.clientX - rect.left + (scrollRef.current?.scrollLeft ?? 0) - GUTTER;
|
||||
setRangeSelection((prev) =>
|
||||
prev
|
||||
? { ...prev, end: Math.max(0, x / pps), anchorX: e.clientX, anchorY: e.clientY }
|
||||
: null,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!isDragging.current) return;
|
||||
seekFromX(e.clientX);
|
||||
autoScrollDuringDrag(e.clientX);
|
||||
},
|
||||
[seekFromX, autoScrollDuringDrag, pps, scrollRef, isDragging],
|
||||
);
|
||||
|
||||
const handlePointerUp = useCallback(() => {
|
||||
if (isRangeSelecting.current) {
|
||||
isRangeSelecting.current = false;
|
||||
const pendingShiftClick = shiftClickClipRef.current;
|
||||
shiftClickClipRef.current = null;
|
||||
setRangeSelection((prev) => {
|
||||
if (prev && pendingShiftClick && Math.abs(prev.end - prev.start) <= 0.2) {
|
||||
setShowPopover(true);
|
||||
return buildClipRangeSelection(pendingShiftClick.element, pendingShiftClick);
|
||||
}
|
||||
if (prev && Math.abs(prev.end - prev.start) > 0.2) {
|
||||
setShowPopover(true);
|
||||
return prev;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
isDragging.current = false;
|
||||
cancelAnimationFrame(dragScrollRaf.current);
|
||||
}, [isDragging, dragScrollRaf, setShowPopover]);
|
||||
|
||||
return {
|
||||
rangeSelection,
|
||||
setRangeSelection,
|
||||
shiftClickClipRef,
|
||||
handlePointerDown,
|
||||
handlePointerMove,
|
||||
handlePointerUp,
|
||||
};
|
||||
}
|
||||
|
||||
/* ── Seek + scroll utilities (used in Timeline only) ──────────────── */
|
||||
export function seekTimeFromScrollX(
|
||||
scrollEl: HTMLDivElement,
|
||||
clientX: number,
|
||||
effectiveDuration: number,
|
||||
pps: number,
|
||||
onSeek?: (time: number) => void,
|
||||
): void {
|
||||
const rect = scrollEl.getBoundingClientRect();
|
||||
const x = clientX - rect.left + scrollEl.scrollLeft - GUTTER;
|
||||
if (x < 0) return;
|
||||
const time = Math.max(0, Math.min(effectiveDuration, x / pps));
|
||||
liveTime.notify(time);
|
||||
onSeek?.(time);
|
||||
}
|
||||
Reference in New Issue
Block a user