Compare commits

...

9 Commits

Author SHA1 Message Date
zsviczian
e72c1676c2 2.0.3 2023-11-21 22:51:56 +01:00
zsviczian
5a17eb7054 updated generator 2023-11-21 06:05:19 +01:00
zsviczian
75d52c07b8 lint 2023-11-20 23:05:55 +01:00
zsviczian
4dc6c17486 updated script 2023-11-20 22:32:13 +01:00
zsviczian
e780930799 draw a UI 2023-11-20 21:27:40 +01:00
zsviczian
49cd6a36a1 draw-a-ui-image 2023-11-20 21:20:12 +01:00
zsviczian
d4830983e2 2.0.2 2023-11-19 17:41:41 +01:00
zsviczian
a69fefffdc beta-2 2023-11-19 08:42:41 +01:00
zsviczian
1d0466dae7 2.0.1-beta-1 2023-11-18 17:43:47 +01:00
19 changed files with 449 additions and 97 deletions

203
ea-scripts/GPT-Draw-a-UI.md Normal file
View File

@@ -0,0 +1,203 @@
/*
Based on https://github.com/SawyerHood/draw-a-ui
<iframe width="560" height="315" src="https://www.youtube.com/embed/y3kHl_6Ll4w" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-draw-a-ui.jpg)
```js*/
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.0.2")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
settings = ea.getScriptSettings();
//set default values on first run
if(!settings["OPENAI_API_KEY"]) {
settings = {
"OPENAI_API_KEY" : {
value: "",
description: `Get your api key at <a href="https://platform.openai.com/">https://platform.openai.com/</a><br>⚠️ Note that the gpt-4-vision-preview
requires a minimum of $5 credit on your account.`
},
"FOLDER" : {
value: "GPTPlayground",
description: `The folder in your vault where you want to store generated html pages`
},
"FILENAME": {
value: "page",
description: `The base name of the html file that will be created. Each time you run the script
a new file will be created using the following pattern "filename_i" where i is a counter`
}
};
await ea.setScriptSettings(settings);
}
const OPENAI_API_KEY = settings["OPENAI_API_KEY"].value;
const FOLDER = settings["FOLDER"].value;
const FILENAME = settings["FILENAME"].value;
if(OPENAI_API_KEY==="") {
new Notice("Please set an OpenAI API key in plugin settings");
return;
}
const systemPrompt = `You are an expert tailwind developer. A user will provide you with a
low-fidelity wireframe of an application and you will return
a single html file that uses tailwind to create the website.
Use creative license to make the application more fleshed out. Write the necessary javascript code.
If you need to insert an image, use placehold.co to create a placeholder image.
Respond only with the html file.`;
const post = async (request) => {
const { image } = await request.json();
const body = {
model: "gpt-4-vision-preview",
max_tokens: 4096,
messages: [
{
role: "system",
content: systemPrompt,
},
{
role: "user",
content: [
{
type: "image_url",
image_url: image,
},
"Turn this into a single html file using tailwind.",
],
},
],
};
let json = null;
try {
const resp = await requestUrl ({
url: "https://api.openai.com/v1/chat/completions",
method: "post",
contentType: "application/json",
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${OPENAI_API_KEY}`,
},
throw: false
});
json = resp.json;
} catch (e) {
console.log(e);
}
return json;
}
const blobToBase64 = async (blob) => {
const arrayBuffer = await blob.arrayBuffer()
const bytes = new Uint8Array(arrayBuffer)
var binary = '';
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
const getRequestObjFromSelectedElements = async (view) => {
await view.forceSave(true);
const viewElements = ea.getViewSelectedElements();
if(viewElements.length === 0) {
new Notice ("Aborting because there is nothing selected.",4000);
return;
}
const bb = ea.getBoundingBox(viewElements);
const size = (bb.width*bb.height);
const minRatio = Math.sqrt(360000/size);
const maxRatio = Math.sqrt(size/16000000);
const scale = minRatio > 1
? minRatio
: (
maxRatio > 1
? 1/maxRatio
: 1
);
const loader = ea.getEmbeddedFilesLoader(false);
const exportSettings = {
withBackground: true,
withTheme: true,
};
const img =
await ea.createPNG(
view.file.path,
scale,
exportSettings,
loader,
"light",
);
const dataURL = `data:image/png;base64,${await blobToBase64(img)}`;
return { json: async () => ({ image: dataURL }) }
}
const extractHTMLFromString = (result) => {
if(!result) return null;
const start = result.indexOf('```html\n');
const end = result.lastIndexOf('```');
if (start !== -1 && end !== -1) {
const htmlString = result.substring(start + 8, end);
return htmlString.trim();
}
return null;
}
const checkAndCreateFolder = async (folderpath) => {
const vault = app.vault;
folderpath = ea.obsidian.normalizePath(folderpath);
const folder = vault.getAbstractFileByPathInsensitive(folderpath);
if (folder) {
return folder;
}
return await vault.createFolder(folderpath);
}
const getNewUniqueFilepath = (filename, folderpath) => {
let fname = ea.obsidian.normalizePath(`${folderpath}/${filename}.html`);
let file = app.vault.getAbstractFileByPath(fname);
let i = 0;
while (file) {
fname = ea.obsidian.normalizePath(`${folderpath}/${filename}_${i++}.html`);
file = app.vault.getAbstractFileByPath(fname);
}
return fname;
}
const requestObject = await getRequestObjFromSelectedElements(ea.targetView);
const result = await post(requestObject);
const errorMessage = () => {
new Notice ("Something went wrong! Check developer console for more.");
console.log(result);
}
if(!result?.hasOwnProperty("choices")) {
errorMessage();
return;
}
const htmlContent = extractHTMLFromString(result.choices[0]?.message?.content);
if(!htmlContent) {
errorMessage();
return;
}
const folder = await checkAndCreateFolder(FOLDER);
const filepath = getNewUniqueFilepath(FILENAME,folder.path);
const file = await app.vault.create(filepath,htmlContent);
const url = app.vault.adapter.getFilePath(file.path).toString();
const bb = ea.getBoundingBox(ea.getViewSelectedElements());
ea.addEmbeddable(bb.topX+bb.width+40,bb.topY,600,800,url);
await ea.addElementsToView(false,true);
ea.viewZoomToElements([]);

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M320 0c17.7 0 32 14.3 32 32V96H472c39.8 0 72 32.2 72 72V440c0 39.8-32.2 72-72 72H168c-39.8 0-72-32.2-72-72V168c0-39.8 32.2-72 72-72H288V32c0-17.7 14.3-32 32-32zM208 384c-8.8 0-16 7.2-16 16s7.2 16 16 16h32c8.8 0 16-7.2 16-16s-7.2-16-16-16H208zm96 0c-8.8 0-16 7.2-16 16s7.2 16 16 16h32c8.8 0 16-7.2 16-16s-7.2-16-16-16H304zm96 0c-8.8 0-16 7.2-16 16s7.2 16 16 16h32c8.8 0 16-7.2 16-16s-7.2-16-16-16H400zM264 256a40 40 0 1 0 -80 0 40 40 0 1 0 80 0zm152 40a40 40 0 1 0 0-80 40 40 0 1 0 0 80zM48 224H64V416H48c-26.5 0-48-21.5-48-48V272c0-26.5 21.5-48 48-48zm544 0c26.5 0 48 21.5 48 48v96c0 26.5-21.5 48-48 48H576V224h16z"/></svg>

After

Width:  |  Height:  |  Size: 694 B

File diff suppressed because one or more lines are too long

View File

@@ -115,6 +115,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/Auto%20Draw%20for%20Pen.svg"/></div>|[[#Auto Draw for Pen]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Boolean%20Operations.svg"/></div>|[[#Boolean Operations]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Copy%20Selected%20Element%20Styles%20to%20Global.svg"/></div>|[[#Copy Selected Element Styles to Global]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/GPT-Draw-a-UI.svg"/></div>|[[#GPT Draw-a-UI]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Hardware%20Eraser%20Support.svg"/></div>|[[#Hardware Eraser Support]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Palette%20loader.svg"/></div>|[[#Palette Loader]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/PDF%20Page%20Text%20to%20Clipboard.svg"/></div>|[[#PDF Page Text to Clipboard]]|
@@ -350,6 +351,13 @@ 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/7flash'>@7flash</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/Grid%20Selected%20Images.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script arranges selected images into compact grid view, removing gaps in-between, resizing when necessary and breaking into multiple rows/columns.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-grid-selected-images.png'></td></tr></table>
## GPT Draw-a-UI
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/GPT-Draw-a-UI.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</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/GPT-Draw-a-UI.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Draw a UI and let GPT create the code for you.<br><iframe width="400" height="225" src="https://www.youtube.com/embed/y3kHl_6Ll4w" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-draw-a-ui.jpg'></td></tr></table>
## Hardware Eraser Support
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Hardware%20Eraser%20Support.md

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "2.0.1-beta.1",
"version": "2.0.1-beta-2",
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "2.0.1",
"version": "2.0.3",
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",

View File

@@ -1235,11 +1235,8 @@ export class ExcalidrawData {
const scene = this.scene as SceneDataWithFiles;
//remove files and equations that no longer have a corresponding image element
const fileIds = (
scene.elements.filter(
(e) => e.type === "image",
) as ExcalidrawImageElement[]
).map((e) => e.fileId);
const images = scene.elements.filter((e) => e.type === "image") as ExcalidrawImageElement[];
const fileIds = (images).map((e) => e.fileId);
this.files.forEach((value, key) => {
if (!fileIds.contains(key)) {
this.files.delete(key);
@@ -1261,22 +1258,26 @@ export class ExcalidrawData {
}
});
//check if there are any images that need to be processed in the new scene
if (!scene.files || Object.keys(scene.files).length === 0) {
return false;
}
//assing new fileId to duplicate equation and markdown files
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/601
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/593
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/297
const processedIds = new Set<string>();
fileIds.forEach(fileId=>{
fileIds.forEach((fileId,idx)=>{
if(processedIds.has(fileId)) {
const file = this.getFile(fileId);
const equation = this.getEquation(fileId);
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.isLocalLink || (file.file && (file.file.extension !== "md" || this.plugin.isExcalidrawFile(file.file))))) {
return;
@@ -1284,6 +1285,12 @@ export class ExcalidrawData {
if(mermaid) {
return;
}
if(getMermaidText(images[idx])) {
this.setMermaid(fileId, {mermaid: getMermaidText(images[idx]), isLoaded: true});
return;
}
const newId = fileid();
(scene
.elements

View File

@@ -127,7 +127,7 @@ import { CanvasNodeFactory, ObsidianCanvasNode } from "./utils/CanvasNodeFactory
import { EmbeddableMenu } from "./menu/EmbeddableActionsMenu";
import { useDefaultExcalidrawFrame } from "./utils/CustomEmbeddableUtils";
import { UniversalInsertFileModal } from "./dialogs/UniversalInsertFileModal";
import { shouldRenderMermaid } from "./utils/MermaidUtils";
import { getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
import { nanoid } from "nanoid";
declare const PLUGIN_VERSION:string;
@@ -1024,13 +1024,14 @@ export default class ExcalidrawView extends TextFileView {
})();
return;
}
if (this.excalidrawData.hasMermaid(selectedImage.fileId)) {
if (this.excalidrawData.hasMermaid(selectedImage.fileId) || getMermaidText(imageElement)) {
if(shouldRenderMermaid) {
const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
api.setActiveTool({type: "mermaid"});
api.updateScene({appState: { openDialog: "mermaid" }});
}
return;
}
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);

View File

@@ -228,7 +228,7 @@ function RenderObsidianView(
return () => {}; //cleanup on unmount
}, [linkText, subpath, containerRef]);
const setColors = (canvasNode: HTMLDivElement, element: NonDeletedExcalidrawElement, mdProps: EmbeddableMDCustomProps, canvas: string) => {
const setColors = (canvasNode: HTMLDivElement, element: NonDeletedExcalidrawElement, mdProps: EmbeddableMDCustomProps, canvasColor: string) => {
if(!mdProps) return;
if (!leafRef.current?.hasOwnProperty("node")) return;
@@ -246,28 +246,43 @@ function RenderObsidianView(
if(mdProps.backgroundMatchElement) {
const opacity = (mdProps?.backgroundOpacity ?? 50)/100;
const color = element?.backgroundColor
? ea.getCM(element.backgroundColor).alphaTo(opacity).stringHEX()
? (element.backgroundColor.toLowerCase() === "transparent"
? "transparent"
: ea.getCM(element.backgroundColor).alphaTo(opacity).stringHEX())
: "transparent";
color === "transparent" ? canvasNode?.addClass("transparent") : canvasNode?.removeClass("transparent");
canvasNode?.style.setProperty("--canvas-background", color);
canvasNode?.style.setProperty("--background-primary", color);
canvasNodeContainer?.style.setProperty("background-color", color);
} else if (!(mdProps?.backgroundMatchElement ?? true )) {
const opacity = (mdProps.backgroundOpacity??100)/100;
const color = mdProps.backgroundMatchCanvas
? ea.getCM(canvasColor).alphaTo((mdProps.backgroundOpacity??100)/100).stringHEX()
? (canvasColor.toLowerCase() === "transparent"
? "transparent"
: ea.getCM(canvasColor).alphaTo(opacity).stringHEX())
: ea.getCM(mdProps.backgroundColor).alphaTo((mdProps.backgroundOpacity??100)/100).stringHEX();
containerRef.current?.style.setProperty("--canvas-background", color);
color === "transparent" ? canvasNode?.addClass("transparent") : canvasNode?.removeClass("transparent");
canvasNode?.style.setProperty("--canvas-background", color);
canvasNode?.style.setProperty("--background-primary", color);
canvasNodeContainer?.style.setProperty("background-color", color);
}
if(mdProps.borderMatchElement) {
const opacity = (mdProps?.borderOpacity ?? 50)/100;
const color = element?.strokeColor
? ea.getCM(element?.strokeColor).alphaTo(opacity).stringHEX()
: "transparent";
? (element.strokeColor.toLowerCase() === "transparent"
? "transparent"
: ea.getCM(element.strokeColor).alphaTo(opacity).stringHEX())
: "transparent";
canvasNode?.style.setProperty("--canvas-border", color);
canvasNode?.style.setProperty("--canvas-color", color);
canvasNodeContainer?.style.setProperty("border-color", color);
} else if(!(mdProps?.borderMatchElement ?? true)) {
const color = ea.getCM(mdProps.borderColor).alphaTo((mdProps.borderOpacity??100)/100).stringHEX();
canvasNode?.style.setProperty("--canvas-border", color);
canvasNode?.style.setProperty("--canvas-color", color);
canvasNodeContainer?.style.setProperty("border-color", color);
}
}
@@ -398,7 +413,7 @@ export const CustomEmbeddable: React.FC<{element: NonDeletedExcalidrawElement; v
color: `var(--text-normal)`,
}}
className={`${theme} canvas-node ${
mdProps?.filenameVisible ? "" : "excalidraw-mdEmbed-hideFilename"}`}
mdProps?.filenameVisible && !mdProps.useObsidianDefaults ? "" : "excalidraw-mdEmbed-hideFilename"}`}
>
<RenderObsidianView
mdProps={mdProps}

View File

@@ -109,7 +109,7 @@ export class EmbeddalbeMDFileCustomDataSettingsComponent {
bgSetting.settingEl.style.display = (this.mdCustomData.backgroundMatchElement || this.mdCustomData.backgroundMatchCanvas) ? "none" : "";
const opacity = (value:number):DocumentFragment => {
return fragWithHTML(`Current transparency is <b>${value}%</b>`);
return fragWithHTML(`Current opacity is <b>${value}%</b>`);
}
const bgOpacitySetting = new Setting(contentEl)

View File

@@ -10,6 +10,8 @@ import { getNewUniqueFilepath, getPathWithoutExtension, splitFolderAndFilename }
import { addAppendUpdateCustomData, fragWithHTML } from "src/utils/Utils";
import { getYouTubeStartAt, isValidYouTubeStart, isYouTube, updateYouTubeStartTime } from "src/utils/YoutTubeUtils";
import { EmbeddalbeMDFileCustomDataSettingsComponent } from "./EmbeddableMDFileCustomDataSettingsComponent";
import { isCTRL } from "src/utils/ModifierkeyHelper";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
export type EmbeddableMDCustomProps = {
useObsidianDefaults: boolean;
@@ -31,6 +33,7 @@ export class EmbeddableSettings extends Modal {
private youtubeStart: string = null;
private isMDFile: boolean;
private mdCustomData: EmbeddableMDCustomProps;
private onKeyDown: (ev: KeyboardEvent) => void;
constructor(
private plugin: ExcalidrawPlugin,
@@ -60,12 +63,12 @@ export class EmbeddableSettings extends Modal {
onOpen(): void {
this.containerEl.classList.add("excalidraw-release");
this.titleEl.setText(t("ES_TITLE"));
//this.titleEl.setText(t("ES_TITLE"));
this.createForm();
}
onClose() {
this.containerEl.removeEventListener("keydown",this.onKeyDown);
}
async createForm() {
@@ -85,11 +88,21 @@ export class EmbeddableSettings extends Modal {
}
const zoomValue = ():DocumentFragment => {
return fragWithHTML(`Current zoom is <b>${Math.round(this.zoomValue*100)}%</b>`);
}
return fragWithHTML(`${t("ES_ZOOM_100_RELATIVE_DESC")}<br>Current zoom is <b>${Math.round(this.zoomValue*100)}%</b>`);
}
const zoomSetting = new Setting(this.contentEl)
.setName(t("ES_ZOOM"))
.setDesc(zoomValue())
.addButton(button =>
button
.setButtonText(t("ES_ZOOM_100"))
.onClick(() => {
const api = this.view.excalidrawAPI as ExcalidrawImperativeAPI;
this.zoomValue = 1/api.getAppState().zoom.value;
zoomSetting.setDesc(zoomValue());
})
)
.addSlider(slider =>
slider
.setLimits(10,400,5)
@@ -117,64 +130,87 @@ export class EmbeddableSettings extends Modal {
this.contentEl.createEl("h3",{text: t("ES_EMBEDDABLE_SETTINGS")});
new EmbeddalbeMDFileCustomDataSettingsComponent(this.contentEl,this.mdCustomData).render();
}
new Setting(this.contentEl)
.addButton(button =>
button
.setButtonText(t("PROMPT_BUTTON_CANCEL"))
.setTooltip("ESC")
.onClick(() => {
this.close();
})
)
.addButton(button =>
button
.setButtonText(t("PROMPT_BUTTON_OK"))
.setTooltip("CTRL/Opt+Enter")
.setCta()
.onClick(()=>this.applySettings())
)
const div = this.contentEl.createDiv({cls: "excalidraw-prompt-buttons-div"});
const bOk = div.createEl("button", { text: t("PROMPT_BUTTON_OK"), cls: "excalidraw-prompt-button"});
bOk.onclick = async () => {
let dirty = false;
const el = this.ea.getElement(this.element.id) as Mutable<ExcalidrawEmbeddableElement>;
if(this.updatedFilepath) {
const newPathWithExt = `${this.updatedFilepath}.${this.file.extension}`;
if(newPathWithExt !== this.file.path) {
const fnparts = splitFolderAndFilename(newPathWithExt);
const newPath = getNewUniqueFilepath(
this.app.vault,
fnparts.folderpath,
fnparts.filename,
);
await this.app.vault.rename(this.file,newPath);
el.link = this.element.link.replace(
/(\[\[)([^#\]]*)([^\]]*]])/,`$1${
this.plugin.app.metadataCache.fileToLinktext(
this.file,this.view.file.path,true)
}$3`);
dirty = true;
}
}
if(this.isYouTube && this.youtubeStart !== getYouTubeStartAt(this.element.link)) {
dirty = true;
if(isValidYouTubeStart(this.youtubeStart)) {
el.link = updateYouTubeStartTime(el.link,this.youtubeStart);
} else {
new Notice(t("ES_YOUTUBE_START_INVALID"));
}
}
if(
this.isMDFile && (
this.mdCustomData.backgroundColor !== this.element.customData?.backgroundColor ||
this.mdCustomData.borderColor !== this.element.customData?.borderColor ||
this.mdCustomData.backgroundOpacity !== this.element.customData?.backgroundOpacity ||
this.mdCustomData.borderOpacity !== this.element.customData?.borderOpacity ||
this.mdCustomData.filenameVisible !== this.element.customData?.filenameVisible)
) {
addAppendUpdateCustomData(el,{mdProps: this.mdCustomData});
dirty = true;
}
if(this.zoomValue !== this.element.scale[0]) {
dirty = true;
el.scale = [this.zoomValue,this.zoomValue];
const onKeyDown = (ev: KeyboardEvent) => {
if(isCTRL(ev) && ev.key === "Enter") {
this.applySettings();
}
if(dirty) {
this.ea.addElementsToView();
}
this.close();
};
const bCancel = div.createEl("button", { text: t("PROMPT_BUTTON_CANCEL"), cls: "excalidraw-prompt-button" });
bCancel.onclick = () => {
this.close();
};
}
this.onKeyDown = onKeyDown;
this.containerEl.ownerDocument.addEventListener("keydown",onKeyDown);
}
private async applySettings() {
let dirty = false;
const el = this.ea.getElement(this.element.id) as Mutable<ExcalidrawEmbeddableElement>;
if(this.updatedFilepath) {
const newPathWithExt = `${this.updatedFilepath}.${this.file.extension}`;
if(newPathWithExt !== this.file.path) {
const fnparts = splitFolderAndFilename(newPathWithExt);
const newPath = getNewUniqueFilepath(
this.app.vault,
fnparts.folderpath,
fnparts.filename,
);
await this.app.vault.rename(this.file,newPath);
el.link = this.element.link.replace(
/(\[\[)([^#\]]*)([^\]]*]])/,`$1${
this.plugin.app.metadataCache.fileToLinktext(
this.file,this.view.file.path,true)
}$3`);
dirty = true;
}
}
if(this.isYouTube && this.youtubeStart !== getYouTubeStartAt(this.element.link)) {
dirty = true;
if(this.youtubeStart === "" || isValidYouTubeStart(this.youtubeStart)) {
el.link = updateYouTubeStartTime(el.link,this.youtubeStart);
} else {
new Notice(t("ES_YOUTUBE_START_INVALID"));
}
}
if(
this.isMDFile && (
this.mdCustomData.backgroundColor !== this.element.customData?.backgroundColor ||
this.mdCustomData.borderColor !== this.element.customData?.borderColor ||
this.mdCustomData.backgroundOpacity !== this.element.customData?.backgroundOpacity ||
this.mdCustomData.borderOpacity !== this.element.customData?.borderOpacity ||
this.mdCustomData.filenameVisible !== this.element.customData?.filenameVisible)
) {
addAppendUpdateCustomData(el,{mdProps: this.mdCustomData});
dirty = true;
}
if(this.zoomValue !== this.element.scale[0]) {
dirty = true;
el.scale = [this.zoomValue,this.zoomValue];
}
if(dirty) {
this.ea.addElementsToView();
}
this.close();
};
}

View File

@@ -17,6 +17,35 @@ I develop this plugin as a hobby, spending my free time doing this. If you find
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://cdn.ko-fi.com/cdn/kofi3.png?v=3" height=45></a></div>
`,
"2.0.3":`
## Fixed
- Mermaid to Excalidraw stopped working after installing the Obsidian 1.5.0 insider build. [#1450](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1450)
- CTRL+Click on a Mermaid diagram did not open the Mermaid editor.
- Embed color settings were not honored when the embedded markdown was focused on a section or block.
- Scrollbars were visible when the embeddable was set to transparent (set background color to match element background, and set element background color to "transparent").
`,
"2.0.2":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/502swdqvZ2A" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## Fixed
- Resolved an issue where the Command Palette's "Toggle between Excalidraw and Markdown mode" failed to uncompress the Excalidraw JSON for editing.
## New
- Scaling feature for embedded objects (markdown documents, pdfs, YouTube, etc.): Hold down the SHIFT key while resizing elements to adjust their size.
- Expanded support for Canvas Candy. Regardless of Canvas Candy, you can apply CSS classes to embedded markdown documents for transparency, shape adjustments, text orientation, and more.
- Added new functionalities to the active embeddable top-left menu:
- Document Properties (cog icon)
- File renaming
- Basic styling options for embedded markdown documents
- Setting YouTube start time
- Zoom to full screen for PDFs
- Improved immersive embedding of Excalidraw into Obsidian Canvas.
- Introduced new Command Palette Actions:
- Embeddable Properties
- Scaling selected embeddable elements to 100% relative to the current canvas zoom.
`,
"2.0.1":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/xmqiBTrlbEM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

View File

@@ -182,7 +182,7 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "createPNG",
code: "createPNG(templatePath?: string, scale?: number, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string,): Promise<any>;",
code: "createPNG(templatePath?: string, scale?: number, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string,padding?: number): Promise<any>;",
desc: "Use ExcalidrawAutomate.getExportSettings(boolean,boolean) to create an ExportSettings object.\nUse ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?) to create an EmbeddedFilesLoader object.",
after: "",
},

View File

@@ -11,6 +11,7 @@ export default {
// main.ts
PUBLISH_SVG_CHECK: "Obsidian Publish: Find SVG and PNG exports that are out of date",
EMBEDDABLE_PROPERTIES: "Embeddable Properties",
EMBEDDABLE_RELATIVE_ZOOM: "Scale selected embeddable elements to 100% relative to the current canvas zoom",
OPEN_IMAGE_SOURCE: "Open Excalidraw drawing",
INSTALL_SCRIPT: "Install the script",
UPDATE_SCRIPT: "Update available - Click to install",
@@ -84,7 +85,7 @@ export default {
OPEN_LINK: "Open selected text as link\n(SHIFT+CLICK to open in a new pane)",
EXPORT_EXCALIDRAW: "Export to an .Excalidraw file",
LINK_BUTTON_CLICK_NO_TEXT:
"Select a ImageElement, or select a TextElement that contains an internal or external link.\n",
"Select an ImageElement, or select a TextElement that contains an internal or external link.\n",
FILENAME_INVALID_CHARS:
'File name cannot contain any of the following characters: * " \\ < > : | ? #',
FORCE_SAVE:
@@ -575,9 +576,9 @@ FILENAME_HEAD: "Filename",
PROPERTIES: "Properties",
//EmbeddableSettings.tsx
ES_TITLE: "Element Settings",
ES_TITLE: "Embeddable Element Settings",
ES_RENAME: "Rename File",
ES_ZOOM: "Embedded Content Zoom",
ES_ZOOM: "Embedded Content Scaling",
ES_YOUTUBE_START: "YouTube Start Time",
ES_YOUTUBE_START_DESC: "ss, mm:ss, hh:mm:ss",
ES_YOUTUBE_START_INVALID: "The YouTube Start Time is invalid. Please check the format and try again",
@@ -589,10 +590,12 @@ FILENAME_HEAD: "Filename",
ES_BORDER_HEAD: "Embedded note border color",
ES_BORDER_COLOR: "Border Color",
ES_BORDER_MATCH_ELEMENT: "Match Element Border Color",
ES_BACKGROUND_OPACITY: "Background Transparency",
ES_BORDER_OPACITY: "Border Transparency",
ES_BACKGROUND_OPACITY: "Background Opacity",
ES_BORDER_OPACITY: "Border Opacity",
ES_EMBEDDABLE_SETTINGS: "Embeddable Markdown Settings",
ES_USE_OBSIDIAN_DEFAULTS: "Use Obsidian Defaults",
ES_ZOOM_100_RELATIVE_DESC: "The button will adjust the element scale so it will show the content at 100% relative to the current zoom level of your canvas",
ES_ZOOM_100: "Relative 100%",
//Prompts.ts
PROMPT_FILE_DOES_NOT_EXIST: "File does not exist. Do you want to create it?",

View File

@@ -50,7 +50,8 @@ import ExcalidrawView, { TextMode, getTextMode } from "./ExcalidrawView";
import {
changeThemeOfExcalidrawMD,
getMarkdownDrawingSection,
ExcalidrawData
ExcalidrawData,
REGEX_LINK
} from "./ExcalidrawData";
import {
ExcalidrawSettings,
@@ -118,6 +119,10 @@ import { StylesManager } from "./utils/StylesManager";
import { MATHJAX_SOURCE_LZCOMPRESSED } from "./constMathJaxSource";
import { PublishOutOfDateFilesDialog } from "./dialogs/PublishOutOfDateFiles";
import { EmbeddableSettings } from "./dialogs/EmbeddableSettings";
import { processLinkText } from "./utils/CustomEmbeddableUtils";
import { getEA } from "src";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
import { Mutable } from "@zsviczian/excalidraw/types/utility-types";
declare const EXCALIDRAW_PACKAGES:string;
declare const react:any;
@@ -868,7 +873,41 @@ export default class ExcalidrawPlugin extends Plugin {
return false;
}
if(checking) return true;
new EmbeddableSettings(view.plugin,view,null,els[0]).open();
const getFile = (el:ExcalidrawEmbeddableElement):TFile => {
const res = REGEX_LINK.getRes(el.link).next();
if(!res || (!res.value && res.done)) {
return null;
}
const link = REGEX_LINK.getLink(res);
const { file } = processLinkText(link, view);
return file;
}
new EmbeddableSettings(view.plugin,view,getFile(els[0]),els[0]).open();
}
})
this.addCommand({
id: "excalidraw-embeddables-relative-scale",
name: t("EMBEDDABLE_RELATIVE_ZOOM"),
checkCallback: (checking: boolean) => {
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if(!view) return false;
if(!view.excalidrawAPI) return false;
const els = view.getViewSelectedElements().filter(el=>el.type==="embeddable") as ExcalidrawEmbeddableElement[];
if(els.length === 0) {
if(checking) return false;
new Notice("Select at least one embeddable element and try again");
return false;
}
if(checking) return true;
const ea = getEA(view) as ExcalidrawAutomate;
const api = ea.getExcalidrawAPI() as ExcalidrawImperativeAPI;
ea.copyViewElementsToEAforEditing(els);
const scale = 1/api.getAppState().zoom.value;
ea.getElements().forEach((el: Mutable<ExcalidrawEmbeddableElement>)=>{
el.scale = [scale,scale];
})
ea.addElementsToView();
}
})
@@ -1644,10 +1683,7 @@ export default class ExcalidrawPlugin extends Plugin {
const excalidrawView = this.app.workspace.getActiveViewOfType(ExcalidrawView)
if (excalidrawView) {
const activeLeaf = excalidrawView.leaf;
this.excalidrawFileModes[(activeLeaf as any).id || activeFile.path] =
"markdown";
this.setMarkdownView(activeLeaf);
excalidrawView.openAsMarkdown();
return;
}
@@ -2088,14 +2124,14 @@ export default class ExcalidrawPlugin extends Plugin {
//!Temporary hack
//https://discord.com/channels/686053708261228577/817515900349448202/1031101635784613968
if (app.isMobile && newActiveviewEV && !previouslyActiveEV) {
if (this.app.isMobile && newActiveviewEV && !previouslyActiveEV) {
const navbar = document.querySelector("body>.app-container>.mobile-navbar");
if(navbar && navbar instanceof HTMLDivElement) {
navbar.style.position="relative";
}
}
if (app.isMobile && !newActiveviewEV && previouslyActiveEV) {
if (this.app.isMobile && !newActiveviewEV && previouslyActiveEV) {
const navbar = document.querySelector("body>.app-container>.mobile-navbar");
if(navbar && navbar instanceof HTMLDivElement) {
navbar.style.position="";

View File

@@ -190,7 +190,7 @@ export class EmbeddableMenu {
title={t("ZOOM_TO_FIT")}
action={() => {
if(!element) return;
api.zoomToFit([element], view.plugin.settings.zoomToFitMaxLevel, 0.1);
api.zoomToFit([element], 30, 0.1);
}}
icon={ICONS.ZoomToSelectedElement}
view={view}

View File

@@ -12,9 +12,12 @@ export const getYouTubeStartAt = (url: string): string => {
const hours = Math.floor(time / 3600);
const minutes = Math.floor((time - hours * 3600) / 60);
const seconds = time - hours * 3600 - minutes * 60;
if(hours === 0 && minutes === 0 && seconds === 0) return "";
if(hours === 0 && minutes === 0) return `${String(seconds).padStart(2, '0')}`;
if(hours === 0) return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
}
return "00:00:00";
return "";
};
export const isValidYouTubeStart = (value: string): boolean => {
@@ -26,7 +29,9 @@ export const isValidYouTubeStart = (value: string): boolean => {
export const updateYouTubeStartTime = (link: string, startTime: string): string => {
const match = link.match(REG_YOUTUBE);
if (match?.[2]) {
const startTimeParam = `t=${timeStringToSeconds(startTime)}`;
const startTimeParam = startTime === ""
? ``
: `t=${timeStringToSeconds(startTime)}`;
let updatedLink = link;
if (match[3]) {
// If start time already exists, update it

View File

@@ -477,6 +477,13 @@ hr.excalidraw-setting-hr {
display: none;
}
.excalidraw__embeddable-container .canvas-node:not(.is-editing).transparent {
::-webkit-scrollbar,
::-webkit-scrollbar-horizontal {
display: none;
}
}
.canvas-node:not(.is-editing):has(.excalidraw-canvas-immersive) {
::-webkit-scrollbar,
::-webkit-scrollbar-horizontal {
@@ -488,4 +495,5 @@ hr.excalidraw-setting-hr {
.canvas-node:not(.is-editing) .canvas-node-container:has(.excalidraw-canvas-immersive) {
border: unset;
box-shadow: unset;
}
}