Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 31 additions & 18 deletions packages/studio/src/components/sidebar/CompositionsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,33 +62,46 @@ function parsePositiveNumber(value: string | null): number | null {
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
}

// fallow-ignore-next-line complexity
function resolveIframeDuration(iframe: HTMLIFrameElement | null): number | null {
const win = iframe?.contentWindow as PreviewWindow | null;
const playerDuration = win?.__player?.getDuration?.();
if (Number.isFinite(playerDuration) && playerDuration != null && playerDuration > 0) {
return playerDuration;
try {
const win = iframe?.contentWindow as PreviewWindow | null;
const playerDuration = win?.__player?.getDuration?.();
if (Number.isFinite(playerDuration) && playerDuration != null && playerDuration > 0) {
return playerDuration;
}
} catch {
/* cross-origin iframe */
}

const doc = iframe?.contentDocument;
const root = doc?.querySelector("[data-composition-id]") ?? doc?.documentElement ?? null;
return (
parsePositiveNumber(root?.getAttribute("data-composition-duration") ?? null) ??
parsePositiveNumber(root?.getAttribute("data-duration") ?? null)
);
try {
const doc = iframe?.contentDocument;
const root = doc?.querySelector("[data-composition-id]") ?? doc?.documentElement ?? null;
return (
parsePositiveNumber(root?.getAttribute("data-composition-duration") ?? null) ??
parsePositiveNumber(root?.getAttribute("data-duration") ?? null)
);
} catch {
return null;
}
}

function syncIframePlayback(iframe: HTMLIFrameElement | null, shouldPlay: boolean): boolean {
const player = (iframe?.contentWindow as PreviewWindow | null)?.__player;
if (!player) return false;
try {
const player = (iframe?.contentWindow as PreviewWindow | null)?.__player;
if (!player) return false;

if (shouldPlay) {
player.play?.();
return true;
}

if (shouldPlay) {
player.play?.();
player.pause?.();
player.seek?.(resolveThumbnailSeekTime(resolveIframeDuration(iframe)));
return true;
} catch {
return false;
}

player.pause?.();
player.seek?.(resolveThumbnailSeekTime(resolveIframeDuration(iframe)));
return true;
}

function CompCard({
Expand Down
99 changes: 70 additions & 29 deletions packages/studio/src/hooks/useAppHotkeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,35 @@ import { STUDIO_MOTION_PATH } from "../components/editor/studioMotion";
import { shouldHandleTimelineToggleHotkey, isEditableTarget } from "../utils/timelineDiscovery";
import { shouldIgnoreHistoryShortcut } from "../utils/studioHelpers";

/** Safely resolves contentWindow for a potentially cross-origin iframe. */
function iframeContentWindow(iframe: HTMLIFrameElement | null): Window | null {
try {
return iframe?.contentWindow ?? null;
} catch {
return null;
}
}

/**
* Handles Cmd/Ctrl+Z (undo) and Cmd/Ctrl+Shift+Z / Ctrl+Y (redo) key events.
* Returns true if the event was handled, false otherwise.
*/
// fallow-ignore-next-line complexity
function handleUndoRedoKey(event: KeyboardEvent, onUndo: () => void, onRedo: () => void): boolean {
const key = event.key.toLowerCase();
if (key === "z" && !event.shiftKey) {
event.preventDefault();
onUndo();
return true;
}
if ((key === "z" && event.shiftKey) || (event.ctrlKey && !event.metaKey && key === "y")) {
event.preventDefault();
onRedo();
return true;
}
return false;
}

// ── Types ──

interface EditHistoryHandle {
Expand Down Expand Up @@ -177,18 +206,15 @@ export function useAppHotkeys({

// Cmd/Ctrl+Z — undo, Cmd/Ctrl+Shift+Z or Ctrl+Y — redo
if (event.metaKey || event.ctrlKey) {
if (!shouldIgnoreHistoryShortcut(event.target)) {
const key = event.key.toLowerCase();
if (key === "z" && !event.shiftKey) {
event.preventDefault();
void handleUndoRef.current();
return;
}
if ((key === "z" && event.shiftKey) || (event.ctrlKey && !event.metaKey && key === "y")) {
event.preventDefault();
void handleRedoRef.current();
return;
}
if (
!shouldIgnoreHistoryShortcut(event.target) &&
handleUndoRedoKey(
event,
() => void handleUndoRef.current(),
() => void handleRedoRef.current(),
)
) {
return;
}

// Cmd/Ctrl+1 — sidebar: Compositions tab
Expand Down Expand Up @@ -310,21 +336,33 @@ export function useAppHotkeys({

const syncPreviewTimelineHotkey = useCallback(
(iframe: HTMLIFrameElement | null) => {
const nextWindow = iframe?.contentWindow ?? null;
const nextWindow = iframeContentWindow(iframe);
if (previewHotkeyWindowRef.current === nextWindow) return;
if (previewHotkeyWindowRef.current) {
previewHotkeyWindowRef.current.removeEventListener("keydown", previewAppKeyDownHandler);
try {
previewHotkeyWindowRef.current.removeEventListener("keydown", previewAppKeyDownHandler);
} catch {
/* cross-origin iframe */
}
}
previewHotkeyWindowRef.current = nextWindow;
nextWindow?.addEventListener("keydown", previewAppKeyDownHandler, true);
try {
nextWindow?.addEventListener("keydown", previewAppKeyDownHandler, true);
} catch {
/* cross-origin iframe */
}
},
[previewAppKeyDownHandler],
);

useEffect(
() => () => {
if (previewHotkeyWindowRef.current) {
previewHotkeyWindowRef.current.removeEventListener("keydown", previewAppKeyDownHandler);
try {
previewHotkeyWindowRef.current.removeEventListener("keydown", previewAppKeyDownHandler);
} catch {
/* cross-origin iframe */
}
previewHotkeyWindowRef.current = null;
}
},
Expand All @@ -336,24 +374,19 @@ export function useAppHotkeys({
const handleHistoryHotkey = useCallback((event: KeyboardEvent) => {
if (!(event.metaKey || event.ctrlKey)) return;
if (shouldIgnoreHistoryShortcut(event.target)) return;
const key = event.key.toLowerCase();
if (key === "z" && !event.shiftKey) {
event.preventDefault();
void handleUndoRef.current();
return;
}
if ((key === "z" && event.shiftKey) || (event.ctrlKey && !event.metaKey && key === "y")) {
event.preventDefault();
void handleRedoRef.current();
}
handleUndoRedoKey(
event,
() => void handleUndoRef.current(),
() => void handleRedoRef.current(),
);
}, []);

const syncPreviewHistoryHotkey = useCallback(
(iframe: HTMLIFrameElement | null) => {
previewHistoryHotkeyCleanupRef.current?.();
previewHistoryHotkeyCleanupRef.current = null;

const win = iframe?.contentWindow ?? null;
const win = iframeContentWindow(iframe);
let doc: Document | null = null;
try {
doc = iframe?.contentDocument ?? null;
Expand All @@ -362,10 +395,18 @@ export function useAppHotkeys({
}
if (!win && !doc) return;

win?.addEventListener("keydown", handleHistoryHotkey, true);
try {
win?.addEventListener("keydown", handleHistoryHotkey, true);
} catch {
/* cross-origin */
}
doc?.addEventListener("keydown", handleHistoryHotkey, true);
previewHistoryHotkeyCleanupRef.current = () => {
win?.removeEventListener("keydown", handleHistoryHotkey, true);
try {
win?.removeEventListener("keydown", handleHistoryHotkey, true);
} catch {
/* cross-origin */
}
doc?.removeEventListener("keydown", handleHistoryHotkey, true);
};
},
Expand Down
7 changes: 6 additions & 1 deletion packages/studio/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,13 @@ function errorProps(value: unknown): {
return { error_message: String(value), error_name: null, stack_trace: null };
}

// fallow-ignore-next-line complexity
function isCompositionAssetError(msg: string): boolean {
return msg.includes("Error fetching") && (msg.includes("404") || msg.includes("Not Found"));
if (msg.includes("Error fetching") && (msg.includes("404") || msg.includes("Not Found")))
return true;
if (msg.includes("unsupported or unrecognizable format")) return true;
if (msg.includes("MEDIA_ERR_SRC_NOT_SUPPORTED")) return true;
return false;
}

const ERROR_CAP = 50;
Expand Down
27 changes: 21 additions & 6 deletions packages/studio/src/player/hooks/usePlaybackKeyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,23 +182,38 @@ export function usePlaybackKeyboard({
playbackKeyDownRef.current = handlePlaybackKeyDown;
playbackKeyUpRef.current = handlePlaybackKeyUp;

// fallow-ignore-next-line complexity
const attachIframeShortcutListeners = useCallback(() => {
iframeShortcutCleanupRef.current?.();
iframeShortcutCleanupRef.current = null;

const iframeWin = iframeRef.current?.contentWindow;
const iframeDoc = iframeRef.current?.contentDocument;
let iframeWin: Window | null = null;
let iframeDoc: Document | null = null;
try {
iframeWin = iframeRef.current?.contentWindow ?? null;
iframeDoc = iframeRef.current?.contentDocument ?? null;
} catch {
return;
}
if (!iframeWin && !iframeDoc) return;

const handleIframeKeyDown = (e: KeyboardEvent) => playbackKeyDownRef.current(e);
const handleIframeKeyUp = (e: KeyboardEvent) => playbackKeyUpRef.current(e);
iframeWin?.addEventListener("keydown", handleIframeKeyDown, true);
iframeWin?.addEventListener("keyup", handleIframeKeyUp, true);
try {
iframeWin?.addEventListener("keydown", handleIframeKeyDown, true);
iframeWin?.addEventListener("keyup", handleIframeKeyUp, true);
} catch {
/* cross-origin iframe */
}
iframeDoc?.addEventListener("keydown", handleIframeKeyDown, true);
iframeDoc?.addEventListener("keyup", handleIframeKeyUp, true);
iframeShortcutCleanupRef.current = () => {
iframeWin?.removeEventListener("keydown", handleIframeKeyDown, true);
iframeWin?.removeEventListener("keyup", handleIframeKeyUp, true);
try {
iframeWin?.removeEventListener("keydown", handleIframeKeyDown, true);
iframeWin?.removeEventListener("keyup", handleIframeKeyUp, true);
} catch {
/* cross-origin iframe */
}
iframeDoc?.removeEventListener("keydown", handleIframeKeyDown, true);
iframeDoc?.removeEventListener("keyup", handleIframeKeyUp, true);
};
Expand Down
Loading