Compare commits

...

6 Commits

Author SHA1 Message Date
zsviczian
81fc788adc 1.9.6.1-beta 2023-06-30 20:14:07 +02:00
zsviczian
834343f821 update package 2023-06-30 06:10:15 +02:00
zsviczian
6b4f9fddae image cache take 1 2023-06-30 06:09:18 +02:00
zsviczian
fa86ef1136 added video to collaboration frame script 2023-06-27 19:38:31 +02:00
zsviczian
bf20919552 publish collababoration Frame 2023-06-27 18:01:27 +02:00
zsviczian
5931be2aa4 publish collaboration frame scripot 2023-06-27 17:56:11 +02:00
13 changed files with 347 additions and 7 deletions

View File

@@ -0,0 +1,12 @@
/*
Creates a new Excalidraw.com collaboration room and places the link to the room on the clipboard.
```js*/
const room = Array.from(window.crypto.getRandomValues(new Uint8Array(10))).map((byte) => `0${byte.toString(16)}`.slice(-2)).join("");
const key = (await window.crypto.subtle.exportKey("jwk",await window.crypto.subtle.generateKey({name:"AES-GCM",length:128},true,["encrypt", "decrypt"]))).k;
const link = `https://excalidraw.com/#room=${room},${key}`;
ea.addIFrame(0,0,800,600,link);
ea.addElementsToView(true,true);
window.navigator.clipboard.writeText(link);
new Notice("The collaboration room link is available on the clipboard.",4000);

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><g stroke-width="1.5"></path><circle cx="9" cy="7" r="4"></circle><path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path><path d="M21 21v-2a4 4 0 0 0 -3 -3.85"></path></g></svg>

After

Width:  |  Height:  |  Size: 382 B

File diff suppressed because one or more lines are too long

View File

