2020-07-27 15:29:19 +03:00
|
|
|
import { loadLibraryFromBlob } from "./blob";
|
|
|
|
import { LibraryItems, LibraryItem } from "../types";
|
2022-04-14 16:20:35 +02:00
|
|
|
import { restoreLibraryItems } from "./restore";
|
2021-06-01 23:52:13 +05:30
|
|
|
import type App from "../components/App";
|
2022-04-20 14:40:03 +02:00
|
|
|
import { ImportedDataState } from "./types";
|
|
|
|
import { atom } from "jotai";
|
|
|
|
import { jotaiStore } from "../jotai";
|
|
|
|
|
2022-04-29 16:45:02 +02:00
|
|
|
export const libraryItemsAtom = atom<{
|
|
|
|
status: "loading" | "loaded";
|
|
|
|
isInitialized: boolean;
|
|
|
|
libraryItems: LibraryItems;
|
|
|
|
}>({ status: "loaded", isInitialized: true, libraryItems: [] });
|
2022-04-20 14:40:03 +02:00
|
|
|
|
|
|
|
const cloneLibraryItems = (libraryItems: LibraryItems): LibraryItems =>
|
|
|
|
JSON.parse(JSON.stringify(libraryItems));
|
|
|
|
|
|
|
|
/**
|
|
|
|
* checks if library item does not exist already in current library
|
|
|
|
*/
|
|
|
|
const isUniqueItem = (
|
|
|
|
existingLibraryItems: LibraryItems,
|
|
|
|
targetLibraryItem: LibraryItem,
|
|
|
|
) => {
|
|
|
|
return !existingLibraryItems.find((libraryItem) => {
|
|
|
|
if (libraryItem.elements.length !== targetLibraryItem.elements.length) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// detect z-index difference by checking the excalidraw elements
|
|
|
|
// are in order
|
|
|
|
return libraryItem.elements.every((libItemExcalidrawItem, idx) => {
|
|
|
|
return (
|
|
|
|
libItemExcalidrawItem.id === targetLibraryItem.elements[idx].id &&
|
|
|
|
libItemExcalidrawItem.versionNonce ===
|
|
|
|
targetLibraryItem.elements[idx].versionNonce
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
2020-07-27 15:29:19 +03:00
|
|
|
|
2022-04-29 16:45:02 +02:00
|
|
|
/** Merges otherItems into localItems. Unique items in otherItems array are
|
|
|
|
sorted first. */
|
|
|
|
export const mergeLibraryItems = (
|
|
|
|
localItems: LibraryItems,
|
|
|
|
otherItems: LibraryItems,
|
|
|
|
): LibraryItems => {
|
|
|
|
const newItems = [];
|
|
|
|
for (const item of otherItems) {
|
|
|
|
if (isUniqueItem(localItems, item)) {
|
|
|
|
newItems.push(item);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return [...newItems, ...localItems];
|
|
|
|
};
|
|
|
|
|
2021-04-21 23:38:24 +05:30
|
|
|
class Library {
|
2022-04-29 16:45:02 +02:00
|
|
|
/** latest libraryItems */
|
2022-04-20 14:40:03 +02:00
|
|
|
private lastLibraryItems: LibraryItems = [];
|
2022-04-29 16:45:02 +02:00
|
|
|
/** indicates whether library is initialized with library items (has gone
|
|
|
|
* though at least one update) */
|
|
|
|
private isInitialized = false;
|
2022-04-20 14:40:03 +02:00
|
|
|
|
2021-04-21 23:38:24 +05:30
|
|
|
private app: App;
|
2020-10-30 21:01:41 +01:00
|
|
|
|
2021-04-21 23:38:24 +05:30
|
|
|
constructor(app: App) {
|
|
|
|
this.app = app;
|
|
|
|
}
|
|
|
|
|
2022-04-29 16:45:02 +02:00
|
|
|
private updateQueue: Promise<LibraryItems>[] = [];
|
|
|
|
|
|
|
|
private getLastUpdateTask = (): Promise<LibraryItems> | undefined => {
|
|
|
|
return this.updateQueue[this.updateQueue.length - 1];
|
|
|
|
};
|
|
|
|
|
|
|
|
private notifyListeners = () => {
|
|
|
|
if (this.updateQueue.length > 0) {
|
|
|
|
jotaiStore.set(libraryItemsAtom, {
|
|
|
|
status: "loading",
|
|
|
|
libraryItems: this.lastLibraryItems,
|
|
|
|
isInitialized: this.isInitialized,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this.isInitialized = true;
|
|
|
|
jotaiStore.set(libraryItemsAtom, {
|
|
|
|
status: "loaded",
|
|
|
|
libraryItems: this.lastLibraryItems,
|
|
|
|
isInitialized: this.isInitialized,
|
|
|
|
});
|
|
|
|
try {
|
|
|
|
this.app.props.onLibraryChange?.(
|
|
|
|
cloneLibraryItems(this.lastLibraryItems),
|
|
|
|
);
|
|
|
|
} catch (error) {
|
|
|
|
console.error(error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
resetLibrary = () => {
|
|
|
|
return this.setLibrary([]);
|
2020-10-30 21:01:41 +01:00
|
|
|
};
|
|
|
|
|
2022-04-29 16:45:02 +02:00
|
|
|
/**
|
|
|
|
* imports library (from blob or libraryItems), merging with current library
|
|
|
|
* (attempting to remove duplicates)
|
|
|
|
*/
|
|
|
|
importLibrary(
|
2022-04-20 14:40:03 +02:00
|
|
|
library:
|
|
|
|
| Blob
|
|
|
|
| Required<ImportedDataState>["libraryItems"]
|
|
|
|
| Promise<Required<ImportedDataState>["libraryItems"]>,
|
2022-04-14 16:20:35 +02:00
|
|
|
defaultStatus: LibraryItem["status"] = "unpublished",
|
2022-04-29 16:45:02 +02:00
|
|
|
): Promise<LibraryItems> {
|
|
|
|
return this.setLibrary(
|
|
|
|
() =>
|
|
|
|
new Promise<LibraryItems>(async (resolve, reject) => {
|
|
|
|
try {
|
|
|
|
let libraryItems: LibraryItems;
|
|
|
|
if (library instanceof Blob) {
|
|
|
|
libraryItems = await loadLibraryFromBlob(library, defaultStatus);
|
|
|
|
} else {
|
|
|
|
libraryItems = restoreLibraryItems(await library, defaultStatus);
|
2022-04-20 14:40:03 +02:00
|
|
|
}
|
|
|
|
|
2022-04-29 16:45:02 +02:00
|
|
|
resolve(mergeLibraryItems(this.lastLibraryItems, libraryItems));
|
|
|
|
} catch (error) {
|
|
|
|
reject(error);
|
|
|
|
}
|
|
|
|
}),
|
2022-04-20 14:40:03 +02:00
|
|
|
);
|
2020-07-27 15:29:19 +03:00
|
|
|
}
|
2020-10-30 21:01:41 +01:00
|
|
|
|
2022-04-29 16:45:02 +02:00
|
|
|
/**
|
|
|
|
* @returns latest cloned libraryItems. Awaits all in-progress updates first.
|
|
|
|
*/
|
|
|
|
getLatestLibrary = (): Promise<LibraryItems> => {
|
2020-10-30 21:01:41 +01:00
|
|
|
return new Promise(async (resolve) => {
|
|
|
|
try {
|
2022-04-29 16:45:02 +02:00
|
|
|
const libraryItems = await (this.getLastUpdateTask() ||
|
|
|
|
this.lastLibraryItems);
|
|
|
|
if (this.updateQueue.length > 0) {
|
|
|
|
resolve(this.getLatestLibrary());
|
|
|
|
} else {
|
|
|
|
resolve(cloneLibraryItems(libraryItems));
|
|
|
|
}
|
2022-04-20 14:40:03 +02:00
|
|
|
} catch (error) {
|
|
|
|
return resolve(this.lastLibraryItems);
|
2020-10-30 21:01:41 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2022-04-29 16:45:02 +02:00
|
|
|
setLibrary = (
|
|
|
|
/**
|
|
|
|
* LibraryItems that will replace current items. Can be a function which
|
|
|
|
* will be invoked after all previous tasks are resolved
|
|
|
|
* (this is the prefered way to update the library to avoid race conditions,
|
|
|
|
* but you'll want to manually merge the library items in the callback
|
|
|
|
* - which is what we're doing in Library.importLibrary()).
|
|
|
|
*
|
|
|
|
* If supplied promise is rejected with AbortError, we swallow it and
|
|
|
|
* do not update the library.
|
|
|
|
*/
|
|
|
|
libraryItems:
|
|
|
|
| LibraryItems
|
|
|
|
| Promise<LibraryItems>
|
|
|
|
| ((
|
|
|
|
latestLibraryItems: LibraryItems,
|
|
|
|
) => LibraryItems | Promise<LibraryItems>),
|
|
|
|
): Promise<LibraryItems> => {
|
|
|
|
const task = new Promise<LibraryItems>(async (resolve, reject) => {
|
|
|
|
try {
|
|
|
|
await this.getLastUpdateTask();
|
2022-04-20 14:40:03 +02:00
|
|
|
|
2022-04-29 16:45:02 +02:00
|
|
|
if (typeof libraryItems === "function") {
|
|
|
|
libraryItems = libraryItems(this.lastLibraryItems);
|
|
|
|
}
|
2022-04-20 14:40:03 +02:00
|
|
|
|
2022-04-29 16:45:02 +02:00
|
|
|
this.lastLibraryItems = cloneLibraryItems(await libraryItems);
|
|
|
|
|
|
|
|
resolve(this.lastLibraryItems);
|
|
|
|
} catch (error: any) {
|
|
|
|
reject(error);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.catch((error) => {
|
|
|
|
if (error.name === "AbortError") {
|
|
|
|
console.warn("Library update aborted by user");
|
|
|
|
return this.lastLibraryItems;
|
|
|
|
}
|
|
|
|
throw error;
|
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
this.updateQueue = this.updateQueue.filter((_task) => _task !== task);
|
|
|
|
this.notifyListeners();
|
2022-04-20 14:40:03 +02:00
|
|
|
});
|
2022-04-29 16:45:02 +02:00
|
|
|
|
|
|
|
this.updateQueue.push(task);
|
|
|
|
this.notifyListeners();
|
|
|
|
|
|
|
|
return task;
|
2020-10-30 21:01:41 +01:00
|
|
|
};
|
2020-07-27 15:29:19 +03:00
|
|
|
}
|
2021-04-21 23:38:24 +05:30
|
|
|
|
|
|
|
export default Library;
|