import { memo, useCallback, useEffect, useRef, useState } from "react"; interface CompositionsTabProps { projectId: string; compositions: string[]; activeComposition: string | null; onSelect: (comp: string) => void; onRenderComposition?: (comp: string) => void; isRendering?: boolean; lintFindingsByFile?: Map; } const DEFAULT_PREVIEW_STAGE = { width: 1920, height: 1080 }; const CARD_W = 80; const CARD_H = 45; const THUMBNAIL_SEEK_TIME_SECONDS = 3; const THUMBNAIL_PLAYBACK_SYNC_ATTEMPTS = 10; type PreviewWindow = Window & { __player?: { play?: () => void; pause?: () => void; seek?: (time: number) => void; getDuration?: () => number; }; }; export function resolveCompositionPreviewScale(input: { cardWidth: number; cardHeight: number; stageWidth: number; stageHeight: number; }): number { const safeStageWidth = Number.isFinite(input.stageWidth) && input.stageWidth > 0 ? input.stageWidth : DEFAULT_PREVIEW_STAGE.width; const safeStageHeight = Number.isFinite(input.stageHeight) && input.stageHeight > 0 ? input.stageHeight : DEFAULT_PREVIEW_STAGE.height; const scaleX = input.cardWidth / safeStageWidth; const scaleY = input.cardHeight / safeStageHeight; return Math.min(scaleX, scaleY); } export function resolveThumbnailSeekTime(durationSeconds: number | null | undefined): number { if ( Number.isFinite(durationSeconds) && durationSeconds != null && durationSeconds > 0 && durationSeconds < THUMBNAIL_SEEK_TIME_SECONDS ) { return durationSeconds / 2; } return THUMBNAIL_SEEK_TIME_SECONDS; } function parsePositiveNumber(value: string | null): number | null { if (value == null) return null; const parsed = Number.parseFloat(value); return Number.isFinite(parsed) && parsed > 0 ? parsed : null; } // fallow-ignore-next-line complexity function resolveIframeDuration(iframe: HTMLIFrameElement | null): number | null { 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 */ } 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 { try { const player = (iframe?.contentWindow as PreviewWindow | null)?.__player; if (!player) return false; if (shouldPlay) { player.play?.(); return true; } player.pause?.(); player.seek?.(resolveThumbnailSeekTime(resolveIframeDuration(iframe))); return true; } catch { return false; } } function CompCard({ projectId, comp, isActive, onSelect, onRender, isRendering, lintInfo, }: { projectId: string; comp: string; isActive: boolean; onSelect: () => void; onRender?: () => void; isRendering?: boolean; lintInfo?: { count: number; messages: string[] }; }) { const [hovered, setHovered] = useState(false); const [stageSize, setStageSize] = useState(DEFAULT_PREVIEW_STAGE); const iframeRef = useRef(null); const hoverTimer = useRef | null>(null); const syncTimer = useRef | null>(null); const requestIframePlaybackSync = useCallback((shouldPlay: boolean) => { if (syncTimer.current) { clearTimeout(syncTimer.current); syncTimer.current = null; } const sync = (remainingAttempts: number) => { if (syncIframePlayback(iframeRef.current, shouldPlay) || remainingAttempts <= 0) return; syncTimer.current = setTimeout(() => sync(remainingAttempts - 1), 100); }; sync(THUMBNAIL_PLAYBACK_SYNC_ATTEMPTS); }, []); const handleEnter = () => { hoverTimer.current = setTimeout(() => setHovered(true), 300); }; const handleLeave = () => { if (hoverTimer.current) { clearTimeout(hoverTimer.current); hoverTimer.current = null; } setHovered(false); }; const name = comp.replace(/^compositions\//, "").replace(/\.html$/, ""); const previewUrl = `/api/projects/${projectId}/preview/comp/${comp}`; const previewScale = resolveCompositionPreviewScale({ cardWidth: CARD_W, cardHeight: CARD_H, stageWidth: stageSize.width, stageHeight: stageSize.height, }); const thumbnailOffsetX = (CARD_W - stageSize.width * previewScale) / 2; const thumbnailOffsetY = (CARD_H - stageSize.height * previewScale) / 2; useEffect(() => { requestIframePlaybackSync(hovered); }, [hovered, requestIframePlaybackSync]); useEffect(() => { return () => { if (hoverTimer.current) clearTimeout(hoverTimer.current); if (syncTimer.current) clearTimeout(syncTimer.current); }; }, []); return (