|  | @@ -0,0 +1,122 @@
 | 
	
		
			
				|  |  | +import {
 | 
	
		
			
				|  |  | +  FileWithHandle,
 | 
	
		
			
				|  |  | +  fileOpen as _fileOpen,
 | 
	
		
			
				|  |  | +  fileSave as _fileSave,
 | 
	
		
			
				|  |  | +  FileSystemHandle,
 | 
	
		
			
				|  |  | +  supported as nativeFileSystemSupported,
 | 
	
		
			
				|  |  | +} from "@dwelle/browser-fs-access";
 | 
	
		
			
				|  |  | +import { EVENT, MIME_TYPES } from "../constants";
 | 
	
		
			
				|  |  | +import { AbortError } from "../errors";
 | 
	
		
			
				|  |  | +import { debounce } from "../utils";
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +type FILE_EXTENSION =
 | 
	
		
			
				|  |  | +  | "jpg"
 | 
	
		
			
				|  |  | +  | "png"
 | 
	
		
			
				|  |  | +  | "svg"
 | 
	
		
			
				|  |  | +  | "json"
 | 
	
		
			
				|  |  | +  | "excalidraw"
 | 
	
		
			
				|  |  | +  | "excalidrawlib";
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const FILE_TYPE_TO_MIME_TYPE: Record<FILE_EXTENSION, string> = {
 | 
	
		
			
				|  |  | +  jpg: "image/jpeg",
 | 
	
		
			
				|  |  | +  png: "image/png",
 | 
	
		
			
				|  |  | +  svg: "image/svg+xml",
 | 
	
		
			
				|  |  | +  json: "application/json",
 | 
	
		
			
				|  |  | +  excalidraw: MIME_TYPES.excalidraw,
 | 
	
		
			
				|  |  | +  excalidrawlib: MIME_TYPES.excalidrawlib,
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const INPUT_CHANGE_INTERVAL_MS = 500;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +export const fileOpen = <M extends boolean | undefined = false>(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(FILE_TYPE_TO_MIME_TYPE[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<RetType>;
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +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 };
 |