mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-11-04 12:54:23 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			1123 lines
		
	
	
		
			34 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			1123 lines
		
	
	
		
			34 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import polyfill from "../packages/excalidraw/polyfill";
 | 
						|
import LanguageDetector from "i18next-browser-languagedetector";
 | 
						|
import { useCallback, useEffect, useRef, useState } from "react";
 | 
						|
import { trackEvent } from "../packages/excalidraw/analytics";
 | 
						|
import { getDefaultAppState } from "../packages/excalidraw/appState";
 | 
						|
import { ErrorDialog } from "../packages/excalidraw/components/ErrorDialog";
 | 
						|
import { TopErrorBoundary } from "./components/TopErrorBoundary";
 | 
						|
import {
 | 
						|
  APP_NAME,
 | 
						|
  EVENT,
 | 
						|
  THEME,
 | 
						|
  TITLE_TIMEOUT,
 | 
						|
  VERSION_TIMEOUT,
 | 
						|
} from "../packages/excalidraw/constants";
 | 
						|
import { loadFromBlob } from "../packages/excalidraw/data/blob";
 | 
						|
import {
 | 
						|
  FileId,
 | 
						|
  NonDeletedExcalidrawElement,
 | 
						|
  OrderedExcalidrawElement,
 | 
						|
} from "../packages/excalidraw/element/types";
 | 
						|
import { useCallbackRefState } from "../packages/excalidraw/hooks/useCallbackRefState";
 | 
						|
import { t } from "../packages/excalidraw/i18n";
 | 
						|
import {
 | 
						|
  Excalidraw,
 | 
						|
  defaultLang,
 | 
						|
  LiveCollaborationTrigger,
 | 
						|
  TTDDialog,
 | 
						|
  TTDDialogTrigger,
 | 
						|
} from "../packages/excalidraw/index";
 | 
						|
import {
 | 
						|
  AppState,
 | 
						|
  ExcalidrawImperativeAPI,
 | 
						|
  BinaryFiles,
 | 
						|
  ExcalidrawInitialDataState,
 | 
						|
  UIAppState,
 | 
						|
} from "../packages/excalidraw/types";
 | 
						|
import {
 | 
						|
  debounce,
 | 
						|
  getVersion,
 | 
						|
  getFrame,
 | 
						|
  isTestEnv,
 | 
						|
  preventUnload,
 | 
						|
  ResolvablePromise,
 | 
						|
  resolvablePromise,
 | 
						|
  isRunningInIframe,
 | 
						|
} from "../packages/excalidraw/utils";
 | 
						|
import {
 | 
						|
  FIREBASE_STORAGE_PREFIXES,
 | 
						|
  isExcalidrawPlusSignedUser,
 | 
						|
  STORAGE_KEYS,
 | 
						|
  SYNC_BROWSER_TABS_TIMEOUT,
 | 
						|
} from "./app_constants";
 | 
						|
import Collab, {
 | 
						|
  CollabAPI,
 | 
						|
  collabAPIAtom,
 | 
						|
  isCollaboratingAtom,
 | 
						|
  isOfflineAtom,
 | 
						|
} from "./collab/Collab";
 | 
						|
import {
 | 
						|
  exportToBackend,
 | 
						|
  getCollaborationLinkData,
 | 
						|
  isCollaborationLink,
 | 
						|
  loadScene,
 | 
						|
} from "./data";
 | 
						|
import {
 | 
						|
  importFromLocalStorage,
 | 
						|
  importUsernameFromLocalStorage,
 | 
						|
} from "./data/localStorage";
 | 
						|
import CustomStats from "./CustomStats";
 | 
						|
import {
 | 
						|
  restore,
 | 
						|
  restoreAppState,
 | 
						|
  RestoredDataState,
 | 
						|
} from "../packages/excalidraw/data/restore";
 | 
						|
import {
 | 
						|
  ExportToExcalidrawPlus,
 | 
						|
  exportToExcalidrawPlus,
 | 
						|
} from "./components/ExportToExcalidrawPlus";
 | 
						|
import { updateStaleImageStatuses } from "./data/FileManager";
 | 
						|
import { newElementWith } from "../packages/excalidraw/element/mutateElement";
 | 
						|
import { isInitializedImageElement } from "../packages/excalidraw/element/typeChecks";
 | 
						|
import { loadFilesFromFirebase } from "./data/firebase";
 | 
						|
import {
 | 
						|
  LibraryIndexedDBAdapter,
 | 
						|
  LibraryLocalStorageMigrationAdapter,
 | 
						|
  LocalData,
 | 
						|
} from "./data/LocalData";
 | 
						|
import { isBrowserStorageStateNewer } from "./data/tabSync";
 | 
						|
import clsx from "clsx";
 | 
						|
import {
 | 
						|
  parseLibraryTokensFromUrl,
 | 
						|
  useHandleLibrary,
 | 
						|
} from "../packages/excalidraw/data/library";
 | 
						|
import { AppMainMenu } from "./components/AppMainMenu";
 | 
						|
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
 | 
						|
import { AppFooter } from "./components/AppFooter";
 | 
						|
import { atom, Provider, useAtom, useAtomValue } from "jotai";
 | 
						|
import { useAtomWithInitialValue } from "../packages/excalidraw/jotai";
 | 
						|
import { appJotaiStore } from "./app-jotai";
 | 
						|
 | 
						|
import "./index.scss";
 | 
						|
import { ResolutionType } from "../packages/excalidraw/utility-types";
 | 
						|
import { ShareableLinkDialog } from "../packages/excalidraw/components/ShareableLinkDialog";
 | 
						|
import { openConfirmModal } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirmState";
 | 
						|
import { OverwriteConfirmDialog } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm";
 | 
						|
import Trans from "../packages/excalidraw/components/Trans";
 | 
						|
import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog";
 | 
						|
import CollabError, { collabErrorIndicatorAtom } from "./collab/CollabError";
 | 
						|
import {
 | 
						|
  RemoteExcalidrawElement,
 | 
						|
  reconcileElements,
 | 
						|
} from "../packages/excalidraw/data/reconcile";
 | 
						|
import {
 | 
						|
  CommandPalette,
 | 
						|
  DEFAULT_CATEGORIES,
 | 
						|
} from "../packages/excalidraw/components/CommandPalette/CommandPalette";
 | 
						|
