feat: Merge upstream/master into b310-digital/excalidraw#master (#3)

This commit is contained in:
Sören Johanson
2024-08-23 15:54:59 +02:00
committed by GitHub
parent aaca099bc3
commit efdae58832
549 changed files with 79065 additions and 30661 deletions

View File

@ -1,14 +1,15 @@
import { StoreAction } from "../../packages/excalidraw";
import { compressData } from "../../packages/excalidraw/data/encode";
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
import {
import type {
ExcalidrawElement,
ExcalidrawImageElement,
FileId,
InitializedExcalidrawImageElement,
} from "../../packages/excalidraw/element/types";
import { t } from "../../packages/excalidraw/i18n";
import {
import type {
BinaryFileData,
BinaryFileMetadata,
ExcalidrawImperativeAPI,
@ -238,5 +239,6 @@ export const updateStaleImageStatuses = (params: {
}
return element;
}),
storeAction: StoreAction.UPDATE,
});
};

View File

@ -10,18 +10,29 @@
* (localStorage, indexedDB).
*/
import { createStore, entries, del, getMany, set, setMany } from "idb-keyval";
import { clearAppStateForLocalStorage } from "../../packages/excalidraw/appState";
import { clearElementsForLocalStorage } from "../../packages/excalidraw/element";
import {
createStore,
entries,
del,
getMany,
set,
setMany,
get,
} from "idb-keyval";
import { clearAppStateForLocalStorage } from "../../packages/excalidraw/appState";
import type { LibraryPersistedData } from "../../packages/excalidraw/data/library";
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
import { clearElementsForLocalStorage } from "../../packages/excalidraw/element";
import type {
ExcalidrawElement,
FileId,
} from "../../packages/excalidraw/element/types";
import {
import type {
AppState,
BinaryFileData,
BinaryFiles,
} from "../../packages/excalidraw/types";
import type { MaybePromise } from "../../packages/excalidraw/utility-types";
import { debounce } from "../../packages/excalidraw/utils";
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants";
import { FileManager } from "./FileManager";
@ -183,3 +194,52 @@ export class LocalData {
},
});
}
export class LibraryIndexedDBAdapter {
/** IndexedDB database and store name */
private static idb_name = STORAGE_KEYS.IDB_LIBRARY;
/** library data store key */
private static key = "libraryData";
private static store = createStore(
`${LibraryIndexedDBAdapter.idb_name}-db`,
`${LibraryIndexedDBAdapter.idb_name}-store`,
);
static async load() {
const IDBData = await get<LibraryPersistedData>(
LibraryIndexedDBAdapter.key,
LibraryIndexedDBAdapter.store,
);
return IDBData || null;
}
static save(data: LibraryPersistedData): MaybePromise<void> {
return set(
LibraryIndexedDBAdapter.key,
data,
LibraryIndexedDBAdapter.store,
);
}
}
/** LS Adapter used only for migrating LS library data
* to indexedDB */
export class LibraryLocalStorageMigrationAdapter {
static load() {
const LSData = localStorage.getItem(
STORAGE_KEYS.__LEGACY_LOCAL_STORAGE_LIBRARY,
);
if (LSData != null) {
const libraryItems: ImportedDataState["libraryItems"] =
JSON.parse(LSData);
if (libraryItems) {
return { libraryItems };
}
}
return null;
}
static clear() {
localStorage.removeItem(STORAGE_KEYS.__LEGACY_LOCAL_STORAGE_LIBRARY);
}
}

View File

