mirror of
https://github.com/Dvorinka/excalidraw-full.git
synced 2026-06-04 22:32:55 +00:00
init frontend
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* This file deals with saving data state (appState, elements, images, ...)
|
||||
* locally to the browser.
|
||||
*
|
||||
* Notes:
|
||||
*
|
||||
* - DataState refers to full state of the app: appState, elements, images,
|
||||
* though some state is saved separately (collab username, library) for one
|
||||
* reason or another. We also save different data to different storage
|
||||
* (localStorage, indexedDB).
|
||||
*/
|
||||
|
||||
import {
|
||||
createStore,
|
||||
entries,
|
||||
del,
|
||||
getMany,
|
||||
set,
|
||||
setMany,
|
||||
get,
|
||||
} from "idb-keyval";
|
||||
import { clearAppStateForLocalStorage } from "../../packages/excalidraw/appState";
|
||||
import { LibraryPersistedData } from "../../packages/excalidraw/data/library";
|
||||
import { ImportedDataState } from "../../packages/excalidraw/data/types";
|
||||
import { clearElementsForLocalStorage } from "../../packages/excalidraw/element";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
FileId,
|
||||
} from "../../packages/excalidraw/element/types";
|
||||
import {
|
||||
AppState,
|
||||
BinaryFileData,
|
||||
BinaryFiles,
|
||||
} from "../../packages/excalidraw/types";
|
||||
import { 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";
|
||||
import { Locker } from "./Locker";
|
||||
import { updateBrowserStateVersion } from "./tabSync";
|
||||
|
||||
const filesStore = createStore("files-db", "files-store");
|
||||
|
||||
class LocalFileManager extends FileManager {
|
||||
clearObsoleteFiles = async (opts: { currentFileIds: FileId[] }) => {
|
||||
await entries(filesStore).then((entries) => {
|
||||
for (const [id, imageData] of entries as [FileId, BinaryFileData][]) {
|
||||
// if image is unused (not on canvas) & is older than 1 day, delete it
|
||||
// from storage. We check `lastRetrieved` we care about the last time
|
||||
// the image was used (loaded on canvas), not when it was initially
|
||||
// created.
|
||||
if (
|
||||
(!imageData.lastRetrieved ||
|
||||
Date.now() - imageData.lastRetrieved > 24 * 3600 * 1000) &&
|
||||
!opts.currentFileIds.includes(id as FileId)
|
||||
) {
|
||||
del(id, filesStore);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const saveDataStateToLocalStorage = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
) => {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
|
||||
JSON.stringify(clearElementsForLocalStorage(elements)),
|
||||
);
|
||||
localStorage.setItem(
|
||||
STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
|
||||
JSON.stringify(clearAppStateForLocalStorage(appState)),
|
||||
);
|
||||
updateBrowserStateVersion(STORAGE_KEYS.VERSION_DATA_STATE);
|
||||
} catch (error: any) {
|
||||
// Unable to access window.localStorage
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
type SavingLockTypes = "collaboration";
|
||||
|
||||
export class LocalData {
|
||||
private static _save = debounce(
|
||||
async (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
files: BinaryFiles,
|
||||
onFilesSaved: () => void,
|
||||
) => {
|
||||
saveDataStateToLocalStorage(elements, appState);
|
||||
|
||||
await this.fileStorage.saveFiles({
|
||||
elements,
|
||||
files,
|
||||
});
|
||||
onFilesSaved();
|
||||
},
|
||||
SAVE_TO_LOCAL_STORAGE_TIMEOUT,
|
||||
);
|
||||
|
||||
/** Saves DataState, including files. Bails if saving is paused */
|
||||
static save = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
files: BinaryFiles,
|
||||
onFilesSaved: () => void,
|
||||
) => {
|
||||
// we need to make the `isSavePaused` check synchronously (undebounced)
|
||||
if (!this.isSavePaused()) {
|
||||
this._save(elements, appState, files, onFilesSaved);
|
||||
}
|
||||
};
|
||||
|
||||
static flushSave = () => {
|
||||
this._save.flush();
|
||||
};
|
||||
|
||||
private static locker = new Locker<SavingLockTypes>();
|
||||
|
||||
static pauseSave = (lockType: SavingLockTypes) => {
|
||||
this.locker.lock(lockType);
|
||||
};
|
||||
|
||||
static resumeSave = (lockType: SavingLockTypes) => {
|
||||
this.locker.unlock(lockType);
|
||||
};
|
||||
|
||||
static isSavePaused = () => {
|
||||
return document.hidden || this.locker.isLocked();
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static fileStorage = new LocalFileManager({
|
||||
getFiles(ids) {
|
||||
return getMany(ids, filesStore).then(
|
||||
async (filesData: (BinaryFileData | undefined)[]) => {
|
||||
const loadedFiles: BinaryFileData[] = [];
|
||||
const erroredFiles = new Map<FileId, true>();
|
||||
|
||||
const filesToSave: [FileId, BinaryFileData][] = [];
|
||||
|
||||
filesData.forEach((data, index) => {
|
||||
const id = ids[index];
|
||||
if (data) {
|
||||
const _data: BinaryFileData = {
|
||||
...data,
|
||||
lastRetrieved: Date.now(),
|
||||
};
|
||||
filesToSave.push([id, _data]);
|
||||
loadedFiles.push(_data);
|
||||
} else {
|
||||
erroredFiles.set(id, true);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
// save loaded files back to storage with updated `lastRetrieved`
|
||||
setMany(filesToSave, filesStore);
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
|
||||
return { loadedFiles, erroredFiles };
|
||||
},
|
||||
);
|
||||
},
|
||||
async saveFiles({ addedFiles }) {
|
||||
const savedFiles = new Map<FileId, true>();
|
||||
const erroredFiles = new Map<FileId, true>();
|
||||
|
||||
// before we use `storage` event synchronization, let's update the flag
|
||||
// optimistically. Hopefully nothing fails, and an IDB read executed
|
||||
// before an IDB write finishes will read the latest value.
|
||||
updateBrowserStateVersion(STORAGE_KEYS.VERSION_FILES);
|
||||
|
||||
await Promise.all(
|
||||
[...addedFiles].map(async ([id, fileData]) => {
|
||||
try {
|
||||
await set(id, fileData, filesStore);
|
||||
savedFiles.set(id, true);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
erroredFiles.set(id, true);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return { savedFiles, erroredFiles };
|
||||
},
|
||||
});
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user