ExportToExcalidrawPlus.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  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 type {
  7. FileId,
  8. NonDeletedExcalidrawElement,
  9. } from "../../packages/excalidraw/element/types";
  10. import type {
  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. name: string,
  33. ) => {
  34. const firebase = 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. await firebase
  48. .storage()
  49. .ref(`/migrations/scenes/${id}`)
  50. .put(blob, {
  51. customMetadata: {
  52. data: JSON.stringify({ version: 2, name }),
  53. created: Date.now().toString(),
  54. },
  55. });
  56. const filesMap = new Map<FileId, BinaryFileData>();
  57. for (const element of elements) {
  58. if (isInitializedImageElement(element) && files[element.fileId]) {
  59. filesMap.set(element.fileId, files[element.fileId]);
  60. }
  61. }
  62. if (filesMap.size) {
  63. const filesToUpload = await encodeFilesForUpload({
  64. files: filesMap,
  65. encryptionKey,
  66. maxBytes: FILE_UPLOAD_MAX_BYTES,
  67. });
  68. await saveFilesToFirebase({
  69. prefix: `/migrations/files/scenes/${id}`,
  70. files: filesToUpload,
  71. });
  72. }
  73. window.open(
  74. `${
  75. import.meta.env.VITE_APP_PLUS_APP
  76. }/import?excalidraw=${id},${encryptionKey}`,
  77. );
  78. };
  79. export const ExportToExcalidrawPlus: React.FC<{
  80. elements: readonly NonDeletedExcalidrawElement[];
  81. appState: Partial<AppState>;
  82. files: BinaryFiles;
  83. name: string;
  84. onError: (error: Error) => void;
  85. onSuccess: () => void;
  86. }> = ({ elements, appState, files, name, onError, onSuccess }) => {
  87. const { t } = useI18n();
  88. return (
  89. <Card color="primary">
  90. <div className="Card-icon">
  91. <ExcalidrawLogo
  92. style={{
  93. [`--color-logo-icon` as any]: "#fff",
  94. width: "2.8rem",
  95. height: "2.8rem",
  96. }}
  97. />
  98. </div>
  99. <h2>Excalidraw+</h2>
  100. <div className="Card-details">
  101. {t("exportDialog.excalidrawplus_description")}
  102. </div>
  103. <ToolButton
  104. className="Card-button"
  105. type="button"
  106. title={t("exportDialog.excalidrawplus_button")}
  107. aria-label={t("exportDialog.excalidrawplus_button")}
  108. showAriaLabel={true}
  109. onClick={async () => {
  110. try {
  111. trackEvent("export", "eplus", `ui (${getFrame()})`);
  112. await exportToExcalidrawPlus(elements, appState, files, name);
  113. onSuccess();
  114. } catch (error: any) {
  115. console.error(error);
  116. if (error.name !== "AbortError") {
  117. onError(new Error(t("exportDialog.excalidrawplus_exportError")));
  118. }
  119. }
  120. }}
  121. />
  122. </Card>
  123. );
  124. };