diff --git a/src/EmbeddedFileLoader.ts b/src/EmbeddedFileLoader.ts index df9e424..e4146ef 100644 --- a/src/EmbeddedFileLoader.ts +++ b/src/EmbeddedFileLoader.ts @@ -1,5 +1,6 @@ import { FileId } from "@zsviczian/excalidraw/types/element/types"; import { BinaryFileData, DataURL } from "@zsviczian/excalidraw/types/types"; +import { EffectTarget } from "html2canvas/dist/types/render/effects"; import { App, MarkdownRenderer, Notice, TFile } from "obsidian"; import { CASCADIA_FONT, @@ -68,7 +69,8 @@ export class EmbeddedFile { public linkParts: LinkParts; private hostPath: string; public attemptCounter: number = 0; - /*public isHyperlink: boolean = false;*/ + public isHyperlink: boolean = false; + public hyperlink:DataURL; constructor(plugin: ExcalidrawPlugin, hostPath: string, imgPath: string) { this.plugin = plugin; @@ -76,14 +78,15 @@ export class EmbeddedFile { } public resetImage(hostPath: string, imgPath: string) { - /*if(imgPath.startsWith("https://") || imgPath.startsWith("http://")) { - this.img=imgPath; - this.imgInverted=imgPath; - this.isHyperlink = true; - return; - }*/ this.imgInverted = this.img = ""; this.mtime = 0; + + if(imgPath.startsWith("https://") || imgPath.startsWith("http://")){ + this.isHyperlink = true; + this.hyperlink = imgPath as DataURL; + return; + }; + this.linkParts = getLinkParts(imgPath); this.hostPath = hostPath; if (!this.linkParts.path) { @@ -111,6 +114,9 @@ export class EmbeddedFile { } private fileChanged(): boolean { + if(this.isHyperlink) { + return false; + } if (!this.file) { this.file = app.metadataCache.getFirstLinkpathDest( this.linkParts.path, @@ -131,13 +137,13 @@ export class EmbeddedFile { isDark: boolean, isSVGwithBitmap: boolean, ) { - if (!this.file) { + if (!this.file && !this.isHyperlink) { return; } if (this.fileChanged()) { this.imgInverted = this.img = ""; } - this.mtime = this.file.stat.mtime; + this.mtime = this.isHyperlink ? 0 : this.file.stat.mtime; this.size = size; this.mimeType = mimeType; switch (isDark && isSVGwithBitmap) { @@ -152,18 +158,20 @@ export class EmbeddedFile { } public isLoaded(isDark: boolean): boolean { - if (!this.file) { - this.file = app.metadataCache.getFirstLinkpathDest( - this.linkParts.path, - this.hostPath, - ); // maybe the file has synchronized in the mean time - if(!this.file) { - this.attemptCounter++; - return true; + if(!this.isHyperlink) { + if (!this.file) { + this.file = app.metadataCache.getFirstLinkpathDest( + this.linkParts.path, + this.hostPath, + ); // maybe the file has synchronized in the mean time + if(!this.file) { + this.attemptCounter++; + return true; + } + } + if (this.fileChanged()) { + return false; } - } - if (this.fileChanged()) { - return false; } if (this.isSVGwithBitmap && isDark) { return this.imgInverted !== ""; @@ -172,10 +180,12 @@ export class EmbeddedFile { } public getImage(isDark: boolean) { + //https://stackoverflow.com/questions/2068344/how-do-i-get-a-youtube-video-thumbnail-from-the-youtube-api + //https://img.youtube.com/vi/uZz5MgzWXiM/maxresdefault.jpg /*if(this.isHyperlink) { return this.img; }*/ - if (!this.file) { + if (!this.file && !this.isHyperlink) { return ""; } if (isDark && this.isSVGwithBitmap) { @@ -189,7 +199,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 !Boolean(this.linkParts && this.linkParts.original && this.linkParts.original.endsWith("|100%")); + return this.isHyperlink || !Boolean(this.linkParts && this.linkParts.original && this.linkParts.original.endsWith("|100%")); } } @@ -216,22 +226,27 @@ export class EmbeddedFilesLoader { if (!this.plugin || !inFile) { return null; } + const isHyperlink = inFile instanceof EmbeddedFile ? inFile.isHyperlink : false; + const hyperlink = inFile instanceof EmbeddedFile ? inFile.hyperlink : ""; const file: TFile = inFile instanceof EmbeddedFile ? inFile.file : inFile; const linkParts = - inFile instanceof EmbeddedFile - ? inFile.linkParts - : { - original: file.path, - path: file.path, - isBlockRef: false, - ref: null, - width: this.plugin.settings.mdSVGwidth, - height: this.plugin.settings.mdSVGmaxHeight, - }; + isHyperlink + ? null + : inFile instanceof EmbeddedFile + ? inFile.linkParts + : { + original: file.path, + path: file.path, + isBlockRef: false, + ref: null, + width: this.plugin.settings.mdSVGwidth, + height: this.plugin.settings.mdSVGmaxHeight, + }; let hasSVGwithBitmap = false; - const isExcalidrawFile = this.plugin.isExcalidrawFile(file); + const isExcalidrawFile = !isHyperlink && this.plugin.isExcalidrawFile(file); if ( + !isHyperlink && !( IMAGE_TYPES.contains(file.extension) || isExcalidrawFile || @@ -240,7 +255,9 @@ export class EmbeddedFilesLoader { ) { return null; } - const ab = await app.vault.readBinary(file); + const ab = isHyperlink + ? null + : await app.vault.readBinary(file); const getExcalidrawSVG = async (isDark: boolean) => { //debug({where:"EmbeddedFileLoader.getExcalidrawSVG",uid:this.uid,file:file.name}); @@ -292,8 +309,11 @@ export class EmbeddedFilesLoader { ? await getExcalidrawSVG(this.isDark) : null; let mimeType: MimeType = "image/svg+xml"; + const extension = isHyperlink + ? hyperlink.substring(hyperlink.lastIndexOf(".")+1) + : file.extension; if (!isExcalidrawFile) { - switch (file.extension) { + switch (extension) { case "png": mimeType = "image/png"; break; @@ -324,12 +344,14 @@ export class EmbeddedFilesLoader { } } let dataURL = - excalidrawSVG ?? - (file.extension === "svg" - ? await getSVGData(app, file) - : file.extension === "md" - ? null - : await getDataURL(ab, mimeType)); + isHyperlink + ? (inFile instanceof EmbeddedFile ? inFile.hyperlink : null) + : excalidrawSVG ?? + (file.extension === "svg" + ? await getSVGData(app, file) + : file.extension === "md" + ? null + : await getDataURL(ab, mimeType)); if(!dataURL) { const result = await this.convertMarkdownToSVG(this.plugin, file, linkParts); @@ -339,9 +361,11 @@ export class EmbeddedFilesLoader { const size = await getImageSize(dataURL); return { mimeType, - fileId: await generateIdFromFile(ab), + fileId: await generateIdFromFile( + isHyperlink? (new TextEncoder()).encode(dataURL as string) : ab + ), dataURL, - created: file.stat.mtime, + created: isHyperlink ? 0 : file.stat.mtime, hasSVGwithBitmap, size, }; diff --git a/src/ExcalidrawAutomate.ts b/src/ExcalidrawAutomate.ts index ace00c2..013f535 100644 --- a/src/ExcalidrawAutomate.ts +++ b/src/ExcalidrawAutomate.ts @@ -39,7 +39,7 @@ import { } from "./utils/Utils"; import { getNewOrAdjacentLeaf, isObsidianThemeDark } from "./utils/ObsidianUtils"; import { AppState, BinaryFileData, Point } from "@zsviczian/excalidraw/types/types"; -import { EmbeddedFilesLoader, FileData } from "./EmbeddedFileLoader"; +import { EmbeddedFile, EmbeddedFilesLoader, FileData } from "./EmbeddedFileLoader"; import { tex2dataURL } from "./LaTeX"; //import Excalidraw from "@zsviczian/excalidraw"; import { Prompt } from "./dialogs/Prompt"; @@ -958,7 +958,7 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface { async addImage( topX: number, topY: number, - imageFile: TFile, + imageFile: TFile | string, scale: boolean = true, //true will scale the image to MAX_IMAGE_SIZE, false will insert image at 100% of its size ): Promise { const id = nanoid(); @@ -966,17 +966,25 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface { this.plugin, this.canvas.theme === "dark", ); - const image = await loader.getObsidianImage(imageFile,0); + const image = (typeof imageFile === "string") + ? await loader.getObsidianImage(new EmbeddedFile(this.plugin, "", imageFile),0) + : await loader.getObsidianImage(imageFile,0); + if (!image) { return null; } - const fileId = imageFile.extension === "md" ? fileid() as FileId : image.fileId; + const fileId = typeof imageFile === "string" + ? image.fileId + : imageFile.extension === "md" ? fileid() as FileId : image.fileId; this.imagesDict[fileId] = { mimeType: image.mimeType, id: fileId, dataURL: image.dataURL, created: image.created, - file: imageFile.path + (scale ? "":"|100%"), + isHyperlink: typeof imageFile === "string", + file: typeof imageFile === "string" + ? null + : imageFile.path + (scale ? "":"|100%"), hasSVGwithBitmap: image.hasSVGwithBitmap, latex: null, }; diff --git a/src/ExcalidrawData.ts b/src/ExcalidrawData.ts index 90afb0d..9ad8b5a 100644 --- a/src/ExcalidrawData.ts +++ b/src/ExcalidrawData.ts @@ -551,6 +551,18 @@ export class ExcalidrawData { this.setFile(parts.value[1] as FileId, embeddedFile); } + //Load links + const REG_LINKID_FILEPATH = /([\w\d]*):\s*(https?:\/\/[^\s]*)\n/gm; + res = data.matchAll(REG_LINKID_FILEPATH); + while (!(parts = res.next()).done) { + const embeddedFile = new EmbeddedFile( + this.plugin, + null, + parts.value[2], + ); + this.setFile(parts.value[1] as FileId, embeddedFile); + } + //Load Equations const REG_FILEID_EQUATION = /([\w\d]*):\s*\$\$(.*)(\$\$\s*\n)/gm; res = data.matchAll(REG_FILEID_EQUATION); @@ -1050,11 +1062,15 @@ export class ExcalidrawData { for (const key of this.files.keys()) { const PATHREG = /(^[^#\|]*)/; const ef = this.files.get(key); - //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/829 - const path = ef.file - ? ef.linkParts.original.replace(PATHREG,app.metadataCache.fileToLinktext(ef.file,this.file.path)) - : ef.linkParts.original; - outString += `${key}: [[${path}]]\n`; + if(ef.isHyperlink) { + outString += `${key}: ${ef.hyperlink}\n`; + } else { + //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/829 + const path = ef.file + ? ef.linkParts.original.replace(PATHREG,app.metadataCache.fileToLinktext(ef.file,this.file.path)) + : ef.linkParts.original; + outString += `${key}: [[${path}]]\n`; + } } } outString += this.equations.size > 0 || this.files.size > 0 ? "\n" : ""; @@ -1121,7 +1137,7 @@ export class ExcalidrawData { const equation = this.getEquation(fileId); //const equation = this.equations.get(fileId as 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.file && (file.file.extension !== "md" || this.plugin.isExcalidrawFile(file.file))) { + if(file && (file.isHyperlink || (file.file && (file.file.extension !== "md" || this.plugin.isExcalidrawFile(file.file))))) { return; } const newId = fileid(); @@ -1448,12 +1464,23 @@ export class ExcalidrawData { } this.files.set(fileId, data); + if(data.isHyperlink) { + this.plugin.filesMaster.set(fileId, { + isHyperlink: true, + path: data.hyperlink, + blockrefData: null, + hasSVGwithBitmap: data.isSVGwithBitmap + }); + return; + } + if (!data.file) { return; } const parts = data.linkParts.original.split("#"); this.plugin.filesMaster.set(fileId, { + isHyperlink: false, path:data.file.path + (data.shouldScale()?"":"|100%"), blockrefData: parts.length === 1 ? null @@ -1499,6 +1526,13 @@ export class ExcalidrawData { } if (this.plugin.filesMaster.has(fileId)) { const masterFile = this.plugin.filesMaster.get(fileId); + if(masterFile.isHyperlink) { + this.files.set( + fileId, + new EmbeddedFile(this.plugin,this.file.path,masterFile.path) + ); + return true; + } const path = masterFile.path.split("|")[0].split("#")[0]; if (!this.app.vault.getAbstractFileByPath(path)) { this.plugin.filesMaster.delete(fileId); diff --git a/src/ExcalidrawView.ts b/src/ExcalidrawView.ts index 576d705..36b5cbf 100644 --- a/src/ExcalidrawView.ts +++ b/src/ExcalidrawView.ts @@ -81,6 +81,7 @@ import { svgToBase64, viewportCoordsToSceneCoords, updateFrontmatterInString, + hyperlinkIsImage, } from "./utils/Utils"; import { getNewOrAdjacentLeaf, getParentOfClass } from "./utils/ObsidianUtils"; import { splitFolderAndFilename } from "./utils/FileUtils"; @@ -102,7 +103,8 @@ import { ICONS, saveIcon } from "./menu/ActionIcons"; //import { MainMenu } from "@zsviczian/excalidraw"; //import {WelcomeScreen} from "@zsviczian/excalidraw"; import { ExportDialog } from "./dialogs/ExportDialog"; -import { execPath } from "process"; +import { getEA } from "src"; + export enum TextMode { @@ -890,8 +892,12 @@ 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) { + window.open(ef.hyperlink,"_blank"); + return; + } if (ev.altKey) { - const ef = this.excalidrawData.getFile(selectedImage.fileId); if ( ef.file.extension === "md" && !this.plugin.isExcalidrawFile(ef.file) @@ -915,8 +921,8 @@ export default class ExcalidrawView extends TextFileView { return; } } - linkText = this.excalidrawData.getFile(selectedImage.fileId).file.path; - file = this.excalidrawData.getFile(selectedImage.fileId).file; + linkText = ef.file.path; + file = ef.file; } } @@ -1067,7 +1073,7 @@ export default class ExcalidrawView extends TextFileView { if (!app.isMobile) { this.addAction( FULLSCREEN_ICON_NAME, - "Press ESC to exit fullscreen mode", + "Use the action on the Excalidraw Obsidian Panel or the Command Palette to exti fullscreen mode. You can set up a hotkey for toggling fullscreen mode in Obsidian settings under Hotkeys.", () => this.gotoFullscreen(), ); } @@ -1756,6 +1762,8 @@ export default class ExcalidrawView extends TextFileView { trayModeEnabled: this.plugin.settings.defaultTrayMode, penMode: penEnabled, penDetected: penEnabled, + allowPinchZoom: this.plugin.settings.allowPinchZoom, + allowWheelZoom: this.plugin.settings.allowWheelZoom, }, //files: excalidrawData.files, //commitToHistory: true, @@ -1784,6 +1792,8 @@ export default class ExcalidrawView extends TextFileView { trayModeEnabled: this.plugin.settings.defaultTrayMode, penMode: penEnabled, penDetected: penEnabled, + allowPinchZoom: this.plugin.settings.allowPinchZoom, + allowWheelZoom: this.plugin.settings.allowWheelZoom, }, files: excalidrawData.files, libraryItems: await this.getLibrary(), @@ -2398,11 +2408,13 @@ export default class ExcalidrawView extends TextFileView { dataURL: images[k].dataURL, created: images[k].created, }); - if (images[k].file) { + if (images[k].file || images[k].isHyperlink) { const embeddedFile = new EmbeddedFile( this.plugin, this.file.path, - images[k].file, + images[k].isHyperlink + ? images[k].dataURL + : images[k].file, ); const st: AppState = api.getAppState(); embeddedFile.setImage( @@ -2478,6 +2490,7 @@ export default class ExcalidrawView extends TextFileView { colorPalette: st.colorPalette, //@ts-ignore currentStrokeOptions: st.currentStrokeOptions, + previousGridSize: st.previousGridSize, }, prevTextMode: this.prevTextMode, files, @@ -2596,6 +2609,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 const ref = ef.linkParts.ref ? `#${ef.linkParts.isBlockRef ? "^" : ""}${ef.linkParts.ref}` : ""; @@ -3026,6 +3040,14 @@ export default class ExcalidrawView extends TextFileView { if (text && onDropHook("text", null, text)) { return false; } + if(text && event[CTRL_OR_CMD] && hyperlinkIsImage(text)) { + (async () => { + const ea = getEA(this) as ExcalidrawAutomate; + await ea.addImage(0,0,text); + ea.addElementsToView(true,true,true); + })(); + return false; + } } return true; } @@ -3797,3 +3819,4 @@ export function getTextMode(data: string): TextMode { data.search("excalidraw-plugin: locked\n") > -1; //locked for backward compatibility return parsed ? TextMode.parsed : TextMode.raw; } + diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index a80a1bf..0f2d9c7 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -198,6 +198,15 @@ FILENAME_HEAD: "Filename", DEFAULT_PEN_MODE_NAME: "Pen mode", DEFAULT_PEN_MODE_DESC: "Should pen mode be automatically enabled when opening Excalidraw?", + + DEFAULT_PINCHZOOM_NAME: "Allow pinch zoom in pen mode", + DEFAULT_PINCHZOOM_DESC: + "Pinch zoom in pen mode is disabled by default to prevent unwanted accidental zooming with your palm. By enabling this setting you can allow pinch zoom in Pen mode.
Re-open drawings for the change to take effect.", + + DEFAULT_WHEELZOOM_NAME: "Mouse wheel to zoom by default", + DEFAULT_WHEELZOOM_DESC: + "Toggle on: Mouse wheel to zoom; CTRL + mouse wheel to scroll
Toggle off: CTRL + mouse wheel to zoom; Mouse wheel to scroll

Re-open drawings for the change to take effect.", + ZOOM_TO_FIT_NAME: "Zoom to fit on view resize", ZOOM_TO_FIT_DESC: "Zoom to fit drawing when the pane is resized" + "
Toggle ON: Zoom to fit
Toggle OFF: Auto zoom disabled", diff --git a/src/main.ts b/src/main.ts index 474869b..164b325 100644 --- a/src/main.ts +++ b/src/main.ts @@ -160,7 +160,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 mathjax: any = null; @@ -187,7 +187,7 @@ export default class ExcalidrawPlugin extends Plugin { super(app, manifest); this.filesMaster = new Map< FileId, - { path: string; hasSVGwithBitmap: boolean; blockrefData: string } + { isHyperlink: boolean; path: string; hasSVGwithBitmap: boolean; blockrefData: string } >(); this.equationsMaster = new Map(); } diff --git a/src/settings.ts b/src/settings.ts index 2e08244..cc0088f 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -45,6 +45,8 @@ export interface ExcalidrawSettings { matchThemeTrigger: boolean; defaultMode: string; defaultPenMode: "never" | "mobile" | "always"; + allowPinchZoom: boolean; + allowWheelZoom: boolean; zoomToFitOnOpen: boolean; zoomToFitOnResize: boolean; zoomToFitMaxLevel: number; @@ -142,6 +144,8 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = { matchThemeTrigger: false, defaultMode: "normal", defaultPenMode: "never", + allowPinchZoom: false, + allowWheelZoom: false, zoomToFitOnOpen: true, zoomToFitOnResize: true, zoomToFitMaxLevel: 2, @@ -591,6 +595,30 @@ export class ExcalidrawSettingTab extends PluginSettingTab { }), ); + new Setting(containerEl) + .setName(t("DEFAULT_PINCHZOOM_NAME")) + .setDesc(fragWithHTML(t("DEFAULT_PINCHZOOM_DESC"))) + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.allowPinchZoom) + .onChange(async (value) => { + this.plugin.settings.allowPinchZoom = value; + this.applySettingsUpdate(); + }), + ); + + new Setting(containerEl) + .setName(t("DEFAULT_WHEELZOOM_NAME")) + .setDesc(fragWithHTML(t("DEFAULT_WHEELZOOM_DESC"))) + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.allowWheelZoom) + .onChange(async (value) => { + this.plugin.settings.allowWheelZoom = value; + this.applySettingsUpdate(); + }), + ); + new Setting(containerEl) .setName(t("ZOOM_TO_FIT_ONOPEN_NAME")) .setDesc(fragWithHTML(t("ZOOM_TO_FIT_ONOPEN_DESC"))) diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 78133ae..946e62e 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -22,7 +22,8 @@ import { ExcalidrawElement } from "@zsviczian/excalidraw/types/element/types"; import { ExportSettings } from "../ExcalidrawView"; import { compressToBase64, decompressFromBase64 } from "lz-string"; import { getIMGFilename } from "./FileUtils"; -import ExcalidrawScene from "lib/svgToExcalidraw/elements/ExcalidrawScene"; +import ExcalidrawScene from "../svgToExcalidraw/elements/ExcalidrawScene"; +import { IMAGE_TYPES } from "../Constants"; declare const PLUGIN_VERSION:string; @@ -682,4 +683,9 @@ export const updateFrontmatterInString = (data:string, keyValuePairs: [string,st : data.replace(/^---\n/,`---\n${kvp[0]}: ${kvp[1]}\n`); } return data; +} + +export const hyperlinkIsImage = (data: string):boolean => { + if ( ! (data.startsWith("https://") || data.startsWith("http://")) ) return false; + return IMAGE_TYPES.contains(data.substring(data.lastIndexOf(".")+1)); } \ No newline at end of file