Browse Source
* feat: support exporting json to excalidraw plus * add Firebase Storage rules to codebase * factor the onClick handler out * move excal icon to icons.tsx * handle export errorpull/3692/head
9 changed files with 187 additions and 11 deletions
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
rules_version = '2'; |
||||
service firebase.storage { |
||||
match /b/{bucket}/o { |
||||
match /{migrations} { |
||||
match /{scenes}/{scene} { |
||||
allow get, write: if true; |
||||
// redundant, but let's be explicit' |
||||
allow list: if false; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,92 @@
@@ -0,0 +1,92 @@
|
||||
import React from "react"; |
||||
import { Card } from "../../components/Card"; |
||||
import { ToolButton } from "../../components/ToolButton"; |
||||
import { serializeAsJSON } from "../../data/json"; |
||||
import { getImportedKey, createIV, generateEncryptionKey } from "../data"; |
||||
import { loadFirebaseStorage } from "../data/firebase"; |
||||
import { NonDeletedExcalidrawElement } from "../../element/types"; |
||||
import { AppState } from "../../types"; |
||||
import { nanoid } from "nanoid"; |
||||
import { t } from "../../i18n"; |
||||
import { excalidrawPlusIcon } from "./icons"; |
||||
|
||||
const encryptData = async ( |
||||
key: string, |
||||
json: string, |
||||
): Promise<{ blob: Blob; iv: Uint8Array }> => { |
||||
const importedKey = await getImportedKey(key, "encrypt"); |
||||
const iv = createIV(); |
||||
const encoded = new TextEncoder().encode(json); |
||||
const ciphertext = await window.crypto.subtle.encrypt( |
||||
{ |
||||
name: "AES-GCM", |
||||
iv, |
||||
}, |
||||
importedKey, |
||||
encoded, |
||||
); |
||||
|
||||
return { blob: new Blob([new Uint8Array(ciphertext)]), iv }; |
||||
}; |
||||
|
||||
const exportToExcalidrawPlus = async ( |
||||
elements: readonly NonDeletedExcalidrawElement[], |
||||
appState: AppState, |
||||
) => { |
||||
const firebase = await loadFirebaseStorage(); |
||||
|
||||
const id = `${nanoid(12)}`; |
||||
|
||||
const key = (await generateEncryptionKey())!; |
||||
const encryptedData = await encryptData( |
||||
key, |
||||
serializeAsJSON(elements, appState), |
||||
); |
||||
|
||||
const blob = new Blob([encryptedData.iv, encryptedData.blob], { |
||||
type: "application/octet-stream", |
||||
}); |
||||
|
||||
await firebase |
||||
.storage() |
||||
.ref(`/migrations/scenes/${id}`) |
||||
.put(blob, { |
||||
customMetadata: { |
||||
data: JSON.stringify({ version: 1, name: appState.name }), |
||||
created: Date.now().toString(), |
||||
}, |
||||
}); |
||||
|
||||
window.open(`https://plus.excalidraw.com/import?excalidraw=${id},${key}`); |
||||
}; |
||||
|
||||
export const ExportToExcalidrawPlus: React.FC<{ |
||||
elements: readonly NonDeletedExcalidrawElement[]; |
||||
appState: AppState; |
||||
onError: (error: Error) => void; |
||||
}> = ({ elements, appState, onError }) => { |
||||
return ( |
||||
<Card color="indigo"> |
||||
<div className="Card-icon">{excalidrawPlusIcon}</div> |
||||
<h2>Excalidraw+</h2> |
||||
<div className="Card-details"> |
||||
{t("exportDialog.excalidrawplus_description")} |
||||
</div> |
||||
<ToolButton |
||||
className="Card-button" |
||||
type="button" |
||||
title={t("exportDialog.excalidrawplus_button")} |
||||
aria-label={t("exportDialog.excalidrawplus_button")} |
||||
showAriaLabel={true} |
||||
onClick={async () => { |
||||
try { |
||||
await exportToExcalidrawPlus(elements, appState); |
||||
} catch (error) { |
||||
console.error(error); |
||||
onError(new Error(t("exportDialog.excalidrawplus_exportError"))); |
||||
} |
||||
}} |
||||
/> |
||||
</Card> |
||||
); |
||||
}; |
||||
File diff suppressed because one or more lines are too long
Loading…
Reference in new issue