mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
1.9.6.1-beta
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "1.9.4-beta",
|
||||
"version": "1.9.6.1-beta",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -298,6 +298,7 @@ FILENAME_HEAD: "Filename",
|
||||
MD_HEAD_DESC:
|
||||
`You can transclude formatted markdown documents into drawings as images ${labelSHIFT()} drop from the file explorer or using ` +
|
||||
"the command palette action.",
|
||||
|
||||
MD_TRANSCLUDE_WIDTH_NAME: "Default width of a transcluded markdown document",
|
||||
MD_TRANSCLUDE_WIDTH_DESC:
|
||||
"The width of the markdown page. This effects the word wrapping when transcluding longer paragraphs, and the width of " +
|
||||
@@ -334,6 +335,10 @@ FILENAME_HEAD: "Filename",
|
||||
"You can add one custom font beyond that using the setting above. " +
|
||||
'You can override this css setting by adding the following frontmatter-key to the embedded markdown file: "excalidraw-css: css_file_in_vault|css-snippet".',
|
||||
EMBED_HEAD: "Embed & Export",
|
||||
EMBED_IMAGE_CACHE_NAME: "Cache images for embedding in markdown",
|
||||
EMBED_IMAGE_CACHE_DESC: "Cache images for embedding in markdown. This will speed up the embedding process, but in case you compose images of several sub-component drawings, " +
|
||||
"the embedded image in Markdown won't update until you open the drawing and save it to trigger an update of the cache.",
|
||||
EMBED_IMAGE_CACHE_CLEAR: "Clear image cache",
|
||||
EMBED_REUSE_EXPORTED_IMAGE_NAME:
|
||||
"If found, use the already exported image for preview",
|
||||
EMBED_REUSE_EXPORTED_IMAGE_DESC:
|
||||
|
||||
@@ -104,6 +104,8 @@ import { emulateCTRLClickForLinks, linkClickModifierType, PaneTarget } from "./u
|
||||
import { InsertPDFModal } from "./dialogs/InsertPDFModal";
|
||||
import { ExportDialog } from "./dialogs/ExportDialog";
|
||||
import { UniversalInsertFileModal } from "./dialogs/UniversalInsertFileModal";
|
||||
import { image } from "html2canvas/dist/types/css/types/image";
|
||||
import { imageCache } from "./utils/ImageCache";
|
||||
|
||||
declare module "obsidian" {
|
||||
interface App {
|
||||
@@ -201,6 +203,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
addIcon(EXPORT_IMG_ICON_NAME, EXPORT_IMG_ICON);
|
||||
|
||||
await this.loadSettings({reEnableAutosave:true});
|
||||
imageCache.plugin = this;
|
||||
|
||||
this.addSettingTab(new ExcalidrawSettingTab(this.app, this));
|
||||
this.ea = await initExcalidrawAutomate(this);
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
fragWithHTML,
|
||||
setLeftHandedMode,
|
||||
} from "./utils/Utils";
|
||||
import { image } from "html2canvas/dist/types/css/types/image";
|
||||
import { imageCache } from "./utils/ImageCache";
|
||||
|
||||
export interface ExcalidrawSettings {
|
||||
folder: string;
|
||||
@@ -40,6 +42,7 @@ export interface ExcalidrawSettings {
|
||||
drawingFilenameDateTime: string;
|
||||
useExcalidrawExtension: boolean;
|
||||
displaySVGInPreview: boolean;
|
||||
allowImageCache: boolean;
|
||||
displayExportedImageIfAvailable: boolean;
|
||||
previewMatchObsidianTheme: boolean;
|
||||
width: string;
|
||||
@@ -153,6 +156,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
drawingFilenameDateTime: "YYYY-MM-DD HH.mm.ss",
|
||||
useExcalidrawExtension: true,
|
||||
displaySVGInPreview: true,
|
||||
allowImageCache: true,
|
||||
displayExportedImageIfAvailable: false,
|
||||
previewMatchObsidianTheme: false,
|
||||
width: "400",
|
||||
@@ -1135,6 +1139,25 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
|
||||
this.containerEl.createEl("h1", { text: t("EMBED_HEAD") });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("EMBED_IMAGE_CACHE_NAME"))
|
||||
.setDesc(fragWithHTML(t("EMBED_IMAGE_CACHE_DESC")))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.allowImageCache)
|
||||
.onChange((value) => {
|
||||
this.plugin.settings.allowImageCache = value;
|
||||
this.applySettingsUpdate();
|
||||
})
|
||||
)
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText(t("EMBED_IMAGE_CACHE_CLEAR"))
|
||||
.onClick(() => {
|
||||
imageCache.clear();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("EMBED_PREVIEW_SVG_NAME"))
|
||||
.setDesc(fragWithHTML(t("EMBED_PREVIEW_SVG_DESC")))
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { TFile } from "obsidian";
|
||||
import { Notice, TFile } from "obsidian";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
|
||||
//@ts-ignore
|
||||
const DB_NAME = "Excalidraw " + app.appId;
|
||||
const STORE_NAME = "imageCache";
|
||||
|
||||
|
||||
type FileCacheData = { mtime: number; imageBase64: string };
|
||||
|
||||
type ImageKey = {
|
||||
filepath: string;
|
||||
blockref: string;
|
||||
@@ -10,31 +17,25 @@ type ImageKey = {
|
||||
scale: number;
|
||||
};
|
||||
|
||||
const getKey = (key: ImageKey): string =>
|
||||
JSON.stringify({
|
||||
filepath: key.filepath,
|
||||
blockref: key.blockref,
|
||||
sectionref: key.sectionref,
|
||||
isDark: key.isDark,
|
||||
isSVG: key.isSVG,
|
||||
scale: key.scale,
|
||||
});
|
||||
const getKey = (key: ImageKey): string => `${key.filepath}#${key.blockref}#${key.sectionref}#${key.isDark?1:0}#${key.isSVG?1:0}#${key.scale}`;
|
||||
|
||||
class ImageCache {
|
||||
private dbName: string;
|
||||
private storeName: string;
|
||||
private db: IDBDatabase | null;
|
||||
private isInitializing: boolean;
|
||||
public plugin: ExcalidrawPlugin;
|
||||
|
||||
constructor(dbName: string, storeName: string) {
|
||||
this.dbName = dbName;
|
||||
this.storeName = storeName;
|
||||
this.db = null;
|
||||
this.isInitializing = false;
|
||||
this.plugin = null;
|
||||
app.workspace.onLayoutReady(()=>this.initializeDB());
|
||||
}
|
||||
|
||||
public async initializeDB(): Promise<void> {
|
||||
const start = Date.now();
|
||||
private async initializeDB(): Promise<void> {
|
||||
if (this.isInitializing || this.db !== null) {
|
||||
return;
|
||||
}
|
||||
@@ -62,9 +63,8 @@ class ImageCache {
|
||||
};
|
||||
});
|
||||
|
||||
// Pre-create the object store to reduce delay when accessing it later
|
||||
// Pre-create the object store to reduce delay when accessing it later
|
||||
if (!this.db.objectStoreNames.contains(this.storeName)) {
|
||||
console.log("Creating object store");
|
||||
const version = this.db.version + 1;
|
||||
this.db.close();
|
||||
|
||||
@@ -97,11 +97,11 @@ class ImageCache {
|
||||
};
|
||||
});
|
||||
}
|
||||
//await this.purgeInvalidFiles();
|
||||
await this.purgeInvalidFiles();
|
||||
|
||||
} finally {
|
||||
this.isInitializing = false;
|
||||
console.log(`Initialized Excalidraw Image Cache database in ${Date.now() - start}ms`);
|
||||
console.log("Initialized Excalidraw Image Cache");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,24 +117,19 @@ class ImageCache {
|
||||
request.onsuccess = (event: Event) => {
|
||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue | null>).result;
|
||||
if (cursor) {
|
||||
const key: ImageKey = JSON.parse(cursor.key as string);
|
||||
const fileExists = files.some((file: TFile) => {
|
||||
return file.path.split("#")[0] === key.filepath;
|
||||
});
|
||||
if (!fileExists) {
|
||||
cursor.delete();
|
||||
} else {
|
||||
const file = files.find((file: TFile) => file.path.split("#")[0] === key.filepath);
|
||||
if (file && file.stat.mtime > cursor.value.mtime) {
|
||||
deletePromises.push(
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const deleteRequest = store.delete(cursor.primaryKey);
|
||||
deleteRequest.onsuccess = () => resolve();
|
||||
deleteRequest.onerror = () =>
|
||||
reject(new Error(`Failed to delete file with key: ${key}`));
|
||||
})
|
||||
);
|
||||
}
|
||||
const key = cursor.key as string;
|
||||
const filepath = key.split("#")[0];
|
||||
const fileExists = files.some((f: TFile) => f.path === filepath);
|
||||
const file = fileExists ? files.find((f: TFile) => f.path === filepath) : null;
|
||||
if (!file || (file && file.stat.mtime > cursor.value.mtime)) {
|
||||
deletePromises.push(
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const deleteRequest = store.delete(cursor.primaryKey);
|
||||
deleteRequest.onsuccess = () => resolve();
|
||||
deleteRequest.onerror = () =>
|
||||
reject(new Error(`Failed to delete file with key: ${key}`));
|
||||
})
|
||||
);
|
||||
}
|
||||
cursor.continue();
|
||||
} else {
|
||||
@@ -186,26 +181,6 @@ class ImageCache {
|
||||
});
|
||||
}
|
||||
|
||||
private async setCacheData(key: string, data: FileCacheData): Promise<void> {
|
||||
const store = await this.getObjectStore("readwrite");
|
||||
const request = store.put(data, key);
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error("Failed to store data in IndexedDB."));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async deleteCacheData(key: string): Promise<void> {
|
||||
const store = await this.getObjectStore("readwrite");
|
||||
store.delete(key);
|
||||
}
|
||||
|
||||
public async isCached(key_: ImageKey): Promise<boolean> {
|
||||
const key = getKey(key_);
|
||||
return this.getCacheData(key).then((cachedData) => {
|
||||
@@ -221,13 +196,11 @@ class ImageCache {
|
||||
}
|
||||
|
||||
public isReady(): boolean {
|
||||
return !!this.db && !this.isInitializing;
|
||||
return !!this.db && !this.isInitializing && !!this.plugin && this.plugin.settings.allowImageCache;
|
||||
}
|
||||
|
||||
public async get(key_: ImageKey): Promise<string | undefined> {
|
||||
const start = Date.now();
|
||||
if (!this.db || this.isInitializing) {
|
||||
console.log(`get from cache FAILED (not ready) in ${Date.now() - start}ms`);
|
||||
if (!this.isReady()) {
|
||||
return null; // Database not initialized yet
|
||||
}
|
||||
|
||||
@@ -236,17 +209,14 @@ class ImageCache {
|
||||
const file = app.vault.getAbstractFileByPath(key_.filepath.split("#")[0]);
|
||||
if (!file || !(file instanceof TFile)) return undefined;
|
||||
if (cachedData && cachedData.mtime === file.stat.mtime) {
|
||||
console.log(`get from cache SUCCEEDED in ${Date.now() - start}ms`);
|
||||
return cachedData.imageBase64;
|
||||
}
|
||||
console.log(`get from cache FAILED in ${Date.now() - start}ms`);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
public add(key_: ImageKey, imageBase64: string): void {
|
||||
const start = Date.now();
|
||||
if (!this.db || this.isInitializing) {
|
||||
if (!this.isReady()) {
|
||||
return; // Database not initialized yet
|
||||
}
|
||||
|
||||
@@ -258,43 +228,29 @@ class ImageCache {
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const key = getKey(key_)
|
||||
store.put(data, key);
|
||||
console.log(`add to cache in ${Date.now() - start}ms`);
|
||||
}
|
||||
|
||||
delete(key_: ImageKey): Promise<void> {
|
||||
const key = getKey(key_);
|
||||
return this.deleteCacheData(key);
|
||||
}
|
||||
}
|
||||
public async clear(): Promise<void> {
|
||||
// deliberately not checking isReady() here
|
||||
if (!this.db || this.isInitializing) {
|
||||
return; // Database not initialized yet
|
||||
}
|
||||
|
||||
const imageCache = new ImageCache("ExcalidrawImageDB", "ImageStore");
|
||||
imageCache.initializeDB();
|
||||
const transaction = this.db.transaction(this.storeName, "readwrite");
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.clear();
|
||||
|
||||
async function searchAndDeleteImages(filepath: string): Promise<void> {
|
||||
const db = await imageCache.openDB();
|
||||
const transaction = db.transaction("ImageStore", "readwrite");
|
||||
const store = transaction.objectStore("ImageStore");
|
||||
const request = store.openCursor();
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
request.onsuccess = (event: Event) => {
|
||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue | null>).result;
|
||||
if (cursor) {
|
||||
const key: ImageKey = JSON.parse(cursor.key as string);
|
||||
if (key.filepath === filepath) {
|
||||
cursor.delete();
|
||||
}
|
||||
cursor.continue();
|
||||
} else {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
request.onsuccess = () => {
|
||||
new Notice("Image cache cleared.");
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error("Failed to search and delete images in IndexedDB."));
|
||||
};
|
||||
});
|
||||
request.onerror = () => {
|
||||
reject(new Error("Failed to clear data in IndexedDB."));
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { imageCache, searchAndDeleteImages };
|
||||
|
||||
export const imageCache = new ImageCache(DB_NAME, STORE_NAME);
|
||||
Reference in New Issue
Block a user