import { useEffect, useState } from 'react'; import { authenticatedFetch } from '../../../../utils/api'; import type { CodeEditorFile } from '../../types/types'; import { getPreviewMimeType, type PreviewKind } from '../../utils/previewableFile'; type CodeEditorMediaPreviewProps = { file: CodeEditorFile; kind: PreviewKind; // DB projectId used to build the raw-content URL; falls back to projectPath // for older callers, mirroring useCodeEditorDocument. projectId?: string; isSidebar: boolean; isFullscreen: boolean; onClose: () => void; onToggleFullscreen: () => void; labels: { loading: string; error: string; openInNewTab: string; fullscreen: string; exitFullscreen: string; close: string; }; }; // Reject a "PDF" whose bytes aren't actually a PDF before handing it to the // same-origin iframe, so a mislabeled HTML/SVG file can't run in the app origin. const PDF_HEADER_SCAN_BYTES = 1024; const looksLikePdf = async (blob: Blob): Promise => { const header = await blob.slice(0, PDF_HEADER_SCAN_BYTES).arrayBuffer(); // PDFs must contain the "%PDF-" marker at the very start of the file. return new TextDecoder('latin1').decode(header).includes('%PDF-'); }; export default function CodeEditorMediaPreview({ file, kind, projectId, isSidebar, isFullscreen, onClose, onToggleFullscreen, labels, }: CodeEditorMediaPreviewProps) { const [url, setUrl] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); // Identifies which file the current `url` was loaded for. Rendering is gated on // this so a blob from a previously-opened file can never show under the new // file (the editor reuses this component instance across files). const [loadedKey, setLoadedKey] = useState(null); const sourceKey = `${projectId ?? ''}:${file.path}:${kind}`; useEffect(() => { if (!projectId) { setUrl(null); setLoadedKey(null); setError(labels.error); setLoading(false); return; } let objectUrl: string | null = null; const controller = new AbortController(); const loadMedia = async () => { try { setLoading(true); setError(null); setUrl(null); // The content endpoint requires the auth header, so we fetch the bytes // ourselves and hand the media element a blob URL instead of a bare src. // Fetching a blob (rather than streaming) also lets