@@ -45,6 +45,7 @@ I would love to include your contribution in the script library. If you have a s
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Darken%20background%20color.svg"/></div>|[[#Darken background color]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Deconstruct%20selected%20elements%20into%20new%20drawing.svg"/></div>|[[#Deconstruct selected elements into new drawing]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Elbow%20connectors.svg"/></div>|[[#Elbow connectors]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Excalidraw%20Collaboration%20Frame.svg"/></div>|[[#Excalidraw Collaboration Frame]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Expand%20rectangles%20horizontally%20keep%20text%20centered.svg"/></div>|[[#Expand rectangles horizontally keep text centered]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Expand%20rectangles%20horizontally.svg"/></div>|[[#Expand rectangles horizontally]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Expand%20rectangles%20vertically%20keep%20text%20centered.svg"/></div>|[[#Expand rectangles vertically keep text centered]]|
@@ -200,6 +201,12 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/1-2-3'>@1-2-3</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Elbow%20connectors.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script converts the selected connectors to elbows.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/elbow-connectors.png'></td></tr></table>
## Excalidraw Collaboration Frame
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Excalidraw%20Collaboration%20Frame.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/1-2-3'>@1-2-3</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Excalidraw%20Collaboration%20Frame.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Creates a new Excalidraw.com collaboration room and places the link to the room on the clipboard.<iframe width="400" height="225" src="https://www.youtube.com/embed/7isRfeAhEH4" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td></tr></table>
## Expand rectangles horizontally keep text centered
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Expand%20rectangles%20horizontally%20keep%20text%20centered.md

View File

@@ -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",

View File

@@ -18,7 +18,7 @@
"license": "MIT",
"dependencies": {
"@types/lz-string": "^1.3.34",
"@zsviczian/excalidraw": "0.15.2-obsidian-4",
"@zsviczian/excalidraw": "0.15.2-obsidian-5",
"chroma-js": "^2.4.2",
"clsx": "^1.2.1",
"colormaster": "^1.2.1",

View File

@@ -22,6 +22,7 @@ import {
} from "./utils/Utils";
import { isObsidianThemeDark } from "./utils/ObsidianUtils";
import { linkClickModifierType } from "./utils/ModifierkeyHelper";
import { imageCache } from "./utils/ImageCache";
interface imgElementAttributes {
file?: TFile;
@@ -106,6 +107,8 @@ const getIMG = async (
theme ? theme === "dark" : undefined,
);
const cacheReady = imageCache.isReady();
if (!plugin.settings.displaySVGInPreview) {
const width = parseInt(imgAttributes.fwidth);
const scale = width >= 2400
@@ -118,12 +121,24 @@ const getIMG = async (
? 2
: 1;
//In case of PNG I cannot change the viewBox to select the area of the element
//being referenced. For PNG only the group reference works
const cacheKey = {...filenameParts, isDark: theme==="dark", isSVG: false, scale};
if(cacheReady) {
const src = await imageCache.get(cacheKey);
//In case of PNG I cannot change the viewBox to select the area of the element
//being referenced. For PNG only the group reference works
if(src) {
img.src = src;
return img;
}
}
const quickPNG = !filenameParts.hasGroupref
? await getQuickImagePreview(plugin, file.path, "png")
: undefined;
const png =
quickPNG ??
(await createPNG(
@@ -144,13 +159,25 @@ const getIMG = async (
return null;
}
img.src = URL.createObjectURL(png);
cacheReady && imageCache.add(cacheKey, img.src);
return img;
}
const cacheKey = {...filenameParts, isDark: theme==="dark", isSVG: false, scale:1};
if(cacheReady) {
const src = await imageCache.get(cacheKey);
if(src) {
img.setAttribute("src", src);
return img;
}
}
if(!(filenameParts.hasBlockref || filenameParts.hasSectionref)) {
const quickSVG = await getQuickImagePreview(plugin, file.path, "svg");
if (quickSVG) {
img.setAttribute("src", svgToBase64(quickSVG));
cacheReady && imageCache.add(cacheKey, img.src);
return img;
}
}
@@ -186,6 +213,7 @@ const getIMG = async (
svg.removeAttribute("width");
svg.removeAttribute("height");
img.setAttribute("src", svgToBase64(svg.outerHTML));
cacheReady && imageCache.add(cacheKey, img.src);
return img;
};

View File

@@ -70,7 +70,7 @@ const getContainerForDocument = (doc:Document) => {
};
export const useDefaultExcalidrawFrame = (element: NonDeletedExcalidrawElement) => {
return element.link.match(YOUTUBE_REG) || element.link.match(VIMEO_REG) || element.link.match(TWITTER_REG);
return element.link.match(YOUTUBE_REG) || element.link.match(VIMEO_REG);
}
const leafMap = new Map<string, WorkspaceLeaf>();
@@ -80,6 +80,11 @@ export const renderWebView = (src: string, radius: number):JSX.Element =>{
return null;
}
const twitterLink = src.match(TWITTER_REG);
if (twitterLink) {
src = `https://twitframe.com/show?url=${encodeURIComponent(src)}`;
}
return (
<webview
className="excalidraw__iframe"

View File

@@ -247,7 +247,7 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
after: "",
},
{
field: "addImage",
field: "addIFrame",
code: "addIFrame(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string;",
desc: "Adds an iframe to the drawing. If url is not null then the iframe will be loaded from the url. The url maybe a markdown link to an note in the Vault or a weblink. If url is null then the iframe will be loaded from the file. Both the url and the file may not be null.",
after: "",

View File

@@ -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:

View File

@@ -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);

View File

@@ -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")))

256
src/utils/ImageCache.ts Normal file
View File

@@ -0,0 +1,256 @@
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;
sectionref: string;
isDark: boolean;
isSVG: boolean;
scale: number;
};
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());
}
private async initializeDB(): Promise<void> {
if (this.isInitializing || this.db !== null) {
return;
}
this.isInitializing = true;
try {
const request = indexedDB.open(this.dbName);
request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName);
}
};
this.db = await new Promise<IDBDatabase>((resolve, reject) => {
request.onsuccess = (event: Event) => {
const db = (event.target as IDBOpenDBRequest).result;
resolve(db);
};
request.onerror = () => {
reject(new Error(`Failed to open or create IndexedDB database: ${this.dbName}`));
};
});
// Pre-create the object store to reduce delay when accessing it later
if (!this.db.objectStoreNames.contains(this.storeName)) {
const version = this.db.version + 1;
this.db.close();
const upgradeRequest = indexedDB.open(this.dbName, version);
upgradeRequest.onupgradeneeded = (event: IDBVersionChangeEvent) => {
const db = (event.target as IDBOpenDBRequest).result;
db.createObjectStore(this.storeName);
};
await new Promise<void>((resolve, reject) => {
upgradeRequest.onsuccess = () => {
const db = (upgradeRequest.result as IDBDatabase);
db.close();
resolve();
};
upgradeRequest.onerror = () => {
reject(new Error(`Failed to upgrade IndexedDB database: ${this.dbName}`));
};
});
this.db = await new Promise<IDBDatabase>((resolve, reject) => {
const openRequest = indexedDB.open(this.dbName);
openRequest.onsuccess = () => {
const db = (openRequest.result as IDBDatabase);
resolve(db);
};
openRequest.onerror = () => {
reject(new Error(`Failed to open IndexedDB database: ${this.dbName}`));
};
});
}
await this.purgeInvalidFiles();
} finally {
this.isInitializing = false;
console.log("Initialized Excalidraw Image Cache");
}
}
private async purgeInvalidFiles(): Promise<void> {
const transaction = this.db!.transaction(this.storeName, "readwrite");
const store = transaction.objectStore(this.storeName);
const files = app.vault.getFiles();
const deletePromises: Promise<void>[] = [];
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 = 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 {
Promise.all(deletePromises)
.then(() => resolve())
.catch((error) => reject(error));
}
};
request.onerror = () => {
reject(new Error("Failed to purge invalid files from IndexedDB."));
};
});
}
private async getObjectStore(mode: IDBTransactionMode): Promise<IDBObjectStore> {
const transaction = this.db!.transaction(this.storeName, mode);
return transaction.objectStore(this.storeName);
}
public async openDB(): Promise<IDBDatabase> {
return new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(this.dbName);
request.onerror = () => {
reject(new Error("Failed to open IndexedDB database."));
};
request.onsuccess = () => {
const db = request.result as IDBDatabase;
resolve(db);
};
});
}
private async getCacheData(key: string): Promise<FileCacheData | null> {
const store = await this.getObjectStore("readonly");
const request = store.get(key);
return new Promise<FileCacheData | null>((resolve, reject) => {
request.onsuccess = () => {
const result = request.result as FileCacheData;
resolve(result || null);
};
request.onerror = () => {
reject(new Error("Failed to retrieve data from IndexedDB."));
};
});
}
public async isCached(key_: ImageKey): Promise<boolean> {
const key = getKey(key_);
return this.getCacheData(key).then((cachedData) => {
if (cachedData) {
const file = app.vault.getAbstractFileByPath(key_.filepath.split("#")[0]);
if (!file || !(file instanceof TFile)) return false;
if (cachedData.mtime === file.stat.mtime) {
return true;
}
}
return false;
});
}
public isReady(): boolean {
return !!this.db && !this.isInitializing && !!this.plugin && this.plugin.settings.allowImageCache;
}
public async get(key_: ImageKey): Promise<string | undefined> {
if (!this.isReady()) {
return null; // Database not initialized yet
}
const key = getKey(key_);
return this.getCacheData(key).then((cachedData) => {
const file = app.vault.getAbstractFileByPath(key_.filepath.split("#")[0]);
if (!file || !(file instanceof TFile)) return undefined;
if (cachedData && cachedData.mtime === file.stat.mtime) {
return cachedData.imageBase64;
}
return undefined;
});
}
public add(key_: ImageKey, imageBase64: string): void {
if (!this.isReady()) {
return; // Database not initialized yet
}
const file = app.vault.getAbstractFileByPath(key_.filepath.split("#")[0]);
if (!file || !(file instanceof TFile)) return;
const data: FileCacheData = { mtime: file.stat.mtime, imageBase64 };
const transaction = this.db.transaction(this.storeName, "readwrite");
const store = transaction.objectStore(this.storeName);
const key = getKey(key_)
store.put(data, key);
}
public async clear(): Promise<void> {
// deliberately not checking isReady() here
if (!this.db || this.isInitializing) {
return; // Database not initialized yet
}
const transaction = this.db.transaction(this.storeName, "readwrite");
const store = transaction.objectStore(this.storeName);
const request = store.clear();
return new Promise<void>((resolve, reject) => {
request.onsuccess = () => {
new Notice("Image cache cleared.");
resolve();
};
request.onerror = () => {
reject(new Error("Failed to clear data in IndexedDB."));
};
});
}
}
export const imageCache = new ImageCache(DB_NAME, STORE_NAME);