123 lines
3.4 KiB
TypeScript
123 lines
3.4 KiB
TypeScript
|
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 };
|