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": "17.0.2",
|
||||||
"@types/react-dom": "17.0.1",
|
"@types/react-dom": "17.0.1",
|
||||||
"@types/socket.io-client": "1.4.35",
|
"@types/socket.io-client": "1.4.35",
|
||||||
"browser-fs-access": "0.14.1",
|
"browser-fs-access": "0.14.2",
|
||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
"firebase": "8.2.10",
|
"firebase": "8.2.10",
|
||||||
"i18next-browser-languagedetector": "6.0.1",
|
"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.scene.addCallback(this.onSceneUpdated);
|
||||||
this.addEventListeners();
|
this.addEventListeners();
|
||||||
|
|
||||||
// optim to avoid extra render on init
|
const searchParams = new URLSearchParams(window.location.search.slice(1));
|
||||||
if (
|
|
||||||
|
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.offsetLeft === "number" &&
|
||||||
typeof this.props.offsetTop === "number"
|
typeof this.props.offsetTop === "number"
|
||||||
) {
|
) {
|
||||||
|
// Optimization to avoid extra render on init.
|
||||||
this.initializeScene();
|
this.initializeScene();
|
||||||
} else {
|
} else {
|
||||||
this.setState(this.getCanvasOffsets(this.props), () => {
|
this.setState(this.getCanvasOffsets(this.props), () => {
|
||||||
@ -1278,6 +1283,22 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
this.setState({ toastMessage: null });
|
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) => {
|
public updateScene = withBatchedUpdates((sceneData: SceneData) => {
|
||||||
if (sceneData.commitToHistory) {
|
if (sceneData.commitToHistory) {
|
||||||
history.resumeRecording();
|
history.resumeRecording();
|
||||||
@ -3576,20 +3597,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
console.warn(error.name, error.message);
|
console.warn(error.name, error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loadFromBlob(file, this.state)
|
this.loadFileToCanvas(file);
|
||||||
.then(({ elements, appState }) =>
|
|
||||||
this.syncActionResult({
|
|
||||||
elements,
|
|
||||||
appState: {
|
|
||||||
...(appState || this.state),
|
|
||||||
isLoading: false,
|
|
||||||
},
|
|
||||||
commitToHistory: true,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.catch((error) => {
|
|
||||||
this.setState({ isLoading: false, errorMessage: error.message });
|
|
||||||
});
|
|
||||||
} else if (
|
} else if (
|
||||||
file?.type === MIME_TYPES.excalidrawlib ||
|
file?.type === MIME_TYPES.excalidrawlib ||
|
||||||
file?.name.endsWith(".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 = (
|
private handleCanvasContextMenu = (
|
||||||
event: React.PointerEvent<HTMLCanvasElement>,
|
event: React.PointerEvent<HTMLCanvasElement>,
|
||||||
) => {
|
) => {
|
||||||
|
@ -30,13 +30,13 @@ export const saveAsJSON = async (
|
|||||||
) => {
|
) => {
|
||||||
const serialized = serializeAsJSON(elements, appState);
|
const serialized = serializeAsJSON(elements, appState);
|
||||||
const blob = new Blob([serialized], {
|
const blob = new Blob([serialized], {
|
||||||
type: "application/json",
|
type: MIME_TYPES.excalidraw,
|
||||||
});
|
});
|
||||||
|
|
||||||
const fileHandle = await fileSave(
|
const fileHandle = await fileSave(
|
||||||
blob,
|
blob,
|
||||||
{
|
{
|
||||||
fileName: appState.name,
|
fileName: `${appState.name}.excalidraw`,
|
||||||
description: "Excalidraw file",
|
description: "Excalidraw file",
|
||||||
extensions: [".excalidraw"],
|
extensions: [".excalidraw"],
|
||||||
},
|
},
|
||||||
@ -48,8 +48,17 @@ export const saveAsJSON = async (
|
|||||||
export const loadFromJSON = async (localAppState: AppState) => {
|
export const loadFromJSON = async (localAppState: AppState) => {
|
||||||
const blob = await fileOpen({
|
const blob = await fileOpen({
|
||||||
description: "Excalidraw files",
|
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"],
|
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);
|
return loadFromBlob(blob, localAppState);
|
||||||
};
|
};
|
||||||
@ -101,8 +110,11 @@ export const saveLibraryAsJSON = async () => {
|
|||||||
export const importLibraryFromJSON = async () => {
|
export const importLibraryFromJSON = async () => {
|
||||||
const blob = await fileOpen({
|
const blob = await fileOpen({
|
||||||
description: "Excalidraw library files",
|
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"],
|
extensions: [".json", ".excalidrawlib"],
|
||||||
mimeTypes: ["application/json"],
|
*/
|
||||||
});
|
});
|
||||||
Library.importLibrary(blob);
|
Library.importLibrary(blob);
|
||||||
};
|
};
|
||||||
|
@ -47,3 +47,20 @@ workbox.routing.registerRoute(
|
|||||||
plugins: [new workbox.expiration.Plugin({ maxEntries: 10 })],
|
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