@ -1,7 +1,10 @@
import { SyncableExcalidrawElement } from ".";
import { ExcalidrawElement, FileId } from "../../packages/excalidraw/element/types";
import { AppState, BinaryFileData } from "../../packages/excalidraw/types";
import Portal from "../collab/Portal";
import type { SyncableExcalidrawElement } from ".";
import type {
ExcalidrawElement,
FileId,
} from "../../packages/excalidraw/element/types";
import type { AppState, BinaryFileData } from "../../packages/excalidraw/types";
import type Portal from "../collab/Portal";
import type { Socket } from "socket.io-client";
export interface StorageBackend {
@ -10,7 +13,7 @@ export interface StorageBackend {
portal: Portal,
elements: readonly SyncableExcalidrawElement[],
appState: AppState,
) => Promise<false | { reconciledElements: any }>;
) => Promise<false | readonly SyncableExcalidrawElement[] | null>;
loadFromStorageBackend: (
roomId: string,
roomKey: string,

View File

@ -1,54 +1,54 @@
import {
isSavedToFirebase,
loadFilesFromFirebase,
loadFromFirebase,
saveFilesToFirebase,
saveToFirebase,
} from "./firebase";
import {
isSavedToHttpStorage,
loadFilesFromHttpStorage,
loadFromHttpStorage,
saveFilesToHttpStorage,
saveToHttpStorage,
} from "./httpStorage";
import { StorageBackend } from "./StorageBackend";
const firebaseStorage: StorageBackend = {
isSaved: isSavedToFirebase,
saveToStorageBackend: saveToFirebase,
loadFromStorageBackend: loadFromFirebase,
saveFilesToStorageBackend: saveFilesToFirebase,
loadFilesFromStorageBackend: loadFilesFromFirebase,
};
const httpStorage: StorageBackend = {
isSaved: isSavedToHttpStorage,
saveToStorageBackend: saveToHttpStorage,
loadFromStorageBackend: loadFromHttpStorage,
saveFilesToStorageBackend: saveFilesToHttpStorage,
loadFilesFromStorageBackend: loadFilesFromHttpStorage,
};
const storageBackends = new Map<string, StorageBackend>()
.set("firebase", firebaseStorage)
.set("http", httpStorage);
export let storageBackend: StorageBackend | null = null;
export async function getStorageBackend() {
if (storageBackend) {
return storageBackend;
}
const storageBackendName = import.meta.env.VITE_APP_STORAGE_BACKEND || '';
if (storageBackends.has(storageBackendName)) {
storageBackend = storageBackends.get(storageBackendName) as StorageBackend;
} else {
console.warn("No storage backend found, default to firebase");
storageBackend = firebaseStorage;
}
isSavedToFirebase,
loadFilesFromFirebase,
loadFromFirebase,
saveFilesToFirebase,
saveToFirebase,
} from "./firebase";
import {
isSavedToHttpStorage,
loadFilesFromHttpStorage,
loadFromHttpStorage,
saveFilesToHttpStorage,
saveToHttpStorage,
} from "./httpStorage";
import type { StorageBackend } from "./StorageBackend";
const firebaseStorage: StorageBackend = {
isSaved: isSavedToFirebase,
saveToStorageBackend: saveToFirebase,
loadFromStorageBackend: loadFromFirebase,
saveFilesToStorageBackend: saveFilesToFirebase,
loadFilesFromStorageBackend: loadFilesFromFirebase,
};
const httpStorage: StorageBackend = {
isSaved: isSavedToHttpStorage,
saveToStorageBackend: saveToHttpStorage,
loadFromStorageBackend: loadFromHttpStorage,
saveFilesToStorageBackend: saveFilesToHttpStorage,
loadFilesFromStorageBackend: loadFilesFromHttpStorage,
};
const storageBackends = new Map<string, StorageBackend>()
.set("firebase", firebaseStorage)
.set("http", httpStorage);
export let storageBackend: StorageBackend | null = null;
export async function getStorageBackend() {
if (storageBackend) {
return storageBackend;
}
}
const storageBackendName = import.meta.env.VITE_APP_STORAGE_BACKEND || "";
if (storageBackends.has(storageBackendName)) {
storageBackend = storageBackends.get(storageBackendName) as StorageBackend;
} else {
console.warn("No storage backend found, default to firebase");
storageBackend = firebaseStorage;
}
return storageBackend;
}

View File

