import { FileWithHandle, fileOpen as _fileOpen, fileSave as _fileSave, FileSystemHandle, supported as nativeFileSystemSupported, } from "browser-fs-access"; import { EVENT, MIME_TYPES } from "../constants"; import { AbortError } from "../errors"; import { debounce } from "../utils"; type FILE_EXTENSION = | "gif" | "jpg" | "png" | "svg" | "json" | "excalidraw" | "excalidrawlib"; const INPUT_CHANGE_INTERVAL_MS = 500; export const fileOpen = (opts: { extensions?: FILE_EXTENSION[]; description: string; multiple?: M; }): Promise< M extends false | undefined ? FileWithHandle : FileWithHandle[] > => { // an unsafe TS hack, alas not much we can do AFAIK type RetType = M extends false | undefined ? FileWithHandle : FileWithHandle[]; const mimeTypes = opts.extensions?.reduce((mimeTypes, type) => { mimeTypes.push(MIME_TYPES[type]); return mimeTypes; }, [] as string[]); const extensions = opts.extensions?.reduce((acc, ext) => { if (ext === "jpg") { return acc.concat(".jpg", ".jpeg"); } return acc.concat(`.${ext}`); }, [] as string[]); return _fileOpen({ description: opts.description, extensions, mimeTypes, multiple: opts.multiple ?? false, legacySetup: (resolve, reject, input) => { const scheduleRejection = debounce(reject, INPUT_CHANGE_INTERVAL_MS); const focusHandler = () => { checkForFile(); document.addEventListener(EVENT.KEYUP, scheduleRejection); document.addEventListener(EVENT.POINTER_UP, scheduleRejection); scheduleRejection(); }; const checkForFile = () => { // this hack might not work when expecting multiple files if (input.files?.length) { const ret = opts.multiple ? [...input.files] : input.files[0]; resolve(ret as RetType); } }; requestAnimationFrame(() => { window.addEventListener(EVENT.FOCUS, focusHandler); }); const interval = window.setInterval(() => { checkForFile(); }, INPUT_CHANGE_INTERVAL_MS); return (rejectPromise) => { clearInterval(interval); scheduleRejection.cancel(); window.removeEventListener(EVENT.FOCUS, focusHandler); document.removeEventListener(EVENT.KEYUP, scheduleRejection); document.removeEventListener(EVENT.POINTER_UP, scheduleRejection); if (rejectPromise) { // so that something is shown in console if we need to debug this console.warn("Opening the file was canceled (legacy-fs)."); rejectPromise(new AbortError()); } }; }, }) as Promise; }; export const fileSave = ( blob: Blob, opts: { /** supply without the extension */ name: string; /** file extension */ extension: FILE_EXTENSION; description: string; /** existing FileSystemHandle */ fileHandle?: FileSystemHandle | null; }, ) => { return _fileSave( blob, { fileName: `${opts.name}.${opts.extension}`, description: opts.description, extensions: [`.${opts.extension}`], }, opts.fileHandle, ); }; export type { FileSystemHandle }; export { nativeFileSystemSupported };