Sfoglia il codice sorgente

Implement Save without re-prompt and Save as (#1709)

* Implement Save without re-prompt and Save as
Fixes #1668

* Add save-as icon

* Make .excalidraw the default extension

* Only show save as button on supporting browsers
vanilla_orig
Thomas Steiner 5 anni fa
parent
commit
5d3867d8ac
Nessun account collegato all'indirizzo email del committer

+ 3
- 3
package-lock.json Vedi File

@@ -3306,9 +3306,9 @@
3306 3306
       "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
3307 3307
     },
3308 3308
     "browser-nativefs": {
3309
-      "version": "0.8.1",
3310
-      "resolved": "https://registry.npmjs.org/browser-nativefs/-/browser-nativefs-0.8.1.tgz",
3311
-      "integrity": "sha512-5XQTR6eg+/hDBVoOKbCnCqUzhD7IP5RG6jCe+J+EaTHo8EnDxjEj3mod3BiEBc/4NfTLEMbrMzUPPY64KwnmNw=="
3309
+      "version": "0.8.2",
3310
+      "resolved": "https://registry.npmjs.org/browser-nativefs/-/browser-nativefs-0.8.2.tgz",
3311
+      "integrity": "sha512-x1dYA6lkpaLZcvvbQ1+/SSDR9H/fbzlcnKi3BDCvEe3fr3HzV5finUMX8fJspzCmPuP7fGLVO8S3UZ8RhQseFw=="
3312 3312
     },
