ExportToExcalidrawPlus.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. import React from "react";
  2. import { Card } from "@excalidraw/excalidraw/components/Card";
  3. import { ToolButton } from "@excalidraw/excalidraw/components/ToolButton";
  4. import { serializeAsJSON } from "@excalidraw/excalidraw/data/json";
  5. import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
  6. import type {
  7. FileId,
  8. NonDeletedExcalidrawElement,
  9. } from "@excalidraw/excalidraw/element/types";
  10. import type {
  11. AppState,
  12. BinaryFileData,
  13. BinaryFiles,
  14. } from "@excalidraw/excalidraw/types";
  15. import { nanoid } from "nanoid";
  16. import { useI18n } from "@excalidraw/excalidraw/i18n";
  17. import {
  18. encryptData,
  19. generateEncryptionKey,
  20. } from "@excalidraw/excalidraw/data/encryption";
  21. import { isInitializedImageElement } from "@excalidraw/excalidraw/element/typeChecks";
  22. import { FILE_UPLOAD_MAX_BYTES } from "../app_constants";
  23. import { encodeFilesForUpload } from "../data/FileManager";
  24. import { uploadBytes, ref } from "firebase/storage";
  25. import { MIME_TYPES } from "@excalidraw/excalidraw/constants";
  26. import { trackEvent } from "@excalidraw/excalidraw/analytics";
  27. import { getFrame } from "@excalidraw/excalidraw/utils";
  28. import { ExcalidrawLogo } from "@excalidraw/excalidraw/components/ExcalidrawLogo";
  29. export const exportToExcalidrawPlus = async (
  30. elements: readonly NonDeletedExcalidrawElement[],
  31. appState: Partial<AppState>,
  32. files: BinaryFiles,
  33. name: string,
  34. ) => {
  35. const storage = await loadFirebaseStorage();
  36. const id = `${nanoid(12)}`;
  37. const encryptionKey = (await generateEncryptionKey())!;
  38. const encryptedData = await encryptData(
  39. encryptionKey,
  40. serializeAsJSON(elements, appState, files, "database"),
  41. );
  42. const blob = new Blob(
  43. [encryptedData.iv, new Uint8Array(encryptedData.encryptedBuffer)],
  44. {
  45. type: MIME_TYPES.binary,
  46. },
  47. );
  48. const storageRef = ref(storage, `/migrations/scenes/${id}`);
  49. await uploadBytes(storageRef, blob, {
  50. customMetadata: {
  51. data: JSON.stringify({ version: 2, 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. name: string;
  83. onError: (error: Error) => void;
  84. onSuccess: () => void;
  85. }> = ({ elements, appState, files, name, onError, onSuccess }) => {
  86. const { t } = useI18n();
  87. return (
  88. <Card color="primary">
  89. <div className="Card-icon">
  90. <ExcalidrawLogo
  91. style={{
  92. [`--color-logo-icon` as any]: "#fff",
  93. width: "2.8rem",
  94. height: "2.8rem",
  95. }}
  96. />
  97. </div>
  98. <h2>Excalidraw+</h2>
  99. <div className="Card-details">
  100. {t("exportDialog.excalidrawplus_description")}
  101. </div>
  102. <ToolButton
  103. className="Card-button"
  104. type="button"
  105. title={t("exportDialog.excalidrawplus_button")}
  106. aria-label={t("exportDialog.excalidrawplus_button")}
  107. showAriaLabel={true}
  108. onClick={async () => {
  109. try {
  110. trackEvent("export", "eplus", `ui (${getFrame()})`);
  111. await exportToExcalidrawPlus(elements, appState, files, name);
  112. onSuccess();
  113. } catch (error: any) {
  114. console.error(error);
  115. if (error.name !== "AbortError") {
  116. onError(new Error(t("exportDialog.excalidrawplus_exportError")));
  117. }
  118. }
  119. }}
  120. />
  121. </Card>
  122. );
  123. };