@ -1,11 +1,13 @@
import {
import { reconcileElements } from "../../packages/excalidraw";
import type {
ExcalidrawElement,
FileId,
OrderedExcalidrawElement,
} from "../../packages/excalidraw/element/types";
import { getSceneVersion } from "../../packages/excalidraw/element";
import Portal from "../collab/Portal";
import type Portal from "../collab/Portal";
import { restoreElements } from "../../packages/excalidraw/data/restore";
import {
import type {
AppState,
BinaryFileData,
BinaryFileMetadata,
@ -18,10 +20,11 @@ import {
decryptData,
} from "../../packages/excalidraw/data/encryption";
import { MIME_TYPES } from "../../packages/excalidraw/constants";
import { reconcileElements } from "../collab/reconciliation";
import { getSyncableElements, SyncableExcalidrawElement } from ".";
import { ResolutionType } from "../../packages/excalidraw/utility-types";
import type { SyncableExcalidrawElement } from ".";
import { getSyncableElements } from ".";
import type { ResolutionType } from "../../packages/excalidraw/utility-types";
import type { Socket } from "socket.io-client";
import type { RemoteExcalidrawElement } from "../../packages/excalidraw/data/reconcile";
// private
// -----------------------------------------------------------------------------
@ -232,7 +235,7 @@ export const saveToFirebase = async (
!socket ||
isSavedToFirebase(portal, elements)
) {
return false;
return null;
}
const firebase = await loadFirestore();
@ -240,56 +243,59 @@ export const saveToFirebase = async (
const docRef = firestore.collection("scenes").doc(roomId);
const savedData = await firestore.runTransaction(async (transaction) => {
const storedScene = await firestore.runTransaction(async (transaction) => {
const snapshot = await transaction.get(docRef);
if (!snapshot.exists) {
const sceneDocument = await createFirebaseSceneDocument(
const storedScene = await createFirebaseSceneDocument(
firebase,
elements,
roomKey,
);
transaction.set(docRef, sceneDocument);
transaction.set(docRef, storedScene);
return {
elements,
reconciledElements: null,
};
return storedScene;
}
const prevDocData = snapshot.data() as FirebaseStoredScene;
const prevElements = getSyncableElements(
await decryptElements(prevDocData, roomKey),
const prevStoredScene = snapshot.data() as FirebaseStoredScene;
const prevStoredElements = getSyncableElements(
restoreElements(await decryptElements(prevStoredScene, roomKey), null),
);
const reconciledElements = getSyncableElements(
reconcileElements(elements, prevElements, appState),
reconcileElements(
elements,
prevStoredElements as OrderedExcalidrawElement[] as RemoteExcalidrawElement[],
appState,
),
);
const sceneDocument = await createFirebaseSceneDocument(
const storedScene = await createFirebaseSceneDocument(
firebase,
reconciledElements,
roomKey,
);
transaction.update(docRef, sceneDocument);
return {
elements,
reconciledElements,
};
transaction.update(docRef, storedScene);
// Return the stored elements as the in memory `reconciledElements` could have mutated in the meantime
return storedScene;
});
FirebaseSceneVersionCache.set(socket, savedData.elements);
const storedElements = getSyncableElements(
restoreElements(await decryptElements(storedScene, roomKey), null),
);
return { reconciledElements: savedData.reconciledElements };
FirebaseSceneVersionCache.set(socket, storedElements);
return storedElements;
};
export const loadFromFirebase = async (
roomId: string,
roomKey: string,
socket: Socket | null,
): Promise<readonly ExcalidrawElement[] | null> => {
): Promise<readonly SyncableExcalidrawElement[] | null> => {
const firebase = await loadFirestore();
const db = firebase.firestore();
@ -300,14 +306,14 @@ export const loadFromFirebase = async (
}
const storedScene = doc.data() as FirebaseStoredScene;
const elements = getSyncableElements(
await decryptElements(storedScene, roomKey),
restoreElements(await decryptElements(storedScene, roomKey), null),
);
if (socket) {
FirebaseSceneVersionCache.set(socket, elements);
}
return restoreElements(elements, null);
return elements;
};
export const loadFilesFromFirebase = async (

View File

@ -1,34 +1,42 @@
// Inspired and partly copied from https://gitlab.com/kiliandeca/excalidraw-fork
// MIT, Kilian Decaderincourt
import { getSyncableElements, SyncableExcalidrawElement } from ".";
import type { SyncableExcalidrawElement } from ".";
import { getSyncableElements } from ".";
import { MIME_TYPES } from "../../packages/excalidraw/constants";
import { decompressData } from "../../packages/excalidraw/data/encode";
import { encryptData, decryptData, IV_LENGTH_BYTES } from "../../packages/excalidraw/data/encryption";
import {
encryptData,
decryptData,
IV_LENGTH_BYTES,
} from "../../packages/excalidraw/data/encryption";
import { restoreElements } from "../../packages/excalidraw/data/restore";
import { getSceneVersion } from "../../packages/excalidraw/element";
import { ExcalidrawElement, FileId } from "../../packages/excalidraw/element/types";
import {
import type {
ExcalidrawElement,
FileId,
OrderedExcalidrawElement,
} from "../../packages/excalidraw/element/types";
import type {
AppState,
BinaryFileData,
BinaryFileMetadata,
DataURL,
} from "../../packages/excalidraw/types";
import Portal from "../collab/Portal";
import { reconcileElements } from "../collab/reconciliation";
import { StoredScene } from "./StorageBackend";
import type Portal from "../collab/Portal";
import type { RemoteExcalidrawElement } from "../../packages/excalidraw/data/reconcile";
import { reconcileElements } from "../../packages/excalidraw/data/reconcile";
import type { StoredScene } from "./StorageBackend";
import type { Socket } from "socket.io-client";
const HTTP_STORAGE_BACKEND_URL = import.meta.env.VITE_APP_HTTP_STORAGE_BACKEND_URL;
const SCENE_VERSION_LENGTH_BYTES = 4
const HTTP_STORAGE_BACKEND_URL = import.meta.env
.VITE_APP_HTTP_STORAGE_BACKEND_URL;
const SCENE_VERSION_LENGTH_BYTES = 4;
// There is a lot of intentional duplication with the firebase file
// to prevent modifying upstream files and ease futur maintenance of this fork
const httpStorageSceneVersionCache = new WeakMap<
Socket,
number
>();
const httpStorageSceneVersionCache = new WeakMap<Socket, number>();
export const isSavedToHttpStorage = (
portal: Portal,
@ -68,17 +76,20 @@ export const saveToHttpStorage = async (
if (!getResponse.ok && getResponse.status !== 404) {
return false;
};
}
if(getResponse.status === 404) {
const result: boolean = await saveElementsToBackend(roomKey, roomId, [...elements], sceneVersion)
if(result) {
return {
reconciledElements: null
}
if (getResponse.status === 404) {
const result: boolean = await saveElementsToBackend(
roomKey,
roomId,
[...elements],
sceneVersion,
);
if (result) {
return null;
}
return false
};
return false;
}
// If room already exist, we compare scene versions to check
// if we're up to date before saving our scene
@ -90,16 +101,23 @@ export const saveToHttpStorage = async (
const existingElements = await getElementsFromBuffer(buffer, roomKey);
const reconciledElements = getSyncableElements(
reconcileElements(elements, existingElements, appState),
reconcileElements(
elements,
existingElements as OrderedExcalidrawElement[] as RemoteExcalidrawElement[],
appState,
),
);
const result: boolean = await saveElementsToBackend(roomKey, roomId, reconciledElements, sceneVersion)
const result: boolean = await saveElementsToBackend(
roomKey,
roomId,
reconciledElements,
sceneVersion,
);
if (result) {
httpStorageSceneVersionCache.set(socket, sceneVersion);
return {
reconciledElements: elements
};
return elements;
}
return false;
};
@ -109,7 +127,8 @@ export const loadFromHttpStorage = async (
roomKey: string,
socket: Socket | null,
): Promise<readonly ExcalidrawElement[] | null> => {
const HTTP_STORAGE_BACKEND_URL = import.meta.env.VITE_APP_HTTP_STORAGE_BACKEND_URL;
const HTTP_STORAGE_BACKEND_URL = import.meta.env
.VITE_APP_HTTP_STORAGE_BACKEND_URL;
const getResponse = await fetch(
`${HTTP_STORAGE_BACKEND_URL}/rooms/${roomId}`,
);
@ -130,12 +149,20 @@ const getElementsFromBuffer = async (
): Promise<readonly ExcalidrawElement[]> => {
// Buffer should contain both the IV (fixed length) and encrypted data
const sceneVersion = parseSceneVersionFromRequest(buffer);
const iv = new Uint8Array(buffer.slice(SCENE_VERSION_LENGTH_BYTES, IV_LENGTH_BYTES + SCENE_VERSION_LENGTH_BYTES));
const encrypted = buffer.slice(IV_LENGTH_BYTES + SCENE_VERSION_LENGTH_BYTES, buffer.byteLength);
const iv = new Uint8Array(
buffer.slice(
SCENE_VERSION_LENGTH_BYTES,
IV_LENGTH_BYTES + SCENE_VERSION_LENGTH_BYTES,
),
);
const encrypted = buffer.slice(
IV_LENGTH_BYTES + SCENE_VERSION_LENGTH_BYTES,
buffer.byteLength,
);
return await decryptElements(
{ sceneVersion: sceneVersion, ciphertext: encrypted, iv },
key
{ sceneVersion, ciphertext: encrypted, iv },
key,
);
};
@ -149,7 +176,8 @@ export const saveFilesToHttpStorage = async ({
const erroredFiles = new Map<FileId, true>();
const savedFiles = new Map<FileId, true>();
const HTTP_STORAGE_BACKEND_URL = import.meta.env.VITE_APP_HTTP_STORAGE_BACKEND_URL;
const HTTP_STORAGE_BACKEND_URL = import.meta.env
.VITE_APP_HTTP_STORAGE_BACKEND_URL;
await Promise.all(
files.map(async ({ id, buffer }) => {
@ -182,7 +210,8 @@ export const loadFilesFromHttpStorage = async (
await Promise.all(
[...new Set(filesIds)].map(async (id) => {
try {
const HTTP_STORAGE_BACKEND_URL = import.meta.env.VITE_APP_HTTP_STORAGE_BACKEND_URL;
const HTTP_STORAGE_BACKEND_URL = import.meta.env
.VITE_APP_HTTP_STORAGE_BACKEND_URL;
const response = await fetch(`${HTTP_STORAGE_BACKEND_URL}/files/${id}`);
if (response.status < 400) {
const arrayBuffer = await response.arrayBuffer();
@ -216,7 +245,12 @@ export const loadFilesFromHttpStorage = async (
return { loadedFiles, erroredFiles };
};
const saveElementsToBackend = async (roomKey: string, roomId: string, elements: SyncableExcalidrawElement[], sceneVersion: number) => {
const saveElementsToBackend = async (
roomKey: string,
roomId: string,
elements: SyncableExcalidrawElement[],
sceneVersion: number,
) => {
const { ciphertext, iv } = await encryptElements(roomKey, elements);
// Concatenate Scene Version, IV with encrypted data (IV does not have to be secret).
@ -224,7 +258,9 @@ const saveElementsToBackend = async (roomKey: string, roomId: string, elements:
const numberView = new DataView(numberBuffer);
numberView.setUint32(0, sceneVersion, false);
const sceneVersionBuffer = numberView.buffer;
const payloadBlob = await new Response(new Blob([sceneVersionBuffer, iv.buffer, ciphertext])).arrayBuffer();
const payloadBlob = await new Response(
new Blob([sceneVersionBuffer, iv.buffer, ciphertext]),
).arrayBuffer();
const putResponse = await fetch(
`${HTTP_STORAGE_BACKEND_URL}/rooms/${roomId}`,
{
@ -233,13 +269,13 @@ const saveElementsToBackend = async (roomKey: string, roomId: string, elements:
},
);
return putResponse.ok
}
return putResponse.ok;
};
const parseSceneVersionFromRequest = (buffer: ArrayBuffer) => {
const view = new DataView(buffer);
return view.getUint32(0, false);
}
};
const decryptElements = async (
data: StoredScene,
@ -264,4 +300,4 @@ const encryptElements = async (
const { encryptedBuffer, iv } = await encryptData(key, encoded);
return { ciphertext: encryptedBuffer, iv };
};
};

View File

@ -4,44 +4,44 @@ import {
} from "../../packages/excalidraw/data/encode";
import {
decryptData,
encryptData,
generateEncryptionKey,
IV_LENGTH_BYTES,
} from "../../packages/excalidraw/data/encryption";
import { serializeAsJSON } from "../../packages/excalidraw/data/json";
import { restore } from "../../packages/excalidraw/data/restore";
import { ImportedDataState } from "../../packages/excalidraw/data/types";
import { SceneBounds } from "../../packages/excalidraw/element/bounds";
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
import type { SceneBounds } from "../../packages/excalidraw/element/bounds";
import { isInvisiblySmallElement } from "../../packages/excalidraw/element/sizeHelpers";
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
import {
import type {
ExcalidrawElement,
FileId,
OrderedExcalidrawElement,
} from "../../packages/excalidraw/element/types";
import { t } from "../../packages/excalidraw/i18n";
import {
import type {
AppState,
BinaryFileData,
BinaryFiles,
SocketId,
UserIdleState,
} from "../../packages/excalidraw/types";
import type { MakeBrand } from "../../packages/excalidraw/utility-types";
import { bytesToHexString } from "../../packages/excalidraw/utils";
import type { WS_SUBTYPES } from "../app_constants";
import {
DELETED_ELEMENT_TIMEOUT,
FILE_UPLOAD_MAX_BYTES,
ROOM_ID_BYTES,
WS_SUBTYPES,
} from "../app_constants";
import { encodeFilesForUpload } from "./FileManager";
import { getStorageBackend } from "./config";
export type SyncableExcalidrawElement = ExcalidrawElement & {
_brand: "SyncableExcalidrawElement";
};
export type SyncableExcalidrawElement = OrderedExcalidrawElement &
MakeBrand<"SyncableExcalidrawElement">;
export const isSyncableElement = (
element: ExcalidrawElement,
element: OrderedExcalidrawElement,
): element is SyncableExcalidrawElement => {
if (element.isDeleted) {
if (element.updated > Date.now() - DELETED_ELEMENT_TIMEOUT) {
@ -52,7 +52,9 @@ export const isSyncableElement = (
return !isInvisiblySmallElement(element);
};
export const getSyncableElements = (elements: readonly ExcalidrawElement[]) =>
export const getSyncableElements = (
elements: readonly OrderedExcalidrawElement[],
) =>
elements.filter((element) =>
isSyncableElement(element),
) as SyncableExcalidrawElement[];
@ -66,35 +68,6 @@ const generateRoomId = async () => {
return bytesToHexString(buffer);
};
/**
* Right now the reason why we resolve connection params (url, polling...)
* from upstream is to allow changing the params immediately when needed without
* having to wait for clients to update the SW.
*
* If REACT_APP_WS_SERVER_URL env is set, we use that instead (useful for forks)
*/
export const getCollabServer = async (): Promise<{
url: string;
polling: boolean;
}> => {
if (import.meta.env.VITE_APP_WS_SERVER_URL) {
return {
url: import.meta.env.VITE_APP_WS_SERVER_URL,
polling: true,
};
}
try {
const resp = await fetch(
`${import.meta.env.VITE_APP_PORTAL_URL}/collab-server`,
);
return await resp.json();
} catch (error) {
console.error(error);
throw new Error(t("errors.cannotResolveCollabServer"));
}
};
export type EncryptedData = {
data: ArrayBuffer;
iv: Uint8Array;
@ -296,7 +269,6 @@ export const loadScene = async (
// in the scene database/localStorage, and instead fetch them async
// from a different database
files: data.files,
commitToHistory: false,
};
};

View File

@ -1,12 +1,11 @@
import { ExcalidrawElement } from "../../packages/excalidraw/element/types";
import { AppState } from "../../packages/excalidraw/types";
import type { ExcalidrawElement } from "../../packages/excalidraw/element/types";
import type { AppState } from "../../packages/excalidraw/types";
import {
clearAppStateForLocalStorage,
getDefaultAppState,
} from "../../packages/excalidraw/appState";
import { clearElementsForLocalStorage } from "../../packages/excalidraw/element";
import { STORAGE_KEYS } from "../app_constants";
import { ImportedDataState } from "../../packages/excalidraw/data/types";
export const saveUsernameToLocalStorage = (username: string) => {
try {
@ -88,28 +87,13 @@ export const getTotalStorageSize = () => {
try {
const appState = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_APP_STATE);
const collab = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_COLLAB);
const library = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY);
const appStateSize = appState?.length || 0;
const collabSize = collab?.length || 0;
const librarySize = library?.length || 0;
return appStateSize + collabSize + librarySize + getElementsStorageSize();
return appStateSize + collabSize + getElementsStorageSize();
} catch (error: any) {
console.error(error);
return 0;
}
};
export const getLibraryItemsFromStorage = () => {
try {
const libraryItems: ImportedDataState["libraryItems"] = JSON.parse(
localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY) as string,
);
return libraryItems || [];
} catch (error) {
console.error(error);
return [];
}
};