3313 3313
     "browser-process-hrtime": {
3314 3314
       "version": "1.0.0",

+ 1
- 1
package.json Vedi File

@@ -28,7 +28,7 @@
28 28
     "@types/react": "16.9.35",
29 29
     "@types/react-dom": "16.9.8",
30 30
     "@types/socket.io-client": "1.4.33",
31
-    "browser-nativefs": "0.8.1",
31
+    "browser-nativefs": "0.8.2",
32 32
     "i18next-browser-languagedetector": "4.2.0",
33 33
     "lodash.throttle": "4.1.1",
34 34
     "nanoid": "2.1.11",

+ 27
- 3
src/actions/actionExport.tsx Vedi File

@@ -1,7 +1,7 @@
1 1
 import React from "react";
2 2
 import { ProjectName } from "../components/ProjectName";
3 3
 import { saveAsJSON, loadFromJSON } from "../data";
4
-import { load, save } from "../components/icons";
4
+import { load, save, saveAs } from "../components/icons";
5 5
 import { ToolButton } from "../components/ToolButton";
6 6
 import { t } from "../i18n";
7 7
 import useIsMobile from "../is-mobile";
@@ -65,11 +65,13 @@ export const actionChangeShouldAddWatermark = register({
65 65
 export const actionSaveScene = register({
66 66
   name: "saveScene",
67 67
   perform: (elements, appState, value) => {
68
-    saveAsJSON(elements, appState).catch((error) => console.error(error));
68
+    saveAsJSON(elements, appState, (window as any).handle).catch((error) =>
69
+      console.error(error),
70
+    );
69 71
     return { commitToHistory: false };
70 72
   },
71 73
   keyTest: (event) => {
72
-    return event.key === "s" && event[KEYS.CTRL_OR_CMD];
74
+    return event.key === "s" && event[KEYS.CTRL_OR_CMD] && !event.shiftKey;
73 75
   },
74 76
   PanelComponent: ({ updateData }) => (
75 77
     <ToolButton
@@ -83,6 +85,28 @@ export const actionSaveScene = register({
83 85
   ),
84 86
 });
85 87
 
88
+export const actionSaveAsScene = register({
89
+  name: "saveAsScene",
90
+  perform: (elements, appState, value) => {
91
+    saveAsJSON(elements, appState, null).catch((error) => console.error(error));
92
+    return { commitToHistory: false };
93
+  },
94
+  keyTest: (event) => {
95
+    return event.key === "s" && event.shiftKey && event[KEYS.CTRL_OR_CMD];
96
+  },
97
+  PanelComponent: ({ updateData }) => (
98
+    <ToolButton
99
+      type="button"
100
+      icon={saveAs}
101
+      title={t("buttons.saveAs")}
102
+      aria-label={t("buttons.saveAs")}
103
+      showAriaLabel={useIsMobile()}
104
+      hidden={!("chooseFileSystemEntries" in window)}
105
+      onClick={() => updateData(null)}
106
+    />
107
+  ),
108
+});
109
+
86 110
 export const actionLoadScene = register({
87 111
   name: "loadScene",
88 112
   perform: (

+ 1
- 0
src/actions/index.ts Vedi File

@@ -34,6 +34,7 @@ export {
34 34
   actionChangeProjectName,
35 35
   actionChangeExportBackground,
36 36
   actionSaveScene,
37
+  actionSaveAsScene,
37 38
   actionLoadScene,
38 39
 } from "./actionExport";
39 40
 

+ 1
- 0
src/actions/types.ts Vedi File

@@ -43,6 +43,7 @@ export type ActionName =
43 43
   | "changeExportBackground"
44 44
   | "changeShouldAddWatermark"
45 45
   | "saveScene"
46
+  | "saveAsScene"
46 47
   | "loadScene"
47 48
   | "duplicateSelection"
48 49
   | "deleteSelectedElements"

+ 1
- 0
src/components/LayerUI.tsx Vedi File

@@ -130,6 +130,7 @@ const LayerUI = ({
130 130
           <Stack.Row gap={1} justifyContent="space-between">
131 131
             {actionManager.renderAction("loadScene")}
132 132
             {actionManager.renderAction("saveScene")}
133
+            {actionManager.renderAction("saveAsScene")}
133 134
             {renderExportDialog()}
134 135
             {actionManager.renderAction("clearCanvas")}
135 136
             <RoomDialog

+ 1
- 0
src/components/MobileMenu.tsx Vedi File

@@ -84,6 +84,7 @@ export const MobileMenu = ({
84 84
               <Stack.Col gap={4}>
85 85
                 {actionManager.renderAction("loadScene")}
86 86
                 {actionManager.renderAction("saveScene")}
87
+                {actionManager.renderAction("saveAsScene")}
87 88
                 {exportButton}
88 89
                 {actionManager.renderAction("clearCanvas")}
89 90
                 <RoomDialog

+ 8
- 4
src/components/ToolButton.tsx Vedi File

@@ -16,6 +16,7 @@ type ToolButtonBaseProps = {
16 16
   size?: ToolIconSize;
17 17
   keyBindingLabel?: string;
18 18
   showAriaLabel?: boolean;
19
+  hidden?: boolean;
19 20
   visible?: boolean;
20 21
   selected?: boolean;
21 22
   className?: string;
@@ -44,13 +45,16 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
44 45
   if (props.type === "button") {
45 46
     return (
46 47
       <button
47
-        className={`ToolIcon_type_button ToolIcon ${sizeCn}${
48
-          props.selected ? " ToolIcon--selected" : ""
49
-        } ${props.className} ${
50
-          props.visible
48
+        className={`ToolIcon_type_button ${
49
+          !props.hidden ? "ToolIcon" : ""
50
+        } ${sizeCn}${props.selected ? " ToolIcon--selected" : ""} ${
51
+          props.className
52
+        } ${
53
+          props.visible && !props.hidden
51 54
             ? "ToolIcon_type_button--show"
52 55
             : "ToolIcon_type_button--hide"
53 56
         }`}
57
+        hidden={props.hidden}
54 58
         title={props.title}
55 59
         aria-label={props["aria-label"]}
56 60
         type="button"

+ 5
- 0
src/components/icons.tsx Vedi File

@@ -39,6 +39,11 @@ export const save = createIcon(
39 39
   { width: 448, height: 512 },
40 40
 );
41 41
 
42
+export const saveAs = createIcon(
43
+  "M252 54L203 8a28 27 0 00-20-8H28C12 0 0 12 0 27v195c0 15 12 26 28 26h204c15 0 28-11 28-26V73a28 27 0 00-8-19zM130 213c-21 0-37-16-37-36 0-19 16-35 37-35 20 0 37 16 37 35 0 20-17 36-37 36zm56-169v56c0 4-4 6-7 6H44c-4 0-7-2-7-6V42c0-4 3-7 7-7h133l4 2 3 2a7 7 0 012 5z M296 201l87 95-188 205-78 9c-10 1-19-8-18-20l9-84zm141-14l-41-44a31 31 0 00-46 0l-38 41 87 95 38-42c13-14 13-36 0-50z",
44
+  { width: 448, height: 512 },
45
+);
46
+
42 47
 export const load = createIcon(
43 48
   "M572.694 292.093L500.27 416.248A63.997 63.997 0 0 1 444.989 448H45.025c-18.523 0-30.064-20.093-20.731-36.093l72.424-124.155A64 64 0 0 1 152 256h399.964c18.523 0 30.064 20.093 20.73 36.093zM152 224h328v-48c0-26.51-21.49-48-48-48H272l-64-64H48C21.49 64 0 85.49 0 112v278.046l69.077-118.418C86.214 242.25 117.989 224 152 224z",
44 49
   { width: 576, height: 512, mirror: true },

+ 25
- 15
src/data/json.ts Vedi File

@@ -24,28 +24,38 @@ export const serializeAsJSON = (
24 24
 export const saveAsJSON = async (
25 25
   elements: readonly ExcalidrawElement[],
26 26
   appState: AppState,
27
+  fileHandle: any,
27 28
 ) => {
28 29
   const serialized = serializeAsJSON(elements, appState);
29
-
30
-  const name = `${appState.name}.excalidraw`;
31
-  await fileSave(
32
-    new Blob([serialized], {
33
-      type: /\b(iPad|iPhone|iPod)\b/.test(navigator.userAgent)
34
-        ? "application/json"
35
-        : "application/vnd.excalidraw+json",
36
-    }),
37
-    {
38
-      fileName: name,
39
-      description: "Excalidraw file",
40
-    },
41
-    (window as any).handle,
42
-  );
30
+  const blob = new Blob([serialized], {
31
+    type: "application/json",
32
+  });
33
+  // Either "Save as" or non-supporting browser
34
+  if (!fileHandle) {
35
+    const name = `${appState.name}.excalidraw`;
36
+    const handle = await fileSave(
37
+      blob,
38
+      {
39
+        fileName: name,
40
+        description: "Excalidraw file",
41
+        extensions: ["excalidraw"],
42
+      },
43
+      fileHandle,
44
+    );
45
+    (window as any).handle = handle;
46
+    return;
47
+  }
48
+  // "Save"
49
+  const writable = await fileHandle.createWritable();
50
+  await writable.write(blob);
51
+  await writable.close();
43 52
 };
53
+
44 54
 export const loadFromJSON = async () => {
45 55
   const blob = await fileOpen({
46 56
     description: "Excalidraw files",
47 57
     extensions: ["json", "excalidraw"],
48
-    mimeTypes: ["application/json", "application/vnd.excalidraw+json"],
58
+    mimeTypes: ["application/json"],
49 59
   });
50 60
   return loadFromBlob(blob);
51 61
 };

+ 1
- 0
src/locales/en.json Vedi File

@@ -71,6 +71,7 @@
71 71
     "copyToClipboard": "Copy to clipboard",
72 72
     "copyPngToClipboard": "Copy PNG to clipboard",
73 73
     "save": "Save",
74
+    "saveAs": "Save as",
74 75
     "load": "Load",
75 76
     "getShareableLink": "Get shareable link",
76 77
     "close": "Close",

Loading…
Annulla
Salva