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
This commit is contained in:
Thomas Steiner 2020-06-12 18:35:04 +02:00 committed by GitHub
parent 0ed6a96b6a
commit 5d3867d8ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 74 additions and 26 deletions

6
package-lock.json generated
View File

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

View File

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

View File

@ -1,7 +1,7 @@
import React from "react";
import { ProjectName } from "../components/ProjectName";
import { saveAsJSON, loadFromJSON } from "../data";
import { load, save } from "../components/icons";
import { load, save, saveAs } from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n";
import useIsMobile from "../is-mobile";
@ -65,11 +65,13 @@ export const actionChangeShouldAddWatermark = register({
export const actionSaveScene = register({
name: "saveScene",
perform: (elements, appState, value) => {
saveAsJSON(elements, appState).catch((error) => console.error(error));
saveAsJSON(elements, appState, (window as any).handle).catch((error) =>
console.error(error),
);
return { commitToHistory: false };
},
keyTest: (event) => {
return event.key === "s" && event[KEYS.CTRL_OR_CMD];
return event.key === "s" && event[KEYS.CTRL_OR_CMD] && !event.shiftKey;
},
PanelComponent: ({ updateData }) => (
<ToolButton
@ -83,6 +85,28 @@ export const actionSaveScene = register({
),
});
export const actionSaveAsScene = register({
name: "saveAsScene",
perform: (elements, appState, value) => {
saveAsJSON(elements, appState, null).catch((error) => console.error(error));
return { commitToHistory: false };
},
keyTest: (event) => {
return event.key === "s" && event.shiftKey && event[KEYS.CTRL_OR_CMD];
},
PanelComponent: ({ updateData }) => (
<ToolButton
type="button"
icon={saveAs}
title={t("buttons.saveAs")}
aria-label={t("buttons.saveAs")}
showAriaLabel={useIsMobile()}
hidden={!("chooseFileSystemEntries" in window)}
onClick={() => updateData(null)}
/>
),
});
export const actionLoadScene = register({
name: "loadScene",
perform: (

View File

@ -34,6 +34,7 @@ export {
actionChangeProjectName,
actionChangeExportBackground,
actionSaveScene,
actionSaveAsScene,
actionLoadScene,
} from "./actionExport";

View File

@ -43,6 +43,7 @@ export type ActionName =
| "changeExportBackground"
| "changeShouldAddWatermark"
| "saveScene"
| "saveAsScene"
| "loadScene"
| "duplicateSelection"
| "deleteSelectedElements"

View File

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

View File

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

View File

@ -16,6 +16,7 @@ type ToolButtonBaseProps = {
size?: ToolIconSize;
keyBindingLabel?: string;
showAriaLabel?: boolean;
hidden?: boolean;
visible?: boolean;
selected?: boolean;
className?: string;
@ -44,13 +45,16 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
if (props.type === "button") {
return (
<button
className={`ToolIcon_type_button ToolIcon ${sizeCn}${
props.selected ? " ToolIcon--selected" : ""
} ${props.className} ${
props.visible
className={`ToolIcon_type_button ${
!props.hidden ? "ToolIcon" : ""
} ${sizeCn}${props.selected ? " ToolIcon--selected" : ""} ${
props.className
} ${
props.visible && !props.hidden
? "ToolIcon_type_button--show"
: "ToolIcon_type_button--hide"
}`}
hidden={props.hidden}
title={props.title}
aria-label={props["aria-label"]}
type="button"

View File

@ -39,6 +39,11 @@ export const save = createIcon(
{ width: 448, height: 512 },
);
export const saveAs = createIcon(
"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",
{ width: 448, height: 512 },
);
export const load = createIcon(
"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",
{ width: 576, height: 512, mirror: true },

View File

@ -24,28 +24,38 @@ export const serializeAsJSON = (
export const saveAsJSON = async (
elements: readonly ExcalidrawElement[],
appState: AppState,
fileHandle: any,
) => {
const serialized = serializeAsJSON(elements, appState);
const name = `${appState.name}.excalidraw`;
await fileSave(
new Blob([serialized], {
type: /\b(iPad|iPhone|iPod)\b/.test(navigator.userAgent)
? "application/json"
: "application/vnd.excalidraw+json",
}),
{
fileName: name,
description: "Excalidraw file",
},
(window as any).handle,
);
const blob = new Blob([serialized], {
type: "application/json",
});
// Either "Save as" or non-supporting browser
if (!fileHandle) {
const name = `${appState.name}.excalidraw`;
const handle = await fileSave(
blob,
{
fileName: name,
description: "Excalidraw file",
extensions: ["excalidraw"],
},
fileHandle,
);
(window as any).handle = handle;
return;
}
// "Save"
const writable = await fileHandle.createWritable();
await writable.write(blob);
await writable.close();
};
export const loadFromJSON = async () => {
const blob = await fileOpen({
description: "Excalidraw files",
extensions: ["json", "excalidraw"],
mimeTypes: ["application/json", "application/vnd.excalidraw+json"],
mimeTypes: ["application/json"],
});
return loadFromBlob(blob);
};

View File

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