ExportToExcalidrawPlus.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. import React from "react";
  2. import { Card } from "../../packages/excalidraw/components/Card";
  3. import { ToolButton } from "../../packages/excalidraw/components/ToolButton";
  4. import { serializeAsJSON } from "../../packages/excalidraw/data/json";
  5. import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
  6. import {
  7. FileId,
  8. NonDeletedExcalidrawElement,
  9. } from "../../packages/excalidraw/element/types";
  10. import {
  11. AppState,
  12. BinaryFileData,
  13. BinaryFiles,
  14. } from "../../packages/excalidraw/types";
  15. import { nanoid } from "nanoid";
  16. import { useI18n } from "../../packages/excalidraw/i18n";
  17. import {
  18. encryptData,
  19. generateEncryptionKey,
  20. } from "../../packages/excalidraw/data/encryption";
  21. import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
  22. import { FILE_UPLOAD_MAX_BYTES } from "../app_constants";
  23. import { encodeFilesForUpload } from "../data/FileManager";
  24. import { MIME_TYPES } from "../../packages/excalidraw/constants";
  25. import { trackEvent } from "../../packages/excalidraw/analytics";
  26. import { getFrame } from "../../packages/excalidraw/utils";
  27. import { ExcalidrawLogo } from "../../packages/excalidraw/components/ExcalidrawLogo";
  28. export const exportToExcalidrawPlus = async (
  29. elements: readonly NonDeletedExcalidrawElement[],
  30. appState: Partial<AppState>,
  31. files: BinaryFiles,
  32. ) => {
  33. const firebase = await loadFirebaseStorage();
  34. const id = `${nanoid(12)}`;
  35. const encryptionKey = (await generateEncryptionKey())!;
  36. const encryptedData = await encryptData(
  37. encryptionKey,
  38. serializeAsJSON(elements, appState, files, "database"),
  39. );
  40. const blob = new Blob(
  41. [encryptedData.iv, new Uint8Array(encryptedData.encryptedBuffer)],
  42. {
  43. type: MIME_TYPES.binary,
  44. },
  45. );
  46. await firebase
  47. .storage()
  48. .ref(`/migrations/scenes/${id}`)
  49. .put(blob, {
  50. customMetadata: {
  51. data: JSON.stringify({ version: 2, name: appState.name }),
  52. created: Date.now().toString(),
  53. },
  54. });
  55. const filesMap = new Map<FileId, BinaryFileData>();
  56. for (const element of elements) {
  57. if (isInitializedImageElement(element) && files[element.fileId]) {
  58. filesMap.set(element.fileId, files[element.fileId]);
  59. }
  60. }
  61. if (filesMap.size) {
  62. const filesToUpload = await encodeFilesForUpload({
  63. files: filesMap,
  64. encryptionKey,
  65. maxBytes: FILE_UPLOAD_MAX_BYTES,
  66. });
  67. await saveFilesToFirebase({
  68. prefix: `/migrations/files/scenes/${id}`,
  69. files: filesToUpload,
  70. });
  71. }
  72. window.open(
  73. `${
  74. import.meta.env.VITE_APP_PLUS_APP
  75. }/import?excalidraw=${id},${encryptionKey}`,
  76. );
  77. };
  78. export const ExportToExcalidrawPlus: React.FC<{
  79. elements: readonly NonDeletedExcalidrawElement[];
  80. appState: Partial<AppState>;
  81. files: BinaryFiles;
  82. onError: (error: Error) => void;
  83. onSuccess: () => void;
  84. }> = ({ elements, appState, files, onError, onSuccess }) => {
  85. const { t } = useI18n();
  86. return (
  87. <Card color="primary">
  88. <div className="Card-icon">
  89. <ExcalidrawLogo
  90. style={{
  91. [`--color-logo-icon` as any]: "#fff",
  92. width: "2.8rem",
  93. height: "2.8rem",
  94. }}
  95. />
  96. </div>
  97. <h2>Excalidraw+</h2>
  98. <div className="Card-details">
  99. {t("exportDialog.excalidrawplus_description")}
  100. </div>
  101. <ToolButton
  102. className="Card-button"
  103. type="button"
  104. title={t("exportDialog.excalidrawplus_button")}
  105. aria-label={t("exportDialog.excalidrawplus_button")}
  106. showAriaLabel={true}
  107. onClick={async () => {
  108. try {
  109. trackEvent("export", "eplus", `ui (${getFrame()})`);
  110. await exportToExcalidrawPlus(elements, appState, files);
  111. onSuccess();
  112. } catch (error: any) {
  113. console.error(error);
  114. if (error.name !== "AbortError") {
  115. onError(new Error(t("exportDialog.excalidrawplus_exportError")));
  116. }
  117. }
  118. }}
  119. />
  120. </Card>
  121. );
  122. };