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 @@ |
|||||||
|
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 @@ |
|||||||
|
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