diff --git a/manifest-beta.json b/manifest-beta.json index 7b29993..7a42248 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -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", diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 5454a4d..ea0acdd 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -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: diff --git a/src/main.ts b/src/main.ts index d958ccc..ce70aac 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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); diff --git a/src/settings.ts b/src/settings.ts index ce18c8e..7cca5b8 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -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"))) diff --git a/src/utils/ImageCache.ts b/src/utils/ImageCache.ts index ef273a4..13df3f5 100644 --- a/src/utils/ImageCache.ts +++ b/src/utils/ImageCache.ts @@ -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 { - const start = Date.now(); + private async initializeDB(): Promise { 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).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((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((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 { - const store = await this.getObjectStore("readwrite"); - const request = store.put(data, key); - - return new Promise((resolve, reject) => { - request.onsuccess = () => { - resolve(); - }; - - request.onerror = () => { - reject(new Error("Failed to store data in IndexedDB.")); - }; - }); - } - - private async deleteCacheData(key: string): Promise { - const store = await this.getObjectStore("readwrite"); - store.delete(key); - } - public async isCached(key_: ImageKey): Promise { 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 { - 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 { - const key = getKey(key_); - return this.deleteCacheData(key); - } -} + public async clear(): Promise { + // 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 { - const db = await imageCache.openDB(); - const transaction = db.transaction("ImageStore", "readwrite"); - const store = transaction.objectStore("ImageStore"); - const request = store.openCursor(); - - return new Promise((resolve, reject) => { - request.onsuccess = (event: Event) => { - const cursor = (event.target as IDBRequest).result; - if (cursor) { - const key: ImageKey = JSON.parse(cursor.key as string); - if (key.filepath === filepath) { - cursor.delete(); - } - cursor.continue(); - } else { + return new Promise((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); \ No newline at end of file