feat: Implement the Web Share Target API (#3230)
* Use the web share target API * Make requested changes * Remove line * Add application/json back * Add application/vnd.excalidraw+json * Add 'POST' check back * Make requested changes * Update src/appState.ts Co-authored-by: Thomas Steiner <tomac@google.com> * Update test * Override initializeScene * Use Excalidraw MIME type * Minor fixes * More MIME type tweaks * More permissive file open * Be overpermissive in file open Co-authored-by: Thomas Steiner <tomac@google.com> Co-authored-by: tomayac <steiner.thomas@gmail.com>
This commit is contained in:
parent
f1daff2437
commit
b9e70ec666
@ -27,7 +27,7 @@
|
||||
"@types/react": "17.0.2",
|
||||
"@types/react-dom": "17.0.1",
|
||||
"@types/socket.io-client": "1.4.35",
|
||||
"browser-fs-access": "0.14.1",
|
||||
"browser-fs-access": "0.14.2",
|
||||
"clsx": "1.1.1",
|
||||
"firebase": "8.2.10",
|
||||
"i18next-browser-languagedetector": "6.0.1",
|
||||
|
@ -26,5 +26,18 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"capture_links": "new_client"
|
||||
"capture_links": "new_client",
|
||||
"share_target": {
|
||||
"action": "/web-share-target",
|
||||
"method": "POST",
|
||||
"enctype": "multipart/form-data",
|
||||
"params": {
|
||||
"files": [
|
||||
{
|
||||
"name": "file",
|
||||
"accept": ["application/vnd.excalidraw+json", "application/json", ".excalidraw"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -737,11 +737,16 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
this.scene.addCallback(this.onSceneUpdated);
|
||||
this.addEventListeners();
|
||||
|
||||
// optim to avoid extra render on init
|
||||
if (
|
||||
const searchParams = new URLSearchParams(window.location.search.slice(1));
|
||||
|
||||
if (searchParams.has("web-share-target")) {
|
||||
// Obtain a file that was shared via the Web Share Target API.
|
||||
this.restoreFileFromShare();
|
||||
} else if (
|
||||
typeof this.props.offsetLeft === "number" &&
|
||||
typeof this.props.offsetTop === "number"
|
||||
) {
|
||||
// Optimization to avoid extra render on init.
|
||||
this.initializeScene();
|
||||
} else {
|
||||
this.setState(this.getCanvasOffsets(this.props), () => {
|
||||
@ -1278,6 +1283,22 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
this.setState({ toastMessage: null });
|
||||
};
|
||||
|
||||
restoreFileFromShare = async () => {
|
||||
try {
|
||||
const webShareTargetCache = await caches.open("web-share-target");
|
||||
|
||||
const file = await webShareTargetCache.match("shared-file");
|
||||
if (file) {
|
||||
const blob = await file.blob();
|
||||
this.loadFileToCanvas(blob);
|
||||
await webShareTargetCache.delete("shared-file");
|
||||
window.history.replaceState(null, APP_NAME, window.location.pathname);
|
||||
}
|
||||
} catch (error) {
|
||||
this.setState({ errorMessage: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
public updateScene = withBatchedUpdates((sceneData: SceneData) => {
|
||||
if (sceneData.commitToHistory) {
|
||||
history.resumeRecording();
|
||||
@ -3576,20 +3597,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
console.warn(error.name, error.message);
|
||||
}
|
||||
}
|
||||
loadFromBlob(file, this.state)
|
||||
.then(({ elements, appState }) =>
|
||||
this.syncActionResult({
|
||||
elements,
|
||||
appState: {
|
||||
...(appState || this.state),
|
||||
isLoading: false,
|
||||
},
|
||||
commitToHistory: true,
|
||||
}),
|
||||
)
|
||||
.catch((error) => {
|
||||
this.setState({ isLoading: false, errorMessage: error.message });
|
||||
});
|
||||
this.loadFileToCanvas(file);
|
||||
} else if (
|
||||
file?.type === MIME_TYPES.excalidrawlib ||
|
||||
file?.name.endsWith(".excalidrawlib")
|
||||
@ -3609,6 +3617,23 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
}
|
||||
};
|
||||
|
||||
loadFileToCanvas = (file: Blob) => {
|
||||
loadFromBlob(file, this.state)
|
||||
.then(({ elements, appState }) =>
|
||||
this.syncActionResult({
|
||||
elements,
|
||||
appState: {
|
||||
...(appState || this.state),
|
||||
isLoading: false,
|
||||
},
|
||||
commitToHistory: true,
|
||||
}),
|
||||
)
|
||||
.catch((error) => {
|
||||
this.setState({ isLoading: false, errorMessage: error.message });
|
||||
});
|
||||
};
|
||||
|
||||
private handleCanvasContextMenu = (
|
||||
event: React.PointerEvent<HTMLCanvasElement>,
|
||||
) => {
|
||||
|
@ -30,13 +30,13 @@ export const saveAsJSON = async (
|
||||
) => {
|
||||
const serialized = serializeAsJSON(elements, appState);
|
||||
const blob = new Blob([serialized], {
|
||||
type: "application/json",
|
||||
type: MIME_TYPES.excalidraw,
|
||||
});
|
||||
|
||||
const fileHandle = await fileSave(
|
||||
blob,
|
||||
{
|
||||
fileName: appState.name,
|
||||
fileName: `${appState.name}.excalidraw`,
|
||||
description: "Excalidraw file",
|
||||
extensions: [".excalidraw"],
|
||||
},
|
||||
@ -48,8 +48,17 @@ export const saveAsJSON = async (
|
||||
export const loadFromJSON = async (localAppState: AppState) => {
|
||||
const blob = await fileOpen({
|
||||
description: "Excalidraw files",
|
||||
// ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
|
||||
// gets resolved. Else, iOS users cannot open `.excalidraw` files.
|
||||
/*
|
||||
extensions: [".json", ".excalidraw", ".png", ".svg"],
|
||||
mimeTypes: ["application/json", "image/png", "image/svg+xml"],
|
||||
mimeTypes: [
|
||||
MIME_TYPES.excalidraw,
|
||||
"application/json",
|
||||
"image/png",
|
||||
"image/svg+xml",
|
||||
],
|
||||
*/
|
||||
});
|
||||
return loadFromBlob(blob, localAppState);
|
||||
};
|
||||
@ -101,8 +110,11 @@ export const saveLibraryAsJSON = async () => {
|
||||
export const importLibraryFromJSON = async () => {
|
||||
const blob = await fileOpen({
|
||||
description: "Excalidraw library files",
|
||||
// ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
|
||||
// gets resolved. Else, iOS users cannot open `.excalidraw` files.
|
||||
/*
|
||||
extensions: [".json", ".excalidrawlib"],
|
||||
mimeTypes: ["application/json"],
|
||||
*/
|
||||
});
|
||||
Library.importLibrary(blob);
|
||||
};
|
||||
|
@ -47,3 +47,20 @@ workbox.routing.registerRoute(
|
||||
plugins: [new workbox.expiration.Plugin({ maxEntries: 10 })],
|
||||
}),
|
||||
);
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
if (
|
||||
event.request.method === "POST" &&
|
||||
event.request.url.endsWith("/web-share-target")
|
||||
) {
|
||||
return event.respondWith(
|
||||
(async () => {
|
||||
const formData = await event.request.formData();
|
||||
const file = formData.get("file");
|
||||
const webShareTargetCache = await caches.open("web-share-target");
|
||||
await webShareTargetCache.put("shared-file", new Response(file));
|
||||
return Response.redirect("/?web-share-target", 303);
|
||||
})(),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user