diff --git a/src/actions/actionToggleAutoSave.tsx b/src/actions/actionToggleAutoSave.tsx new file mode 100644 index 000000000..077dcea30 --- /dev/null +++ b/src/actions/actionToggleAutoSave.tsx @@ -0,0 +1,20 @@ +import { register } from "./register"; +import { AppState } from "../types"; +import { trackEvent } from "../analytics"; + +export const actionToggleAutoSave = register({ + name: "toggleAutoSave", + perform(elements, appState) { + trackEvent("toggle", "autoSave"); + return { + appState: { + ...appState, + autoSave: !appState.autoSave, + }, + commitToHistory: false, + }; + }, + checked: (appState: AppState) => appState.autoSave, + contextItemLabel: "labels.toggleAutoSave", + keyTest: (event) => false, +}); diff --git a/src/actions/index.ts b/src/actions/index.ts index b335e44e6..8b6a900f9 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -74,6 +74,7 @@ export { } from "./actionClipboard"; export { actionToggleGridMode } from "./actionToggleGridMode"; +export { actionToggleAutoSave } from "./actionToggleAutoSave"; export { actionToggleZenMode } from "./actionToggleZenMode"; export { actionToggleStats } from "./actionToggleStats"; diff --git a/src/actions/types.ts b/src/actions/types.ts index 9d90efd88..da71df782 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -48,6 +48,7 @@ export type ActionName = | "changeOpacity" | "changeFontSize" | "toggleCanvasMenu" + | "toggleAutoSave" | "toggleEditMenu" | "undo" | "redo" diff --git a/src/appState.ts b/src/appState.ts index f77e6cffa..080277fe5 100644 --- a/src/appState.ts +++ b/src/appState.ts @@ -13,6 +13,7 @@ export const getDefaultAppState = (): Omit< "offsetTop" | "offsetLeft" > => { return { + autoSave: false, appearance: "light", collaborators: new Map(), currentChartType: "bar", @@ -91,6 +92,7 @@ const APP_STATE_STORAGE_CONF = (< >( config: { [K in keyof T]: K extends keyof AppState ? T[K] : never }, ) => config)({ + autoSave: { browser: true, export: false }, appearance: { browser: true, export: false }, collaborators: { browser: false, export: false }, currentChartType: { browser: true, export: false }, diff --git a/src/components/App.tsx b/src/components/App.tsx index 072a3aaf1..0fcb29db9 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -21,6 +21,7 @@ import { actionSelectAll, actionSendBackward, actionSendToBack, + actionToggleAutoSave, actionToggleGridMode, actionToggleStats, actionToggleZenMode, @@ -52,13 +53,14 @@ import { MIME_TYPES, POINTER_BUTTON, SCROLL_TIMEOUT, + AUTO_SAVE_TIMEOUT, TAP_TWICE_TIMEOUT, TEXT_TO_CENTER_SNAP_THRESHOLD, TOUCH_CTX_MENU_TIMEOUT, ZOOM_STEP, } from "../constants"; import { loadFromBlob } from "../data"; -import { isValidLibrary } from "../data/json"; +import { saveAsJSON, isValidLibrary } from "../data/json"; import { Library } from "../data/library"; import { restore } from "../data/restore"; import { @@ -879,6 +881,13 @@ class App extends React.Component { .querySelector(".excalidraw") ?.classList.toggle("Appearance_dark", this.state.appearance === "dark"); + if (this.state.autoSave) { + this.saveLocalSceneDebounced( + this.scene.getElementsIncludingDeleted(), + this.state, + ); + } + if ( this.state.editingLinearElement && !this.state.selectedElementIds[this.state.editingLinearElement.elementId] @@ -1004,6 +1013,13 @@ class App extends React.Component { this.setState({ ...this.getCanvasOffsets() }); }, SCROLL_TIMEOUT); + private saveLocalSceneDebounced = debounce( + (elements: readonly ExcalidrawElement[], state: AppState) => { + saveAsJSON(elements, state); + }, + AUTO_SAVE_TIMEOUT, + ); + // Copy/paste private onCut = withBatchedUpdates((event: ClipboardEvent) => { @@ -3716,6 +3732,7 @@ class App extends React.Component { typeof this.props.zenModeEnabled === "undefined" && actionToggleZenMode, typeof this.props.viewModeEnabled === "undefined" && actionToggleViewMode, + actionToggleAutoSave, actionToggleStats, ]; @@ -3762,6 +3779,8 @@ class App extends React.Component { actionToggleZenMode, typeof this.props.viewModeEnabled === "undefined" && actionToggleViewMode, + actionToggleAutoSave, + separator, actionToggleStats, ], top: clientY, diff --git a/src/constants.ts b/src/constants.ts index 75bb63911..cadacd31b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -94,6 +94,7 @@ export const TITLE_TIMEOUT = 10000; export const TOAST_TIMEOUT = 5000; export const VERSION_TIMEOUT = 30000; export const SCROLL_TIMEOUT = 500; +export const AUTO_SAVE_TIMEOUT = 500; export const ZOOM_STEP = 0.1; diff --git a/src/locales/en.json b/src/locales/en.json index 937f780a5..de6803287 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -78,6 +78,7 @@ "ungroup": "Ungroup selection", "collaborators": "Collaborators", "showGrid": "Show grid", + "toggleAutoSave": "Auto save", "addToLibrary": "Add to library", "removeFromLibrary": "Remove from library", "libraryLoadingMessage": "Loading library…", diff --git a/src/types.ts b/src/types.ts index ba0a3497e..75b5a7948 100644 --- a/src/types.ts +++ b/src/types.ts @@ -40,6 +40,7 @@ export type Collaborator = { }; export type AppState = { + autoSave: boolean; isLoading: boolean; errorMessage: string | null; draggingElement: NonDeletedExcalidrawElement | null;