Преглед изворни кода

persist fileHandle to IDB across sessions

dwelle пре 4 година
родитељ
комит
ba705a099a
7 измењених фајлова са 71 додато и 3 уклоњено
  1. 1 0
      package.json
  2. 13 2
      src/actions/actionExport.tsx
  3. 11 0
      src/components/App.tsx
  4. 4 0
      src/constants.ts
  5. 28 1
      src/data/json.ts
  6. 9 0
      src/errors.ts
  7. 5 0
      yarn.lock

+ 1 - 0
package.json

@@ -31,6 +31,7 @@
     "clsx": "1.1.1",
     "firebase": "8.3.3",
     "i18next-browser-languagedetector": "6.1.0",
+    "idb-keyval": "5.0.6",
     "lodash.throttle": "4.1.1",
     "nanoid": "3.1.22",
     "open-color": "1.8.0",

+ 13 - 2
src/actions/actionExport.tsx

@@ -14,10 +14,11 @@ import { register } from "./register";
 import { supported as fsSupported } from "browser-fs-access";
 import { CheckboxItem } from "../components/CheckboxItem";
 import { getExportSize } from "../scene/export";
-import { DEFAULT_EXPORT_PADDING, EXPORT_SCALES } from "../constants";
+import { DEFAULT_EXPORT_PADDING, EXPORT_SCALES, IDB_KEYS } from "../constants";
 import { getSelectedElements, isSomeElementSelected } from "../scene";
 import { getNonDeletedElements } from "../element";
 import { ActiveFile } from "../components/ActiveFile";
+import * as idb from "idb-keyval";
 
 export const actionChangeProjectName = register({
   name: "changeProjectName",
@@ -149,7 +150,10 @@ export const actionSaveToActiveFile = register({
       if (error?.name !== "AbortError") {
         console.error(error);
       }
-      return { commitToHistory: false };
+      return {
+        commitToHistory: false,
+        appState: { ...appState, fileHandle: null },
+      };
     }
   },
   keyTest: (event) =>
@@ -170,6 +174,13 @@ export const actionSaveFileToDisk = register({
         ...appState,
         fileHandle: null,
       });
+      try {
+        if (fileHandle) {
+          await idb.set(IDB_KEYS.fileHandle, fileHandle);
+        }
+      } catch (error) {
+        console.error(error);
+      }
       return { commitToHistory: false, appState: { ...appState, fileHandle } };
     } catch (error) {
       if (error?.name !== "AbortError") {

+ 11 - 0
src/components/App.tsx

@@ -52,6 +52,7 @@ import {
   ENV,
   EVENT,
   GRID_SIZE,
+  IDB_KEYS,
   LINE_CONFIRM_THRESHOLD,
   MIME_TYPES,
   MQ_MAX_HEIGHT_LANDSCAPE,
@@ -194,6 +195,7 @@ import LayerUI from "./LayerUI";
 import { Stats } from "./Stats";
 import { Toast } from "./Toast";
 import { actionToggleViewMode } from "../actions/actionToggleViewMode";
+import * as idb from "idb-keyval";
 
 const IsMobileContext = React.createContext(false);
 export const useIsMobile = () => useContext(IsMobileContext);
@@ -807,6 +809,15 @@ class App extends React.Component<AppProps, AppState> {
     } else {
       this.updateDOMRect(this.initializeScene);
     }
+
+    try {
+      const fileHandle = await idb.get(IDB_KEYS.fileHandle);
+      if (fileHandle) {
+        this.setState({ fileHandle });
+      }
+    } catch (error) {
+      console.error(error);
+    }
   }
 
   public componentWillUnmount() {

+ 4 - 0
src/constants.ts

@@ -97,6 +97,10 @@ export const STORAGE_KEYS = {
   LOCAL_STORAGE_LIBRARY: "excalidraw-library",
 } as const;
 
+export const IDB_KEYS = {
+  fileHandle: "fileHandle",
+} as const;
+
 // time in milliseconds
 export const TAP_TWICE_TIMEOUT = 300;
 export const TOUCH_CTX_MENU_TIMEOUT = 500;

+ 28 - 1
src/data/json.ts

@@ -1,4 +1,4 @@
-import { fileOpen, fileSave } from "browser-fs-access";
+import { fileOpen, fileSave, FileSystemHandle } from "browser-fs-access";
 import { cleanAppStateForExport } from "../appState";
 import { EXPORT_DATA_TYPES, EXPORT_SOURCE, MIME_TYPES } from "../constants";
 import { clearElementsForExport } from "../element";
@@ -12,6 +12,7 @@ import {
   ExportedLibraryData,
 } from "./types";
 import Library from "./library";
+import { AbortError } from "../errors";
 
 export const serializeAsJSON = (
   elements: readonly ExcalidrawElement[],
@@ -28,6 +29,26 @@ export const serializeAsJSON = (
   return JSON.stringify(data, null, 2);
 };
 
+// adapted from https://web.dev/file-system-access
+const verifyPermission = async (fileHandle: FileSystemHandle) => {
+  try {
+    const options = { mode: "readwrite" } as any;
+    // Check if permission was already granted. If so, return true.
+    if ((await fileHandle.queryPermission(options)) === "granted") {
+      return true;
+    }
+    // Request permission. If the user grants permission, return true.
+    if ((await fileHandle.requestPermission(options)) === "granted") {
+      return true;
+    }
+    // The user didn't grant permission, so return false.
+    return false;
+  } catch (error) {
+    console.error(error);
+    return false;
+  }
+};
+
 export const saveAsJSON = async (
   elements: readonly ExcalidrawElement[],
   appState: AppState,
@@ -37,6 +58,12 @@ export const saveAsJSON = async (
     type: MIME_TYPES.excalidraw,
   });
 
+  if (appState.fileHandle) {
+    if (!(await verifyPermission(appState.fileHandle))) {
+      throw new AbortError();
+    }
+  }
+
   const fileHandle = await fileSave(
     blob,
     {

+ 9 - 0
src/errors.ts

@@ -1,4 +1,5 @@
 type CANVAS_ERROR_NAMES = "CANVAS_ERROR" | "CANVAS_POSSIBLY_TOO_BIG";
+
 export class CanvasError extends Error {
   constructor(
     message: string = "Couldn't export canvas.",
@@ -9,3 +10,11 @@ export class CanvasError extends Error {
     this.message = message;
   }
 }
+
+export class AbortError extends Error {
+  constructor(message: string = "Request aborted") {
+    super();
+    this.name = "AbortError";
+    this.message = message;
+  }
+}

+ 5 - 0
yarn.lock

@@ -6536,6 +6536,11 @@ icss-utils@^4.0.0, icss-utils@^4.1.1:
   dependencies:
     postcss "^7.0.14"
 
[email protected]:
+  version "5.0.6"
+  resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-5.0.6.tgz#62fe4a6703fb5ec86661f41330c94fda65e6d0e6"
+  integrity sha512-6lJuVbwyo82mKSH6Wq2eHkt9LcbwHAelMIcMe0tP4p20Pod7tTxq9zf0ge2n/YDfMOpDryerfmmYyuQiaFaKOg==
+
 [email protected]:
   version "3.0.2"
   resolved "https://registry.npmjs.org/idb/-/idb-3.0.2.tgz"