persist fileHandle to IDB across sessions
This commit is contained in:
parent
6dee02e320
commit
ba705a099a
@ -31,6 +31,7 @@
|
|||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
"firebase": "8.3.3",
|
"firebase": "8.3.3",
|
||||||
"i18next-browser-languagedetector": "6.1.0",
|
"i18next-browser-languagedetector": "6.1.0",
|
||||||
|
"idb-keyval": "5.0.6",
|
||||||
"lodash.throttle": "4.1.1",
|
"lodash.throttle": "4.1.1",
|
||||||
"nanoid": "3.1.22",
|
"nanoid": "3.1.22",
|
||||||
"open-color": "1.8.0",
|
"open-color": "1.8.0",
|
||||||
|
@ -14,10 +14,11 @@ import { register } from "./register";
|
|||||||
import { supported as fsSupported } from "browser-fs-access";
|
import { supported as fsSupported } from "browser-fs-access";
|
||||||
import { CheckboxItem } from "../components/CheckboxItem";
|
import { CheckboxItem } from "../components/CheckboxItem";
|
||||||
import { getExportSize } from "../scene/export";
|
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 { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { ActiveFile } from "../components/ActiveFile";
|
import { ActiveFile } from "../components/ActiveFile";
|
||||||
|
import * as idb from "idb-keyval";
|
||||||
|
|
||||||
export const actionChangeProjectName = register({
|
export const actionChangeProjectName = register({
|
||||||
name: "changeProjectName",
|
name: "changeProjectName",
|
||||||
@ -149,7 +150,10 @@ export const actionSaveToActiveFile = register({
|
|||||||
if (error?.name !== "AbortError") {
|
if (error?.name !== "AbortError") {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
return { commitToHistory: false };
|
return {
|
||||||
|
commitToHistory: false,
|
||||||
|
appState: { ...appState, fileHandle: null },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
@ -170,6 +174,13 @@ export const actionSaveFileToDisk = register({
|
|||||||
...appState,
|
...appState,
|
||||||
fileHandle: null,
|
fileHandle: null,
|
||||||
});
|
});
|
||||||
|
try {
|
||||||
|
if (fileHandle) {
|
||||||
|
await idb.set(IDB_KEYS.fileHandle, fileHandle);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
return { commitToHistory: false, appState: { ...appState, fileHandle } };
|
return { commitToHistory: false, appState: { ...appState, fileHandle } };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error?.name !== "AbortError") {
|
if (error?.name !== "AbortError") {
|
||||||
|
@ -52,6 +52,7 @@ import {
|
|||||||
ENV,
|
ENV,
|
||||||
EVENT,
|
EVENT,
|
||||||
GRID_SIZE,
|
GRID_SIZE,
|
||||||
|
IDB_KEYS,
|
||||||
LINE_CONFIRM_THRESHOLD,
|
LINE_CONFIRM_THRESHOLD,
|
||||||
MIME_TYPES,
|
MIME_TYPES,
|
||||||
MQ_MAX_HEIGHT_LANDSCAPE,
|
MQ_MAX_HEIGHT_LANDSCAPE,
|
||||||
@ -194,6 +195,7 @@ import LayerUI from "./LayerUI";
|
|||||||
import { Stats } from "./Stats";
|
import { Stats } from "./Stats";
|
||||||
import { Toast } from "./Toast";
|
import { Toast } from "./Toast";
|
||||||
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
|
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
|
||||||
|
import * as idb from "idb-keyval";
|
||||||
|
|
||||||
const IsMobileContext = React.createContext(false);
|
const IsMobileContext = React.createContext(false);
|
||||||
export const useIsMobile = () => useContext(IsMobileContext);
|
export const useIsMobile = () => useContext(IsMobileContext);
|
||||||
@ -807,6 +809,15 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
} else {
|
} else {
|
||||||
this.updateDOMRect(this.initializeScene);
|
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() {
|
public componentWillUnmount() {
|
||||||
|
@ -97,6 +97,10 @@ export const STORAGE_KEYS = {
|
|||||||
LOCAL_STORAGE_LIBRARY: "excalidraw-library",
|
LOCAL_STORAGE_LIBRARY: "excalidraw-library",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const IDB_KEYS = {
|
||||||
|
fileHandle: "fileHandle",
|
||||||
|
} as const;
|
||||||
|
|
||||||
// time in milliseconds
|
// time in milliseconds
|
||||||
export const TAP_TWICE_TIMEOUT = 300;
|
export const TAP_TWICE_TIMEOUT = 300;
|
||||||
export const TOUCH_CTX_MENU_TIMEOUT = 500;
|
export const TOUCH_CTX_MENU_TIMEOUT = 500;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { fileOpen, fileSave } from "browser-fs-access";
|
import { fileOpen, fileSave, FileSystemHandle } from "browser-fs-access";
|
||||||
import { cleanAppStateForExport } from "../appState";
|
import { cleanAppStateForExport } from "../appState";
|
||||||
import { EXPORT_DATA_TYPES, EXPORT_SOURCE, MIME_TYPES } from "../constants";
|
import { EXPORT_DATA_TYPES, EXPORT_SOURCE, MIME_TYPES } from "../constants";
|
||||||
import { clearElementsForExport } from "../element";
|
import { clearElementsForExport } from "../element";
|
||||||
@ -12,6 +12,7 @@ import {
|
|||||||
ExportedLibraryData,
|
ExportedLibraryData,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import Library from "./library";
|
import Library from "./library";
|
||||||
|
import { AbortError } from "../errors";
|
||||||
|
|
||||||
export const serializeAsJSON = (
|
export const serializeAsJSON = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
@ -28,6 +29,26 @@ export const serializeAsJSON = (
|
|||||||
return JSON.stringify(data, null, 2);
|
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 (
|
export const saveAsJSON = async (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
@ -37,6 +58,12 @@ export const saveAsJSON = async (
|
|||||||
type: MIME_TYPES.excalidraw,
|
type: MIME_TYPES.excalidraw,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (appState.fileHandle) {
|
||||||
|
if (!(await verifyPermission(appState.fileHandle))) {
|
||||||
|
throw new AbortError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fileHandle = await fileSave(
|
const fileHandle = await fileSave(
|
||||||
blob,
|
blob,
|
||||||
{
|
{
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
type CANVAS_ERROR_NAMES = "CANVAS_ERROR" | "CANVAS_POSSIBLY_TOO_BIG";
|
type CANVAS_ERROR_NAMES = "CANVAS_ERROR" | "CANVAS_POSSIBLY_TOO_BIG";
|
||||||
|
|
||||||
export class CanvasError extends Error {
|
export class CanvasError extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
message: string = "Couldn't export canvas.",
|
message: string = "Couldn't export canvas.",
|
||||||
@ -9,3 +10,11 @@ export class CanvasError extends Error {
|
|||||||
this.message = message;
|
this.message = message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AbortError extends Error {
|
||||||
|
constructor(message: string = "Request aborted") {
|
||||||
|
super();
|
||||||
|
this.name = "AbortError";
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -6536,6 +6536,11 @@ icss-utils@^4.0.0, icss-utils@^4.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
postcss "^7.0.14"
|
postcss "^7.0.14"
|
||||||
|
|
||||||
|
idb-keyval@5.0.6:
|
||||||
|
version "5.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-5.0.6.tgz#62fe4a6703fb5ec86661f41330c94fda65e6d0e6"
|
||||||
|
integrity sha512-6lJuVbwyo82mKSH6Wq2eHkt9LcbwHAelMIcMe0tP4p20Pod7tTxq9zf0ge2n/YDfMOpDryerfmmYyuQiaFaKOg==
|
||||||
|
|
||||||
idb@3.0.2:
|
idb@3.0.2:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.npmjs.org/idb/-/idb-3.0.2.tgz"
|
resolved "https://registry.npmjs.org/idb/-/idb-3.0.2.tgz"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user