ExportToExcalidrawPlus.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. import React from "react";
  2. import { uploadBytes, ref } from "firebase/storage";
  3. import { nanoid } from "nanoid";
  4. import { trackEvent } from "@excalidraw/excalidraw/analytics";
  5. import { Card } from "@excalidraw/excalidraw/components/Card";
  6. import { ExcalidrawLogo } from "@excalidraw/excalidraw/components/ExcalidrawLogo";
  7. import { ToolButton } from "@excalidraw/excalidraw/components/ToolButton";
  8. import { MIME_TYPES, getFrame } from "@excalidraw/common";
  9. import {
  10. encryptData,
  11. generateEncryptionKey,
  12. } from "@excalidraw/excalidraw/data/encryption";
  13. import { serializeAsJSON } from "@excalidraw/excalidraw/data/json";
  14. import { isInitializedImageElement } from "@excalidraw/element/typeChecks";
  15. import { useI18n } from "@excalidraw/excalidraw/i18n";
  16. import type {
  17. FileId,
  18. NonDeletedExcalidrawElement,
  19. } from "@excalidraw/element/types";
  20. import type {
  21. AppState,
  22. BinaryFileData,
  23. BinaryFiles,
  24. } from "@excalidraw/excalidraw/types";
  25. import { FILE_UPLOAD_MAX_BYTES } from "../app_constants";
  26. import { encodeFilesForUpload } from "../data/FileManager";
  27. import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
  28. export const exportToExcalidrawPlus = async (
  29. elements: readonly NonDeletedExcalidrawElement[],
  30. appState: Partial<AppState>,
  31. files: BinaryFiles,
  32. name: string,
  33. ) => {
  34. const storage = await loadFirebaseStorage();
  35. const id = `${nanoid(12)}`;
  36. const encryptionKey = (await generateEncryptionKey())!;
  37. const encryptedData = await encryptData(
  38. encryptionKey,
  39. serializeAsJSON(elements, appState, files, "database"),
  40. );
  41. const blob = new Blob(
  42. [encryptedData.iv, new Uint8Array(encryptedData.encryptedBuffer)],
  43. {
  44. type: MIME_TYPES.binary,
  45. },
  46. );
  47. const storageRef = ref(storage, `/migrations/scenes/${id}`);
  48. await uploadBytes(storageRef, blob, {
  49. customMetadata: {
  50. data: JSON.stringify({ version: 2, name }),
  51. created: Date.now().toString(),
  52. },
  53. });
  54. const filesMap = new Map<FileId, BinaryFileData>();
  55. for (const element of elements) {
  56. if (isInitializedImageElement(element) && files[element.fileId]) {
  57. filesMap.set(element.fileId, files[element.fileId]);
  58. }
  59. }
  60. if (filesMap.size) {
  61. const filesToUpload = await encodeFilesForUpload({
  62. files: filesMap,
  63. encryptionKey,
  64. maxBytes: FILE_UPLOAD_MAX_BYTES,
  65. });
  66. await saveFilesToFirebase({
  67. prefix: `/migrations/files/scenes/${id}`,
  68. files: filesToUpload,
  69. });
  70. }
  71. window.open(
  72. `${
  73. import.meta.env.VITE_APP_PLUS_APP
  74. }/import?excalidraw=${id},${encryptionKey}`,
  75. );
  76. };
  77. export const ExportToExcalidrawPlus: React.FC<{
  78. elements: readonly NonDeletedExcalidrawElement[];
  79. appState: Partial<AppState>;
  80. files: BinaryFiles;
  81. name: string;
  82. onError: (error: Error) => void;
  83. onSuccess: () => void;
  84. }> = ({ elements, appState, files, name, 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, name);
  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. };