diff --git a/images/excalidraw-modifiers.png b/images/excalidraw-modifiers.png index 40c7392..6a9adec 100644 Binary files a/images/excalidraw-modifiers.png and b/images/excalidraw-modifiers.png differ diff --git a/manifest.json b/manifest.json index 0940ceb..7505b60 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-excalidraw-plugin", "name": "Excalidraw", - "version": "1.9.23", + "version": "1.9.24", "minAppVersion": "1.1.6", "description": "An Obsidian plugin to edit and view Excalidraw drawings", "author": "Zsolt Viczian", diff --git a/src/EmbeddedFileLoader.ts b/src/EmbeddedFileLoader.ts index 9390df5..907dd97 100644 --- a/src/EmbeddedFileLoader.ts +++ b/src/EmbeddedFileLoader.ts @@ -23,7 +23,7 @@ import { ExportSettings } from "./ExcalidrawView"; import { t } from "./lang/helpers"; import { tex2dataURL } from "./LaTeX"; import ExcalidrawPlugin from "./main"; -import { blobToBase64, getDataURLFromURL, getMimeType, getPDFDoc, getURLImageExtension } from "./utils/FileUtils"; +import { blobToBase64, getDataURLFromURL, getMimeType, getPDFDoc, getURLImageExtension, readLocalFileBinary } from "./utils/FileUtils"; import { errorlog, getDataURL, @@ -151,7 +151,8 @@ export class EmbeddedFile { public linkParts: LinkParts; private hostPath: string; public attemptCounter: number = 0; - public isHyperlink: boolean = false; + public isHyperLink: boolean = false; + public isLocalLink: boolean = false; public hyperlink:DataURL; public colorMap: ColorMap | null = null; @@ -171,12 +172,18 @@ export class EmbeddedFile { this.imgInverted = this.img = ""; this.mtime = 0; - if(imgPath.startsWith("https://") || imgPath.startsWith("http://")){ - this.isHyperlink = true; + if(imgPath.startsWith("https://") || imgPath.startsWith("http://") || imgPath.startsWith("ftp://") || imgPath.startsWith("ftps://")) { + this.isHyperLink = true; this.hyperlink = imgPath as DataURL; return; }; + if(imgPath.startsWith("file://")) { + this.isLocalLink = true; + this.hyperlink = imgPath as DataURL; + return; + } + this.linkParts = getLinkParts(imgPath); this.hostPath = hostPath; if (!this.linkParts.path) { @@ -204,11 +211,11 @@ export class EmbeddedFile { } private fileChanged(): boolean { - if(this.isHyperlink) { + if(this.isHyperLink || this.isLocalLink) { return false; } if (!this.file) { - this.file = app.metadataCache.getFirstLinkpathDest( + this.file = this.plugin.app.metadataCache.getFirstLinkpathDest( this.linkParts.path, this.hostPath, ); // maybe the file has synchronized in the mean time @@ -227,13 +234,13 @@ export class EmbeddedFile { isDark: boolean, isSVGwithBitmap: boolean, ) { - if (!this.file && !this.isHyperlink) { + if (!this.file && !this.isHyperLink && !this.isLocalLink) { return; } if (this.fileChanged()) { this.imgInverted = this.img = ""; } - this.mtime = this.isHyperlink ? 0 : this.file.stat.mtime; + this.mtime = this.isHyperLink || this.isLocalLink ? 0 : this.file.stat.mtime; this.size = size; this.mimeType = mimeType; switch (isDark && isSVGwithBitmap) { @@ -248,7 +255,7 @@ export class EmbeddedFile { } public isLoaded(isDark: boolean): boolean { - if(!this.isHyperlink) { + if(!this.isHyperLink && !this.isLocalLink) { if (!this.file) { this.file = app.metadataCache.getFirstLinkpathDest( this.linkParts.path, @@ -270,7 +277,7 @@ export class EmbeddedFile { } public getImage(isDark: boolean) { - if (!this.file && !this.isHyperlink) { + if (!this.file && !this.isHyperLink && !this.isLocalLink) { return ""; } if (isDark && this.isSVGwithBitmap) { @@ -284,7 +291,7 @@ export class EmbeddedFile { * @returns true if image should scale such as the updated images has the same area as the previous images, false if the image should be displayed at 100% */ public shouldScale() { - return this.isHyperlink || !Boolean(this.linkParts && this.linkParts.original && this.linkParts.original.endsWith("|100%")); + return this.isHyperLink || this.isLocalLink || !Boolean(this.linkParts && this.linkParts.original && this.linkParts.original.endsWith("|100%")); } } @@ -388,7 +395,8 @@ export class EmbeddedFilesLoader { return null; } - const isHyperlink = inFile instanceof EmbeddedFile ? inFile.isHyperlink : false; + const isHyperLink = inFile instanceof EmbeddedFile ? inFile.isHyperLink : false; + const isLocalLink = inFile instanceof EmbeddedFile ? inFile.isLocalLink : false; const hyperlink = inFile instanceof EmbeddedFile ? inFile.hyperlink : ""; const file: TFile = inFile instanceof EmbeddedFile ? inFile.file : inFile; if(file && markdownRendererRecursionWatcthdog.has(file)) { @@ -397,7 +405,7 @@ export class EmbeddedFilesLoader { } const linkParts = - isHyperlink + isHyperLink ? null : inFile instanceof EmbeddedFile ? inFile.linkParts @@ -412,11 +420,11 @@ export class EmbeddedFilesLoader { }; let hasSVGwithBitmap = false; - const isExcalidrawFile = !isHyperlink && this.plugin.isExcalidrawFile(file); - const isPDF = !isHyperlink && file.extension.toLowerCase() === "pdf"; + const isExcalidrawFile = !isHyperLink && !isLocalLink && this.plugin.isExcalidrawFile(file); + const isPDF = !isHyperLink && !isLocalLink && file.extension.toLowerCase() === "pdf"; if ( - !isHyperlink && !isPDF && + !isHyperLink && !isPDF && !isLocalLink && !( IMAGE_TYPES.contains(file.extension) || isExcalidrawFile || @@ -425,9 +433,11 @@ export class EmbeddedFilesLoader { ) { return null; } - const ab = isHyperlink || isPDF + const ab = isHyperLink || isPDF ? null - : await app.vault.readBinary(file); + : isLocalLink + ? await readLocalFileBinary((inFile as EmbeddedFile).hyperlink.split("file://")[1]) + : await app.vault.readBinary(file); let dURL: DataURL = null; if (isExcalidrawFile) { @@ -452,7 +462,7 @@ export class EmbeddedFilesLoader { ? "image/png" : "image/svg+xml"; - const extension = isHyperlink + const extension = isHyperLink || isLocalLink ? getURLImageExtension(hyperlink) : file.extension; if (!isExcalidrawFile && !isPDF) { @@ -460,20 +470,20 @@ export class EmbeddedFilesLoader { } let dataURL = - isHyperlink + isHyperLink ? ( inFile instanceof EmbeddedFile ? await getDataURLFromURL(inFile.hyperlink, mimeType) : null ) : excalidrawSVG ?? pdfDataURL ?? - (file.extension === "svg" + (file?.extension === "svg" ? await getSVGData(app, file, inFile instanceof EmbeddedFile ? inFile.colorMap : null) - : file.extension === "md" + : file?.extension === "md" ? null : await getDataURL(ab, mimeType)); - if(!isHyperlink && !dataURL) { + if(!isHyperLink && !dataURL && !isLocalLink) { markdownRendererRecursionWatcthdog.add(file); const result = await this.convertMarkdownToSVG(this.plugin, file, linkParts, depth); markdownRendererRecursionWatcthdog.delete(file); @@ -485,10 +495,10 @@ export class EmbeddedFilesLoader { return { mimeType, fileId: await generateIdFromFile( - isHyperlink || isPDF ? (new TextEncoder()).encode(dataURL as string) : ab + isHyperLink || isPDF ? (new TextEncoder()).encode(dataURL as string) : ab ), dataURL, - created: isHyperlink ? 0 : file.stat.mtime, + created: isHyperLink || isLocalLink ? 0 : file.stat.mtime, hasSVGwithBitmap, size, }; diff --git a/src/ExcalidrawAutomate.ts b/src/ExcalidrawAutomate.ts index a5bd13d..7f3a828 100644 --- a/src/ExcalidrawAutomate.ts +++ b/src/ExcalidrawAutomate.ts @@ -402,7 +402,7 @@ export class ExcalidrawAutomate { * get all elements from ExcalidrawAutomate elementsDict * @returns elements from elemenetsDict */ - getElements(): ExcalidrawElement[] { + getElements(): Mutable[] { const elements = []; const elementIds = Object.keys(this.elementsDict); for (let i = 0; i < elementIds.length; i++) { @@ -416,7 +416,7 @@ export class ExcalidrawAutomate { * @param id * @returns */ - getElement(id: string): ExcalidrawElement { + getElement(id: string): Mutable { return this.elementsDict[id]; }; @@ -1164,7 +1164,7 @@ export class ExcalidrawAutomate { this.imagesDict[key as FileId] = { ...result.files[key], created: Date.now(), - isHyperlink: false, + isHyperLink: false, hyperlink: null, file: null, hasSVGwithBitmap: false, @@ -1209,7 +1209,7 @@ export class ExcalidrawAutomate { id: fileId, dataURL: image.dataURL, created: image.created, - isHyperlink: typeof imageFile === "string", + isHyperLink: typeof imageFile === "string", hyperlink: typeof imageFile === "string" ? imageFile : null, diff --git a/src/ExcalidrawData.ts b/src/ExcalidrawData.ts index b63f01c..3b66cc1 100644 --- a/src/ExcalidrawData.ts +++ b/src/ExcalidrawData.ts @@ -604,7 +604,7 @@ export class ExcalidrawData { } //Load links - const REG_LINKID_FILEPATH = /([\w\d]*):\s*(https?:\/\/[^\s]*)\n/gm; + const REG_LINKID_FILEPATH = /([\w\d]*):\s*((?:https?|file|ftps?):\/\/[^\s]*)\n/gm; res = data.matchAll(REG_LINKID_FILEPATH); while (!(parts = res.next()).done) { const embeddedFile = new EmbeddedFile( @@ -999,8 +999,9 @@ export class ExcalidrawData { if (parsedLink) { outString += parsedLink; if (!(urlIcon || linkIcon)) { - if (REGEX_LINK.getLink(parts).match(REG_LINKINDEX_HYPERLINK)) { - urlIcon = true; + const linkText = REGEX_LINK.getLink(parts); + if (linkText.match(REG_LINKINDEX_HYPERLINK)) { + urlIcon = !linkText.startsWith("cmd://"); //don't display the url icon for cmd:// links } else { linkIcon = true; } @@ -1076,8 +1077,9 @@ export class ExcalidrawData { if (parsedLink) { outString += parsedLink; if (!(urlIcon || linkIcon)) { - if (REGEX_LINK.getLink(parts).match(REG_LINKINDEX_HYPERLINK)) { - urlIcon = true; + const linkText = REGEX_LINK.getLink(parts); + if (linkText.match(REG_LINKINDEX_HYPERLINK)) { + urlIcon = !linkText.startsWith("cmd://"); //don't display the url icon for cmd:// links } else { linkIcon = true; } @@ -1132,7 +1134,7 @@ export class ExcalidrawData { for (const key of this.files.keys()) { const PATHREG = /(^[^#\|]*)/; const ef = this.files.get(key); - if(ef.isHyperlink) { + if(ef.isHyperLink || ef.isLocalLink) { outString += `${key}: ${ef.hyperlink}\n`; } else { //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/829 @@ -1266,14 +1268,18 @@ export class ExcalidrawData { const mermaid = this.getMermaid(fileId); //images should have a single reference, but equations, and markdown embeds should have as many as instances of the file in the scene - if(file && (file.isHyperlink || (file.file && (file.file.extension !== "md" || this.plugin.isExcalidrawFile(file.file))))) { + if(file && (file.isHyperLink || file.isLocalLink || (file.file && (file.file.extension !== "md" || this.plugin.isExcalidrawFile(file.file))))) { return; } if(mermaid) { return; } const newId = fileid(); - (scene.elements.filter((el:ExcalidrawImageElement)=>el.fileId === fileId)[0] as any).fileId = newId; + (scene + .elements + .filter((el:ExcalidrawImageElement)=>el.fileId === fileId) + .sort((a,b)=>a.updated { - if (!formula || formula === equation) { - return; - } - this.excalidrawData.setEquation(selectedImage.fileId, { - latex: formula, - isLoaded: false, - }); + (async () => { + debugger; await this.save(false); - await updateEquation( - formula, + selectedImage.fileId = this.getScene().elements.filter((el:ExcalidrawElement)=>el.id === selectedImage.id)[0].fileId; + const equation = this.excalidrawData.getEquation( selectedImage.fileId, - this, - addFiles, - this.plugin, - ); - this.setDirty(1); - }); + ).latex; + const prompt = new Prompt(this.app, t("ENTER_LATEX"), equation, ""); + prompt.openAndGetValue(async (formula: string) => { + if (!formula || formula === equation) { + return; + } + this.excalidrawData.setEquation(selectedImage.fileId, { + latex: formula, + isLoaded: false, + }); + await this.save(false); + await updateEquation( + formula, + selectedImage.fileId, + this, + addFiles, + this.plugin, + ); + this.setDirty(1); + }); + })(); return; } if (this.excalidrawData.hasMermaid(selectedImage.fileId)) { @@ -1007,7 +1019,7 @@ export default class ExcalidrawView extends TextFileView { await this.save(false); //in case pasted images haven't been saved yet if (this.excalidrawData.hasFile(selectedImage.fileId)) { const ef = this.excalidrawData.getFile(selectedImage.fileId); - if(ef.isHyperlink) { + if(ef.isHyperLink || ef.isLocalLink) { window.open(ef.hyperlink,"_blank"); return; } @@ -2717,11 +2729,11 @@ export default class ExcalidrawView extends TextFileView { dataURL: images[k].dataURL, created: images[k].created, }); - if (images[k].file || images[k].isHyperlink) { + if (images[k].file || images[k].isHyperLink || images[k].isLocalLink) { const embeddedFile = new EmbeddedFile( this.plugin, this.file.path, - images[k].isHyperlink + images[k].isHyperLink && !images[k].isLocalLink ? images[k].hyperlink : images[k].file, ); @@ -2922,7 +2934,7 @@ export default class ExcalidrawView extends TextFileView { return; } const ef = this.excalidrawData.getFile(selectedImgElement.fileId); - if(ef.isHyperlink) return; //web images don't have a preview + if(ef.isHyperLink || ef.isLocalLink) return; //web images don't have a preview if(IMAGE_TYPES.contains(ef.file.extension)) return; //images don't have a preview if(ef.file.extension.toLowerCase() === "pdf") return; //pdfs don't have a preview if(this.plugin.ea.isExcalidrawFile(ef.file)) return; //excalidraw files don't have a preview @@ -3097,7 +3109,11 @@ export default class ExcalidrawView extends TextFileView { } } else if(e.dataTransfer.types.length === 1 && e.dataTransfer.types.includes("Files")) { //drag from OS file manager - msg = "External file" + switch (localFileDragModifierType(e)) { + case "image-import": msg = "Import image to Vault"; break; + case "image-uri": msg = `Insert image with local URI`; break; + case "insert-link": msg = "Insert link"; break; + } } else { //drag from Internet switch (externalDragModifierType(e)) { @@ -3378,6 +3394,7 @@ export default class ExcalidrawView extends TextFileView { const draggable = (app as any).dragManager.draggable; const internalDragAction = internalDragModifierType(event); const externalDragAction = externalDragModifierType(event); + const localFileDragAction = localFileDragModifierType(event); //Call Excalidraw Automate onDropHook const onDropHook = ( @@ -3604,6 +3621,35 @@ export default class ExcalidrawView extends TextFileView { return false; } } + + if(event.dataTransfer.types.length >= 1 && localFileDragAction === "image-uri") { + (async () => { + for(let i=0;i= 1 && localFileDragAction === "insert-link") { + const ea = getEA(this) as ExcalidrawAutomate; + for(let i=0;i { + public app: App; + private addText: Function; + + constructor(app: App) { + super(app); + this.app = app; + this.limit = 20; + this.setInstructions([ + { + command: t("SELECT_COMMAND"), + purpose: "", + }, + ]); + this.setPlaceholder(t("SELECT_COMMAND_PLACEHOLDER")); + this.emptyStateText = t("NO_MATCHING_COMMAND"); + } + + getItems(): any[] { + //@ts-ignore + return this.app.commands.listCommands(); + } + + getItemText(item: any): string { + return item.name; + } + + onChooseItem(item: any): void { + const cmdId = item?.id; + this.addText(`⚙️[${item.name}](cmd://${item.id})`); + } + + public start(addText: Function) { + this.addText = addText; + this.open(); + } +} diff --git a/src/dialogs/Messages.ts b/src/dialogs/Messages.ts index 9aa5dda..7b13d5b 100644 --- a/src/dialogs/Messages.ts +++ b/src/dialogs/Messages.ts @@ -17,6 +17,21 @@ I develop this plugin as a hobby, spending my free time doing this. If you find
`, +"1.9.24":` +## Fixed +- Resolved some hidden Image and Backup Cache initialization errors. + +## New Features +- Introducing the ${String.fromCharCode(96)}[[cmd://cmd-id]]${String.fromCharCode(96)} link type, along with a new Command Palette Action: ${String.fromCharCode(96)}Insert Obsidian Command as a link${String.fromCharCode(96)}. With this update, you can now add any command available on the Obsidian Command palette as a link in Excalidraw. When you click the link, the corresponding command will be executed. This feature opens up exciting possibilities for automating your drawings by creating Excalidraw Scripts and attaching them to elements. + +- I am thrilled to announce that you can now embed images directly from your local hard drive in Excalidraw. These files won't be moved into Obsidian. Please note, however, that these images won't be synchronized across your other devices. [#1365](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1365) + +Check out the [updated keyboard map](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/excalidraw-modifiers.png) + +Keyboard map + +Stay creative and productive with Excalidraw! +`, "1.9.23":` ## Fixed - Link navigation error in view mode introduced with 1.9.21 [#7120](https://github.com/excalidraw/excalidraw/pull/7120) diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index a6beab6..44ec888 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -57,6 +57,7 @@ export default { INSERT_LINK_TO_ELEMENT_ERROR: "Select a single element in the scene", INSERT_LINK_TO_ELEMENT_READY: "Link is READY and available on the clipboard", INSERT_LINK: "Insert link to file", + INSERT_COMMAND: "Insert Obsidian Command as a link", INSERT_IMAGE: "Insert image or Excalidraw drawing from your vault", IMPORT_SVG: "Import an SVG file as Excalidraw strokes (limited SVG support, TEXT currently not supported)", INSERT_MD: "Insert markdown file from vault", @@ -489,9 +490,12 @@ FILENAME_HEAD: "Filename", //openDrawings.ts SELECT_FILE: "Select a file then press enter.", + SELECT_COMMAND: "Select a command then press enter.", SELECT_FILE_WITH_OPTION_TO_SCALE: `Select a file then press ENTER, or ${labelSHIFT()}+${labelMETA()}+ENTER to insert at 100% scale.`, NO_MATCH: "No file matches your query.", + NO_MATCHING_COMMAND: "No command matches your query.", SELECT_FILE_TO_LINK: "Select the file you want to insert the link for.", + SELECT_COMMAND_PLACEHOLDER: "Select the command you want to insert the link for.", SELECT_DRAWING: "Select the image or drawing you want to insert", TYPE_FILENAME: "Type name of drawing to select.", SELECT_FILE_OR_TYPE_NEW: diff --git a/src/main.ts b/src/main.ts index 750f8cf..afe2e43 100644 --- a/src/main.ts +++ b/src/main.ts @@ -56,6 +56,7 @@ import { } from "./settings"; import { openDialogAction, OpenFileDialog } from "./dialogs/OpenDrawing"; import { InsertLinkDialog } from "./dialogs/InsertLinkDialog"; +import { InsertCommandDialog } from "./dialogs/InsertCommandDialog"; import { InsertImageDialog } from "./dialogs/InsertImageDialog"; import { ImportSVGDialog } from "./dialogs/ImportSVGDialog"; import { InsertMDDialog } from "./dialogs/InsertMDDialog"; @@ -126,6 +127,7 @@ export default class ExcalidrawPlugin extends Plugin { public settings: ExcalidrawSettings; private openDialog: OpenFileDialog; public insertLinkDialog: InsertLinkDialog; + public insertCommandDialog: InsertCommandDialog; public insertImageDialog: InsertImageDialog; public importSVGDialog: ImportSVGDialog; public insertMDDialog: InsertMDDialog; @@ -144,7 +146,7 @@ export default class ExcalidrawPlugin extends Plugin { public opencount: number = 0; public ea: ExcalidrawAutomate; //A master list of fileIds to facilitate copy / paste - public filesMaster: Map = + public filesMaster: Map = null; //fileId, path public equationsMaster: Map = null; //fileId, formula public mermaidsMaster: Map = null; //fileId, mermaidText @@ -163,7 +165,7 @@ export default class ExcalidrawPlugin extends Plugin { super(app, manifest); this.filesMaster = new Map< FileId, - { isHyperlink: boolean; path: string; hasSVGwithBitmap: boolean; blockrefData: string; colorMapJSON?: string } + { isHyperLink: boolean; isLocalLink: boolean; path: string; hasSVGwithBitmap: boolean; blockrefData: string; colorMapJSON?: string } >(); this.equationsMaster = new Map(); this.mermaidsMaster = new Map(); @@ -194,7 +196,6 @@ export default class ExcalidrawPlugin extends Plugin { await this.loadSettings({reEnableAutosave:true}); await loadMermaid(); - imageCache.plugin = this; this.addSettingTab(new ExcalidrawSettingTab(this.app, this)); this.ea = await initExcalidrawAutomate(this); @@ -243,6 +244,7 @@ export default class ExcalidrawPlugin extends Plugin { const self = this; this.app.workspace.onLayoutReady(() => { this.scriptEngine = new ScriptEngine(self); + imageCache.initializeDB(self); }); this.taskbone = new Taskbone(this); } @@ -677,6 +679,7 @@ export default class ExcalidrawPlugin extends Plugin { private registerCommands() { this.openDialog = new OpenFileDialog(this.app, this); this.insertLinkDialog = new InsertLinkDialog(this.app); + this.insertCommandDialog = new InsertCommandDialog(this.app); this.insertImageDialog = new InsertImageDialog(this); this.importSVGDialog = new ImportSVGDialog(this); this.insertMDDialog = new InsertMDDialog(this); @@ -1169,6 +1172,22 @@ export default class ExcalidrawPlugin extends Plugin { }, }); + this.addCommand({ + id: "insert-command", + name: t("INSERT_COMMAND"), + checkCallback: (checking: boolean) => { + if (checking) { + return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView)) + } + const view = this.app.workspace.getActiveViewOfType(ExcalidrawView); + if (view) { + this.insertCommandDialog.start(view.addText); + return true; + } + return false; + }, + }); + this.addCommand({ id: "insert-link-to-element", hotkeys: [{ modifiers: ["Ctrl" || "Meta", "Shift"], key: "k" }], diff --git a/src/utils/FileUtils.ts b/src/utils/FileUtils.ts index 951e32b..fe38b41 100644 --- a/src/utils/FileUtils.ts +++ b/src/utils/FileUtils.ts @@ -285,4 +285,32 @@ export const getPDFDoc = async (f: TFile): Promise => { if(typeof window.pdfjsLib === "undefined") await loadPdfJs(); //@ts-ignore return await window.pdfjsLib.getDocument(app.vault.getResourcePath(f)).promise; +} + +export const readLocalFile = async (filePath:string): Promise => { + return new Promise((resolve, reject) => { + //@ts-ignore + app.vault.adapter.fs.readFile(filePath, 'utf8', (err:any, data:any) => { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + }); +} + +export const readLocalFileBinary = async (filePath:string): Promise => { + return new Promise((resolve, reject) => { + const path = decodeURI(filePath); + //@ts-ignore + app.vault.adapter.fs.readFile(path, (err, data) => { + if (err) { + reject(err); + } else { + const arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength); + resolve(arrayBuffer); + } + }); + }); } \ No newline at end of file diff --git a/src/utils/ImageCache.ts b/src/utils/ImageCache.ts index 06a984a..593185b 100644 --- a/src/utils/ImageCache.ts +++ b/src/utils/ImageCache.ts @@ -1,4 +1,4 @@ -import { Notice, TFile } from "obsidian"; +import { App, Notice, TFile } from "obsidian"; import ExcalidrawPlugin from "src/main"; import { convertSVGStringToElement } from "./Utils"; import { PreviewImageType } from "./UtilTypes"; @@ -36,7 +36,8 @@ class ImageCache { private backupStoreName: string; private db: IDBDatabase | null; private isInitializing: boolean; - public plugin: ExcalidrawPlugin; + private plugin: ExcalidrawPlugin; + private app: App; public initializationNotice: boolean = false; private obsidanURLCache = new Map(); @@ -47,10 +48,11 @@ class ImageCache { this.db = null; this.isInitializing = false; this.plugin = null; - app.workspace.onLayoutReady(() => this.initializeDB()); } - private async initializeDB(): Promise { + public async initializeDB(plugin: ExcalidrawPlugin): Promise { + this.plugin = plugin; + this.app = plugin.app; if (this.isInitializing || this.db !== null) { return; } @@ -124,8 +126,8 @@ class ImageCache { }); } - await this.purgeInvalidCacheFiles(); - await this.purgeInvalidBackupFiles(); + setTimeout(async ()=>this.purgeInvalidCacheFiles(), 60000); + setTimeout(async ()=>this.purgeInvalidBackupFiles(), 120000); } finally { this.isInitializing = false; if(this.initializationNotice) { @@ -137,41 +139,48 @@ class ImageCache { } private async purgeInvalidCacheFiles(): Promise { - const transaction = this.db!.transaction(this.cacheStoreName, "readwrite"); - const store = transaction.objectStore(this.cacheStoreName); - const files = app.vault.getFiles(); - - const deletePromises: Promise[] = []; - - const request = store.openCursor(); return new Promise((resolve, reject) => { + const transaction = this.db!.transaction(this.cacheStoreName, "readwrite"); + const store = transaction.objectStore(this.cacheStoreName); + const files = this.app.vault.getFiles(); + const deletePromises: Promise[] = []; + const request = store.openCursor(); request.onsuccess = (event: Event) => { const cursor = (event.target as IDBRequest).result; - if (cursor) { + if(cursor) { 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) || !cursor.value.blob) { + if (!file || (file && file.stat.mtime > cursor.value.mtime) || (!cursor.value.blob && !cursor.value.svg)) { deletePromises.push( - new Promise((resolve, reject) => { + new Promise((innerResolve, innerReject) => { const deleteRequest = store.delete(cursor.primaryKey); - deleteRequest.onsuccess = () => resolve(); - deleteRequest.onerror = () => - reject(new Error(`Failed to delete file with key: ${key}`)); + deleteRequest.onsuccess = () => innerResolve(); + deleteRequest.onerror = (ev: Event) => { + const error = deleteRequest.error; + const errorMsg = `Failed to delete file with key: ${key}. Error: ${error.message}` + innerReject(new Error(errorMsg)); + } }) ); } cursor.continue(); } else { Promise.all(deletePromises) - .then(() => resolve()) + .then(() => { + transaction.commit(); + resolve(); + }) .catch((error) => reject(error)); } }; request.onerror = () => { - reject(new Error("Failed to purge invalid files from IndexedDB.")); + const error = request.error; + console.log(error); + const errorMsg = `Failed to purge invalid files from IndexedDB. Error: ${error.message}` + reject(new Error(errorMsg)); }; }); } @@ -179,12 +188,10 @@ class ImageCache { private async purgeInvalidBackupFiles(): Promise { const transaction = this.db!.transaction(this.backupStoreName, "readwrite"); const store = transaction.objectStore(this.backupStoreName); - const files = app.vault.getFiles(); - + const files = this.app.vault.getFiles(); const deletePromises: Promise[] = []; - const request = store.openCursor(); - return new Promise((resolve, reject) => { + return await new Promise((resolve, reject) => { request.onsuccess = (event: Event) => { const cursor = (event.target as IDBRequest).result; if (cursor) { @@ -203,13 +210,19 @@ class ImageCache { cursor.continue(); } else { Promise.all(deletePromises) - .then(() => resolve()) + .then(() => { + transaction.commit(); + resolve(); + }) .catch((error) => reject(error)); } }; request.onerror = () => { - reject(new Error("Failed to purge invalid backup files from IndexedDB.")); + const error = request.error; + const errorMsg = `Failed to purge invalid backup files from IndexedDB. Error: ${error.message}` + console.log(error); + reject(new Error(errorMsg)); }; }); } @@ -262,7 +275,7 @@ class ImageCache { const key = getKey(key_); const cachedData = await this.getCacheData(key); - const file = app.vault.getAbstractFileByPath(key_.filepath.split("#")[0]); + const file = this.app.vault.getAbstractFileByPath(key_.filepath.split("#")[0]); if (!file || !(file instanceof TFile)) return undefined; if (cachedData && cachedData.mtime === file.stat.mtime) { if(cachedData.svg) { @@ -291,7 +304,7 @@ class ImageCache { return; // Database not initialized yet } - const file = app.vault.getAbstractFileByPath(key_.filepath.split("#")[0]); + const file = this.app.vault.getAbstractFileByPath(key_.filepath.split("#")[0]); if (!file || !(file instanceof TFile)) return; diff --git a/src/utils/ModifierkeyHelper.ts b/src/utils/ModifierkeyHelper.ts index eafc938..da90ffe 100644 --- a/src/utils/ModifierkeyHelper.ts +++ b/src/utils/ModifierkeyHelper.ts @@ -3,6 +3,7 @@ export type ModifierKeys = {shiftKey:boolean, ctrlKey: boolean, metaKey: boolean export type KeyEvent = PointerEvent | MouseEvent | KeyboardEvent | React.DragEvent | React.PointerEvent | React.MouseEvent | ModifierKeys; export type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties"; export type ExternalDragAction = "insert-link"|"image-url"|"image-import"|"embeddable"; +export type LocalFileDragAction = "insert-link"|"image-uri"|"image-import"; export type InternalDragAction = "link"|"image"|"image-fullsize"|"embeddable"; export const labelCTRL = () => DEVICE.isIOS || DEVICE.isMacOS ? "CMD" : "CTRL"; @@ -63,6 +64,17 @@ export const externalDragModifierType = (ev: KeyEvent):ExternalDragAction => { return "image-url"; } +export const localFileDragModifierType = (ev: KeyEvent):LocalFileDragAction => { + if(DEVICE.isWindows && isSHIFT(ev) && isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "image-uri"; + if(DEVICE.isMacOS && !isSHIFT(ev) && !isCTRL(ev) && isALT(ev) && !isMETA(ev)) return "image-uri"; + if(DEVICE.isWindows && !isSHIFT(ev) && isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "insert-link"; + if(DEVICE.isMacOS && isSHIFT(ev) && !isCTRL(ev) && isALT(ev) && !isMETA(ev)) return "insert-link"; + if( isSHIFT(ev) && !isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "image-import"; + if(DEVICE.isWindows && !isSHIFT(ev) && !isCTRL(ev) && isALT(ev) && !isMETA(ev)) return "image-import"; + return "image-import"; +} + + //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/468 export const internalDragModifierType = (ev: KeyEvent):InternalDragAction => { if( !(DEVICE.isIOS || DEVICE.isMacOS) && isSHIFT(ev) && isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "embeddable"; diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index a974c40..5a2d891 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -707,18 +707,18 @@ export const updateFrontmatterInString = (data:string, keyValuePairs: [string,st return data; } -const isHyperlink = (link:string) => link && !link.includes("\n") && !link.includes("\r") && link.match(/^https?:(\d*)?\/\/[^\s]*$/); +const isHyperLink = (link:string) => link && !link.includes("\n") && !link.includes("\r") && link.match(/^https?:(\d*)?\/\/[^\s]*$/); export const isContainer = (el: ExcalidrawElement) => el.type!=="arrow" && el.boundElements?.map((e) => e.type).includes("text"); export const hyperlinkIsImage = (data: string):boolean => { - if(!isHyperlink(data)) false; + if(!isHyperLink(data)) false; const corelink = data.split("?")[0]; return IMAGE_TYPES.contains(corelink.substring(corelink.lastIndexOf(".")+1)); } export const hyperlinkIsYouTubeLink = (link:string): boolean => - isHyperlink(link) && + isHyperLink(link) && (link.startsWith("https://youtu.be") || link.startsWith("https://www.youtube.com") || link.startsWith("https://youtube.com") || link.startsWith("https//www.youtu.be")) && link.match(/(youtu.be\/|v=)([^?\/\&]*)/)!==null