Browse Source

Move file system operations to separate module (#510)

Thomas Steiner 5 năm trước cách đây
mục cha
commit
d1fb824369
4 tập tin đã thay đổi với 123 bổ sung144 xóa
  1. 65 22
      package-lock.json
  2. 7 1
      package.json
  3. 1 0
      src/scene/browser-native.d.ts
  4. 50 121
      src/scene/data.ts

+ 65 - 22
package-lock.json

@@ -2721,6 +2721,11 @@
       "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
       "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
     },
+    "browser-nativefs": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/browser-nativefs/-/browser-nativefs-0.0.5.tgz",
+      "integrity": "sha512-0yS+D32qmIgg7YAUpaSfLEMfG6Co5ajPhbCT7agHsF6PuF6p7VVFNT5x8yAEWLAfPJHyNW/1nxNL54JZLzn6jg=="
+    },
     "browser-process-hrtime": {
       "version": "0.1.3",
       "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz",
@@ -3054,7 +3059,8 @@
             },
             "ansi-regex": {
               "version": "2.1.1",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "aproba": {
               "version": "1.2.0",
@@ -3072,11 +3078,13 @@
             },
             "balanced-match": {
               "version": "1.0.0",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "brace-expansion": {
               "version": "1.1.11",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "balanced-match": "^1.0.0",
                 "concat-map": "0.0.1"
@@ -3089,15 +3097,18 @@
             },
             "code-point-at": {
               "version": "1.1.0",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "concat-map": {
               "version": "0.0.1",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "console-control-strings": {
               "version": "1.1.0",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "core-util-is": {
               "version": "1.0.2",
@@ -3200,7 +3211,8 @@
             },
             "inherits": {
               "version": "2.0.4",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "ini": {
               "version": "1.3.5",
@@ -3210,6 +3222,7 @@
             "is-fullwidth-code-point": {
               "version": "1.0.0",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "number-is-nan": "^1.0.0"
               }
@@ -3222,17 +3235,20 @@
             "minimatch": {
               "version": "3.0.4",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "brace-expansion": "^1.1.7"
               }
             },
             "minimist": {
               "version": "0.0.8",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "minipass": {
               "version": "2.9.0",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "safe-buffer": "^5.1.2",
                 "yallist": "^3.0.0"
@@ -3249,6 +3265,7 @@
             "mkdirp": {
               "version": "0.5.1",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "minimist": "0.0.8"
               }
@@ -3329,7 +3346,8 @@
             },
             "number-is-nan": {
               "version": "1.0.1",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "object-assign": {
               "version": "4.1.1",
@@ -3339,6 +3357,7 @@
             "once": {
               "version": "1.4.0",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "wrappy": "1"
               }
@@ -3414,7 +3433,8 @@
             },
             "safe-buffer": {
               "version": "5.1.2",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "safer-buffer": {
               "version": "2.1.2",
@@ -3444,6 +3464,7 @@
             "string-width": {
               "version": "1.0.2",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "code-point-at": "^1.0.0",
                 "is-fullwidth-code-point": "^1.0.0",
@@ -3461,6 +3482,7 @@
             "strip-ansi": {
               "version": "3.0.1",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "ansi-regex": "^2.0.0"
               }
@@ -3499,11 +3521,13 @@
             },
             "wrappy": {
               "version": "1.0.2",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "yallist": {
               "version": "3.1.1",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             }
           }
         },
@@ -7687,7 +7711,8 @@
             },
             "ansi-regex": {
               "version": "2.1.1",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "aproba": {
               "version": "1.2.0",
@@ -7705,11 +7730,13 @@
             },
             "balanced-match": {
               "version": "1.0.0",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "brace-expansion": {
               "version": "1.1.11",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "balanced-match": "^1.0.0",
                 "concat-map": "0.0.1"
@@ -7722,15 +7749,18 @@
             },
             "code-point-at": {
               "version": "1.1.0",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "concat-map": {
               "version": "0.0.1",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "console-control-strings": {
               "version": "1.1.0",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "core-util-is": {
               "version": "1.0.2",
@@ -7833,7 +7863,8 @@
             },
             "inherits": {
               "version": "2.0.4",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "ini": {
               "version": "1.3.5",
@@ -7843,6 +7874,7 @@
             "is-fullwidth-code-point": {
               "version": "1.0.0",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "number-is-nan": "^1.0.0"
               }
@@ -7855,17 +7887,20 @@
             "minimatch": {
               "version": "3.0.4",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "brace-expansion": "^1.1.7"
               }
             },
             "minimist": {
               "version": "0.0.8",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "minipass": {
               "version": "2.9.0",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "safe-buffer": "^5.1.2",
                 "yallist": "^3.0.0"
@@ -7882,6 +7917,7 @@
             "mkdirp": {
               "version": "0.5.1",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "minimist": "0.0.8"
               }
@@ -7962,7 +7998,8 @@
             },
             "number-is-nan": {
               "version": "1.0.1",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "object-assign": {
               "version": "4.1.1",
@@ -7972,6 +8009,7 @@
             "once": {
               "version": "1.4.0",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "wrappy": "1"
               }
@@ -8047,7 +8085,8 @@
             },
             "safe-buffer": {
               "version": "5.1.2",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "safer-buffer": {
               "version": "2.1.2",
@@ -8077,6 +8116,7 @@
             "string-width": {
               "version": "1.0.2",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "code-point-at": "^1.0.0",
                 "is-fullwidth-code-point": "^1.0.0",
@@ -8094,6 +8134,7 @@
             "strip-ansi": {
               "version": "3.0.1",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "ansi-regex": "^2.0.0"
               }
@@ -8132,11 +8173,13 @@
             },
             "wrappy": {
               "version": "1.0.2",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "yallist": {
               "version": "3.1.1",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             }
           }
         }

+ 7 - 1
package.json

@@ -6,6 +6,7 @@
     "not op_mini all"
   ],
   "dependencies": {
+    "browser-nativefs": "0.0.5",
     "i18next": "19.0.3",
     "i18next-browser-languagedetector": "4.0.1",
     "i18next-xhr-backend": "3.2.2",
@@ -61,5 +62,10 @@
     "test:app": "react-scripts test --env=jsdom --passWithNoTests",
     "test:code": "npm run prettier -- --list-different"
   },
-  "version": "1.0.0"
+  "version": "1.0.0",
+  "license": "MIT",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/excalidraw/excalidraw.git"
+  }
 }

+ 1 - 0
src/scene/browser-native.d.ts

@@ -0,0 +1 @@
+declare module "browser-nativefs";

+ 50 - 121
src/scene/data.ts

@@ -6,70 +6,26 @@ import { AppState } from "../types";
 import { ExportType } from "./types";
 import { getExportCanvasPreview } from "./getExportCanvasPreview";
 import nanoid from "nanoid";
+import { fileOpenPromise, fileSavePromise } from "browser-nativefs";
 
 const LOCAL_STORAGE_KEY = "excalidraw";
 const LOCAL_STORAGE_KEY_STATE = "excalidraw-state";
 const BACKEND_POST = "https://json.excalidraw.com/api/v1/post/";
 const BACKEND_GET = "https://json.excalidraw.com/api/v1/";
 
+let fileOpen: Function;
+let fileSave: Function;
+
+(async () => {
+  fileOpen = (await fileOpenPromise).default;
+  fileSave = (await fileSavePromise).default;
+})();
+
 // TODO: Defined globally, since file handles aren't yet serializable.
 // Once `FileSystemFileHandle` can be serialized, make this
 // part of `AppState`.
 (window as any).handle = null;
 
-function saveFile(name: string, data: string) {
-  // create a temporary <a> elem which we'll use to download the image
-  const link = document.createElement("a");
-  link.setAttribute("download", name);
-  link.setAttribute("href", data);
-  link.click();
-
-  // clean up
-  link.remove();
-}
-
-async function saveFileNative(name: string, data: Blob) {
-  const options = {
-    type: "saveFile",
-    accepts: [
-      {
-        description: `Excalidraw ${
-          data.type === "image/png" ? "image" : "file"
-        }`,
-        extensions: [data.type.split("/")[1]],
-        mimeTypes: [data.type]
-      }
-    ]
-  };
-  try {
-    let handle;
-    if (data.type === "application/json") {
-      // For Excalidraw files (i.e., `application/json` files):
-      // If it exists, write back to a previously opened file.
-      // Else, create a new file.
-      if ((window as any).handle) {
-        handle = (window as any).handle;
-      } else {
-        handle = await (window as any).chooseFileSystemEntries(options);
-        (window as any).handle = handle;
-      }
-    } else {
-      // For image export files (i.e., `image/png` files):
-      // Always create a new file.
-      handle = await (window as any).chooseFileSystemEntries(options);
-    }
-    const writer = await handle.createWriter();
-    await writer.truncate(0);
-    await writer.write(0, data, data.type);
-    await writer.close();
-  } catch (err) {
-    if (err.name !== "AbortError") {
-      console.error(err.name, err.message);
-    }
-    throw err;
-  }
-}
-
 interface DataState {
   elements: readonly ExcalidrawElement[];
   appState: AppState;
@@ -94,17 +50,14 @@ export async function saveAsJSON(
   const serialized = serializeAsJSON(elements, appState);
 
   const name = `${appState.name}.json`;
-  if ("chooseFileSystemEntries" in window) {
-    await saveFileNative(
-      name,
-      new Blob([serialized], { type: "application/json" })
-    );
-  } else {
-    saveFile(
-      name,
-      "data:application/json;charset=utf-8," + encodeURIComponent(serialized)
-    );
-  }
+  await fileSave(
+    new Blob([serialized], { type: "application/json" }),
+    {
+      fileName: name,
+      description: "Excalidraw file"
+    },
+    (window as any).handle
+  );
 }
 
 export async function loadFromJSON() {
@@ -122,57 +75,34 @@ export async function loadFromJSON() {
     return { elements, appState };
   };
 
-  if ("chooseFileSystemEntries" in window) {
-    try {
-      (window as any).handle = await (window as any).chooseFileSystemEntries({
-        accepts: [
-          {
-            description: "Excalidraw files",
-            extensions: ["json"],
-            mimeTypes: ["application/json"]
+  const blob = await fileOpen({
+    description: "Excalidraw files",
+    extensions: ["json"],
+    mimeTypes: ["application/json"]
+  });
+  if (blob.handle) {
+    (window as any).handle = blob.handle;
+  }
+  let contents;
+  if ("text" in Blob) {
+    contents = await blob.text();
+  } else {
+    contents = await (async () => {
+      return new Promise(resolve => {
+        const reader = new FileReader();
+        reader.readAsText(blob, "utf8");
+        reader.onloadend = () => {
+          if (reader.readyState === FileReader.DONE) {
+            resolve(reader.result as string);
           }
-        ]
-      });
-      const file = await (window as any).handle.getFile();
-      const contents = await file.text();
-      const { elements, appState } = updateAppState(contents);
-      return new Promise<DataState>(resolve => {
-        resolve(restore(elements, appState));
+        };
       });
-    } catch (err) {
-      if (err.name !== "AbortError") {
-        console.error(err.name, err.message);
-      }
-      throw err;
-    }
-  } else {
-    const input = document.createElement("input");
-    const reader = new FileReader();
-    input.type = "file";
-    input.accept = ".json";
-
-    input.onchange = () => {
-      if (!input.files!.length) {
-        alert("A file was not selected.");
-        return;
-      }
-
-      reader.readAsText(input.files![0], "utf8");
-    };
-
-    input.click();
-
-    return new Promise<DataState>(resolve => {
-      reader.onloadend = () => {
-        if (reader.readyState === FileReader.DONE) {
-          const { elements, appState } = updateAppState(
-            reader.result as string
-          );
-          resolve(restore(elements, appState));
-        }
-      };
-    });
+    })();
   }
+  const { elements, appState } = updateAppState(contents);
+  return new Promise<DataState>(resolve => {
+    resolve(restore(elements, appState));
+  });
 }
 
 export async function exportToBackend(
@@ -246,15 +176,14 @@ export async function exportCanvas(
 
   if (type === "png") {
     const fileName = `${name}.png`;
-    if ("chooseFileSystemEntries" in window) {
-      tempCanvas.toBlob(async (blob: any) => {
-        if (blob) {
-          await saveFileNative(fileName, blob);
-        }
-      });
-    } else {
-      saveFile(fileName, tempCanvas.toDataURL("image/png"));
-    }
+    tempCanvas.toBlob(async (blob: any) => {
+      if (blob) {
+        await fileSave(blob, {
+          fileName: fileName,
+          description: "Excalidraw image"
+        });
+      }
+    });
   } else if (type === "clipboard") {
     try {
       tempCanvas.toBlob(async function(blob: any) {