import {
 | 
						|
  GithubIcon,
 | 
						|
  XBrandIcon,
 | 
						|
  DiscordIcon,
 | 
						|
  ExcalLogo,
 | 
						|
  usersIcon,
 | 
						|
  exportToPlus,
 | 
						|
  share,
 | 
						|
  youtubeIcon,
 | 
						|
} from "../packages/excalidraw/components/icons";
 | 
						|
import { appThemeAtom, useHandleAppTheme } from "./useHandleAppTheme";
 | 
						|
 | 
						|
polyfill();
 | 
						|
 | 
						|
window.EXCALIDRAW_THROTTLE_RENDER = true;
 | 
						|
 | 
						|
let isSelfEmbedding = false;
 | 
						|
 | 
						|
if (window.self !== window.top) {
 | 
						|
  try {
 | 
						|
    const parentUrl = new URL(document.referrer);
 | 
						|
    const currentUrl = new URL(window.location.href);
 | 
						|
    if (parentUrl.origin === currentUrl.origin) {
 | 
						|
      isSelfEmbedding = true;
 | 
						|
    }
 | 
						|
  } catch (error) {
 | 
						|
    // ignore
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
const languageDetector = new LanguageDetector();
 | 
						|
languageDetector.init({
 | 
						|
  languageUtils: {},
 | 
						|
});
 | 
						|
 | 
						|
const shareableLinkConfirmDialog = {
 | 
						|
  title: t("overwriteConfirm.modal.shareableLink.title"),
 | 
						|
  description: (
 | 
						|
    <Trans
 | 
						|
      i18nKey="overwriteConfirm.modal.shareableLink.description"
 | 
						|
      bold={(text) => <strong>{text}</strong>}
 | 
						|
      br={() => <br />}
 | 
						|
    />
 | 
						|
  ),
 | 
						|
  actionLabel: t("overwriteConfirm.modal.shareableLink.button"),
 | 
						|
  color: "danger",
 | 
						|
} as const;
 | 
						|
 | 
						|
const initializeScene = async (opts: {
 | 
						|
  collabAPI: CollabAPI | null;
 | 
						|
  excalidrawAPI: ExcalidrawImperativeAPI;
 | 
						|
}): Promise<
 | 
						|
  { scene: ExcalidrawInitialDataState | null } & (
 | 
						|
    | { isExternalScene: true; id: string; key: string }
 | 
						|
    | { isExternalScene: false; id?: null; key?: null }
 | 
						|
  )
 | 
						|
> => {
 | 
						|
  const searchParams = new URLSearchParams(window.location.search);
 | 
						|
  const id = searchParams.get("id");
 | 
						|
  const jsonBackendMatch = window.location.hash.match(
 | 
						|
    /^#json=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/,
 | 
						|
  );
 | 
						|
  const externalUrlMatch = window.location.hash.match(/^#url=(.*)$/);
 | 
						|
 | 
						|
  const localDataState = importFromLocalStorage();
 | 
						|
 | 
						|
  let scene: RestoredDataState & {
 | 
						|
    scrollToContent?: boolean;
 | 
						|
  } = await loadScene(null, null, localDataState);
 | 
						|
 | 
						|
  let roomLinkData = getCollaborationLinkData(window.location.href);
 | 
						|
  const isExternalScene = !!(id || jsonBackendMatch || roomLinkData);
 | 
						|
  if (isExternalScene) {
 | 
						|
    if (
 | 
						|
      // don't prompt if scene is empty
 | 
						|
      !scene.elements.length ||
 | 
						|
      // don't prompt for collab scenes because we don't override local storage
 | 
						|
      roomLinkData ||
 | 
						|
      // otherwise, prompt whether user wants to override current scene
 | 
						|
      (await openConfirmModal(shareableLinkConfirmDialog))
 | 
						|
    ) {
 | 
						|
      if (jsonBackendMatch) {
 | 
						|
        scene = await loadScene(
 | 
						|
          jsonBackendMatch[1],
 | 
						|
          jsonBackendMatch[2],
 | 
						|
          localDataState,
 | 
						|
        );
 | 
						|
      }
 | 
						|
      scene.scrollToContent = true;
 | 
						|
      if (!roomLinkData) {
 | 
						|
        window.history.replaceState({}, APP_NAME, window.location.origin);
 | 
						|
      }
 | 
						|
    } else {
 | 
						|
      // https://github.com/excalidraw/excalidraw/issues/1919
 | 
						|
      if (document.hidden) {
 | 
						|
        return new Promise((resolve, reject) => {
 | 
						|
          window.addEventListener(
 | 
						|
            "focus",
 | 
						|
            () => initializeScene(opts).then(resolve).catch(reject),
 | 
						|
            {
 | 
						|
              once: true,
 | 
						|
            },
 | 
						|
          );
 | 
						|
        });
 | 
						|
      }
 | 
						|
 | 
						|
      roomLinkData = null;
 | 
						|
      window.history.replaceState({}, APP_NAME, window.location.origin);
 | 
						|
    }
 | 
						|
  } else if (externalUrlMatch) {
 | 
						|
    window.history.replaceState({}, APP_NAME, window.location.origin);
 | 
						|
 | 
						|
    const url = externalUrlMatch[1];
 | 
						|
    try {
 | 
						|
      const request = await fetch(window.decodeURIComponent(url));
 | 
						|
      const data = await loadFromBlob(await request.blob(), null, null);
 | 
						|
      if (
 | 
						|
        !scene.elements.length ||
 | 
						|
        (await openConfirmModal(shareableLinkConfirmDialog))
 | 
						|
      ) {
 | 
						|
        return { scene: data, isExternalScene };
 | 
						|
      }
 | 
						|
    } catch (error: any) {
 | 
						|
      return {
 | 
						|
        scene: {
 | 
						|
          appState: {
 | 
						|
            errorMessage: t("alerts.invalidSceneUrl"),
 | 
						|
          },
 | 
						|
        },
 | 
						|
        isExternalScene,
 | 
						|
      };
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  if (roomLinkData && opts.collabAPI) {
 | 
						|
    const { excalidrawAPI } = opts;
 | 
						|
 | 
						|
    const scene = await opts.collabAPI.startCollaboration(roomLinkData);
 | 
						|
 | 
						|
    return {
 | 
						|
      // when collaborating, the state may have already been updated at this
 | 
						|
      // point (we may have received updates from other clients), so reconcile
 | 
						|
      // elements and appState with existing state
 | 
						|
      scene: {
 | 
						|
        ...scene,
 | 
						|
        appState: {
 | 
						|
          ...restoreAppState(
 | 
						|
            {
 | 
						|
              ...scene?.appState,
 | 
						|
              theme: localDataState?.appState?.theme || scene?.appState?.theme,
 | 
						|
            },
 | 
						|
            excalidrawAPI.getAppState(),
 | 
						|
          ),
 | 
						|
          // necessary if we're invoking from a hashchange handler which doesn't
 | 
						|
          // go through App.initializeScene() that resets this flag
 | 
						|
          isLoading: false,
 | 
						|
        },
 | 
						|
        elements: reconcileElements(
 | 
						|
          scene?.elements || [],
 | 
						|
          excalidrawAPI.getSceneElementsIncludingDeleted() as RemoteExcalidrawElement[],
 | 
						|
          excalidrawAPI.getAppState(),
 | 
						|
        ),
 | 
						|
      },
 | 
						|
      isExternalScene: true,
 | 
						|
      id: roomLinkData.roomId,
 | 
						|
      key: roomLinkData.roomKey,
 | 
						|
    };
 | 
						|
  } else if (scene) {
 | 
						|
    return isExternalScene && jsonBackendMatch
 | 
						|
      ? {
 | 
						|
          scene,
 | 
						|
          isExternalScene,
 | 
						|
          id: jsonBackendMatch[1],
 | 
						|
          key: jsonBackendMatch[2],
 | 
						|
        }
 | 
						|
      : { scene, isExternalScene: false };
 | 
						|
  }
 | 
						|
  return { scene: null, isExternalScene: false };
 | 
						|
};
 | 
						|
 | 
						|
const detectedLangCode = languageDetector.detect() || defaultLang.code;
 | 
						|
export const appLangCodeAtom = atom(
 | 
						|
  Array.isArray(detectedLangCode) ? detectedLangCode[0] : detectedLangCode,
 | 
						|
);
 | 
						|
 | 
						|
const ExcalidrawWrapper = () => {
 | 
						|
  const [errorMessage, setErrorMessage] = useState("");
 | 
						|
  const [langCode, setLangCode] = useAtom(appLangCodeAtom);
 | 
						|
  const isCollabDisabled = isRunningInIframe();
 | 
						|
 | 
						|
  const [appTheme, setAppTheme] = useAtom(appThemeAtom);
 | 
						|
  const { editorTheme } = useHandleAppTheme();
 | 
						|
 | 
						|
  // initial state
 | 
						|
  // ---------------------------------------------------------------------------
 | 
						|
 | 
						|
  const initialStatePromiseRef = useRef<{
 | 
						|
    promise: ResolvablePromise<ExcalidrawInitialDataState | null>;
 | 
						|
  }>({ promise: null! });
 | 
						|
  if (!initialStatePromiseRef.current.promise) {
 | 
						|
    initialStatePromiseRef.current.promise =
 | 
						|
      resolvablePromise<ExcalidrawInitialDataState | null>();
 | 
						|
  }
 | 
						|
 | 
						|
  useEffect(() => {
 | 
						|
    trackEvent("load", "frame", getFrame());
 | 
						|
    // Delayed so that the app has a time to load the latest SW
 | 
						|
    setTimeout(() => {
 | 
						|
      trackEvent("load", "version", getVersion());
 | 
						|
    }, VERSION_TIMEOUT);
 | 
						|
  }, []);
 | 
						|
 | 
						|
  const [excalidrawAPI, excalidrawRefCallback] =
 | 
						|
    useCallbackRefState<ExcalidrawImperativeAPI>();
 | 
						|
 | 
						|
  const [, setShareDialogState] = useAtom(shareDialogStateAtom);
 | 
						|
  const [collabAPI] = useAtom(collabAPIAtom);
 | 
						|
  const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
 | 
						|
    return isCollaborationLink(window.location.href);
 | 
						|
  });
 | 
						|
  const collabError = useAtomValue(collabErrorIndicatorAtom);
 | 
						|
 | 
						|
  useHandleLibrary({
 | 
						|
    excalidrawAPI,
 | 
						|
    adapter: LibraryIndexedDBAdapter,
 | 
						|
    // TODO maybe remove this in several months (shipped: 24-03-11)
 | 
						|
    migrationAdapter: LibraryLocalStorageMigrationAdapter,
 | 
						|
  });
 | 
						|
 | 
						|
  useEffect(() => {
 | 
						|
    if (!excalidrawAPI || (!isCollabDisabled && !collabAPI)) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    const loadImages = (
 | 
						|
      data: ResolutionType<typeof initializeScene>,
 | 
						|
      isInitialLoad = false,
 | 
						|
    ) => {
 | 
						|
      if (!data.scene) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      if (collabAPI?.isCollaborating()) {
 | 
						|
        if (data.scene.elements) {
 | 
						|
          collabAPI
 | 
						|
            .fetchImageFilesFromFirebase({
 | 
						|
              elements: data.scene.elements,
 | 
						|
              forceFetchFiles: true,
 | 
						|
            })
 | 
						|
            .then(({ loadedFiles, erroredFiles }) => {
 | 
						|
              excalidrawAPI.addFiles(loadedFiles);
 | 
						|
              updateStaleImageStatuses({
 | 
						|
                excalidrawAPI,
 | 
						|
                erroredFiles,
 | 
						|
                elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
 | 
						|
              });
 | 
						|
            });
 | 
						|
        }
 | 
						|
      } else {
 | 
						|
        const fileIds =
 | 
						|
          data.scene.elements?.reduce((acc, element) => {
 | 
						|
            if (isInitializedImageElement(element)) {
 | 
						|
              return acc.concat(element.fileId);
 | 
						|
            }
 | 
						|
            return acc;
 | 
						|
          }, [] as FileId[]) || [];
 | 
						|
 | 
						|
        if (data.isExternalScene) {
 | 
						|
          loadFilesFromFirebase(
 | 
						|
            `${FIREBASE_STORAGE_PREFIXES.shareLinkFiles}/${data.id}`,
 | 
						|
            data.key,
 | 
						|
            fileIds,
 | 
						|
          ).then(({ loadedFiles, erroredFiles }) => {
 | 
						|
            excalidrawAPI.addFiles(loadedFiles);
 | 
						|
            updateStaleImageStatuses({
 | 
						|
              excalidrawAPI,
 | 
						|
              erroredFiles,
 | 
						|
              elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
 | 
						|
            });
 | 
						|
          });
 | 
						|
        } else if (isInitialLoad) {
 | 
						|
          if (fileIds.length) {
 | 
						|
            LocalData.fileStorage
 | 
						|
              .getFiles(fileIds)
 | 
						|
              .then(({ loadedFiles, erroredFiles }) => {
 | 
						|
                if (loadedFiles.length) {
 | 
						|
                  excalidrawAPI.addFiles(loadedFiles);
 | 
						|
                }
 | 
						|
                updateStaleImageStatuses({
 | 
						|
                  excalidrawAPI,
 | 
						|
                  erroredFiles,
 | 
						|
                  elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
 | 
						|
                });
 | 
						|
              });
 | 
						|
          }
 | 
						|
          // on fresh load, clear unused files from IDB (from previous
 | 
						|
          // session)
 | 
						|
          LocalData.fileStorage.clearObsoleteFiles({ currentFileIds: fileIds });
 | 
						|
        }
 | 
						|
      }
 | 
						|
    };
 | 
						|
 | 
						|
    initializeScene({ collabAPI, excalidrawAPI }).then(async (data) => {
 | 
						|
      loadImages(data, /* isInitialLoad */ true);
 | 
						|
      initialStatePromiseRef.current.promise.resolve(data.scene);
 | 
						|
    });
 | 
						|
 | 
						|
    const onHashChange = async (event: HashChangeEvent) => {
 | 
						|
      event.preventDefault();
 | 
						|
      const libraryUrlTokens = parseLibraryTokensFromUrl();
 | 
						|
      if (!libraryUrlTokens) {
 | 
						|
        if (
 | 
						|
          collabAPI?.isCollaborating() &&
 | 
						|
          !isCollaborationLink(window.location.href)
 | 
						|
        ) {
 | 
						|
          collabAPI.stopCollaboration(false);
 | 
						|
        }
 | 
						|
        excalidrawAPI.updateScene({ appState: { isLoading: true } });
 | 
						|
 | 
						|
        initializeScene({ collabAPI, excalidrawAPI }).then((data) => {
 | 
						|
          loadImages(data);
 | 
						|
          if (data.scene) {
 | 
						|
            excalidrawAPI.updateScene({
 | 
						|
              ...data.scene,
 | 
						|
              ...restore(data.scene, null, null, { repairBindings: true }),
 | 
						|
              commitToHistory: true,
 | 
						|
            });
 | 
						|
          }
 | 
						|
        });
 | 
						|
      }
 | 
						|
    };
 | 
						|
 | 
						|
    const titleTimeout = setTimeout(
 | 
						|
      () => (document.title = APP_NAME),
 | 
						|
      TITLE_TIMEOUT,
 | 
						|
    );
 | 
						|
 | 
						|
    const syncData = debounce(() => {
 | 
						|
      if (isTestEnv()) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      if (
 | 
						|
        !document.hidden &&
 | 
						|
        ((collabAPI && !collabAPI.isCollaborating()) || isCollabDisabled)
 | 
						|
      ) {
 | 
						|
        // don't sync if local state is newer or identical to browser state
 | 
						|
        if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) {
 | 
						|
          const localDataState = importFromLocalStorage();
 | 
						|
          const username = importUsernameFromLocalStorage();
 | 
						|
          let langCode = languageDetector.detect() || defaultLang.code;
 | 
						|
          if (Array.isArray(langCode)) {
 | 
						|
            langCode = langCode[0];
 | 
						|
          }
 | 
						|
          setLangCode(langCode);
 | 
						|
          excalidrawAPI.updateScene({
 | 
						|
            ...localDataState,
 | 
						|
          });
 | 
						|
          LibraryIndexedDBAdapter.load().then((data) => {
 | 
						|
            if (data) {
 | 
						|
              excalidrawAPI.updateLibrary({
 | 
						|
                libraryItems: data.libraryItems,
 | 
						|
              });
 | 
						|
            }
 | 
						|
          });
 | 
						|
          collabAPI?.setUsername(username || "");
 | 
						|
        }
 | 
						|
 | 
						|
        if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_FILES)) {
 | 
						|
          const elements = excalidrawAPI.getSceneElementsIncludingDeleted();
 | 
						|
          const currFiles = excalidrawAPI.getFiles();
 | 
						|
          const fileIds =
 | 
						|
            elements?.reduce((acc, element) => {
 | 
						|
              if (
 | 
						|
                isInitializedImageElement(element) &&
 | 
						|
                // only load and update images that aren't already loaded
 | 
						|
                !currFiles[element.fileId]
 | 
						|
              ) {
 | 
						|
                return acc.concat(element.fileId);
 | 
						|
              }
 | 
						|
              return acc;
 | 
						|
            }, [] as FileId[]) || [];
 | 
						|
          if (fileIds.length) {
 | 
						|
            LocalData.fileStorage
 | 
						|
              .getFiles(fileIds)
 | 
						|
              .then(({ loadedFiles, erroredFiles }) => {
 | 
						|
                if (loadedFiles.length) {
 | 
						|
                  excalidrawAPI.addFiles(loadedFiles);
 | 
						|
                }
 | 
						|
                updateStaleImageStatuses({
 | 
						|
                  excalidrawAPI,
 | 
						|
                  erroredFiles,
 | 
						|
                  elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
 | 
						|
                });
 | 
						|
              });
 | 
						|
          }
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }, SYNC_BROWSER_TABS_TIMEOUT);
 | 
						|
 | 
						|
    const onUnload = () => {
 | 
						|
      LocalData.flushSave();
 | 
						|
    };
 | 
						|
 | 
						|
    const visibilityChange = (event: FocusEvent | Event) => {
 | 
						|
      if (event.type === EVENT.BLUR || document.hidden) {
 | 
						|
        LocalData.flushSave();
 | 
						|
      }
 | 
						|
      if (
 | 
						|
        event.type === EVENT.VISIBILITY_CHANGE ||
 | 
						|
        event.type === EVENT.FOCUS
 | 
						|
      ) {
 | 
						|
        syncData();
 | 
						|
      }
 | 
						|
    };
 | 
						|
 | 
						|
    window.addEventListener(EVENT.HASHCHANGE, onHashChange, false);
 | 
						|
    window.addEventListener(EVENT.UNLOAD, onUnload, false);
 | 
						|
    window.addEventListener(EVENT.BLUR, visibilityChange, false);
 | 
						|
    document.addEventListener(EVENT.VISIBILITY_CHANGE, visibilityChange, false);
 | 
						|
    window.addEventListener(EVENT.FOCUS, visibilityChange, false);
 | 
						|
    return () => {
 | 
						|
      window.removeEventListener(EVENT.HASHCHANGE, onHashChange, false);
 | 
						|
      window.removeEventListener(EVENT.UNLOAD, onUnload, false);
 | 
						|
      window.removeEventListener(EVENT.BLUR, visibilityChange, false);
 | 
						|
      window.removeEventListener(EVENT.FOCUS, visibilityChange, false);
 | 
						|
      document.removeEventListener(
 | 
						|
        EVENT.VISIBILITY_CHANGE,
 | 
						|
        visibilityChange,
 | 
						|
        false,
 | 
						|
      );
 | 
						|
      clearTimeout(titleTimeout);
 | 
						|
    };
 | 
						|
  }, [isCollabDisabled, collabAPI, excalidrawAPI, setLangCode]);
 | 
						|
 | 
						|
  useEffect(() => {
 | 
						|
    const unloadHandler = (event: BeforeUnloadEvent) => {
 | 
						|
      LocalData.flushSave();
 | 
						|
 | 
						|
      if (
 | 
						|
        excalidrawAPI &&
 | 
						|
        LocalData.fileStorage.shouldPreventUnload(
 | 
						|
          excalidrawAPI.getSceneElements(),
 | 
						|
        )
 | 
						|
      ) {
 | 
						|
        preventUnload(event);
 | 
						|
      }
 | 
						|
    };
 | 
						|
    window.addEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);
 | 
						|
    return () => {
 | 
						|
      window.removeEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);
 | 
						|
    };
 | 
						|
  }, [excalidrawAPI]);
 | 
						|
 | 
						|
  useEffect(() => {
 | 
						|
    languageDetector.cacheUserLanguage(langCode);
 | 
						|
  }, [langCode]);
 | 
						|
 | 
						|
  const onChange = (
 | 
						|
    elements: readonly OrderedExcalidrawElement[],
 | 
						|
    appState: AppState,
 | 
						|
    files: BinaryFiles,
 | 
						|
  ) => {
 | 
						|
    if (collabAPI?.isCollaborating()) {
 | 
						|
      collabAPI.syncElements(elements);
 | 
						|
    }
 | 
						|
 | 
						|
    // this check is redundant, but since this is a hot path, it's best
 | 
						|
    // not to evaludate the nested expression every time
 | 
						|
    if (!LocalData.isSavePaused()) {
 | 
						|
      LocalData.save(elements, appState, files, () => {
 | 
						|
        if (excalidrawAPI) {
 | 
						|
          let didChange = false;
 | 
						|
 | 
						|
          const elements = excalidrawAPI
 | 
						|
            .getSceneElementsIncludingDeleted()
 | 
						|
            .map((element) => {
 | 
						|
              if (
 | 
						|
                LocalData.fileStorage.shouldUpdateImageElementStatus(element)
 | 
						|
              ) {
 | 
						|
                const newElement = newElementWith(element, { status: "saved" });
 | 
						|
                if (newElement !== element) {
 | 
						|
                  didChange = true;
 | 
						|
                }
 | 
						|
                return newElement;
 | 
						|
              }
 | 
						|
              return element;
 | 
						|
            });
 | 
						|
 | 
						|
          if (didChange) {
 | 
						|
            excalidrawAPI.updateScene({
 | 
						|
              elements,
 | 
						|
            });
 | 
						|
          }
 | 
						|
        }
 | 
						|
      });
 | 
						|
    }
 | 
						|
  };
 | 
						|
 | 
						|
  const [latestShareableLink, setLatestShareableLink] = useState<string | null>(
 | 
						|
    null,
 | 
						|
  );
 | 
						|
 | 
						|
  const onExportToBackend = async (
 | 
						|
    exportedElements: readonly NonDeletedExcalidrawElement[],
 | 
						|
    appState: Partial<AppState>,
 | 
						|
    files: BinaryFiles,
 | 
						|
  ) => {
 | 
						|
    if (exportedElements.length === 0) {
 | 
						|
      throw new Error(t("alerts.cannotExportEmptyCanvas"));
 | 
						|
    }
 | 
						|
    try {
 | 
						|
      const { url, errorMessage } = await exportToBackend(
 | 
						|
        exportedElements,
 | 
						|
        {
 | 
						|
          ...appState,
 | 
						|
          viewBackgroundColor: appState.exportBackground
 | 
						|
            ? appState.viewBackgroundColor
 | 
						|
            : getDefaultAppState().viewBackgroundColor,
 | 
						|
        },
 | 
						|
        files,
 | 
						|
      );
 | 
						|
 | 
						|
      if (errorMessage) {
 | 
						|
        throw new Error(errorMessage);
 | 
						|
      }
 | 
						|
 | 
						|
      if (url) {
 | 
						|
        setLatestShareableLink(url);
 | 
						|
      }
 | 
						|
    } catch (error: any) {
 | 
						|
      if (error.name !== "AbortError") {
 | 
						|
        const { width, height } = appState;
 | 
						|
        console.error(error, {
 | 
						|
          width,
 | 
						|
          height,
 | 
						|
          devicePixelRatio: window.devicePixelRatio,
 | 
						|
        });
 | 
						|
        throw new Error(error.message);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  };
 | 
						|
 | 
						|
  const renderCustomStats = (
 | 
						|
    elements: readonly NonDeletedExcalidrawElement[],
 | 
						|
    appState: UIAppState,
 | 
						|
  ) => {
 | 
						|
    return (
 | 
						|
      <CustomStats
 | 
						|
        setToast={(message) => excalidrawAPI!.setToast({ message })}
 | 
						|
        appState={appState}
 | 
						|
        elements={elements}
 | 
						|
      />
 | 
						|
    );
 | 
						|
  };
 | 
						|
 | 
						|
  const isOffline = useAtomValue(isOfflineAtom);
 | 
						|
 | 
						|
  const onCollabDialogOpen = useCallback(
 | 
						|
    () => setShareDialogState({ isOpen: true, type: "collaborationOnly" }),
 | 
						|
    [setShareDialogState],
 | 
						|
  );
 | 
						|
 | 
						|
  // browsers generally prevent infinite self-embedding, there are
 | 
						|
  // cases where it still happens, and while we disallow self-embedding
 | 
						|
  // by not whitelisting our own origin, this serves as an additional guard
 | 
						|
  if (isSelfEmbedding) {
 | 
						|
    return (
 | 
						|
      <div
 | 
						|
        style={{
 | 
						|
          display: "flex",
 | 
						|
          alignItems: "center",
 | 
						|
          justifyContent: "center",
 | 
						|
          textAlign: "center",
 | 
						|
          height: "100%",
 | 
						|
        }}
 | 
						|
      >
 | 
						|
        <h1>I'm not a pretzel!</h1>
 | 
						|
      </div>
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  const ExcalidrawPlusCommand = {
 | 
						|
    label: "Excalidraw+",
 | 
						|
    category: DEFAULT_CATEGORIES.links,
 | 
						|
    predicate: true,
 | 
						|
    icon: <div style={{ width: 14 }}>{ExcalLogo}</div>,
 | 
						|
    keywords: ["plus", "cloud", "server"],
 | 
						|
    perform: () => {
 | 
						|
      window.open(
 | 
						|
        `${
 | 
						|
          import.meta.env.VITE_APP_PLUS_LP
 | 
						|
        }/plus?utm_source=excalidraw&utm_medium=app&utm_content=command_palette`,
 | 
						|
        "_blank",
 | 
						|
      );
 | 
						|
    },
 | 
						|
  };
 | 
						|
  const ExcalidrawPlusAppCommand = {
 | 
						|
    label: "Sign up",
 | 
						|
    category: DEFAULT_CATEGORIES.links,
 | 
						|
    predicate: true,
 | 
						|
    icon: <div style={{ width: 14 }}>{ExcalLogo}</div>,
 | 
						|
    keywords: [
 | 
						|
      "excalidraw",
 | 
						|
      "plus",
 | 
						|
      "cloud",
 | 
						|
      "server",
 | 
						|
      "signin",
 | 
						|
      "login",
 | 
						|
      "signup",
 | 
						|
    ],
 | 
						|
    perform: () => {
 | 
						|
      window.open(
 | 
						|
        `${
 | 
						|
          import.meta.env.VITE_APP_PLUS_APP
 | 
						|
        }?utm_source=excalidraw&utm_medium=app&utm_content=command_palette`,
 | 
						|
        "_blank",
 | 
						|
      );
 | 
						|
    },
 | 
						|
  };
 | 
						|
 | 
						|
  return (
 | 
						|
    <div
 | 
						|
      style={{ height: "100%" }}
 | 
						|
      className={clsx("excalidraw-app", {
 | 
						|
        "is-collaborating": isCollaborating,
 | 
						|
      })}
 | 
						|
    >
 | 
						|
      <Excalidraw
 | 
						|
        excalidrawAPI={excalidrawRefCallback}
 | 
						|
        onChange={onChange}
 | 
						|
        initialData={initialStatePromiseRef.current.promise}
 | 
						|
        isCollaborating={isCollaborating}
 | 
						|
        onPointerUpdate={collabAPI?.onPointerUpdate}
 | 
						|
        UIOptions={{
 | 
						|
          canvasActions: {
 | 
						|
            toggleTheme: true,
 | 
						|
            export: {
 | 
						|
              onExportToBackend,
 | 
						|
              renderCustomUI: excalidrawAPI
 | 
						|
                ? (elements, appState, files) => {
 | 
						|
                    return (
 | 
						|
                      <ExportToExcalidrawPlus
 | 
						|
                        elements={elements}
 | 
						|
                        appState={appState}
 | 
						|
                        files={files}
 | 
						|
                        name={excalidrawAPI.getName()}
 | 
						|
                        onError={(error) => {
 | 
						|
                          excalidrawAPI?.updateScene({
 | 
						|
                            appState: {
 | 
						|
                              errorMessage: error.message,
 | 
						|
                            },
 | 
						|
                          });
 | 
						|
                        }}
 | 
						|
                        onSuccess={() => {
 | 
						|
                          excalidrawAPI.updateScene({
 | 
						|
                            appState: { openDialog: null },
 | 
						|
                          });
 | 
						|
                        }}
 | 
						|
                      />
 | 
						|
                    );
 | 
						|
                  }
 | 
						|
                : undefined,
 | 
						|
            },
 | 
						|
          },
 | 
						|
        }}
 | 
						|
        langCode={langCode}
 | 
						|
        renderCustomStats={renderCustomStats}
 | 
						|
        detectScroll={false}
 | 
						|
        handleKeyboardGlobally={true}
 | 
						|
        autoFocus={true}
 | 
						|
        theme={editorTheme}
 | 
						|
        renderTopRightUI={(isMobile) => {
 | 
						|
          if (isMobile || !collabAPI || isCollabDisabled) {
 | 
						|
            return null;
 | 
						|
          }
 | 
						|
          return (
 | 
						|
            <div className="top-right-ui">
 | 
						|
              {collabError.message && <CollabError collabError={collabError} />}
 | 
						|
              <LiveCollaborationTrigger
 | 
						|
                isCollaborating={isCollaborating}
 | 
						|
                onSelect={() =>
 | 
						|
                  setShareDialogState({ isOpen: true, type: "share" })
 | 
						|
                }
 | 
						|
              />
 | 
						|
            </div>
 | 
						|
          );
 | 
						|
        }}
 | 
						|
      >
 | 
						|
        <AppMainMenu
 | 
						|
          onCollabDialogOpen={onCollabDialogOpen}
 | 
						|
          isCollaborating={isCollaborating}
 | 
						|
          isCollabEnabled={!isCollabDisabled}
 | 
						|
          theme={appTheme}
 | 
						|
          setTheme={(theme) => setAppTheme(theme)}
 | 
						|
        />
 | 
						|
        <AppWelcomeScreen
 | 
						|
          onCollabDialogOpen={onCollabDialogOpen}
 | 
						|
          isCollabEnabled={!isCollabDisabled}
 | 
						|
        />
 | 
						|
        <OverwriteConfirmDialog>
 | 
						|
          <OverwriteConfirmDialog.Actions.ExportToImage />
 | 
						|
          <OverwriteConfirmDialog.Actions.SaveToDisk />
 | 
						|
          {excalidrawAPI && (
 | 
						|
            <OverwriteConfirmDialog.Action
 | 
						|
              title={t("overwriteConfirm.action.excalidrawPlus.title")}
 | 
						|
              actionLabel={t("overwriteConfirm.action.excalidrawPlus.button")}
 | 
						|
              onClick={() => {
 | 
						|
                exportToExcalidrawPlus(
 | 
						|
                  excalidrawAPI.getSceneElements(),
 | 
						|
                  excalidrawAPI.getAppState(),
 | 
						|
                  excalidrawAPI.getFiles(),
 | 
						|
                  excalidrawAPI.getName(),
 | 
						|
                );
 | 
						|
              }}
 | 
						|
            >
 | 
						|
              {t("overwriteConfirm.action.excalidrawPlus.description")}
 | 
						|
            </OverwriteConfirmDialog.Action>
 | 
						|
          )}
 | 
						|
        </OverwriteConfirmDialog>
 | 
						|
        <AppFooter />
 | 
						|
        <TTDDialog
 | 
						|
          onTextSubmit={async (input) => {
 | 
						|
            try {
 | 
						|
              const response = await fetch(
 | 
						|
                `${
 | 
						|
                  import.meta.env.VITE_APP_AI_BACKEND
 | 
						|
                }/v1/ai/text-to-diagram/generate`,
 | 
						|
                {
 | 
						|
                  method: "POST",
 | 
						|
                  headers: {
 | 
						|
                    Accept: "application/json",
 | 
						|
                    "Content-Type": "application/json",
 | 
						|
                  },
 | 
						|
                  body: JSON.stringify({ prompt: input }),
 | 
						|
                },
 | 
						|
              );
 | 
						|
 | 
						|
              const rateLimit = response.headers.has("X-Ratelimit-Limit")
 | 
						|
                ? parseInt(response.headers.get("X-Ratelimit-Limit") || "0", 10)
 | 
						|
                : undefined;
 | 
						|
 | 
						|
              const rateLimitRemaining = response.headers.has(
 | 
						|
                "X-Ratelimit-Remaining",
 | 
						|
              )
 | 
						|
                ? parseInt(
 | 
						|
                    response.headers.get("X-Ratelimit-Remaining") || "0",
 | 
						|
                    10,
 | 
						|
                  )
 | 
						|
                : undefined;
 | 
						|
 | 
						|
              const json = await response.json();
 | 
						|
 | 
						|
              if (!response.ok) {
 | 
						|
                if (response.status === 429) {
 | 
						|
                  return {
 | 
						|
                    rateLimit,
 | 
						|
                    rateLimitRemaining,
 | 
						|
                    error: new Error(
 | 
						|
                      "Too many requests today, please try again tomorrow!",
 | 
						|
                    ),
 | 
						|
                  };
 | 
						|
                }
 | 
						|
 | 
						|
                throw new Error(json.message || "Generation failed...");
 | 
						|
              }
 | 
						|
 | 
						|
              const generatedResponse = json.generatedResponse;
 | 
						|
              if (!generatedResponse) {
 | 
						|
                throw new Error("Generation failed...");
 | 
						|
              }
 | 
						|
 | 
						|
              return { generatedResponse, rateLimit, rateLimitRemaining };
 | 
						|
            } catch (err: any) {
 | 
						|
              throw new Error("Request failed");
 | 
						|
            }
 | 
						|
          }}
 | 
						|
        />
 | 
						|
        <TTDDialogTrigger />
 | 
						|
        {isCollaborating && isOffline && (
 | 
						|
          <div className="collab-offline-warning">
 | 
						|
            {t("alerts.collabOfflineWarning")}
 | 
						|
          </div>
 | 
						|
        )}
 | 
						|
        {latestShareableLink && (
 | 
						|
          <ShareableLinkDialog
 | 
						|
            link={latestShareableLink}
 | 
						|
            onCloseRequest={() => setLatestShareableLink(null)}
 | 
						|
            setErrorMessage={setErrorMessage}
 | 
						|
          />
 | 
						|
        )}
 | 
						|
        {excalidrawAPI && !isCollabDisabled && (
 | 
						|
          <Collab excalidrawAPI={excalidrawAPI} />
 | 
						|
        )}
 | 
						|
 | 
						|
        <ShareDialog
 | 
						|
          collabAPI={collabAPI}
 | 
						|
          onExportToBackend={async () => {
 | 
						|
            if (excalidrawAPI) {
 | 
						|
              try {
 | 
						|
                await onExportToBackend(
 | 
						|
                  excalidrawAPI.getSceneElements(),
 | 
						|
                  excalidrawAPI.getAppState(),
 | 
						|
                  excalidrawAPI.getFiles(),
 | 
						|
                );
 | 
						|
              } catch (error: any) {
 | 
						|
                setErrorMessage(error.message);
 | 
						|
              }
 | 
						|
            }
 | 
						|
          }}
 | 
						|
        />
 | 
						|
 | 
						|
        {errorMessage && (
 | 
						|
          <ErrorDialog onClose={() => setErrorMessage("")}>
 | 
						|
            {errorMessage}
 | 
						|
          </ErrorDialog>
 | 
						|
        )}
 | 
						|
 | 
						|
        <CommandPalette
 | 
						|
          customCommandPaletteItems={[
 | 
						|
            {
 | 
						|
              label: t("labels.liveCollaboration"),
 | 
						|
              category: DEFAULT_CATEGORIES.app,
 | 
						|
              keywords: [
 | 
						|
                "team",
 | 
						|
                "multiplayer",
 | 
						|
                "share",
 | 
						|
                "public",
 | 
						|
                "session",
 | 
						|
                "invite",
 | 
						|
              ],
 | 
						|
              icon: usersIcon,
 | 
						|
              perform: () => {
 | 
						|
                setShareDialogState({
 | 
						|
                  isOpen: true,
 | 
						|
                  type: "collaborationOnly",
 | 
						|
                });
 | 
						|
              },
 | 
						|
            },
 | 
						|
            {
 | 
						|
              label: t("roomDialog.button_stopSession"),
 | 
						|
              category: DEFAULT_CATEGORIES.app,
 | 
						|
              predicate: () => !!collabAPI?.isCollaborating(),
 | 
						|
              keywords: [
 | 
						|
                "stop",
 | 
						|
                "session",
 | 
						|
                "end",
 | 
						|
                "leave",
 | 
						|
                "close",
 | 
						|
                "exit",
 | 
						|
                "collaboration",
 | 
						|
              ],
 | 
						|
              perform: () => {
 | 
						|
                if (collabAPI) {
 | 
						|
                  collabAPI.stopCollaboration();
 | 
						|
                  if (!collabAPI.isCollaborating()) {
 | 
						|
                    setShareDialogState({ isOpen: false });
 | 
						|
                  }
 | 
						|
                }
 | 
						|
              },
 | 
						|
            },
 | 
						|
            {
 | 
						|
              label: t("labels.share"),
 | 
						|
              category: DEFAULT_CATEGORIES.app,
 | 
						|
              predicate: true,
 | 
						|
              icon: share,
 | 
						|
              keywords: [
 | 
						|
                "link",
 | 
						|
                "shareable",
 | 
						|
                "readonly",
 | 
						|
                "export",
 | 
						|
                "publish",
 | 
						|
                "snapshot",
 | 
						|
                "url",
 | 
						|
                "collaborate",
 | 
						|
                "invite",
 | 
						|
              ],
 | 
						|
              perform: async () => {
 | 
						|
                setShareDialogState({ isOpen: true, type: "share" });
 | 
						|
              },
 | 
						|
            },
 | 
						|
            {
 | 
						|
              label: "GitHub",
 | 
						|
              icon: GithubIcon,
 | 
						|
              category: DEFAULT_CATEGORIES.links,
 | 
						|
              predicate: true,
 | 
						|
              keywords: [
 | 
						|
                "issues",
 | 
						|
                "bugs",
 | 
						|
                "requests",
 | 
						|
                "report",
 | 
						|
                "features",
 | 
						|
                "social",
 | 
						|
                "community",
 | 
						|
              ],
 | 
						|
              perform: () => {
 | 
						|
                window.open(
 | 
						|
                  "https://github.com/excalidraw/excalidraw",
 | 
						|
                  "_blank",
 | 
						|
                  "noopener noreferrer",
 | 
						|
                );
 | 
						|
              },
 | 
						|
            },
 | 
						|
            {
 | 
						|
              label: t("labels.followUs"),
 | 
						|
              icon: XBrandIcon,
 | 
						|
              category: DEFAULT_CATEGORIES.links,
 | 
						|
              predicate: true,
 | 
						|
              keywords: ["twitter", "contact", "social", "community"],
 | 
						|
              perform: () => {
 | 
						|
                window.open(
 | 
						|
                  "https://x.com/excalidraw",
 | 
						|
                  "_blank",
 | 
						|
                  "noopener noreferrer",
 | 
						|
                );
 | 
						|
              },
 | 
						|
            },
 | 
						|
            {
 | 
						|
              label: t("labels.discordChat"),
 | 
						|
              category: DEFAULT_CATEGORIES.links,
 | 
						|
              predicate: true,
 | 
						|
              icon: DiscordIcon,
 | 
						|
              keywords: [
 | 
						|
                "chat",
 | 
						|
                "talk",
 | 
						|
                "contact",
 | 
						|
                "bugs",
 | 
						|
                "requests",
 | 
						|
                "report",
 | 
						|
                "feedback",
 | 
						|
                "suggestions",
 | 
						|
                "social",
 | 
						|
                "community",
 | 
						|
              ],
 | 
						|
              perform: () => {
 | 
						|
                window.open(
 | 
						|
                  "https://discord.gg/UexuTaE",
 | 
						|
                  "_blank",
 | 
						|
                  "noopener noreferrer",
 | 
						|
                );
 | 
						|
              },
 | 
						|
            },
 | 
						|
            {
 | 
						|
              label: "YouTube",
 | 
						|
              icon: youtubeIcon,
 | 
						|
              category: DEFAULT_CATEGORIES.links,
 | 
						|
              predicate: true,
 | 
						|
              keywords: ["features", "tutorials", "howto", "help", "community"],
 | 
						|
              perform: () => {
 | 
						|
                window.open(
 | 
						|
                  "https://youtube.com/@excalidraw",
 | 
						|
                  "_blank",
 | 
						|
                  "noopener noreferrer",
 | 
						|
                );
 | 
						|
              },
 | 
						|
            },
 | 
						|
            ...(isExcalidrawPlusSignedUser
 | 
						|
              ? [
 | 
						|
                  {
 | 
						|
                    ...ExcalidrawPlusAppCommand,
 | 
						|
                    label: "Sign in / Go to Excalidraw+",
 | 
						|
                  },
 | 
						|
                ]
 | 
						|
              : [ExcalidrawPlusCommand, ExcalidrawPlusAppCommand]),
 | 
						|
 | 
						|
            {
 | 
						|
              label: t("overwriteConfirm.action.excalidrawPlus.button"),
 | 
						|
              category: DEFAULT_CATEGORIES.export,
 | 
						|
              icon: exportToPlus,
 | 
						|
              predicate: true,
 | 
						|
              keywords: ["plus", "export", "save", "backup"],
 | 
						|
              perform: () => {
 | 
						|
                if (excalidrawAPI) {
 | 
						|
                  exportToExcalidrawPlus(
 | 
						|
                    excalidrawAPI.getSceneElements(),
 | 
						|
                    excalidrawAPI.getAppState(),
 | 
						|
                    excalidrawAPI.getFiles(),
 | 
						|
                    excalidrawAPI.getName(),
 | 
						|
                  );
 | 
						|
                }
 | 
						|
              },
 | 
						|
            },
 | 
						|
            {
 | 
						|
              ...CommandPalette.defaultItems.toggleTheme,
 | 
						|
              perform: () => {
 | 
						|
                setAppTheme(
 | 
						|
                  editorTheme === THEME.DARK ? THEME.LIGHT : THEME.DARK,
 | 
						|
                );
 | 
						|
              },
 | 
						|
            },
 | 
						|
          ]}
 | 
						|
        />
 | 
						|
      </Excalidraw>
 | 
						|
    </div>
 | 
						|
  );
 | 
						|
};
 | 
						|
 | 
						|
const ExcalidrawApp = () => {
 | 
						|
  return (
 | 
						|
    <TopErrorBoundary>
 | 
						|
      <Provider unstable_createStore={() => appJotaiStore}>
 | 
						|
        <ExcalidrawWrapper />
 | 
						|
      </Provider>
 | 
						|
    </TopErrorBoundary>
 | 
						|
  );
 | 
						|
};
 | 
						|
 | 
						|
export default ExcalidrawApp;
 |