Compare commits

...

62 Commits

Author SHA1 Message Date
zsviczian
51cf3a9219 2.0.17 2024-01-10 22:12:02 +01:00
zsviczian
8700405af8 Merge pull request #1548 from tswwe/patch-4
Update zh-cn.ts
2024-01-10 20:51:15 +01:00
thxnder
d1d082b4f9 Update zh-cn.ts
keep up with en.ts
2024-01-10 20:20:04 +08:00
zsviczian
6dd9d1a056 2.0.16 2024-01-07 21:22:56 +01:00
zsviczian
46b03725e9 2.0.15 2024-01-07 10:57:53 +01:00
zsviczian
65d6577b28 Update README.md 2024-01-03 21:35:01 +01:00
zsviczian
97967f5b70 2.0.14 2024-01-03 16:32:07 +01:00
zsviczian
9323e1fad4 minor formatting change in rollup.config.json 2024-01-03 09:51:04 +01:00
zsviczian
3e4e741b54 2.0.13 2023-12-23 15:28:54 +01:00
zsviczian
4e2d8374e6 2.0.12 2023-12-23 11:02:57 +01:00
zsviczian
2b3037402a 2.0.11 2023-12-21 20:51:21 +01:00
zsviczian
bc67c27a82 updated ExcaliAI script 2023-12-21 19:49:15 +01:00
zsviczian
ffb8f6f00f css rework 2023-12-19 13:52:27 +01:00
zsviczian
d179dfe703 sliding panes support disabled 2023-12-18 17:26:28 +01:00
zsviczian
5701020901 Golden Ratio updated 2023-12-17 12:54:53 +01:00
zsviczian
1f2d795b58 stroke color currentColor GoldenRatio.svg 2023-12-17 09:36:54 +01:00
zsviczian
c123b3ef51 golden ratio svg 2023-12-17 07:57:13 +01:00
zsviczian
d565c7e9e9 Golden Ratio script 2023-12-17 07:54:15 +01:00
zsviczian
710a40c36b golden ratio image 2023-12-17 07:48:25 +01:00
zsviczian
64f9a5dd7d 2.0.10 2023-12-10 21:58:04 +01:00
zsviczian
323fe33c2f 2.0.9 2023-12-10 12:35:10 +01:00
zsviczian
2470f6f531 2.0.8 2023-12-08 17:19:16 +01:00
zsviczian
42968d8299 publishing relative font size cycle 2023-12-07 19:24:31 +01:00
zsviczian
7a50b6c77e Merge pull request #1425 from soraliu/master
feat(scripts): support repeat texts
2023-12-07 18:18:46 +01:00
zsviczian
0a10d5fbc9 Merge branch 'master' of https://github.com/zsviczian/obsidian-excalidraw-plugin 2023-12-06 18:14:47 +01:00
zsviczian
40d2345501 LaTeX Cleanup 2023-12-06 18:14:44 +01:00
zsviczian
ab97ae5ebc updated modifier key combinations table 2023-12-04 17:25:10 +01:00
zsviczian
7c93e90fc0 2.0.7 2023-12-04 14:49:33 +01:00
zsviczian
d44cf3306b 2.0.6 manifest 2023-12-04 08:44:30 +01:00
zsviczian
324609999f rollback 2023-12-03 23:35:03 +01:00
zsviczian
3f54b851ae updated grid selected images 2023-12-03 20:17:22 +01:00
zsviczian
8bbb04b421 Merge pull request #1465 from 7flash/patch-6
Update Grid Selected Images.md
2023-12-03 20:13:04 +01:00
zsviczian
dc396c8707 fix file utils var 2023-12-03 20:11:22 +01:00
zsviczian
52cc5d3aa7 2.0.5 2023-12-03 20:00:47 +01:00
zsviczian
87b6335905 cleaned out React.createElement 2023-12-01 20:45:37 +01:00
zsviczian
17358f16c8 fix getFileFromURL, remove observers when not needed 2023-12-01 17:21:41 +01:00
zsviczian
23eb268031 Merge branch 'master' of https://github.com/zsviczian/obsidian-excalidraw-plugin 2023-11-29 17:28:12 +01:00
zsviczian
27f4cb248d better error handling in ExcaliAI 2023-11-29 17:26:07 +01:00
zsviczian
bbaf4f7a34 updated ExcaliAI and GPT-Draw-a-UI timestamps 2023-11-26 19:59:39 +01:00
zsviczian
559455bf5b fix element.src to element.link 2023-11-26 19:57:42 +01:00
zsviczian
bd519aff08 fixed element.src to element.link 2023-11-26 19:57:04 +01:00
zsviczian
febeb787b5 Address undefined after installing the new version 2023-11-26 18:51:22 +01:00
zsviczian
9f8a9bfa8a 2.0.4 2023-11-26 18:38:31 +01:00
zsviczian
8b1daed0ef excaliAI 2023-11-26 16:06:55 +01:00
zsviczian
44c828c7e7 corrected minor error 2023-11-25 07:06:14 +01:00
Igor Berlenko
afabeaa2f3 Update Grid Selected Images.md 2023-11-24 22:41:51 +08:00
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
zsviczian
6e5a853d0f 2.0.2-beta-1 2023-11-18 17:40:59 +01:00
zsviczian
0c702ddf7b Merge branch 'master' of https://github.com/zsviczian/obsidian-excalidraw-plugin 2023-11-18 17:16:26 +01:00
zsviczian
fdbffce1f9 Embeddable Settings 2023-11-18 17:16:24 +01:00
zsviczian
2872b4e3ce Merge pull request #1441 from heinrich26/patch-1
Fixed some typos
2023-11-18 17:14:45 +01:00
Hendrik Horstmann
0ba55e51e9 Fixed some typos 2023-11-15 10:50:16 +01:00
zsviczian
5887bf377d 2.0.1 2023-11-14 20:35:47 +01:00
Sora Liu
f80a96c703 feat(scripts): support repeat texts 2023-11-08 17:11:55 +04:00
90 changed files with 8489 additions and 2977 deletions

1
.gitignore vendored
View File

@@ -13,6 +13,7 @@ stats.html
hot-reload.bat
data.json
lib
dist
#VSCode
.vscode

2
.nvmrc
View File

@@ -1 +1 @@
18
16

View File

@@ -7,6 +7,9 @@ The Obsidian-Excalidraw plugin integrates [Excalidraw](https://excalidraw.com/),
<a href="https://youtu.be/o0exK-xFP3k" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/156931370-aa4d88de-c4a8-46cc-aeb2-dc09aa0bea39.jpg" width="300"/></a>
<a href="https://youtu.be/QKnQgSjJVuc" target="_blank"><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/thumbnail-getting-started.jpg" width="300"/></a>
### Here's my complete catalog of videos:
<a href="https://excalidraw-obsidian.online/Hobbies/Excalidraw+Blog/Catalogue+of+Videos"><img width="380" alt="image" src="https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/2577e5ad-7a21-4c62-acd5-4fe80c8a8a95"></a>
<br>
<details><summary>10 Part (slightly outdated) Video Walkthrough</summary>
<a href="https://youtu.be/sY4FoflGaiM" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160304-7f211180-e17c-11eb-8363-c52723de1ffd.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;1 Getting Started</a><br>

View File

@@ -1,15 +1,15 @@
/// <reference types="react" />
import ExcalidrawPlugin from "src/main";
import { FillStyle, StrokeStyle, ExcalidrawElement, ExcalidrawBindableElement, FileId, NonDeletedExcalidrawElement, ExcalidrawImageElement, StrokeRoundness, RoundnessType } from "@zsviczian/excalidraw/types/element/types";
import { FillStyle, StrokeStyle, ExcalidrawElement, ExcalidrawBindableElement, FileId, NonDeletedExcalidrawElement, ExcalidrawImageElement, StrokeRoundness, RoundnessType } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { Editor, OpenViewState, TFile, WorkspaceLeaf } from "obsidian";
import * as obsidian_module from "obsidian";
import ExcalidrawView, { ExportSettings } from "src/ExcalidrawView";
import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI, Point } from "@zsviczian/excalidraw/types/types";
import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI, Point } from "@zsviczian/excalidraw/types/excalidraw/types";
import { EmbeddedFilesLoader } from "src/EmbeddedFileLoader";
import { ConnectionPoint, DeviceType } from "src/types";
import { ColorMaster } from "colormaster";
import { TInput } from "colormaster/types";
import { ClipboardData } from "@zsviczian/excalidraw/types/clipboard";
import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard";
import { PaneTarget } from "src/utils/ModifierkeyHelper";
export declare class ExcalidrawAutomate {
/**

673
ea-scripts/ExcaliAI.md Normal file
View File

@@ -0,0 +1,673 @@
/*
<iframe width="560" height="315" src="https://www.youtube.com/embed/A1vrSGBbWgo" 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*/
let dirty=false;
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.0.12")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
const outputTypes = {
"html": {
instruction: "Turn this into a single html file using tailwind. Return a single message containing only the html file in a codeblock.",
blocktype: "html"
},
"mermaid": {
instruction: "Return a single message containing only the mermaid diagram in a codeblock.",
blocktype: "mermaid"
},
"svg": {
instruction: "Return a single message containing only the SVG code in an html codeblock.",
blocktype: "svg"
},
"image-gen": {
instruction: "Return a single message with the generated image prompt in a codeblock",
blocktype: "image"
},
"image-edit": {
instruction: "",
blocktype: "image"
}
}
const systemPrompts = {
"Challenge my thinking": {
prompt: `Your task is to interpret a screenshot of a whiteboard, translating its ideas into a Mermaid graph. The whiteboard will encompass thoughts on a subject. Within the mind map, distinguish ideas that challenge, dispute, or contradict the whiteboard content. Additionally, include concepts that expand, complement, or advance the user's thinking. Utilize the Mermaid graph diagram type and present the resulting Mermaid diagram within a code block. Ensure the Mermaid script excludes the use of parentheses ().`,
type: "mermaid",
help: "Translate your image and optional text prompt into a Mermaid mindmap. If there are conversion errors, edit the Mermaid script under 'More Tools'."
},
"Convert sketch to shapes": {
prompt: `Given an image featuring various geometric shapes drawn by the user, your objective is to analyze the input and generate SVG code that accurately represents these shapes. Your output will be the SVG code enclosed in an HTML code block.`,
type: "svg",
help: "Convert selected scribbles into shapes; works better with fewer shapes. Experimental and may not produce good drawings."
},
"Create a simple Excalidraw icon": {
prompt: `Given a description of an SVG image from the user, your objective is to generate the corresponding SVG code. Avoid incorporating textual elements within the generated SVG. Your output should be the resulting SVG code enclosed in an HTML code block.`,
type: "svg",
help: "Convert text prompts into simple icons inserted as Excalidraw elements. Expect only a text prompt. Experimental and may not produce good drawings."
},
"Edit an image": {
prompt: null,
type: "image-edit",
help: "Image elements will be used as the Image. Shapes on top of the image will be the Mask. Use the prompt to instruct Dall-e about the changes. Dall-e-2 model will be used."
},
"Generate an image from image and prompt": {
prompt: "Your task involves receiving an image and a textual prompt from the user. Your goal is to craft a detailed, accurate, and descriptive narrative of the image, tailored for effective image generation. Utilize the user-provided text prompt to inform and guide your depiction of the image. Ensure the resulting image remains text-free.",
type: "image-gen",
help: "Generate an image based on the drawing and prompt using ChatGPT-Vision and Dall-e. Provide a contextual text-prompt for accurate interpretation."
},
"Generate an image from prompt": {
prompt: null,
type: "image-gen",
help: "Send only the text prompt to OpenAI. Provide a detailed description; OpenAI will enrich your prompt automatically. To avoid it, start your prompt like this 'DO NOT add any detail, just use it AS-IS:'"
},
"Generate an image to illustrate a quote": {
prompt: "Your task involves transforming a user-provided quote into a detailed and imaginative illustration. Craft a visual representation that captures the essence of the quote and resonates well with a broad audience. If the Author's name is provided, aim to establish a connection between the illustration and the Author. This can be achieved by referencing a well-known story from the Author, situating the image in the Author's era or setting, or employing other creative methods of association. Additionally, provide preferences for styling, such as the chosen medium and artistic direction, to guide the image creation process. Ensure the resulting image remains text-free. Your task output should comprise a descriptive and detailed narrative aimed at facilitating the creation of a captivating illustration from the quote.",
type: "image-gen",
help: "ExcaliAI will create an image prompt to illustrate your text input - a quote - with GPT, then generate an image using Dall-e. In case you include the Author's name, GPT will try to generate an image that in some way references the Author."
},
"Visual brainstorm": {
prompt: "Your objective is to interpret a screenshot of a whiteboard, creating an image aimed at sparking further thoughts on the subject. The whiteboard will present diverse ideas about a specific topic. Your generated image should achieve one of two purposes: highlighting concepts that challenge, dispute, or contradict the whiteboard content, or introducing ideas that expand, complement, or enrich the user's thinking. You have the option to include multiple tiles in the resulting image, resembling a sequence akin to a comic strip. Ensure that the image remains devoid of text.",
type: "image-gen",
help: "Use ChatGPT Visions and Dall-e to create an image based on your text prompt and image to spark new ideas."
},
"Wireframe to code": {
prompt: `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.`,
type: "html",
help: "Use GPT Visions to interpret the wireframe and generate a web application. YOu may copy the resulting code from the active embeddable's top left menu."
},
}
const IMAGE_WARNING = "The generated image is linked through a temporary OpenAI URL and will be removed in approximately 30 minutes. To save it permanently, choose 'Save image from URL to local file' from the Obsidian Command Palette."
// --------------------------------------
// Initialize values and settings
// --------------------------------------
let settings = ea.getScriptSettings();
if(!settings["Agent's Task"]) {
settings = {
"Agent's Task": "Wireframe to code",
"User Prompt": "",
};
await ea.setScriptSettings(settings);
}
const OPENAI_API_KEY = ea.plugin.settings.openAIAPIToken;
if(!OPENAI_API_KEY || OPENAI_API_KEY === "") {
new Notice("You must first configure your API key in Excalidraw Plugin Settings");
return;
}
let userPrompt = settings["User Prompt"] ?? "";
let agentTask = settings["Agent's Task"];
let imageSize = settings["Image Size"]??"1024x1024";
if(!systemPrompts.hasOwnProperty(agentTask)) {
agentTask = Object.keys(systemPrompts)[0];
}
let imageModel, valideSizes;
const setImageModelAndSizes = () => {
imageModel = systemPrompts[agentTask].type === "image-edit"
? "dall-e-2"
: ea.plugin.settings.openAIDefaultImageGenerationModel;
validSizes = imageModel === "dall-e-2"
? [`256x256`, `512x512`, `1024x1024`]
: (imageModel === "dall-e-3"
? [`1024x1024`, `1792x1024`, `1024x1792`]
: [`1024x1024`])
if(!validSizes.includes(imageSize)) {
imageSize = "1024x1024";
dirty = true;
}
}
setImageModelAndSizes();
// --------------------------------------
// Generate Image Blob From Selected Excalidraw Elements
// --------------------------------------
const calculateImageScale = (elements) => {
const bb = ea.getBoundingBox(elements);
const size = (bb.width*bb.height);
const minRatio = Math.sqrt(360000/size);
const maxRatio = Math.sqrt(size/16000000);
return minRatio > 1
? minRatio
: (
maxRatio > 1
? 1/maxRatio
: 1
);
}
const createMask = async (dataURL) => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
// If opaque (alpha > 0), make it transparent
if (data[i + 3] > 0) {
data[i + 3] = 0; // Set alpha to 0 (transparent)
} else if (data[i + 3] === 0) {
// If fully transparent, make it red
data[i] = 255; // Red
data[i + 1] = 0; // Green
data[i + 2] = 0; // Blue
data[i + 3] = 255; // make it opaque
}
}
ctx.putImageData(imageData, 0, 0);
const maskDataURL = canvas.toDataURL();
resolve(maskDataURL);
};
img.onerror = error => {
reject(error);
};
img.src = dataURL;
});
}
//https://platform.openai.com/docs/api-reference/images/createEdit
//dall-e-2 image edit only works on square images
//if targetDalleImageEdit === true then the image and the mask will be returned in two separate dataURLs
let squareBB;
const generateCanvasDataURL = async (view, targetDalleImageEdit=false) => {
let PADDING = 5;
await view.forceSave(true); //to ensure recently embedded PNG and other images are saved to file
const viewElements = ea.getViewSelectedElements();
if(viewElements.length === 0) {
return {imageDataURL: null, maskDataURL: null} ;
}
ea.copyViewElementsToEAforEditing(viewElements, true); //copying the images objects over to EA for PNG generation
let maskDataURL;
const loader = ea.getEmbeddedFilesLoader(false);
let scale = calculateImageScale(ea.getElements());
const bb = ea.getBoundingBox(viewElements);
if(ea.getElements()
.filter(el=>el.type==="image")
.some(el=>Math.round(el.width) === Math.round(bb.width) && Math.round(el.height) === Math.round(bb.height))
) { PADDING = 0; }
let exportSettings = {withBackground: true, withTheme: true};
if(targetDalleImageEdit) {
PADDING = 0;
const strokeColor = ea.style.strokeColor;
const backgroundColor = ea.style.backgroundColor;
ea.style.backgroundColor = "transparent";
ea.style.strokeColor = "transparent";
let rectID;
if(bb.height > bb.width) {
rectID = ea.addRect(bb.topX-(bb.height-bb.width)/2, bb.topY,bb.height, bb.height);
}
if(bb.width > bb.height) {
rectID = ea.addRect(bb.topX, bb.topY-(bb.width-bb.height)/2,bb.width, bb.width);
}
if(bb.height === bb.width) {
rectID = ea.addRect(bb.topX, bb.topY, bb.width, bb.height);
}
const rect = ea.getElement(rectID);
squareBB = {topX: rect.x-PADDING, topY: rect.y-PADDING, width: rect.width + 2*PADDING, height: rect.height + 2*PADDING};
ea.style.strokeColor = strokeColor;
ea.style.backgroundColor = backgroundColor;
ea.getElements().filter(el=>el.type === "image").forEach(el=>{el.isDeleted = true});
dalleWidth = parseInt(imageSize.split("x")[0]);
scale = dalleWidth/squareBB.width;
exportSettings = {withBackground: false, withTheme: true};
maskDataURL= await ea.createPNGBase64(
null, scale, exportSettings, loader, "light", PADDING
);
maskDataURL = await createMask(maskDataURL)
ea.getElements().filter(el=>el.type === "image").forEach(el=>{el.isDeleted = false});
ea.getElements().filter(el=>el.type !== "image" && el.id !== rectID).forEach(el=>{el.isDeleted = true});
}
const imageDataURL = await ea.createPNGBase64(
null, scale, exportSettings, loader, "light", PADDING
);
ea.clear();
return {imageDataURL, maskDataURL};
}
let {imageDataURL, maskDataURL} = await generateCanvasDataURL(ea.targetView, systemPrompts[agentTask].type === "image-edit");
// --------------------------------------
// Support functions - embeddable spinner and error
// --------------------------------------
const spinner = await ea.convertStringToDataURL(`
<html><head><style>
html, body {width: 100%; height: 100%; color: ${ea.getExcalidrawAPI().getAppState().theme === "dark" ? "white" : "black"};}
body {display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 1rem; overflow: hidden;}
.Spinner {display: flex; align-items: center; justify-content: center; margin-left: auto; margin-right: auto;}
.Spinner svg {animation: rotate 1.6s linear infinite; transform-origin: center center; width: 40px; height: 40px;}
.Spinner circle {stroke: currentColor; animation: dash 1.6s linear 0s infinite; stroke-linecap: round;}
@keyframes rotate {100% {transform: rotate(360deg);}}
@keyframes dash {
0% {stroke-dasharray: 1, 300; stroke-dashoffset: 0;}
50% {stroke-dasharray: 150, 300; stroke-dashoffset: -200;}
100% {stroke-dasharray: 1, 300; stroke-dashoffset: -280;}
}
</style></head><body>
<div class="Spinner">
<svg viewBox="0 0 100 100">
<circle cx="50" cy="50" r="46" stroke-width="8" fill="none" stroke-miter-limit="10"/>
</svg>
</div>
<div>Generating...</div>
</body></html>`);
const errorMessage = async (spinnerID, message) => {
const error = "Something went wrong! Check developer console for more.";
const details = message ? `<p>${message}</p>` : "";
const errorDataURL = await ea.convertStringToDataURL(`
<html><head><style>
html, body {height: 100%;}
body {display: flex; flex-direction: column; align-items: center; justify-content: center; color: red;}
h1, h3 {margin-top: 0;margin-bottom: 0.5rem;}
</style></head><body>
<h1>Error!</h1>
<h3>${error}</h3>${details}
</body></html>`);
new Notice (error);
ea.getElement(spinnerID).link = errorDataURL;
ea.addElementsToView(false,true);
}
// --------------------------------------
// Utility to write Mermaid to dialog
// --------------------------------------
const EDITOR_LS_KEYS = {
OAI_API_KEY: "excalidraw-oai-api-key",
MERMAID_TO_EXCALIDRAW: "mermaid-to-excalidraw",
PUBLISH_LIBRARY: "publish-library-data",
};
const setMermaidDataToStorage = (mermaidDefinition) => {
try {
window.localStorage.setItem(
EDITOR_LS_KEYS.MERMAID_TO_EXCALIDRAW,
JSON.stringify(mermaidDefinition)
);
return true;
} catch (error) {
console.warn(`localStorage.setItem error: ${error.message}`);
return false;
}
};
// --------------------------------------
// Submit Prompt
// --------------------------------------
const generateImage = async(text, spinnerID, bb) => {
const requestObject = {
text,
imageGenerationProperties: {
size: imageSize,
//quality: "standard", //not supported by dall-e-2
n:1,
},
};
const result = await ea.postOpenAI(requestObject);
console.log({result, json:result?.json});
if(!result?.json?.data?.[0]?.url) {
await errorMessage(spinnerID, result?.json?.error?.message);
return;
}
const spinner = ea.getElement(spinnerID)
spinner.isDeleted = true;
const imageID = await ea.addImage(spinner.x, spinner.y, result.json.data[0].url);
const imageEl = ea.getElement(imageID);
const revisedPrompt = result.json.data[0].revised_prompt;
if(revisedPrompt) {
ea.style.fontSize = 16;
const rectID = ea.addText(imageEl.x+15, imageEl.y + imageEl.height + 50, revisedPrompt, {
width: imageEl.width-30,
textAlign: "center",
textVerticalAlign: "top",
box: true,
})
ea.getElement(rectID).strokeColor = "transparent";
ea.getElement(rectID).backgroundColor = "transparent";
ea.addToGroup(ea.getElements().filter(el=>el.id !== spinnerID).map(el=>el.id));
}
await ea.addElementsToView(false, true, true);
ea.getExcalidrawAPI().setToast({
message: IMAGE_WARNING,
duration: 15000,
closable: true
});
}
const run = async (text) => {
if(!text && !imageDataURL) {
new Notice("No prompt, aborting");
return;
}
const systemPrompt = systemPrompts[agentTask];
const outputType = outputTypes[systemPrompt.type];
const isImageGenRequest = outputType.blocktype === "image";
const isImageEditRequest = systemPrompt.type === "image-edit";
if(isImageEditRequest) {
if(!text) {
new Notice("You must provide a text prompt with instructions for how the image should be modified");
return;
}
if(!imageDataURL || !maskDataURL) {
new Notice("You must provide an image and a mask");
return;
}
}
//place spinner next to selected elements
const bb = ea.getBoundingBox(ea.getViewSelectedElements());
const spinnerID = ea.addEmbeddable(bb.topX+bb.width+100,bb.topY-(720-bb.height)/2,550,720,spinner);
//this block is in an async call using the isEACompleted flag because otherwise during debug Obsidian
//goes black (not freezes, but does not get a new frame for some reason)
//palcing this in an async call solves this issue
//If you know why this is happening and can offer a better solution, please reach out to @zsviczian
let isEACompleted = false;
setTimeout(async()=>{
await ea.addElementsToView(false,true);
ea.clear();
const embeddable = ea.getViewElements().filter(el=>el.id===spinnerID);
ea.copyViewElementsToEAforEditing(embeddable);
const els = ea.getViewSelectedElements();
ea.viewZoomToElements(false, els.concat(embeddable));
isEACompleted = true;
});
if(isImageGenRequest && !systemPrompt.prompt && !isImageEditRequest) {
generateImage(text,spinnerID,bb);
return;
}
const requestObject = isImageEditRequest
? {
...imageDataURL ? {image: imageDataURL} : {},
...(text && text.trim() !== "") ? {text} : {},
imageGenerationProperties: {
size: imageSize,
//quality: "standard", //not supported by dall-e-2
n:1,
mask: maskDataURL,
},
}
: {
...imageDataURL ? {image: imageDataURL} : {},
...(text && text.trim() !== "") ? {text} : {},
systemPrompt: systemPrompt.prompt,
instruction: outputType.instruction,
}
//Get result from GPT
const result = await ea.postOpenAI(requestObject);
console.log({result, json:result?.json});
//checking that EA has completed. Because the postOpenAI call is an async await
//I don't expect EA not to be completed by now. However the devil never sleeps.
//This (the insomnia of the Devil) is why I have a watchdog here as well
let counter = 0
while(!isEACompleted && counter++<10) sleep(50);
if(!isEACompleted) {
await errorMessage(spinnerID, "Unexpected issue with ExcalidrawAutomate");
return;
}
if(isImageEditRequest) {
if(!result?.json?.data?.[0]?.url) {
await errorMessage(spinnerID, result?.json?.error?.message);
return;
}
const spinner = ea.getElement(spinnerID)
spinner.isDeleted = true;
const imageID = await ea.addImage(spinner.x, spinner.y, result.json.data[0].url);
await ea.addElementsToView(false, true, true);
ea.getExcalidrawAPI().setToast({
message: IMAGE_WARNING,
duration: 15000,
closable: true
});
return;
}
if(!result?.json?.hasOwnProperty("choices")) {
await errorMessage(spinnerID, result?.json?.error?.message);
return;
}
//exctract codeblock and display result
let content = ea.extractCodeBlocks(result.json.choices[0]?.message?.content)[0]?.data;
if(!content) {
await errorMessage(spinnerID);
return;
}
if(isImageGenRequest) {
generateImage(content,spinnerID,bb);
return;
}
switch(outputType.blocktype) {
case "html":
ea.getElement(spinnerID).link = await ea.convertStringToDataURL(content);
ea.addElementsToView(false,true);
break;
case "svg":
ea.getElement(spinnerID).isDeleted = true;
ea.importSVG(content);
ea.addToGroup(ea.getElements().map(el=>el.id));
if(ea.getViewSelectedElements().length>0) {
ea.targetView.currentPosition = {x: bb.topX+bb.width+100, y: bb.topY};
}
ea.addElementsToView(true, false);
break;
case "mermaid":
if(content.startsWith("mermaid")) {
content = content.replace(/^mermaid/,"").trim();
}
try {
result = await ea.addMermaid(content);
if(typeof result === "string") {
await errorMessage(spinnerID, "Open [More Tools / Mermaid to Excalidraw] to manually fix the received mermaid script<br><br>" + result);
return;
}
} catch (e) {
ea.addText(0,0,content);
}
ea.getElement(spinnerID).isDeleted = true;
ea.targetView.currentPosition = {x: bb.topX+bb.width+100, y: bb.topY-bb.height};
await ea.addElementsToView(true, false);
setMermaidDataToStorage(content);
new Notice("Open More Tools/Mermaid to Excalidraw in the top tools menu to edit the generated diagram",8000);
break;
}
}
// --------------------------------------
// User Interface
// --------------------------------------
let previewDiv;
const fragWithHTML = (html) => createFragment((frag) => (frag.createDiv().innerHTML = html));
const isImageGenerationTask = () => systemPrompts[agentTask].type === "image-gen" || systemPrompts[agentTask].type === "image-edit";
const addPreviewImage = () => {
if(!previewDiv) return;
previewDiv.empty();
previewDiv.createEl("img",{
attr: {
style: `max-width: 100%;max-height: 30vh;`,
src: imageDataURL,
}
});
if(maskDataURL) {
previewDiv.createEl("img",{
attr: {
style: `max-width: 100%;max-height: 30vh;`,
src: maskDataURL,
}
});
}
}
const configModal = new ea.obsidian.Modal(app);
configModal.modalEl.style.width="100%";
configModal.modalEl.style.maxWidth="1000px";
configModal.onOpen = async () => {
const contentEl = configModal.contentEl;
contentEl.createEl("h1", {text: "ExcaliAI"});
let systemPromptTextArea, systemPromptDiv, imageSizeSetting, imageSizeSettingDropdown, helpEl;
new ea.obsidian.Setting(contentEl)
.setName("What would you like to do?")
.addDropdown(dropdown=>{
Object.keys(systemPrompts).forEach(key=>dropdown.addOption(key,key));
dropdown
.setValue(agentTask)
.onChange(async (value) => {
dirty = true;
const prevTask = agentTask;
agentTask = value;
if(
(systemPrompts[prevTask].type === "image-edit" && systemPrompts[value].type !== "image-edit") ||
(systemPrompts[prevTask].type !== "image-edit" && systemPrompts[value].type === "image-edit")
) {
({imageDataURL, maskDataURL} = await generateCanvasDataURL(ea.targetView, systemPrompts[value].type === "image-edit"));
addPreviewImage();
setImageModelAndSizes();
while (imageSizeSettingDropdown.selectEl.options.length > 0) { imageSizeSettingDropdown.selectEl.remove(0); }
validSizes.forEach(size=>imageSizeSettingDropdown.addOption(size,size));
imageSizeSettingDropdown.setValue(imageSize);
}
imageSizeSetting.settingEl.style.display = isImageGenerationTask() ? "" : "none";
const prompt = systemPrompts[value].prompt;
helpEl.innerHTML = `<b>Help: </b>` + systemPrompts[value].help;
if(prompt) {
systemPromptDiv.style.display = "";
systemPromptTextArea.setValue(systemPrompts[value].prompt);
} else {
systemPromptDiv.style.display = "none";
}
});
})
helpEl = contentEl.createEl("p");
helpEl.innerHTML = `<b>Help: </b>` + systemPrompts[agentTask].help;
systemPromptDiv = contentEl.createDiv();
systemPromptDiv.createEl("h4", {text: "Customize System Prompt"});
systemPromptDiv.createEl("span", {text: "Unless you know what you are doing I do not recommend changing the system prompt"})
const systemPromptSetting = new ea.obsidian.Setting(systemPromptDiv)
.addTextArea(text => {
systemPromptTextArea = text;
const prompt = systemPrompts[agentTask].prompt;
text.inputEl.style.minHeight = "10em";
text.inputEl.style.width = "100%";
text.setValue(prompt);
text.onChange(value => {
systemPrompts[value].prompt = value;
});
if(!prompt) systemPromptDiv.style.display = "none";
})
systemPromptSetting.nameEl.style.display = "none";
systemPromptSetting.descEl.style.display = "none";
systemPromptSetting.infoEl.style.display = "none";
contentEl.createEl("h4", {text: "User Prompt"});
const userPromptSetting = new ea.obsidian.Setting(contentEl)
.addTextArea(text => {
text.inputEl.style.minHeight = "10em";
text.inputEl.style.width = "100%";
text.setValue(userPrompt);
text.onChange(value => {
userPrompt = value;
dirty = true;
})
})
userPromptSetting.nameEl.style.display = "none";
userPromptSetting.descEl.style.display = "none";
userPromptSetting.infoEl.style.display = "none";
imageSizeSetting = new ea.obsidian.Setting(contentEl)
.setName("Select image size")
.setDesc(fragWithHTML("<mark>⚠️ Important ⚠️</mark>: " + IMAGE_WARNING))
.addDropdown(dropdown=>{
validSizes.forEach(size=>dropdown.addOption(size,size));
imageSizeSettingDropdown = dropdown;
dropdown
.setValue(imageSize)
.onChange(async (value) => {
dirty = true;
imageSize = value;
if(systemPrompts[agentTask].type === "image-edit") {
({imageDataURL, maskDataURL} = await generateCanvasDataURL(ea.targetView, true));
addPreviewImage();
}
});
})
imageSizeSetting.settingEl.style.display = isImageGenerationTask() ? "" : "none";
if(imageDataURL) {
previewDiv = contentEl.createDiv({
attr: {
style: "text-align: center;",
}
});
addPreviewImage();
} else {
contentEl.createEl("h4", {text: "No elements are selected from your canvas"});
contentEl.createEl("span", {text: "Because there are no Excalidraw elements selected on the canvas, only the text prompt will be sent to OpenAI."});
}
new ea.obsidian.Setting(contentEl)
.addButton(button =>
button
.setButtonText("Run")
.onClick((event)=>{
run(userPrompt); //Obsidian crashes otherwise, likely has to do with requesting an new frame for react
configModal.close();
})
);
}
configModal.onClose = () => {
if(dirty) {
settings["User Prompt"] = userPrompt;
settings["Agent's Task"] = agentTask;
settings["Image Size"] = imageSize;
ea.setScriptSettings(settings);
}
}
configModal.open();

1
ea-scripts/ExcaliAI.svg Normal file
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

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

@@ -0,0 +1,673 @@
/*
<iframe width="560" height="315" src="https://www.youtube.com/embed/A1vrSGBbWgo" 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*/
let dirty=false;
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.0.12")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
const outputTypes = {
"html": {
instruction: "Turn this into a single html file using tailwind. Return a single message containing only the html file in a codeblock.",
blocktype: "html"
},
"mermaid": {
instruction: "Return a single message containing only the mermaid diagram in a codeblock.",
blocktype: "mermaid"
},
"svg": {
instruction: "Return a single message containing only the SVG code in an html codeblock.",
blocktype: "svg"
},
"image-gen": {
instruction: "Return a single message with the generated image prompt in a codeblock",
blocktype: "image"
},
"image-edit": {
instruction: "",
blocktype: "image"
}
}
const systemPrompts = {
"Challenge my thinking": {
prompt: `Your task is to interpret a screenshot of a whiteboard, translating its ideas into a Mermaid graph. The whiteboard will encompass thoughts on a subject. Within the mind map, distinguish ideas that challenge, dispute, or contradict the whiteboard content. Additionally, include concepts that expand, complement, or advance the user's thinking. Utilize the Mermaid graph diagram type and present the resulting Mermaid diagram within a code block. Ensure the Mermaid script excludes the use of parentheses ().`,
type: "mermaid",
help: "Translate your image and optional text prompt into a Mermaid mindmap. If there are conversion errors, edit the Mermaid script under 'More Tools'."
},
"Convert sketch to shapes": {
prompt: `Given an image featuring various geometric shapes drawn by the user, your objective is to analyze the input and generate SVG code that accurately represents these shapes. Your output will be the SVG code enclosed in an HTML code block.`,
type: "svg",
help: "Convert selected scribbles into shapes; works better with fewer shapes. Experimental and may not produce good drawings."
},
"Create a simple Excalidraw icon": {
prompt: `Given a description of an SVG image from the user, your objective is to generate the corresponding SVG code. Avoid incorporating textual elements within the generated SVG. Your output should be the resulting SVG code enclosed in an HTML code block.`,
type: "svg",
help: "Convert text prompts into simple icons inserted as Excalidraw elements. Expect only a text prompt. Experimental and may not produce good drawings."
},
"Edit an image": {
prompt: null,
type: "image-edit",
help: "Image elements will be used as the Image. Shapes on top of the image will be the Mask. Use the prompt to instruct Dall-e about the changes. Dall-e-2 model will be used."
},
"Generate an image from image and prompt": {
prompt: "Your task involves receiving an image and a textual prompt from the user. Your goal is to craft a detailed, accurate, and descriptive narrative of the image, tailored for effective image generation. Utilize the user-provided text prompt to inform and guide your depiction of the image. Ensure the resulting image remains text-free.",
type: "image-gen",
help: "Generate an image based on the drawing and prompt using ChatGPT-Vision and Dall-e. Provide a contextual text-prompt for accurate interpretation."
},
"Generate an image from prompt": {
prompt: null,
type: "image-gen",
help: "Send only the text prompt to OpenAI. Provide a detailed description; OpenAI will enrich your prompt automatically. To avoid it, start your prompt like this 'DO NOT add any detail, just use it AS-IS:'"
},
"Generate an image to illustrate a quote": {
prompt: "Your task involves transforming a user-provided quote into a detailed and imaginative illustration. Craft a visual representation that captures the essence of the quote and resonates well with a broad audience. If the Author's name is provided, aim to establish a connection between the illustration and the Author. This can be achieved by referencing a well-known story from the Author, situating the image in the Author's era or setting, or employing other creative methods of association. Additionally, provide preferences for styling, such as the chosen medium and artistic direction, to guide the image creation process. Ensure the resulting image remains text-free. Your task output should comprise a descriptive and detailed narrative aimed at facilitating the creation of a captivating illustration from the quote.",
type: "image-gen",
help: "ExcaliAI will create an image prompt to illustrate your text input - a quote - with GPT, then generate an image using Dall-e. In case you include the Author's name, GPT will try to generate an image that in some way references the Author."
},
"Visual brainstorm": {
prompt: "Your objective is to interpret a screenshot of a whiteboard, creating an image aimed at sparking further thoughts on the subject. The whiteboard will present diverse ideas about a specific topic. Your generated image should achieve one of two purposes: highlighting concepts that challenge, dispute, or contradict the whiteboard content, or introducing ideas that expand, complement, or enrich the user's thinking. You have the option to include multiple tiles in the resulting image, resembling a sequence akin to a comic strip. Ensure that the image remains devoid of text.",
type: "image-gen",
help: "Use ChatGPT Visions and Dall-e to create an image based on your text prompt and image to spark new ideas."
},
"Wireframe to code": {
prompt: `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.`,
type: "html",
help: "Use GPT Visions to interpret the wireframe and generate a web application. YOu may copy the resulting code from the active embeddable's top left menu."
},
}
const IMAGE_WARNING = "The generated image is linked through a temporary OpenAI URL and will be removed in approximately 30 minutes. To save it permanently, choose 'Save image from URL to local file' from the Obsidian Command Palette."
// --------------------------------------
// Initialize values and settings
// --------------------------------------
let settings = ea.getScriptSettings();
if(!settings["Agent's Task"]) {
settings = {
"Agent's Task": "Wireframe to code",
"User Prompt": "",
};
await ea.setScriptSettings(settings);
}
const OPENAI_API_KEY = ea.plugin.settings.openAIAPIToken;
if(!OPENAI_API_KEY || OPENAI_API_KEY === "") {
new Notice("You must first configure your API key in Excalidraw Plugin Settings");
return;
}
let userPrompt = settings["User Prompt"] ?? "";
let agentTask = settings["Agent's Task"];
let imageSize = settings["Image Size"]??"1024x1024";
if(!systemPrompts.hasOwnProperty(agentTask)) {
agentTask = Object.keys(systemPrompts)[0];
}
let imageModel, valideSizes;
const setImageModelAndSizes = () => {
imageModel = systemPrompts[agentTask].type === "image-edit"
? "dall-e-2"
: ea.plugin.settings.openAIDefaultImageGenerationModel;
validSizes = imageModel === "dall-e-2"
? [`256x256`, `512x512`, `1024x1024`]
: (imageModel === "dall-e-3"
? [`1024x1024`, `1792x1024`, `1024x1792`]
: [`1024x1024`])
if(!validSizes.includes(imageSize)) {
imageSize = "1024x1024";
dirty = true;
}
}
setImageModelAndSizes();
// --------------------------------------
// Generate Image Blob From Selected Excalidraw Elements
// --------------------------------------
const calculateImageScale = (elements) => {
const bb = ea.getBoundingBox(elements);
const size = (bb.width*bb.height);
const minRatio = Math.sqrt(360000/size);
const maxRatio = Math.sqrt(size/16000000);
return minRatio > 1
? minRatio
: (
maxRatio > 1
? 1/maxRatio
: 1
);
}
const createMask = async (dataURL) => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
// If opaque (alpha > 0), make it transparent
if (data[i + 3] > 0) {
data[i + 3] = 0; // Set alpha to 0 (transparent)
} else if (data[i + 3] === 0) {
// If fully transparent, make it red
data[i] = 255; // Red
data[i + 1] = 0; // Green
data[i + 2] = 0; // Blue
data[i + 3] = 255; // make it opaque
}
}
ctx.putImageData(imageData, 0, 0);
const maskDataURL = canvas.toDataURL();
resolve(maskDataURL);
};
img.onerror = error => {
reject(error);
};
img.src = dataURL;
});
}
//https://platform.openai.com/docs/api-reference/images/createEdit
//dall-e-2 image edit only works on square images
//if targetDalleImageEdit === true then the image and the mask will be returned in two separate dataURLs
let squareBB;
const generateCanvasDataURL = async (view, targetDalleImageEdit=false) => {
let PADDING = 5;
await view.forceSave(true); //to ensure recently embedded PNG and other images are saved to file
const viewElements = ea.getViewSelectedElements();
if(viewElements.length === 0) {
return {imageDataURL: null, maskDataURL: null} ;
}
ea.copyViewElementsToEAforEditing(viewElements, true); //copying the images objects over to EA for PNG generation
let maskDataURL;
const loader = ea.getEmbeddedFilesLoader(false);
let scale = calculateImageScale(ea.getElements());
const bb = ea.getBoundingBox(viewElements);
if(ea.getElements()
.filter(el=>el.type==="image")
.some(el=>Math.round(el.width) === Math.round(bb.width) && Math.round(el.height) === Math.round(bb.height))
) { PADDING = 0; }
let exportSettings = {withBackground: true, withTheme: true};
if(targetDalleImageEdit) {
PADDING = 0;
const strokeColor = ea.style.strokeColor;
const backgroundColor = ea.style.backgroundColor;
ea.style.backgroundColor = "transparent";
ea.style.strokeColor = "transparent";
let rectID;
if(bb.height > bb.width) {
rectID = ea.addRect(bb.topX-(bb.height-bb.width)/2, bb.topY,bb.height, bb.height);
}
if(bb.width > bb.height) {
rectID = ea.addRect(bb.topX, bb.topY-(bb.width-bb.height)/2,bb.width, bb.width);
}
if(bb.height === bb.width) {
rectID = ea.addRect(bb.topX, bb.topY, bb.width, bb.height);
}
const rect = ea.getElement(rectID);
squareBB = {topX: rect.x-PADDING, topY: rect.y-PADDING, width: rect.width + 2*PADDING, height: rect.height + 2*PADDING};
ea.style.strokeColor = strokeColor;
ea.style.backgroundColor = backgroundColor;
ea.getElements().filter(el=>el.type === "image").forEach(el=>{el.isDeleted = true});
dalleWidth = parseInt(imageSize.split("x")[0]);
scale = dalleWidth/squareBB.width;
exportSettings = {withBackground: false, withTheme: true};
maskDataURL= await ea.createPNGBase64(
null, scale, exportSettings, loader, "light", PADDING
);
maskDataURL = await createMask(maskDataURL)
ea.getElements().filter(el=>el.type === "image").forEach(el=>{el.isDeleted = false});
ea.getElements().filter(el=>el.type !== "image" && el.id !== rectID).forEach(el=>{el.isDeleted = true});
}
const imageDataURL = await ea.createPNGBase64(
null, scale, exportSettings, loader, "light", PADDING
);
ea.clear();
return {imageDataURL, maskDataURL};
}
let {imageDataURL, maskDataURL} = await generateCanvasDataURL(ea.targetView, systemPrompts[agentTask].type === "image-edit");
// --------------------------------------
// Support functions - embeddable spinner and error
// --------------------------------------
const spinner = await ea.convertStringToDataURL(`
<html><head><style>
html, body {width: 100%; height: 100%; color: ${ea.getExcalidrawAPI().getAppState().theme === "dark" ? "white" : "black"};}
body {display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 1rem; overflow: hidden;}
.Spinner {display: flex; align-items: center; justify-content: center; margin-left: auto; margin-right: auto;}
.Spinner svg {animation: rotate 1.6s linear infinite; transform-origin: center center; width: 40px; height: 40px;}
.Spinner circle {stroke: currentColor; animation: dash 1.6s linear 0s infinite; stroke-linecap: round;}
@keyframes rotate {100% {transform: rotate(360deg);}}
@keyframes dash {
0% {stroke-dasharray: 1, 300; stroke-dashoffset: 0;}
50% {stroke-dasharray: 150, 300; stroke-dashoffset: -200;}
100% {stroke-dasharray: 1, 300; stroke-dashoffset: -280;}
}
</style></head><body>
<div class="Spinner">
<svg viewBox="0 0 100 100">
<circle cx="50" cy="50" r="46" stroke-width="8" fill="none" stroke-miter-limit="10"/>
</svg>
</div>
<div>Generating...</div>
</body></html>`);
const errorMessage = async (spinnerID, message) => {
const error = "Something went wrong! Check developer console for more.";
const details = message ? `<p>${message}</p>` : "";
const errorDataURL = await ea.convertStringToDataURL(`
<html><head><style>
html, body {height: 100%;}
body {display: flex; flex-direction: column; align-items: center; justify-content: center; color: red;}
h1, h3 {margin-top: 0;margin-bottom: 0.5rem;}
</style></head><body>
<h1>Error!</h1>
<h3>${error}</h3>${details}
</body></html>`);
new Notice (error);
ea.getElement(spinnerID).link = errorDataURL;
ea.addElementsToView(false,true);
}
// --------------------------------------
// Utility to write Mermaid to dialog
// --------------------------------------
const EDITOR_LS_KEYS = {
OAI_API_KEY: "excalidraw-oai-api-key",
MERMAID_TO_EXCALIDRAW: "mermaid-to-excalidraw",
PUBLISH_LIBRARY: "publish-library-data",
};
const setMermaidDataToStorage = (mermaidDefinition) => {
try {
window.localStorage.setItem(
EDITOR_LS_KEYS.MERMAID_TO_EXCALIDRAW,
JSON.stringify(mermaidDefinition)
);
return true;
} catch (error) {
console.warn(`localStorage.setItem error: ${error.message}`);
return false;
}
};
// --------------------------------------
// Submit Prompt
// --------------------------------------
const generateImage = async(text, spinnerID, bb) => {
const requestObject = {
text,
imageGenerationProperties: {
size: imageSize,
//quality: "standard", //not supported by dall-e-2
n:1,
},
};
const result = await ea.postOpenAI(requestObject);
console.log({result, json:result?.json});
if(!result?.json?.data?.[0]?.url) {
await errorMessage(spinnerID, result?.json?.error?.message);
return;
}
const spinner = ea.getElement(spinnerID)
spinner.isDeleted = true;
const imageID = await ea.addImage(spinner.x, spinner.y, result.json.data[0].url);
const imageEl = ea.getElement(imageID);
const revisedPrompt = result.json.data[0].revised_prompt;
if(revisedPrompt) {
ea.style.fontSize = 16;
const rectID = ea.addText(imageEl.x+15, imageEl.y + imageEl.height + 50, revisedPrompt, {
width: imageEl.width-30,
textAlign: "center",
textVerticalAlign: "top",
box: true,
})
ea.getElement(rectID).strokeColor = "transparent";
ea.getElement(rectID).backgroundColor = "transparent";
ea.addToGroup(ea.getElements().filter(el=>el.id !== spinnerID).map(el=>el.id));
}
await ea.addElementsToView(false, true, true);
ea.getExcalidrawAPI().setToast({
message: IMAGE_WARNING,
duration: 15000,
closable: true
});
}
const run = async (text) => {
if(!text && !imageDataURL) {
new Notice("No prompt, aborting");
return;
}
const systemPrompt = systemPrompts[agentTask];
const outputType = outputTypes[systemPrompt.type];
const isImageGenRequest = outputType.blocktype === "image";
const isImageEditRequest = systemPrompt.type === "image-edit";
if(isImageEditRequest) {
if(!text) {
new Notice("You must provide a text prompt with instructions for how the image should be modified");
return;
}
if(!imageDataURL || !maskDataURL) {
new Notice("You must provide an image and a mask");
return;
}
}
//place spinner next to selected elements
const bb = ea.getBoundingBox(ea.getViewSelectedElements());
const spinnerID = ea.addEmbeddable(bb.topX+bb.width+100,bb.topY-(720-bb.height)/2,550,720,spinner);
//this block is in an async call using the isEACompleted flag because otherwise during debug Obsidian
//goes black (not freezes, but does not get a new frame for some reason)
//palcing this in an async call solves this issue
//If you know why this is happening and can offer a better solution, please reach out to @zsviczian
let isEACompleted = false;
setTimeout(async()=>{
await ea.addElementsToView(false,true);
ea.clear();
const embeddable = ea.getViewElements().filter(el=>el.id===spinnerID);
ea.copyViewElementsToEAforEditing(embeddable);
const els = ea.getViewSelectedElements();
ea.viewZoomToElements(false, els.concat(embeddable));
isEACompleted = true;
});
if(isImageGenRequest && !systemPrompt.prompt && !isImageEditRequest) {
generateImage(text,spinnerID,bb);
return;
}
const requestObject = isImageEditRequest
? {
...imageDataURL ? {image: imageDataURL} : {},
...(text && text.trim() !== "") ? {text} : {},
imageGenerationProperties: {
size: imageSize,
//quality: "standard", //not supported by dall-e-2
n:1,
mask: maskDataURL,
},
}
: {
...imageDataURL ? {image: imageDataURL} : {},
...(text && text.trim() !== "") ? {text} : {},
systemPrompt: systemPrompt.prompt,
instruction: outputType.instruction,
}
//Get result from GPT
const result = await ea.postOpenAI(requestObject);
console.log({result, json:result?.json});
//checking that EA has completed. Because the postOpenAI call is an async await
//I don't expect EA not to be completed by now. However the devil never sleeps.
//This (the insomnia of the Devil) is why I have a watchdog here as well
let counter = 0
while(!isEACompleted && counter++<10) sleep(50);
if(!isEACompleted) {
await errorMessage(spinnerID, "Unexpected issue with ExcalidrawAutomate");
return;
}
if(isImageEditRequest) {
if(!result?.json?.data?.[0]?.url) {
await errorMessage(spinnerID, result?.json?.error?.message);
return;
}
const spinner = ea.getElement(spinnerID)
spinner.isDeleted = true;
const imageID = await ea.addImage(spinner.x, spinner.y, result.json.data[0].url);
await ea.addElementsToView(false, true, true);
ea.getExcalidrawAPI().setToast({
message: IMAGE_WARNING,
duration: 15000,
closable: true
});
return;
}
if(!result?.json?.hasOwnProperty("choices")) {
await errorMessage(spinnerID, result?.json?.error?.message);
return;
}
//exctract codeblock and display result
let content = ea.extractCodeBlocks(result.json.choices[0]?.message?.content)[0]?.data;
if(!content) {
await errorMessage(spinnerID);
return;
}
if(isImageGenRequest) {
generateImage(content,spinnerID,bb);
return;
}
switch(outputType.blocktype) {
case "html":
ea.getElement(spinnerID).link = await ea.convertStringToDataURL(content);
ea.addElementsToView(false,true);
break;
case "svg":
ea.getElement(spinnerID).isDeleted = true;
ea.importSVG(content);
ea.addToGroup(ea.getElements().map(el=>el.id));
if(ea.getViewSelectedElements().length>0) {
ea.targetView.currentPosition = {x: bb.topX+bb.width+100, y: bb.topY};
}
ea.addElementsToView(true, false);
break;
case "mermaid":
if(content.startsWith("mermaid")) {
content = content.replace(/^mermaid/,"").trim();
}
try {
result = await ea.addMermaid(content);
if(typeof result === "string") {
await errorMessage(spinnerID, "Open [More Tools / Mermaid to Excalidraw] to manually fix the received mermaid script<br><br>" + result);
return;
}
} catch (e) {
ea.addText(0,0,content);
}
ea.getElement(spinnerID).isDeleted = true;
ea.targetView.currentPosition = {x: bb.topX+bb.width+100, y: bb.topY-bb.height};
await ea.addElementsToView(true, false);
setMermaidDataToStorage(content);
new Notice("Open More Tools/Mermaid to Excalidraw in the top tools menu to edit the generated diagram",8000);
break;
}
}
// --------------------------------------
// User Interface
// --------------------------------------
let previewDiv;
const fragWithHTML = (html) => createFragment((frag) => (frag.createDiv().innerHTML = html));
const isImageGenerationTask = () => systemPrompts[agentTask].type === "image-gen" || systemPrompts[agentTask].type === "image-edit";
const addPreviewImage = () => {
if(!previewDiv) return;
previewDiv.empty();
previewDiv.createEl("img",{
attr: {
style: `max-width: 100%;max-height: 30vh;`,
src: imageDataURL,
}
});
if(maskDataURL) {
previewDiv.createEl("img",{
attr: {
style: `max-width: 100%;max-height: 30vh;`,
src: maskDataURL,
}
});
}
}
const configModal = new ea.obsidian.Modal(app);
configModal.modalEl.style.width="100%";
configModal.modalEl.style.maxWidth="1000px";
configModal.onOpen = async () => {
const contentEl = configModal.contentEl;
contentEl.createEl("h1", {text: "ExcaliAI"});
let systemPromptTextArea, systemPromptDiv, imageSizeSetting, imageSizeSettingDropdown, helpEl;
new ea.obsidian.Setting(contentEl)
.setName("What would you like to do?")
.addDropdown(dropdown=>{
Object.keys(systemPrompts).forEach(key=>dropdown.addOption(key,key));
dropdown
.setValue(agentTask)
.onChange(async (value) => {
dirty = true;
const prevTask = agentTask;
agentTask = value;
if(
(systemPrompts[prevTask].type === "image-edit" && systemPrompts[value].type !== "image-edit") ||
(systemPrompts[prevTask].type !== "image-edit" && systemPrompts[value].type === "image-edit")
) {
({imageDataURL, maskDataURL} = await generateCanvasDataURL(ea.targetView, systemPrompts[value].type === "image-edit"));
addPreviewImage();
setImageModelAndSizes();
while (imageSizeSettingDropdown.selectEl.options.length > 0) { imageSizeSettingDropdown.selectEl.remove(0); }
validSizes.forEach(size=>imageSizeSettingDropdown.addOption(size,size));
imageSizeSettingDropdown.setValue(imageSize);
}
imageSizeSetting.settingEl.style.display = isImageGenerationTask() ? "" : "none";
const prompt = systemPrompts[value].prompt;
helpEl.innerHTML = `<b>Help: </b>` + systemPrompts[value].help;
if(prompt) {
systemPromptDiv.style.display = "";
systemPromptTextArea.setValue(systemPrompts[value].prompt);
} else {
systemPromptDiv.style.display = "none";
}
});
})
helpEl = contentEl.createEl("p");
helpEl.innerHTML = `<b>Help: </b>` + systemPrompts[agentTask].help;
systemPromptDiv = contentEl.createDiv();
systemPromptDiv.createEl("h4", {text: "Customize System Prompt"});
systemPromptDiv.createEl("span", {text: "Unless you know what you are doing I do not recommend changing the system prompt"})
const systemPromptSetting = new ea.obsidian.Setting(systemPromptDiv)
.addTextArea(text => {
systemPromptTextArea = text;
const prompt = systemPrompts[agentTask].prompt;
text.inputEl.style.minHeight = "10em";
text.inputEl.style.width = "100%";
text.setValue(prompt);
text.onChange(value => {
systemPrompts[value].prompt = value;
});
if(!prompt) systemPromptDiv.style.display = "none";
})
systemPromptSetting.nameEl.style.display = "none";
systemPromptSetting.descEl.style.display = "none";
systemPromptSetting.infoEl.style.display = "none";
contentEl.createEl("h4", {text: "User Prompt"});
const userPromptSetting = new ea.obsidian.Setting(contentEl)
.addTextArea(text => {
text.inputEl.style.minHeight = "10em";
text.inputEl.style.width = "100%";
text.setValue(userPrompt);
text.onChange(value => {
userPrompt = value;
dirty = true;
})
})
userPromptSetting.nameEl.style.display = "none";
userPromptSetting.descEl.style.display = "none";
userPromptSetting.infoEl.style.display = "none";
imageSizeSetting = new ea.obsidian.Setting(contentEl)
.setName("Select image size")
.setDesc(fragWithHTML("<mark>⚠️ Important ⚠️</mark>: " + IMAGE_WARNING))
.addDropdown(dropdown=>{
validSizes.forEach(size=>dropdown.addOption(size,size));
imageSizeSettingDropdown = dropdown;
dropdown
.setValue(imageSize)
.onChange(async (value) => {
dirty = true;
imageSize = value;
if(systemPrompts[agentTask].type === "image-edit") {
({imageDataURL, maskDataURL} = await generateCanvasDataURL(ea.targetView, true));
addPreviewImage();
}
});
})
imageSizeSetting.settingEl.style.display = isImageGenerationTask() ? "" : "none";
if(imageDataURL) {
previewDiv = contentEl.createDiv({
attr: {
style: "text-align: center;",
}
});
addPreviewImage();
} else {
contentEl.createEl("h4", {text: "No elements are selected from your canvas"});
contentEl.createEl("span", {text: "Because there are no Excalidraw elements selected on the canvas, only the text prompt will be sent to OpenAI."});
}
new ea.obsidian.Setting(contentEl)
.addButton(button =>
button
.setButtonText("Run")
.onClick((event)=>{
run(userPrompt); //Obsidian crashes otherwise, likely has to do with requesting an new frame for react
configModal.close();
})
);
}
configModal.onClose = () => {
if(dirty) {
settings["User Prompt"] = userPrompt;
settings["Agent's Task"] = agentTask;
settings["Image Size"] = imageSize;
ea.setScriptSettings(settings);
}
}
configModal.open();

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

673
ea-scripts/Golden Ratio.md Normal file
View File

@@ -0,0 +1,673 @@
/*
The script performs two different functions depending on the elements selected in the view.
1) In case you select text elements, the script will cycle through a set of font scales. First the 2 larger fonts following the Fibonacci sequence (fontsize * φ; fonsize * φ^2), then the 2 smaller fonts (fontsize / φ; fontsize / φ^2), finally the original size, followed again by the 2 larger fonts. If you wait 2 seconds, the sequence clears and starts from which ever font size you are on. So if you want the 3rd larges font, then toggle twice, wait 2 sec, then toggle again.
2) In case you select a single rectangle, the script will open the "Golden Grid", "Golden Spiral" window, where you can set up the type of grid or spiral you want to insert into the document.
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/golden-ratio.jpg)
<iframe width="560" height="315" src="https://www.youtube.com/embed/2SHn_ruax-s" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
Gravitational point of spiral: $$\left[x,y\right]=\left[ x + \frac{{\text{width} \cdot \phi^2}}{{\phi^2 + 1}}\;, \; y + \frac{{\text{height} \cdot \phi^2}}{{\phi^2 + 1}} \right]$$
Dimensions of inner rectangles in case of Double Spiral: $$[width, height] = \left[\frac{width\cdot(\phi^2+1)}{2\phi^2}\;, \;\frac{height\cdot(\phi^2+1)}{2\phi^2}\right]$$
```js*/
const phi = (1 + Math.sqrt(5)) / 2; // Golden Ratio (φ)
const inversePhi = (1-1/phi);
const pointsPerCurve = 20; // Number of points per curve segment
const ownerWindow = ea.targetView.ownerWindow;
const hostLeaf = ea.targetView.leaf;
let dirty = false;
const ids = [];
const textEls = ea.getViewSelectedElements().filter(el=>el.type === "text");
let rect = ea.getViewSelectedElements().length === 1 ? ea.getViewSelectedElement() : null;
if(!rect || rect.type !== "rectangle") {
//Fontsize cycle
if(textEls.length>0) {
if(window.excalidrawGoldenRatio) {
clearTimeout(window.excalidrawGoldenRatio?.timer);
} else {
window.excalidrawGoldenRatio = {timer: null, cycle:-1};
}
window.excalidrawGoldenRatio.timer = setTimeout(()=>{delete window.excalidrawGoldenRatio;},2000);
window.excalidrawGoldenRatio.cycle = (window.excalidrawGoldenRatio.cycle+1)%5;
ea.copyViewElementsToEAforEditing(textEls);
ea.getElements().forEach(el=> {
el.fontSize = window.excalidrawGoldenRatio.cycle === 2
? el.fontSize / Math.pow(phi,4)
: el.fontSize * phi;
const font = ExcalidrawLib.getFontString(el);
const lineHeight = ExcalidrawLib.getDefaultLineHeight(el.fontFamily);
const {width, height, baseline} = ExcalidrawLib.measureText(el.originalText, font, lineHeight);
el.width = width;
el.height = height;
el.baseline = baseline;
});
ea.addElementsToView();
return;
}
new Notice("Select text elements, or a select a single rectangle");
return;
}
ea.copyViewElementsToEAforEditing([rect]);
rect = ea.getElement(rect.id);
ea.style.strokeColor = rect.strokeColor;
ea.style.strokeWidth = rect.strokeWidth;
ea.style.roughness = rect.roughness;
ea.style.angle = rect.angle;
let {x,y,width,height} = rect;
// --------------------------------------------
// Load Settings
// --------------------------------------------
let settings = ea.getScriptSettings();
if(!settings["Horizontal Grid"]) {
settings = {
"Horizontal Grid" : {
value: "left-right",
valueset: ["none","letf-right","right-left","center-out","center-in"]
},
"Vertical Grid": {
value: "none",
valueset: ["none","top-down","bottom-up","center-out","center-in"]
},
"Size": {
value: "6",
valueset: ["2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20"]
},
"Aspect Choice": {
value: "none",
valueset: ["none","adjust-width","adjust-height"]
},
"Type": "grid",
"Spiral Orientation": {
value: "top-left",
valueset: ["double","top-left","top-right","bottom-right","bottom-left"]
},
"Lock Elements": false,
"Send to Back": false,
"Update Style": false,
"Bold Spiral": false,
};
await ea.setScriptSettings(settings);
}
let hDirection = settings["Horizontal Grid"].value;
let vDirection = settings["Vertical Grid"].value;
let aspectChoice = settings["Aspect Choice"].value;
let type = settings["Type"];
let spiralOrientation = settings["Spiral Orientation"].value;
let lockElements = settings["Lock Elements"];
let sendToBack = settings["Send to Back"];
let size = parseInt(settings["Size"].value);
let updateStyle = settings["Update Style"];
let boldSpiral = settings["Bold Spiral"];
// --------------------------------------------
// Rotation
// --------------------------------------------
let centerX, centerY;
const rotatePointAndAddToElementList = (elementID) => {
ids.push(elementID);
const line = ea.getElement(elementID);
// Calculate the initial position of the line's center
const lineCenterX = line.x + line.width / 2;
const lineCenterY = line.y + line.height / 2;
// Calculate the difference between the line's center and the rectangle's center
const diffX = lineCenterX - (rect.x + rect.width / 2);
const diffY = lineCenterY - (rect.y + rect.height / 2);
// Apply the rotation to the difference
const cosTheta = Math.cos(rect.angle);
const sinTheta = Math.sin(rect.angle);
const rotatedX = diffX * cosTheta - diffY * sinTheta;
const rotatedY = diffX * sinTheta + diffY * cosTheta;
// Calculate the new position of the line's center with respect to the rectangle's center
const newLineCenterX = rotatedX + (rect.x + rect.width / 2);
const newLineCenterY = rotatedY + (rect.y + rect.height / 2);
// Update the line's coordinates by adjusting for the change in the center
line.x += newLineCenterX - lineCenterX;
line.y += newLineCenterY - lineCenterY;
}
const rotatePointsWithinRectangle = (points) => {
const centerX = rect.x + rect.width / 2;
const centerY = rect.y + rect.height / 2;
const cosTheta = Math.cos(rect.angle);
const sinTheta = Math.sin(rect.angle);
const rotatedPoints = points.map(([x, y]) => {
// Translate the point relative to the rectangle's center
const translatedX = x - centerX;
const translatedY = y - centerY;
// Apply the rotation to the translated coordinates
const rotatedX = translatedX * cosTheta - translatedY * sinTheta;
const rotatedY = translatedX * sinTheta + translatedY * cosTheta;
// Translate back to the original coordinate system
const finalX = rotatedX + centerX;
const finalY = rotatedY + centerY;
return [finalX, finalY];
});
return rotatedPoints;
}
// --------------------------------------------
// Grid
// --------------------------------------------
const calculateGoldenSum = (baseOfGoldenGrid, pow) => {
const ratio = 1 / phi;
const geometricSum = baseOfGoldenGrid * ((1 - Math.pow(ratio, pow)) / (1 - ratio));
return geometricSum;
};
const findBaseForGoldenGrid = (targetValue, n, scenario) => {
const ratio = 1 / phi;
if (scenario === "center-out") {
return targetValue * (2-2*ratio) / (1 + ratio + 2*Math.pow(ratio,n));
} else if (scenario === "center-in") {
return targetValue*2*(1-ratio)*Math.pow(phi,n-1) /(2*Math.pow(phi,n-1)*(1-Math.pow(ratio,n))-1+ratio);
} else {
return targetValue * (1-ratio)/(1-Math.pow(ratio,n));
}
}
const calculateOffsetVertical = (scenario, base) => {
if (scenario === "center-out") return base / 2;
if (scenario === "center-in") return base / Math.pow(phi, size + 1) / 2;
return 0;
};
const horizontal = (direction, scenario) => {
const base = findBaseForGoldenGrid(width, size + 1, scenario);
const totalGridWidth = calculateGoldenSum(base, size + 1);
for (i = 1; i <= size; i++) {
const offset =
scenario === "center-out"
? totalGridWidth - calculateGoldenSum(base, i)
: calculateGoldenSum(base, size + 1 - i);
const x2 =
direction === "left"
? x + offset
: x + width - offset;
rotatePointAndAddToElementList(
ea.addLine([
[x2, y],
[x2, y + height],
])
);
}
};
const vertical = (direction, scenario) => {
const base = findBaseForGoldenGrid(height, size + 1, scenario);
const totalGridWidth = calculateGoldenSum(base, size + 1);
for (i = 1; i <= size; i++) {
const offset =
scenario === "center-out"
? totalGridWidth - calculateGoldenSum(base, i)
: calculateGoldenSum(base, size + 1 - i);
const y2 =
direction === "top"
? y + offset
: y + height - offset;
rotatePointAndAddToElementList(
ea.addLine([
[x, y2],
[x+width, y2],
])
);
}
};
const centerHorizontal = (scenario) => {
width = width / 2;
horizontal("left", scenario);
x += width;
horizontal("right", scenario);
x -= width;
width = 2*width;
};
const centerVertical = (scenario) => {
height = height / 2;
vertical("top", scenario);
y += height;
vertical("bottom", scenario);
y -= height;
height = 2*height;
};
const drawGrid = () => {
switch(hDirection) {
case "none": break;
case "left-right": horizontal("left"); break;
case "right-left": horizontal("right"); break;
case "center-out": centerHorizontal("center-out"); break;
case "center-in": centerHorizontal("center-in"); break;
}
switch(vDirection) {
case "none": break;
case "top-down": vertical("top"); break;
case "bottom-up": vertical("bottom"); break;
case "center-out": centerVertical("center-out"); break;
case "center-in": centerVertical("center-in"); break;
}
}
// --------------------------------------------
// Draw Spiral
// --------------------------------------------
const drawSpiral = () => {
let nextX, nextY, nextW, nextH;
let spiralPoints = [];
let curveEndX, curveEndY, curveX, curveY;
const phaseShift = {
"bottom-right": 0,
"bottom-left": 2,
"top-left": 2,
"top-right": 0,
}[spiralOrientation];
let curveStartX = {
"bottom-right": x,
"bottom-left": x+width,
"top-left": x+width,
"top-right": x,
}[spiralOrientation];
let curveStartY = {
"bottom-right": y+height,
"bottom-left": y+height,
"top-left": y,
"top-right": y,
}[spiralOrientation];
const mirror = spiralOrientation === "bottom-left" || spiralOrientation === "top-right";
for (let i = phaseShift; i < size+phaseShift; i++) {
const curvePhase = i%4;
const linePhase = mirror?[0,3,2,1][curvePhase]:curvePhase;
const longHorizontal = width/phi;
const shortHorizontal = width*inversePhi;
const longVertical = height/phi;
const shortVertical = height*inversePhi;
switch(linePhase) {
case 0: //right
nextX = x + longHorizontal;
nextY = y;
nextW = shortHorizontal;
nextH = height;
break;
case 1: //down
nextX = x;
nextY = y + longVertical;
nextW = width;
nextH = shortVertical;
break;
case 2: //left
nextX = x;
nextY = y;
nextW = shortHorizontal;
nextH = height;
break;
case 3: //up
nextX = x;
nextY = y;
nextW = width;
nextH = shortVertical;
break;
}
switch(curvePhase) {
case 0: //right
curveEndX = nextX;
curveEndY = mirror ? nextY + nextH : nextY;
break;
case 1: //down
curveEndX = nextX + nextW;
curveEndY = mirror ? nextY + nextH : nextY;
break;
case 2: //left
curveEndX = nextX + nextW;
curveEndY = mirror ? nextY : nextY + nextH;
break;
case 3: //up
curveEndX = nextX;
curveEndY = mirror ? nextY : nextY + nextH;
break;
}
// Add points for the curve segment
for (let j = 0; j <= pointsPerCurve; j++) {
const t = j / pointsPerCurve;
const angle = -Math.PI / 2 * t;
switch(curvePhase) {
case 0:
curveX = curveEndX + (curveStartX - curveEndX) * Math.cos(angle);
curveY = curveStartY + (curveStartY - curveEndY) * Math.sin(angle);
break;
case 1:
curveX = curveStartX + (curveStartX - curveEndX) * Math.sin(angle);
curveY = curveEndY + (curveStartY - curveEndY) * Math.cos(angle);
break;
case 2:
curveX = curveEndX + (curveStartX - curveEndX) * Math.cos(angle);
curveY = curveStartY + (curveStartY - curveEndY) * Math.sin(angle);
break;
case 3:
curveX = curveStartX + (curveStartX - curveEndX) * Math.sin(angle);
curveY = curveEndY + (curveStartY - curveEndY) * Math.cos(angle);
break;
}
spiralPoints.push([curveX, curveY]);
}
x = nextX;
y = nextY;
curveStartX = curveEndX;
curveStartY = curveEndY;
width = nextW;
height = nextH;
switch(linePhase) {
case 0: rotatePointAndAddToElementList(ea.addLine([[x,y],[x,y+height]]));break;
case 1: rotatePointAndAddToElementList(ea.addLine([[x,y],[x+width,y]]));break;
case 2: rotatePointAndAddToElementList(ea.addLine([[x+width,y],[x+width,y+height]]));break;
case 3: rotatePointAndAddToElementList(ea.addLine([[x,y+height],[x+width,y+height]]));break;
}
}
const strokeWidth = ea.style.strokeWidth;
ea.style.strokeWidth = strokeWidth * (boldSpiral ? 3 : 1);
const angle = ea.style.angle;
ea.style.angle = 0;
ids.push(ea.addLine(rotatePointsWithinRectangle(spiralPoints)));
ea.style.angle = angle;
ea.style.strokeWidth = strokeWidth;
}
// --------------------------------------------
// Update Aspect Ratio
// --------------------------------------------
const updateAspectRatio = () => {
switch(aspectChoice) {
case "none": break;
case "adjust-width": rect.width = rect.height/phi; break;
case "adjust-height": rect.height = rect.width/phi; break;
}
({x,y,width,height} = rect);
centerX = x + width/2;
centerY = y + height/2;
}
// --------------------------------------------
// UI
// --------------------------------------------
draw = async () => {
if(updateStyle) {
ea.style.strokeWidth = 0.5; rect.strokeWidth;
ea.style.roughness = 0; rect.roughness;
ea.style.roundness = null;
rect.strokeWidth = 0.5;
rect.roughness = 0;
rect.roundness = null;
}
updateAspectRatio();
switch(type) {
case "grid": drawGrid(); break;
case "spiral":
if(spiralOrientation === "double") {
wInner = width * (Math.pow(phi,2)+1)/(2*Math.pow(phi,2));
hInner = height * (Math.pow(phi,2)+1)/(2*Math.pow(phi,2));
x2 = width - wInner + x;
y2 = height - hInner + y;
width = wInner;
height = hInner;
rotatePointAndAddToElementList(ea.addRect(x,y,width,height));
spiralOrientation = "bottom-right";
drawSpiral();
x = x2;
y = y2;
width = wInner;
height = hInner;
rotatePointAndAddToElementList(ea.addRect(x,y,width,height));
spiralOrientation = "top-left";
drawSpiral();
spiralOrientation = "double";
} else {
drawSpiral();
}
break;
}
ea.addToGroup(ids);
ids.push(rect.id);
ea.addToGroup(ids);
lockElements && ea.getElements().forEach(el=>{el.locked = true;});
await ea.addElementsToView(false,false,!sendToBack);
!lockElements && ea.selectElementsInView(ea.getViewElements().filter(el => ids.includes(el.id)));
}
const modal = new ea.obsidian.Modal(app);
const fragWithHTML = (html) => createFragment((frag) => (frag.createDiv().innerHTML = html));
const keydownListener = (e) => {
if(hostLeaf !== app.workspace.activeLeaf) return;
if(hostLeaf.width === 0 && hostLeaf.height === 0) return;
if(e.key === "Enter" && (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey)) {
e.preventDefault();
modal.close();
draw()
}
}
ownerWindow.addEventListener('keydown',keydownListener);
modal.onOpen = async () => {
const contentEl = modal.contentEl;
contentEl.createEl("h1", {text: "Golden Ratio"});
new ea.obsidian.Setting(contentEl)
.setName("Adjust Rectangle Aspect Ratio to Golden Ratio")
.addDropdown(dropdown=>dropdown
.addOption("none","None")
.addOption("adjust-width","Adjust Width")
.addOption("adjust-height","Adjust Height")
.setValue(aspectChoice)
.onChange(value => {
aspectChoice = value;
dirty = true;
})
);
new ea.obsidian.Setting(contentEl)
.setName("Change Line Style To: thin, architect, sharp")
.addToggle(toggle=>
toggle
.setValue(updateStyle)
.onChange(value => {
dirty = true;
updateStyle = value;
})
)
let sizeEl;
new ea.obsidian.Setting(contentEl)
.setName("Number of lines")
.addSlider(slider => slider
.setLimits(2, 20, 1)
.setValue(size)
.onChange(value => {
sizeEl.innerText = ` ${value.toString()}`;
size = value;
dirty = true;
}),
)
.settingEl.createDiv("", el => {
sizeEl = el;
el.style.minWidth = "2.3em";
el.style.textAlign = "right";
el.innerText = ` ${size.toString()}`;
});
new ea.obsidian.Setting(contentEl)
.setName("Lock Rectangle and Gridlines")
.addToggle(toggle=>
toggle
.setValue(lockElements)
.onChange(value => {
dirty = true;
lockElements = value;
})
)
new ea.obsidian.Setting(contentEl)
.setName("Send to Back")
.addToggle(toggle=>
toggle
.setValue(sendToBack)
.onChange(value => {
dirty = true;
sendToBack = value;
})
)
let bGrid, bSpiral;
let sHGrid, sVGrid, sSpiral, sBoldSpiral;
const showGridSettings = (value) => {
value
? (bGrid.setCta(), bSpiral.removeCta())
: (bGrid.removeCta(), bSpiral.setCta());
sHGrid.settingEl.style.display = value ? "" : "none";
sVGrid.settingEl.style.display = value ? "" : "none";
sSpiral.settingEl.style.display = !value ? "" : "none";
sBoldSpiral.settingEl.style.display = !value ? "" : "none";
}
new ea.obsidian.Setting(contentEl)
.setName(fragWithHTML("<h3>Output Type</h3>"))
.addButton(button => {
bGrid = button;
button
.setButtonText("Grid")
.setCta(type === "grid")
.onClick(event => {
type = "grid";
showGridSettings(true);
dirty = true;
})
})
.addButton(button => {
bSpiral = button;
button
.setButtonText("Spiral")
.setCta(type === "spiral")
.onClick(event => {
type = "spiral";
showGridSettings(false);
dirty = true;
})
});
sSpiral = new ea.obsidian.Setting(contentEl)
.setName("Spiral Orientation")
.addDropdown(dropdown=>dropdown
.addOption("double","Double")
.addOption("top-left","Top left")
.addOption("top-right","Top right")
.addOption("bottom-right","Bottom right")
.addOption("bottom-left","Bottom left")
.setValue(spiralOrientation)
.onChange(value => {
spiralOrientation = value;
dirty = true;
})
);
sBoldSpiral = new ea.obsidian.Setting(contentEl)
.setName("Spiral with Bold Line")
.addToggle(toggle=>
toggle
.setValue(boldSpiral)
.onChange(value => {
dirty = true;
boldSpiral = value;
})
)
sHGrid = new ea.obsidian.Setting(contentEl)
.setName("Horizontal Grid")
.addDropdown(dropdown=>dropdown
.addOption("none","None")
.addOption("left-right","Left to right")
.addOption("right-left","Right to left")
.addOption("center-out","Center out")
.addOption("center-in","Center in")
.setValue(hDirection)
.onChange(value => {
hDirection = value;
dirty = true;
})
);
sVGrid = new ea.obsidian.Setting(contentEl)
.setName("Vertical Grid")
.addDropdown(dropdown=>dropdown
.addOption("none","None")
.addOption("top-down","Top down")
.addOption("bottom-up","Bootom up")
.addOption("center-out","Center out")
.addOption("center-in","Center in")
.setValue(vDirection)
.onChange(value => {
vDirection = value;
dirty = true;
})
);
showGridSettings(type === "grid");
new ea.obsidian.Setting(contentEl)
.addButton(button => button
.setButtonText("Run")
.setCta(true)
.onClick(async (event) => {
draw();
modal.close();
})
);
}
modal.onClose = () => {
if(dirty) {
settings["Horizontal Grid"].value = hDirection;
settings["Vertical Grid"].value = vDirection;
settings["Size"].value = size.toString();
settings["Aspect Choice"].value = aspectChoice;
settings["Type"] = type;
settings["Spiral Orientation"].value = spiralOrientation;
settings["Lock Elements"] = lockElements;
settings["Send to Back"] = sendToBack;
settings["Update Style"] = updateStyle;
settings["Bold Spiral"] = boldSpiral;
ea.setScriptSettings(settings);
}
ownerWindow.removeEventListener('keydown',keydownListener);
}
modal.open();

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 642.8 373.7" stroke="currentColor"><path stroke-linecap="round" stroke-width="4" d="M5 5h633M5 5h633m0 0v364m0-364v364m0 0H5m633 0H5m0 0V5m0 364V5m242 0v364m0-364v364M5 144h242M5 144h242M154 5v139m0-139v139m0-53h93m-93 0h93m-57 0v53m0-53v53m-36-33h36m-36 0h36m-14-20v20m0-20v20m0-8h14m-14 0h14"/><path stroke-linecap="round" stroke-width="12" d="m638 5-5 57m5-57-5 57m0 0-14 55m14-55-14 55m0 0-24 53m24-53-24 53m0 0-32 49m32-49-32 49m0 0-40 43m40-43-40 43m0 0-46 37m46-37-46 37m0 0-53 30m53-30-53 30m0 0-56 22m56-22-56 22m0 0-60 13m60-13-60 13m0 0-61 5m61-5-61 5m0 0s0 0 0 0m0 0s0 0 0 0m0 0-38-3m38 3-38-3m0 0-37-8m37 8-37-8m0 0-35-14m35 14-35-14m0 0-32-18m32 18-32-18m0 0-29-23m29 23-29-23m0 0-25-27m25 27-25-27m0 0-20-30m20 30-20-30m0 0-14-33m14 33-14-33m0 0-9-34m9 34-9-34m0 0-3-35m3 35-3-35m0 0s0 0 0 0m0 0s0 0 0 0m0 0 2-22m-2 22 2-22m0 0 5-21m-5 21 5-21m0 0 9-20m-9 20 9-20m0 0 13-19M21 81l13-19m0 0 15-16M34 62l15-16m0 0 18-14M49 46l18-14m0 0 20-12M67 32c4-3 8-6 20-12m0 0 21-8m-21 8 21-8m0 0 23-5m-23 5 23-5m0 0 23-2m-23 2 23-2m0 0s0 0 0 0m0 0s0 0 0 0m0 0 15 1m-15-1 15 1m0 0 14 3m-14-3 14 3m0 0 13 5m-13-5 13 5m0 0 13 7m-13-7 13 7m0 0 11 9m-11-9 11 9m0 0 9 10m-9-10 9 10m0 0 8 12m-8-12 8 12m0 0 5 12m-5-12 5 12m0 0 4 13m-4-13 4 13m0 0 1 14m-1-14 1 14m0 0s0 0 0 0m0 0s0 0 0 0m0 0-1 8m1-8-1 8m0 0-2 8m2-8-2 8m0 0-4 8m4-8-4 8m0 0-4 7m4-7-4 7m0 0-6 6m6-6-6 6m0 0-7 6m7-6-7 6m0 0-7 4m7-4-7 4m0 0-9 3m9-3-9 3m0 0-8 2m8-2-8 2m0 0-9 1m9-1-9 1m0 0s0 0 0 0m0 0s0 0 0 0m0 0h-6m6 0h-6m0 0-5-2m5 2-5-2m0 0-5-2m5 2-5-2m0 0-5-2m5 2-5-2m0 0-4-4m4 4-4-4m0 0-4-4m4 4-4-4m0 0-3-4m3 4-3-4m0 0-2-5m2 5-2-5m0 0-1-5m1 5-1-5m0 0-1-5m1 5-1-5m0 0s0 0 0 0m0 0s0 0 0 0m0 0 1-3m-1 3 1-3m0 0v-3m0 3v-3m0 0 2-3m-2 3 2-3m0 0 2-3m-2 3 2-3m0 0 2-2m-2 2 2-2m0 0 2-2m-2 2 2-2m0 0 3-2m-3 2 3-2m0 0 3-1m-3 1 3-1m0 0 4-1m-4 1 4-1m0 0h3m-3 0h3m0 0s0 0 0 0m0 0s0 0 0 0m0 0h2m-2 0h2m0 0h2m-2 0h2m0 0 2 1m-2-1 2 1m0 0 2 1m-2-1 2 1m0 0 2 2m-2-2 2 2m0 0 1 1m-1-1 1 1m0 0 1 2m-1-2 1 2m0 0 1 2m-1-2 1 2m0 0v1m0-1v1m0 0 1 2m-1-2 1 2"/></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -29,7 +29,7 @@ try {
elHeight /= 1.1;
}
} else if (elWidth * elHeight < areaAvailable) {
while (elWidth * elHeight > areaAvailable) {
while (elWidth * elHeight < areaAvailable) {
elWidth *= 1.1;
elHeight *= 1.1;
}
@@ -64,4 +64,4 @@ try {
ea.addElementsToView(false, true, true);
} catch (err) {
_ = new Notice(err.toString())
}
}

View File

@@ -0,0 +1,40 @@
/*
The script will cycle through S, M, L, XL font sizes scaled to the current canvas zoom.
```js*/
const FONTSIZES = [16, 20, 28, 36];
const api = ea.getExcalidrawAPI();
const st = api.getAppState();
const zoom = st.zoom.value;
const currentItemFontSize = st.currentItemFontSize;
const fontsizes = FONTSIZES.map(s=>s/zoom);
const els = ea.getViewSelectedElements().filter(el=>el.type === "text");
const findClosestIndex = (val, list) => {
let closestIndex = 0;
let closestDifference = Math.abs(list[0] - val);
for (let i = 1; i < list.length; i++) {
const difference = Math.abs(list[i] - val);
if (difference <= closestDifference) {
closestDifference = difference;
closestIndex = i;
}
}
return closestIndex;
}
ea.viewUpdateScene({appState:{currentItemFontSize: fontsizes[(findClosestIndex(currentItemFontSize, fontsizes)+1) % fontsizes.length] }});
if(els.length>0) {
ea.copyViewElementsToEAforEditing(els);
ea.getElements().forEach(el=> {
el.fontSize = fontsizes[(findClosestIndex(el.fontSize, fontsizes)+1) % fontsizes.length];
const font = ExcalidrawLib.getFontString(el);
const lineHeight = ExcalidrawLib.getDefaultLineHeight(el.fontFamily);
const {width, height, baseline} = ExcalidrawLib.measureText(el.originalText, font, lineHeight);
el.width = width;
el.height = height;
el.baseline = baseline;
});
ea.addElementsToView();
}

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 50 30" xmlns="http://www.w3.org/2000/svg">
<text fill="currentColor" x="10" y="30" font-size="16px" font-weight="light">A</text>
<text fill="currentColor" x="22" y="30" font-size="36px" font-weight="light">A</text>
</svg>

After

Width:  |  Height:  |  Size: 243 B

View File

@@ -0,0 +1,96 @@
/*
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-repeat-texts.png)
In the following script, we address the concept of repetition through the lens of numerical progression. As visualized by the image, where multiple circles each labeled with an even task number are being condensed into a linear sequence, our script will similarly iterate through a set of numbers.
Inspired from [Repeat Elements](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Repeat%20Elements.md)
```javascript
*/
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.7.19")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
let repeatNum = parseInt(await utils.inputPrompt("repeat times?","number","5"));
if(!repeatNum) {
new Notice("Please enter a number.");
return;
}
const selectedElements = ea.getViewSelectedElements().sort((lha,rha) => lha.x === rha.x ? lha.y - rha.y : lha.x - rha.x);
const selectedBounds = selectedElements.filter(e => e.type !== "text");
const selectedTexts = selectedElements.filter(e => e.type === "text");
const selectedTextsById = selectedTexts.reduce((prev, next) => (prev[next.id] = next, prev), {})
if(selectedTexts.length !== 2 || ![0, 2].includes(selectedBounds.length)) {
new Notice("Please select only 2 text elements.");
return;
}
if(selectedBounds.length === 2) {
if(selectedBounds[0].type !== selectedBounds[1].type) {
new Notice("The selected elements must be of the same type.");
return;
}
if (!selectedBounds.every(e => e.boundElements?.length === 1)) {
new Notice("Only support the bound element with 1 text element.");
return;
}
if (!selectedBounds.every(e => !!selectedTextsById[e.boundElements?.[0]?.id])) {
new Notice("Bound element must refer to the text element.");
return;
}
}
const prevBoundEl = selectedBounds.length ? selectedBounds[0] : selectedTexts[0];
const nextBoundEl = selectedBounds.length ? selectedBounds[1] : selectedTexts[1];
const prevTextEl = prevBoundEl.type === 'text' ? prevBoundEl : selectedTextsById[prevBoundEl.boundElements[0].id]
const nextTextEl = nextBoundEl.type === 'text' ? nextBoundEl : selectedTextsById[nextBoundEl.boundElements[0].id]
const xDistance = nextBoundEl.x - prevBoundEl.x;
const yDistance = nextBoundEl.y - prevBoundEl.y;
const numReg = /\d+/
let textNumDiff
try {
const num0 = +prevTextEl.text.match(numReg)
const num1 = +nextTextEl.text.match(numReg)
textNumDiff = num1 - num0
} catch(e) {
new Notice("Text must include a number!")
return;
}
const repeatEl = (newEl, step) => {
ea.elementsDict[newEl.id] = newEl;
newEl.x += xDistance * (step + 1);
newEl.y += yDistance * (step + 1);
if(newEl.text) {
const text = newEl.text.replace(numReg, (match) => +match + (step + 1) * textNumDiff)
newEl.originalText = text
newEl.rawText = text
newEl.text = text
}
}
ea.copyViewElementsToEAforEditing(selectedBounds);
for(let i=0; i<repeatNum; i++) {
const newTextEl = ea.cloneElement(nextTextEl);
repeatEl(newTextEl, i)
if (selectedBounds.length) {
const newBoundEl = ea.cloneElement(selectedBounds[1]);
newBoundEl.boundElements[0].id = newTextEl.id
newTextEl.containerId = newBoundEl.id
repeatEl(newBoundEl, i)
}
}
await ea.addElementsToView(false, false, true);

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve">
<metadata> Svg Vector Icons : http://www.onlinewebfonts.com/icon </metadata>
<g><g><g><path fill="#000000" d="M101.2,18.7c-2,0.9-38,36.6-39.2,39c-1,1.9-1.1,4.2-0.4,6.2c0.6,1.5,37.3,38.6,39.3,39.6c1.7,0.9,5.2,0.8,7.1-0.1c1.9-0.9,3.7-3.5,4.1-5.8c0.6-3.5-0.2-4.5-12.5-16.8L88.2,69.2h44.3c38.7,0,44.8,0.1,48.6,0.7c24.2,4.2,43.2,22.6,48.1,46.8c3.3,16.4-1,34.2-11.5,47.6c-3.5,4.6-4.1,6.9-2.4,10.4c1.8,3.7,6.6,5.3,10.4,3.5c2.8-1.3,8.7-9.4,12.5-17.2c10.5-20.8,10.5-45.2,0-66.2c-10.3-20.6-28.6-34.8-51.8-40c-4.6-1.1-4.8-1.1-51.3-1.2c-25.7-0.1-46.7-0.3-46.7-0.5s5.2-5.5,11.5-11.9c9.4-9.4,11.6-11.9,12-13.3C113.6,21.6,107.3,16.1,101.2,18.7z"/><path fill="#000000" d="M34.9,73.4c-1.8,0.5-5.8,4.4-9.7,9.6c-7.9,10.4-12.8,22.4-14.5,35.4c-5.3,40.1,22.9,77.3,63.1,83.3c4,0.6,11.3,0.7,45.4,0.7c24,0,40.6,0.2,40.6,0.4c0,0.2-5,5.3-11.1,11.1c-11.3,11-12.9,13-12.9,16c0,4.1,3.8,7.9,7.9,7.9c0.7,0,2.1-0.3,3.1-0.7c2.3-1,38.1-36.6,39.3-39.2c1-2.1,1.1-4.2,0.4-6.1c-0.6-1.5-37.3-38.6-39.3-39.7c-1.9-1-6.1-0.6-8.1,0.8c-2.9,2-4,6.4-2.5,9.4c0.4,0.8,5.9,6.6,12.1,12.9l11.4,11.4h-40.4c-35,0-40.9-0.1-44.7-0.7c-24.2-4.1-43.4-22.9-48.2-47.1c-1.1-5.5-1.1-16.4,0-21.9c2.1-10.7,7-20.2,14.6-28.7c2.1-2.3,3.5-4.3,3.8-5.4c1-3.6-0.7-7.3-4.2-9C38.7,72.8,37.4,72.7,34.9,73.4z"/></g></g></g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

File diff suppressed because one or more lines are too long

View File

@@ -44,6 +44,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/Fixed%20spacing.svg"/></div>|[[#Fixed spacing]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Fixed%20vertical%20distance%20between%20centers.svg"/></div>|[[#Fixed vertical distance between centers]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Fixed%20vertical%20distance.svg"/></div>|[[#Fixed vertical distance]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Golden%20Ratio.svg"/></div>|[[#Golden Ratio]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Grid%20Selected%20Images.svg"/></div>|[[#Grid selected images]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Mindmap%20format.svg"/></div>|[[#Mindmap format]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Zoom%20to%20Fit%20Selected%20Elements.svg"/></div>|[[#Zoom to Fit Selected Elements]]|
@@ -67,6 +68,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/Convert%20selected%20text%20elements%20to%20sticky%20notes.svg"/></div>|[[#Convert selected text elements to sticky notes]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Relative%20Font%20Size%20Cycle.svg"/></div>|[[#Relative Font Size Cycle]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Scribble%20Helper.svg"/></div>|[[#Scribble Helper]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Font%20Family.svg"/></div>|[[#Set Font Family]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Text%20Alignment.svg"/></div>|[[#Set Text Alignment]]|
@@ -115,11 +117,14 @@ 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/ExcaliAI.svg"/></div>|[[#ExcaliAI]]|
|<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]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Rename%20Image.svg"/></div>|[[#Rename Image]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Repeat%20Elements.svg"/></div>|[[#Repeat Elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Repeat%20Texts.svg"/></div>|[[#Repeat Texts]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Select%20Elements%20of%20Type.svg"/></div>|[[#Select Elements of Type]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Select%20Similar%20Elements.svg"/></div>|[[#Select Similar Elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Slideshow.svg"/></div>|[[#Slideshow]]|
@@ -344,12 +349,33 @@ 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/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/Folder%20Note%20Core%20-%20Make%20Current%20Drawing%20a%20Folder.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script adds the `Folder Note Core: Make current document folder note` function to Excalidraw drawings. Running this script will convert the active Excalidraw drawing into a folder note. If you already have embedded images in your drawing, those attachments will not be moved when the folder note is created. You need to take care of those attachments separately, or convert the drawing to a folder note prior to adding the attachments. The script requires the <a href="https://github.com/aidenlx/folder-note-core" target="_blank">Folder Note Core</a> plugin.</td></tr></table>
## Golden Ratio
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Golden%20Ratio.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/Golden%20Ratio.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">The script performs two different functions depending on the elements selected in the view.<br>
1) In case you select text elements, the script will cycle through a set of font scales. First the 2 larger fonts following the Fibonacci sequence (fontsize * φ; fonsize * φ^2), then the 2 smaller fonts (fontsize / φ; fontsize / φ^2), finally the original size, followed again by the 2 larger fonts. If you wait 2 seconds, the sequence clears and starts from which ever font size you are on. So if you want the 3rd larges font, then toggle twice, wait 2 sec, then toggle again.<br>
2) In case you select a single rectangle, the script will open the "Golden Grid", "Golden Spiral" window, where you can set up the type of grid or spiral you want to insert into the document.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/golden-ratio.jpg'><br><iframe width="400" height="225" src="https://www.youtube.com/embed/2SHn_ruax-s" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td></tr></table>
## Grid selected images
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Grid%20Selected%20Images.md
```
<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>
## ExcaliAI
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/ExcaliAI.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/ExcaliAI.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Various AI features based on GPT Vision.<br><iframe width="400" height="225" src="https://www.youtube.com/embed/A1vrSGBbWgo" 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>
## 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">This script was discontinued in favor of ExcaliAI. 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
@@ -416,6 +442,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/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/PDF%20Page%20Text%20to%20Clipboard.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Copies the text from the selected PDF page on the Excalidraw canvas to the clipboard.<br><iframe width="400" height="225" src="https://www.youtube.com/embed/Kwt_8WdOUT4" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><br><a href='https://youtu.be/Kwt_8WdOUT4' target='_blank'>Link to video on YouTube</a></td></tr></table>
## Relative Font Size Cycle
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Relative%20Font%20Size%20Cycle.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/Relative%20Font%20Size%20Cycle.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">The script will cycle through S, M, L, XL font sizes scaled to the current canvas zoom.</td></tr></table>
## Rename Image
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Rename%20Image.md
@@ -428,6 +460,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/Repeat%20Elements.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script will detect the difference between 2 selected elements, including position, size, angle, stroke and background color, and create several elements that repeat these differences based on the number of repetitions entered by the user.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-repeat-elements.png'></td></tr></table>
## Repeat Texts
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Repeat%20Texts.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/soraliu'>@soraliu</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/Repeat%20Texts.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">In the following script, we address the concept of repetition through the lens of numerical progression. As visualized by the image, where multiple circles each labeled with an even task number are being condensed into a linear sequence, our script will similarly iterate through a set of numbers</td></tr></table>
## Reverse arrows
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Reverse%20arrows.md
@@ -546,4 +584,4 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Zoom%20to%20Fit%20Selected%20Elements.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/Zoom%20to%20Fit%20Selected%20Elements.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Similar to Excalidraw standard <kbd>SHIFT+2</kbd> feature: Zoom to fit selected elements, but with the ability to zoom to 1000%. Inspiration: [#272](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/272)</td></tr></table>
<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/Zoom%20to%20Fit%20Selected%20Elements.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Similar to Excalidraw standard <kbd>SHIFT+2</kbd> feature: Zoom to fit selected elements, but with the ability to zoom to 1000%. Inspiration: [#272](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/272)</td></tr></table>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 949 KiB

After

Width:  |  Height:  |  Size: 341 KiB

BIN
images/golden-ratio.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
images/scripts-repeat-texts.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "1.9.29",
"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.0",
"version": "2.0.17",
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",
@@ -9,4 +9,4 @@
"fundingUrl": "https://ko-fi.com/zsolt",
"helpUrl": "https://github.com/zsviczian/obsidian-excalidraw-plugin#readme",
"isDesktopOnly": false
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "obsidian-excalidraw-plugin",
"version": "1.9.15",
"version": "2.0.14",
"description": "This is an Obsidian.md plugin that lets you view and edit Excalidraw drawings",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@@ -18,23 +18,23 @@
"author": "",
"license": "MIT",
"dependencies": {
"@zsviczian/excalidraw": "0.16.1-obsidian-8",
"@zsviczian/excalidraw": "0.17.1-obsidian-11",
"chroma-js": "^2.4.2",
"clsx": "^2.0.0",
"colormaster": "^1.2.1",
"gl-matrix": "^3.4.3",
"lz-string": "^1.5.0",
"monkey-around": "^2.3.0",
"polybooljs": "^1.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"roughjs": "^4.5.2",
"html2canvas": "^1.4.1",
"@popperjs/core": "^2.11.8",
"nanoid": "^4.0.2",
"lucide-react": "^0.263.1"
"lucide-react": "^0.263.1",
"mathjax-full": "^3.2.2"
},
"devDependencies": {
"lz-string": "^1.5.0",
"@babel/core": "^7.22.9",
"@babel/preset-env": "^7.22.10",
"@babel/preset-react": "^7.22.5",
@@ -47,8 +47,9 @@
"@rollup/plugin-typescript": "^11.1.2",
"@types/chroma-js": "^2.4.0",
"@types/js-beautify": "^1.14.0",
"@types/node": "^20.5.6",
"@types/react-dom": "^18.2.7",
"@types/node": "^20.10.5",
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18",
"@zerollup/ts-transform-paths": "^1.7.18",
"cross-env": "^7.0.3",
"eslint-config-prettier": "^9.0.0",
@@ -56,14 +57,16 @@
"obsidian": "^1.4.0",
"prettier": "^3.0.1",
"rollup": "^2.70.1",
"rollup-plugin-copy": "^3.4.0",
"rollup-plugin-copy": "^3.5.0",
"rollup-plugin-postprocess": "github:brettz9/rollup-plugin-postprocess#update",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.34.1",
"rollup-plugin-web-worker-loader": "^1.6.1",
"tslib": "^2.6.1",
"ttypescript": "^1.5.15",
"typescript": "^5.2.2"
"typescript": "^5.2.2",
"cssnano": "^6.0.2"
},
"resolutions": {
"@typescript-eslint/typescript-estree": "5.3.0"

View File

@@ -10,7 +10,9 @@ import webWorker from "rollup-plugin-web-worker-loader";
import fs from'fs';
import LZString from 'lz-string';
import postprocess from 'rollup-plugin-postprocess';
import cssnano from 'cssnano';
const DIST_FOLDER = 'dist';
const isProd = (process.env.NODE_ENV === "production")
const isLib = (process.env.NODE_ENV === "lib");
console.log(`Running: ${process.env.NODE_ENV}`);
@@ -25,16 +27,34 @@ const reactdom_pkg = isLib ? "" : isProd
? fs.readFileSync("./node_modules/react-dom/umd/react-dom.production.min.js", "utf8")
: fs.readFileSync("./node_modules/react-dom/umd/react-dom.development.js", "utf8");
const lzstring_pkg = isLib ? "" : fs.readFileSync("./node_modules/lz-string/libs/lz-string.min.js", "utf8");
if(!isLib) {
const excalidraw_styles = isProd
? fs.readFileSync("./node_modules/@zsviczian/excalidraw/dist/styles.production.css", "utf8")
: fs.readFileSync("./node_modules/@zsviczian/excalidraw/dist/styles.development.css", "utf8");
const plugin_styles = fs.readFileSync("./styles.css", "utf8")
const styles = plugin_styles + excalidraw_styles;
cssnano()
.process(styles) // Process the CSS
.then(result => {
fs.writeFileSync(`./${DIST_FOLDER}/styles.css`, result.css);
})
.catch(error => {
console.error('Error while processing CSS:', error);
});
}
const manifestStr = isLib ? "" : fs.readFileSync("manifest.json", "utf-8");
const manifest = isLib ? {} : JSON.parse(manifestStr);
!isLib && console.log(manifest.version);
const packageString = isLib ? "" : ';'+lzstring_pkg+'const EXCALIDRAW_PACKAGES = "' + LZString.compressToBase64(react_pkg + reactdom_pkg + excalidraw_pkg) + '";' +
'const {react, reactDOM, excalidrawLib} = window.eval.call(window, `(function() {' +
'${LZString.decompressFromBase64(EXCALIDRAW_PACKAGES)};' +
'return {react:React, reactDOM:ReactDOM, excalidrawLib: ExcalidrawLib};})();`);' +
'const PLUGIN_VERSION="'+manifest.version+'";';
const packageString = isLib
? ""
: ';' + lzstring_pkg +
'\nconst EXCALIDRAW_PACKAGES = "' + LZString.compressToBase64(react_pkg + reactdom_pkg + excalidraw_pkg) + '";\n' +
'const {react, reactDOM, excalidrawLib} = window.eval.call(window, `(function() {' +
'${LZString.decompressFromBase64(EXCALIDRAW_PACKAGES)};' +
'return {react:React, reactDOM:ReactDOM, excalidrawLib: ExcalidrawLib};})();`);\n' +
'const PLUGIN_VERSION="'+manifest.version+'";';
const BASE_CONFIG = {
input: 'src/main.ts',
@@ -52,15 +72,16 @@ const getRollupPlugins = (tsconfig, ...plugins) =>
const BUILD_CONFIG = {
...BASE_CONFIG,
output: {
dir: '.',
sourcemap: isProd?false:'inline',
dir: DIST_FOLDER,
entryFileNames: 'main.js',
//sourcemap: isProd?false:'inline',
format: 'cjs',
exports: 'default',
},
plugins: [
typescript2({
tsconfig: isProd ? "tsconfig.json" : "tsconfig.dev.json",
inlineSources: !isProd
//inlineSources: !isProd
}),
replace({
preventAssignment: true,
@@ -78,7 +99,10 @@ const BUILD_CONFIG = {
nodeResolve({ browser: true, preferBuiltins: false }),
...isProd
? [
terser({toplevel: false, compress: {passes: 2}}),
terser({
toplevel: false,
compress: {passes: 2}
}),
//!postprocess - the version available on npmjs does not work, need this update:
// npm install brettz9/rollup-plugin-postprocess#update --save-dev
// https://github.com/developit/rollup-plugin-postprocess/issues/10
@@ -91,6 +115,12 @@ const BUILD_CONFIG = {
[/var React = require\('react'\);/, packageString],
])
],
copy({
targets: [
{ src: 'manifest.json', dest: DIST_FOLDER },
],
verbose: true, // Optional: To display copied files in the console
}),
],
}

View File

@@ -1,14 +1,14 @@
//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
import { ExcalidrawElement, ExcalidrawImageElement, FileId } from "@zsviczian/excalidraw/types/element/types";
import { BinaryFileData, DataURL } from "@zsviczian/excalidraw/types/types";
import { ExcalidrawElement, ExcalidrawImageElement, FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { BinaryFileData, DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
import { App, MarkdownRenderer, Notice, TFile } from "obsidian";
import {
ASSISTANT_FONT,
CASCADIA_FONT,
VIRGIL_FONT,
} from "./constFonts";
} from "./constants/constFonts";
import {
DEFAULT_MD_EMBED_CSS,
fileid,
@@ -19,7 +19,7 @@ import {
IMAGE_TYPES,
nanoid,
THEME_FILTER,
} from "./constants";
} from "./constants/constants";
import { createSVG } from "./ExcalidrawAutomate";
import { ExcalidrawData, getTransclusion } from "./ExcalidrawData";
import { ExportSettings } from "./ExcalidrawView";
@@ -40,10 +40,11 @@ import {
hasExportTheme,
LinkParts,
svgToBase64,
isMaskFile,
} from "./utils/Utils";
import { ValueOf } from "./types";
import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
import { mermaidToExcalidraw } from "src/constants";
import { mermaidToExcalidraw } from "src/constants/constants";
//An ugly workaround for the following situation.
//File A is a markdown file that has an embedded Excalidraw file B
@@ -58,6 +59,7 @@ export const IMAGE_MIME_TYPES = {
svg: "image/svg+xml",
png: "image/png",
jpg: "image/jpeg",
jpeg: "image/jpeg",
gif: "image/gif",
webp: "image/webp",
bmp: "image/bmp",
@@ -348,6 +350,7 @@ export class EmbeddedFilesLoader {
elements?: ExcalidrawElement[];
}) : Promise<{dataURL: DataURL, hasSVGwithBitmap:boolean}> {
//debug({where:"EmbeddedFileLoader.getExcalidrawSVG",uid:this.uid,file:file.name});
const isMask = isMaskFile(this.plugin, file);
const forceTheme = hasExportTheme(this.plugin, file)
? getExportTheme(this.plugin, file, "light")
: undefined;
@@ -356,6 +359,7 @@ export class EmbeddedFilesLoader {
? getWithBackground(this.plugin, file)
: false,
withTheme: !!forceTheme,
isMask,
};
const svg = replaceSVGColors(
await createSVG(
@@ -396,6 +400,15 @@ export class EmbeddedFilesLoader {
return {dataURL: dURL as DataURL, hasSVGwithBitmap};
};
//this is a fix for backward compatibility - I messed up with generating the local link
private getLocalPath(path: string) {
const localPath = path.split("file://")[1]
if(localPath.startsWith("/")) {
return localPath.substring(1);
}
return localPath;
}
private async _getObsidianImage(inFile: TFile | EmbeddedFile, depth: number): Promise<ImgData> {
if (!this.plugin || !inFile) {
return null;
@@ -442,7 +455,7 @@ export class EmbeddedFilesLoader {
const ab = isHyperLink || isPDF
? null
: isLocalLink
? await readLocalFileBinary((inFile as EmbeddedFile).hyperlink.split("file://")[1])
? await readLocalFileBinary(this.getLocalPath((inFile as EmbeddedFile).hyperlink))
: await app.vault.readBinary(file);
let dURL: DataURL = null;
@@ -535,7 +548,7 @@ export class EmbeddedFilesLoader {
//debug({where:"EmbeddedFileLoader.loadSceneFiles",uid:this.uid,status:"embedded Files are not loaded"});
const data = await this._getObsidianImage(embeddedFile, depth);
if (data) {
const fileData = {
const fileData: FileData = {
mimeType: data.mimeType,
id: entry.value[0],
dataURL: data.dataURL,
@@ -577,7 +590,7 @@ export class EmbeddedFilesLoader {
while (!this.terminate && !(equation = equations.next()).done) {
if (!excalidrawData.getEquation(equation.value[0]).isLoaded) {
const latex = equation.value[1].latex;
const data = await tex2dataURL(latex, this.plugin);
const data = await tex2dataURL(latex);
if (data) {
const fileData = {
mimeType: data.mimeType,

View File

@@ -10,10 +10,11 @@ import {
ExcalidrawTextElement,
StrokeRoundness,
RoundnessType,
} from "@zsviczian/excalidraw/types/element/types";
import { Editor, normalizePath, Notice, OpenViewState, TFile, WorkspaceLeaf } from "obsidian";
ExcalidrawFrameElement,
} from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { Editor, normalizePath, Notice, OpenViewState, RequestUrlResponse, TFile, TFolder, WorkspaceLeaf } from "obsidian";
import * as obsidian_module from "obsidian";
import ExcalidrawView, { ExportSettings, TextMode } from "src/ExcalidrawView";
import ExcalidrawView, { ExportSettings, TextMode, getTextMode } from "src/ExcalidrawView";
import { ExcalidrawData, getMarkdownDrawingSection, REGEX_LINK } from "src/ExcalidrawData";
import {
FRONTMATTER,
@@ -34,8 +35,8 @@ import {
REG_LINKINDEX_INVALIDCHARS,
THEME_FILTER,
mermaidToExcalidraw,
} from "src/constants";
import { getDrawingFilename, getNewUniqueFilepath, } from "src/utils/FileUtils";
} from "src/constants/constants";
import { blobToBase64, checkAndCreateFolder, getDrawingFilename, getNewUniqueFilepath, } from "src/utils/FileUtils";
import {
//debug,
embedFontsInSVG,
@@ -51,7 +52,7 @@ import {
wrapTextAtCharLength,
} from "src/utils/Utils";
import { getAttachmentsFolderAndFilePath, getLeaf, getNewOrAdjacentLeaf, isObsidianThemeDark } from "src/utils/ObsidianUtils";
import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI, Point } from "@zsviczian/excalidraw/types/types";
import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI, Point } from "@zsviczian/excalidraw/types/excalidraw/types";
import { EmbeddedFile, EmbeddedFilesLoader, FileData } from "src/EmbeddedFileLoader";
import { tex2dataURL } from "src/LaTeX";
import { GenericInputPrompt, NewFileActions, Prompt } from "src/dialogs/Prompt";
@@ -74,12 +75,19 @@ import RYBPlugin from "colormaster/plugins/ryb";
import CMYKPlugin from "colormaster/plugins/cmyk";
import { TInput } from "colormaster/types";
import {ConversionResult, svgToExcalidraw} from "src/svgToExcalidraw/parser"
import { ROUNDNESS } from "src/constants";
import { ClipboardData } from "@zsviczian/excalidraw/types/clipboard";
import { ROUNDNESS } from "src/constants/constants";
import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard";
import { emulateKeysForLinkClick, KeyEvent, PaneTarget } from "src/utils/ModifierkeyHelper";
import { Mutable } from "@zsviczian/excalidraw/types/utility-types";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import PolyBool from "polybooljs";
import { compressToBase64, decompressFromBase64 } from "lz-string";
import { EmbeddableMDCustomProps } from "./dialogs/EmbeddableSettings";
import {
AIRequest,
postOpenAI as _postOpenAI,
extractCodeBlocks as _extractCodeBlocks,
} from "./utils/AIUtils";
import { EXCALIDRAW_AUTOMATE_INFO } from "./dialogs/SuggesterInfo";
import { CropImage } from "./utils/CropImage";
extendPlugins([
HarmonyPlugin,
@@ -98,6 +106,7 @@ extendPlugins([
]);
declare const PLUGIN_VERSION:string;
declare var LZString: any;
const GAP = 4;
@@ -116,7 +125,108 @@ export class ExcalidrawAutomate {
get DEVICE():DeviceType {
return DEVICE;
}
public help(target: Function | string) {
if (!target) {
console.log("Usage: ea.help(ea.functionName) or ea.help('propertyName')");
return;
}
let funcInfo;
if (typeof target === 'function') {
funcInfo = EXCALIDRAW_AUTOMATE_INFO.find((info) => info.field === target.name);
} else if (typeof target === 'string') {
funcInfo = EXCALIDRAW_AUTOMATE_INFO.find((info) => info.field === target);
}
if(!funcInfo) {
console.log("Usage: ea.help(ea.functionName) or\nea.help('propertyName') - notice property name is in quotes");
return;
}
if (funcInfo.desc) {
const formattedDesc = funcInfo.desc
.replaceAll("<br>", "\n")
.replace(/<code>(.*?)<\/code>/g, '%c\u200b$1%c') // Zero-width space
.replace(/<b>(.*?)<\/b>/g, '%c\u200b$1%c') // Zero-width space
.replace(/<a onclick='window\.open\("(.*?)"\)'>(.*?)<\/a>/g, (_, href, text) => `%c\u200b${text}%c\u200b (link: ${href})`); // Zero-width non-joiner
const styles = Array.from({ length: (formattedDesc.match(/%c/g) || []).length }, (_, i) => i % 2 === 0 ? 'color: #007bff;' : '');
console.log(`Declaration: ${funcInfo.code}`);
console.log(`Description: ${formattedDesc}`, ...styles);
} else {
console.log("Description not available for this function.");
}
}
/**
* Post's an AI request to the OpenAI API and returns the response.
* @param request
* @returns
*/
public async postOpenAI (request: AIRequest): Promise<RequestUrlResponse> {
return await _postOpenAI(request);
}
/**
* Grabs the codeblock contents from the supplied markdown string.
* @param markdown
* @param codeblockType
* @returns an array of dictionaries with the codeblock contents and type
*/
public extractCodeBlocks(markdown: string): { data: string, type: string }[] {
return _extractCodeBlocks(markdown);
}
/**
* converts a string to a DataURL
* @param htmlString
* @returns dataURL
*/
public async convertStringToDataURL (data:string, type: string = "text/html"):Promise<string> {
// Create a blob from the HTML string
const blob = new Blob([data], { type });
// Read the blob as Data URL
const base64String = await new Promise((resolve) => {
const reader = new FileReader();
reader.onload = () => {
if(typeof reader.result === "string") {
const base64String = reader.result.split(',')[1];
resolve(base64String);
} else {
resolve(null);
}
};
reader.readAsDataURL(blob);
});
if(base64String) {
return `data:${type};base64,${base64String}`;
}
return "about:blank";
}
/**
* Checks if the folder exists, if not, creates it.
* @param folderpath
* @returns
*/
public async checkAndCreateFolder(folderpath: string): Promise<TFolder> {
return await checkAndCreateFolder(folderpath);
}
/**
* Checks if the filepath already exists, if so, returns a new filepath with a number appended to the filename.
* @param filename
* @param folderpath
* @returns
*/
public getNewUniqueFilepath(filename: string, folderpath: string): string {
return getNewUniqueFilepath(app.vault, filename, folderpath);
}
public async getAttachmentFilepath(filename: string): Promise<string> {
if (!this.targetView || !this.targetView?.file) {
errorMessage("targetView not set", "getAttachmentFolderAndFilePath()");
@@ -127,11 +237,11 @@ export class ExcalidrawAutomate {
}
public compressToBase64(str:string):string {
return compressToBase64(str);
return LZString.compressToBase64(str);
}
public decompressFromBase64(str:string):string {
return decompressFromBase64(str);
return LZString.decompressFromBase64(str);
}
/**
@@ -230,7 +340,7 @@ export class ExcalidrawAutomate {
fontSize: number;
textAlign: string; //"left"|"right"|"center"
verticalAlign: string; //"top"|"bottom"|"middle" :for future use, has no effect currently
startArrowHead: string; //"triangle"|"dot"|"arrow"|"bar"|null
startArrowHead: string; //"arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null
endArrowHead: string;
};
canvas: {
@@ -459,6 +569,7 @@ export class ExcalidrawAutomate {
"excalidraw-onload-script"?: string;
"excalidraw-linkbutton-opacity"?: number;
"excalidraw-autoexport"?: boolean;
"excalidraw-mask"?: boolean;
};
plaintext?: string; //text to insert above the `# Text Elements` section
}): Promise<string> {
@@ -570,7 +681,15 @@ export class ExcalidrawAutomate {
if(item.latex) {
outString += `${key}: $$${item.latex}$$\n`;
} else {
outString += `${key}: [[${item.file}]]\n`;
if(item.file) {
if(item.file instanceof TFile) {
outString += `${key}: [[${item.file.path}]]\n`;
} else {
outString += `${key}: [[${item.file}]]\n`;
}
} else {
outString += `${key}: ${item.hyperlink}\n`;
}
}
})
return outString;
@@ -597,6 +716,14 @@ export class ExcalidrawAutomate {
}
};
getCropImageObject(): CropImage {
const scene = this.targetView.getScene();
return new CropImage(
scene.elements,
scene.files,
);
}
/**
*
* @param templatePath
@@ -627,6 +754,7 @@ export class ExcalidrawAutomate {
exportSettings = {
withBackground: this.plugin.settings.exportWithBackground,
withTheme: true,
isMask: false,
};
}
if (!loader) {
@@ -652,6 +780,7 @@ export class ExcalidrawAutomate {
);
};
/**
*
* @param templatePath
@@ -682,6 +811,7 @@ export class ExcalidrawAutomate {
exportSettings = {
withBackground: this.plugin.settings.exportWithBackground,
withTheme: true,
isMask: false,
};
}
if (!loader) {
@@ -707,6 +837,28 @@ export class ExcalidrawAutomate {
);
};
/**
* Wrapper for createPNG() that returns a base64 encoded string
* @param templatePath
* @param scale
* @param exportSettings
* @param loader
* @param theme
* @param padding
* @returns
*/
async createPNGBase64(
templatePath?: string,
scale: number = 1,
exportSettings?: ExportSettings,
loader?: EmbeddedFilesLoader,
theme?: string,
padding?: number,
): Promise<string> {
const png = await this.createPNG(templatePath,scale,exportSettings,loader,theme,padding);
return `data:image/png;base64,${await blobToBase64(png)}`
}
/**
*
* @param text
@@ -725,6 +877,7 @@ export class ExcalidrawAutomate {
w: number,
h: number,
link: string | null = null,
scale?: [number, number],
) {
return {
id,
@@ -755,6 +908,7 @@ export class ExcalidrawAutomate {
boundElements: [] as any,
link,
locked: false,
...scale ? {scale} : {},
};
}
@@ -770,7 +924,15 @@ export class ExcalidrawAutomate {
* @param height
* @returns
*/
public addEmbeddable(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string {
public addEmbeddable(
topX: number,
topY: number,
width: number,
height: number,
url?: string,
file?: TFile,
embeddableCustomData?: EmbeddableMDCustomProps,
): string {
//@ts-ignore
if (!this.targetView || !this.targetView?._loaded) {
errorMessage("targetView not set", "addEmbeddable()");
@@ -797,10 +959,35 @@ export class ExcalidrawAutomate {
false, //file.extension === "md", //changed this to false because embedable link navigation in ExcaliBrain
)
}]]` : "",
[1,1],
);
this.elementsDict[id].customData = {mdProps: embeddableCustomData ?? this.plugin.settings.embeddableMarkdownDefaults};
return id;
};
/**
*
* @param topX
* @param topY
* @param width
* @param height
* @param name: the display name of the frame
* @returns
*/
addFrame(topX: number, topY: number, width: number, height: number, name?: string): string {
const id = this.addRect(topX, topY, width, height);
const frame = this.getElement(id) as Mutable<ExcalidrawFrameElement>;
frame.type = "frame";
frame.backgroundColor = "transparent";
frame.strokeColor = "#000";
frame.strokeStyle = "solid";
frame.strokeWidth = 2;
frame.roughness = 0;
frame.roundness = null;
if(name) frame.name = name;
return id;
}
/**
*
* @param topX
@@ -1086,8 +1273,8 @@ export class ExcalidrawAutomate {
addArrow(
points: [x: number, y: number][],
formatting?: {
startArrowHead?: string;
endArrowHead?: string;
startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
startObjectId?: string;
endObjectId?: string;
},
@@ -1155,15 +1342,18 @@ export class ExcalidrawAutomate {
/**
* Adds a mermaid diagram to ExcalidrawAutomate elements
* @param diagram
* @returns the ids of the elements that were created
* @param diagram string containing the mermaid diagram
* @param groupElements default is trud. If true, the elements will be grouped
* @returns the ids of the elements that were created or null if there was an error
*/
async addMermaid(
diagram: string,
): Promise<string[]> {
groupElements: boolean = true,
): Promise<string[]|string> {
const result = await mermaidToExcalidraw(diagram, {fontSize: this.style.fontSize});
const ids:string[] = [];
if(!result) return ids;
if(!result) return null;
if(result?.error) return result.error;
if(result?.elements) {
result.elements.forEach(el=>{
@@ -1185,6 +1375,10 @@ export class ExcalidrawAutomate {
}
}
}
if(groupElements && result?.elements && ids.length > 1) {
this.addToGroup(ids);
}
return ids;
}
@@ -1263,7 +1457,7 @@ export class ExcalidrawAutomate {
*/
async addLaTex(topX: number, topY: number, tex: string): Promise<string> {
const id = nanoid();
const image = await tex2dataURL(tex, this.plugin);
const image = await tex2dataURL(tex);
if (!image) {
return null;
}
@@ -1297,8 +1491,8 @@ export class ExcalidrawAutomate {
* @param connectionB when passed null, Excalidraw will automatically decide
* @param formatting
* numberOfPoints: points on the line. Default is 0 ie. line will only have a start and end point
* startArrowHead: "triangle"|"dot"|"arrow"|"bar"|null
* endArrowHead: "triangle"|"dot"|"arrow"|"bar"|null
* startArrowHead: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null
* endArrowHead: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null
* padding:
* @returns
*/
@@ -1309,8 +1503,8 @@ export class ExcalidrawAutomate {
connectionB: ConnectionPoint | null,
formatting?: {
numberOfPoints?: number;
startArrowHead?: "triangle"|"dot"|"arrow"|"bar"|null;
endArrowHead?: "triangle"|"dot"|"arrow"|"bar"|null;
startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
padding?: number;
},
): string {
@@ -1634,10 +1828,47 @@ export class ExcalidrawAutomate {
* copies elements from view to elementsDict for editing
* @param elements
*/
copyViewElementsToEAforEditing(elements: ExcalidrawElement[]): void {
elements.forEach((el) => {
this.elementsDict[el.id] = cloneElement(el);
});
copyViewElementsToEAforEditing(elements: ExcalidrawElement[], copyImages: boolean = false): void {
if(copyImages && elements.some(el=>el.type === "image")) {
//@ts-ignore
if (!this.targetView || !this.targetView?._loaded) {
errorMessage("targetView not set", "copyViewElementsToEAforEditing()");
return;
}
const sceneFiles = this.targetView.getScene().files;
elements.forEach((el) => {
this.elementsDict[el.id] = cloneElement(el);
if(el.type === "image") {
const ef = this.targetView.excalidrawData.getFile(el.fileId);
const equation = this.targetView.excalidrawData.getEquation(el.fileId);
const sceneFile = sceneFiles?.[el.fileId];
this.imagesDict[el.fileId] = {
mimeType: sceneFile.mimeType,
id: el.fileId,
dataURL: sceneFile.dataURL,
created: sceneFile.created,
...ef ? {
isHyperLink: ef.isHyperLink,
hyperlink: ef.hyperlink,
file: ef.file,
hasSVGwithBitmap: ef.isSVGwithBitmap,
latex: null,
} : {},
...equation ? {
file: null,
isHyperLink: false,
hyperlink: null,
hasSVGwithBitmap: false,
latex: equation.latex,
} : {},
};
}
});
} else {
elements.forEach((el) => {
this.elementsDict[el.id] = cloneElement(el);
});
}
};
/**
@@ -1719,8 +1950,8 @@ export class ExcalidrawAutomate {
connectionB: ConnectionPoint | null,
formatting?: {
numberOfPoints?: number;
startArrowHead?: "triangle"|"dot"|"arrow"|"bar"|null;
endArrowHead?: "triangle"|"dot"|"arrow"|"bar"|null;
startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
padding?: number;
},
): boolean {
@@ -1943,8 +2174,9 @@ export class ExcalidrawAutomate {
getExportSettings(
withBackground: boolean,
withTheme: boolean,
isMask: boolean = false,
): ExportSettings {
return { withBackground, withTheme };
return { withBackground, withTheme, isMask };
};
/**
@@ -2456,13 +2688,11 @@ async function getTemplate(
};
}
const parsed =
data.search("excalidraw-plugin: parsed\n") > -1 ||
data.search("excalidraw-plugin: locked\n") > -1; //locked for backward compatibility
const textMode = getTextMode(data);
await excalidrawData.loadData(
data,
file,
parsed ? TextMode.parsed : TextMode.raw,
textMode,
);
let trimLocation = data.search("# Text Elements\n");
@@ -2593,6 +2823,7 @@ export async function createPNG(
withBackground:
exportSettings?.withBackground ?? plugin.settings.exportWithBackground,
withTheme: exportSettings?.withTheme ?? plugin.settings.exportWithTheme,
isMask: exportSettings?.isMask ?? false,
},
padding,
scale,
@@ -2690,6 +2921,7 @@ export async function createSVG(
withBackground:
exportSettings?.withBackground ?? plugin.settings.exportWithBackground,
withTheme,
isMask: exportSettings?.isMask ?? false,
},
padding,
null,

View File

@@ -25,7 +25,7 @@ import {
wrapText,
ERROR_IFRAME_CONVERSION_CANCELED,
JSON_parse,
} from "./constants";
} from "./constants/constants";
import { _measureText } from "./ExcalidrawAutomate";
import ExcalidrawPlugin from "./main";
import { TextMode } from "./ExcalidrawView";
@@ -42,6 +42,7 @@ import {
hasExportTheme,
isVersionNewerThanOther,
LinkParts,
updateFrontmatterInString,
wrapTextAtCharLength,
} from "./utils/Utils";
import { cleanBlockRef, cleanSectionHeading, getAttachmentsFolderAndFilePath, isObsidianThemeDark } from "./utils/ObsidianUtils";
@@ -49,8 +50,8 @@ import {
ExcalidrawElement,
ExcalidrawImageElement,
FileId,
} from "@zsviczian/excalidraw/types/element/types";
import { BinaryFiles, DataURL, SceneData } from "@zsviczian/excalidraw/types/types";
} from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { BinaryFiles, DataURL, SceneData } from "@zsviczian/excalidraw/types/excalidraw/types";
import { EmbeddedFile, MimeType } from "./EmbeddedFileLoader";
import { ConfirmationPrompt } from "./dialogs/Prompt";
import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
@@ -122,7 +123,7 @@ export const REGEX_LINK = {
//added \n at and of DRAWING_REG: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/357
const DRAWING_REG = /\n# Drawing\n[^`]*(```json\n)([\s\S]*?)```\n/gm; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/182
const DRAWING_REG_FALLBACK = /\n# Drawing\n(```json\n)?(.*)(```)?(%%)?/gm;
const DRAWING_COMPRESSED_REG =
export const DRAWING_COMPRESSED_REG =
/(\n# Drawing\n[^`]*(?:```compressed\-json\n))([\s\S]*?)(```\n)/gm; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/182
const DRAWING_COMPRESSED_REG_FALLBACK =
/(\n# Drawing\n(?:```compressed\-json\n)?)(.*)((```)?(%%)?)/gm;
@@ -243,6 +244,26 @@ const estimateMaxLineLen = (text: string, originalText: string): number => {
const wrap = (text: string, lineLen: number) =>
lineLen ? wrapTextAtCharLength(text, lineLen, false, 0) : text;
export const getExcalidrawMarkdownHeaderSection = (data:string, keys:[string,string][]):string => {
let trimLocation = data.search(/(^%%\n)?# Text Elements\n/m);
if (trimLocation == -1) {
trimLocation = data.search(/(%%\n)?# Drawing\n/);
}
if (trimLocation == -1) {
return data;
}
let header = updateFrontmatterInString(data.substring(0, trimLocation),keys);
//this should be removed at a later time. Left it here to remediate 1.4.9 mistake
const REG_IMG = /(^---[\w\W]*?---\n)(!\[\[.*?]]\n(%%\n)?)/m; //(%%\n)? because of 1.4.8-beta... to be backward compatible with anyone who installed that version
if (header.match(REG_IMG)) {
header = header.replace(REG_IMG, "$1");
}
//end of remove
return header;
}
export class ExcalidrawData {
public textElements: Map<
string,
@@ -287,7 +308,7 @@ export class ExcalidrawData {
const elements = this.scene.elements;
for (const el of elements) {
if(el.type === "iframe") {
if(el.type === "iframe" && !el.customData) {
el.type = "embeddable";
}
@@ -515,7 +536,7 @@ export class ExcalidrawData {
}
//once off migration of legacy scenes
if(this.scene?.elements?.some((el:any)=>el.type==="iframe")) {
if(this.scene?.elements?.some((el:any)=>el.type==="iframe" && !el.customData)) {
const prompt = new ConfirmationPrompt(
this.plugin,
"This file contains embedded frames " +
@@ -1235,11 +1256,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 +1279,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 +1306,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

@@ -1,9 +1,9 @@
import { RestoredDataState } from "@zsviczian/excalidraw/types/data/restore";
import { ImportedDataState } from "@zsviczian/excalidraw/types/data/types";
import { BoundingBox } from "@zsviczian/excalidraw/types/element/bounds";
import { ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameElement, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, NonDeletedExcalidrawElement, Theme } from "@zsviczian/excalidraw/types/element/types";
import { AppState, BinaryFiles, ExportOpts, Point, Zoom } from "@zsviczian/excalidraw/types/types";
import { Mutable } from "@zsviczian/excalidraw/types/utility-types";
import { RestoredDataState } from "@zsviczian/excalidraw/types/excalidraw/data/restore";
import { ImportedDataState } from "@zsviczian/excalidraw/types/excalidraw/data/types";
import { BoundingBox } from "@zsviczian/excalidraw/types/excalidraw/element/bounds";
import { ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameElement, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, NonDeletedExcalidrawElement, Theme } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { AppState, BinaryFiles, ExportOpts, Point, Zoom } from "@zsviczian/excalidraw/types/excalidraw/types";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
type EmbeddedLink =
| ({
@@ -132,7 +132,8 @@ declare namespace ExcalidrawLib {
opts: {fontSize: number},
forceSVG?: boolean,
): Promise<{
elements: ExcalidrawElement[],
files:any
elements?: ExcalidrawElement[];
files?: any;
error?: string;
} | undefined>;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,20 @@
import { DataURL } from "@zsviczian/excalidraw/types/types";
import { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
import {mathjax} from "mathjax-full/js/mathjax";
import {TeX} from 'mathjax-full/js/input/tex.js';
import {SVG} from 'mathjax-full/js/output/svg.js';
import {LiteAdaptor, liteAdaptor} from 'mathjax-full/js/adaptors/liteAdaptor.js';
import {RegisterHTMLHandler} from 'mathjax-full/js/handlers/html.js';
import {AllPackages} from 'mathjax-full/js/input/tex/AllPackages.js';
import ExcalidrawView from "./ExcalidrawView";
import ExcalidrawPlugin from "./main";
import { FileData, MimeType } from "./EmbeddedFileLoader";
import { FileId } from "@zsviczian/excalidraw/types/element/types";
import { errorlog, getImageSize, log, sleep, svgToBase64 } from "./utils/Utils";
import { fileid } from "./constants";
import html2canvas from "html2canvas";
import { Notice } from "obsidian";
declare let window: any;
import { FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { getImageSize, svgToBase64 } from "./utils/Utils";
import { fileid } from "./constants/constants";
import { TFile } from "obsidian";
import { MathDocument } from "mathjax-full/js/core/MathDocument";
import { stripVTControlCharacters } from "util";
export const updateEquation = async (
equation: string,
@@ -17,7 +23,7 @@ export const updateEquation = async (
addFiles: Function,
plugin: ExcalidrawPlugin,
) => {
const data = await tex2dataURL(equation, plugin);
const data = await tex2dataURL(equation);
if (data) {
const files: FileData[] = [];
files.push({
@@ -33,9 +39,23 @@ export const updateEquation = async (
}
};
let adaptor: LiteAdaptor;
let input: TeX<unknown, unknown, unknown>;
let output: SVG<unknown, unknown, unknown>;
let html: MathDocument<any, any, any>;
let preamble: string;
//https://github.com/xldenis/obsidian-latex/blob/master/main.ts
const loadPreamble = async () => {
const file = app.vault.getAbstractFileByPath("preamble.sty");
preamble = file && file instanceof TFile
? await app.vault.read(file)
: null;
};
export async function tex2dataURL(
tex: string,
plugin: ExcalidrawPlugin,
scale: number = 4 // Default scale value, adjust as needed
): Promise<{
mimeType: MimeType;
fileId: FileId;
@@ -43,102 +63,44 @@ export async function tex2dataURL(
created: number;
size: { height: number; width: number };
}> {
//if network is slow, or not available, or mathjax has not yet fully loaded
let counter = 0;
while (!plugin.mathjax && !plugin.mathjaxLoaderFinished && counter < 10) {
await sleep(100);
counter++;
if(!adaptor) {
await loadPreamble();
adaptor = liteAdaptor();
RegisterHTMLHandler(adaptor);
input = new TeX({
packages: AllPackages,
...Boolean(preamble) ? {
inlineMath: [['$', '$']],
displayMath: [['$$', '$$']]
} : {},
});
output = new SVG({ fontCache: "local" });
html = mathjax.document("", { InputJax: input, OutputJax: output });
}
if(!plugin.mathjaxLoaderFinished) {
errorlog({where: "text2dataURL", fn: tex2dataURL, message:"mathjaxLoader not ready, using fallback. Try reloading Obsidian or restarting the Excalidraw plugin"});
}
//it is not clear why this works, but it seems that after loading the plugin sometimes only the third attempt is successful.
try {
return await mathjaxSVG(tex, plugin);
} catch (e) {
await sleep(100);
try {
return await mathjaxSVG(tex, plugin);
} catch (e) {
await sleep(100);
try {
return await mathjaxSVG(tex, plugin);
} catch (e) {
if (plugin.mathjax) {
new Notice(
"Unknown error loading LaTeX. Using fallback solution. Try closing and reopening this drawing.",
);
} else {
new Notice(
"LaTeX support did not load. Using fallback solution. Try checking your network connection.",
);
}
//fallback
return await mathjaxImage2html(tex);
const node = html.convert(
Boolean(preamble) ? `${preamble}${tex}` : tex,
{ display: true, scale }
);
const svg = new DOMParser().parseFromString(adaptor.innerHTML(node), "image/svg+xml").firstChild as SVGSVGElement;
if (svg) {
if(svg.width.baseVal.valueInSpecifiedUnits < 2) {
svg.width.baseVal.valueAsString = `${(svg.width.baseVal.valueInSpecifiedUnits+1).toFixed(3)}ex`;
}
const img = svgToBase64(svg.outerHTML);
svg.width.baseVal.valueAsString = (svg.width.baseVal.valueInSpecifiedUnits * 10).toFixed(3);
svg.height.baseVal.valueAsString = (svg.height.baseVal.valueInSpecifiedUnits * 10).toFixed(3);
const dataURL = svgToBase64(svg.outerHTML);
return {
mimeType: "image/svg+xml",
fileId: fileid() as FileId,
dataURL: dataURL as DataURL,
created: Date.now(),
size: await getImageSize(img),
};
}
}
}
export async function mathjaxSVG(
tex: string,
plugin: ExcalidrawPlugin,
): Promise<{
mimeType: MimeType;
fileId: FileId;
dataURL: DataURL;
created: number;
size: { height: number; width: number };
}> {
const eq = plugin.mathjax.tex2svg(tex, { display: true, scale: 4 });
const svg = eq.querySelector("svg");
if (svg) {
if(svg.width.baseVal.valueInSpecifiedUnits < 2) {
svg.width.baseVal.valueAsString = `${(svg.width.baseVal.valueInSpecifiedUnits+1).toFixed(3)}ex`;
}
const dataURL = svgToBase64(svg.outerHTML);
return {
mimeType: "image/svg+xml",
fileId: fileid() as FileId,
dataURL: dataURL as DataURL,
created: Date.now(),
size: await getImageSize(dataURL),
};
} catch (e) {
console.error(e);
}
return null;
}
async function mathjaxImage2html(tex: string): Promise<{
mimeType: MimeType;
fileId: FileId;
dataURL: DataURL;
created: number;
size: { height: number; width: number };
}> {
const div = document.body.createDiv();
div.style.display = "table"; //this will ensure div fits width of formula exactly
//@ts-ignore
const eq = window.MathJax.tex2chtml(tex, { display: true, scale: 4 }); //scale to ensure good resolution
eq.style.margin = "3px";
eq.style.color = "black";
//ipad support - removing mml as that was causing phantom double-image blur.
const el = eq.querySelector("mjx-assistive-mml");
if (el) {
el.parentElement.removeChild(el);
}
div.appendChild(eq);
window.MathJax.typeset();
const canvas = await html2canvas(div, { backgroundColor: null }); //transparent
document.body.removeChild(div);
return {
mimeType: "image/png",
fileId: fileid() as FileId,
dataURL: canvas.toDataURL() as DataURL,
created: Date.now(),
size: { height: canvas.height, width: canvas.width },
};
}
}

View File

@@ -4,7 +4,7 @@ import {
TFile,
Vault,
} from "obsidian";
import { RERENDER_EVENT } from "./constants";
import { RERENDER_EVENT } from "./constants/constants";
import { EmbeddedFilesLoader } from "./EmbeddedFileLoader";
import { createPNG, createSVG } from "./ExcalidrawAutomate";
import { ExportSettings } from "./ExcalidrawView";
@@ -19,18 +19,20 @@ import {
getWithBackground,
hasExportTheme,
convertSVGStringToElement,
isMaskFile,
} from "./utils/Utils";
import { getParentOfClass, isObsidianThemeDark, getFileCSSClasses } from "./utils/ObsidianUtils";
import { linkClickModifierType } from "./utils/ModifierkeyHelper";
import { ImageKey, imageCache } from "./utils/ImageCache";
import { FILENAMEPARTS, PreviewImageType } from "./utils/UtilTypes";
import { CustomMutationObserver, isDebugMode } from "./utils/DebugHelper";
interface imgElementAttributes {
file?: TFile;
fname: string; //Excalidraw filename
fwidth: string; //Display width of image
fheight: string; //Display height of image
style: string; //css style to apply to IMG element
style: string[]; //css style to apply to IMG element
}
let plugin: ExcalidrawPlugin;
@@ -123,8 +125,16 @@ const setStyle = ({element,imgAttributes,onCanvas}:{
style += `height:${imgAttributes.fheight}px;`;
}
if(!onCanvas) element.setAttribute("style", style);
element.addClass(imgAttributes.style);
element.addClass("excalidraw-embedded-img");
element.classList.add(...Array.from(imgAttributes.style))
if(!element.hasClass("excalidraw-embedded-img")) {
element.addClass("excalidraw-embedded-img");
}
if(
window?.ExcalidrawAutomate?.plugin?.settings?.canvasImmersiveEmbed &&
!element.hasClass("excalidraw-canvas-immersive")
) {
element.addClass("excalidraw-canvas-immersive");
}
}
const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSettings,loader}:{
@@ -254,7 +264,7 @@ const getIMG = async (
const filenameParts = getEmbeddedFilenameParts(imgAttributes.fname);
// https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/387
imgAttributes.style = imgAttributes.style.replaceAll(" ", "-");
imgAttributes.style = imgAttributes.style.map(s=>s.replaceAll(" ", "-"));
const forceTheme = hasExportTheme(plugin, file)
? getExportTheme(plugin, file, "light")
@@ -263,6 +273,7 @@ const getIMG = async (
const exportSettings: ExportSettings = {
withBackground: getWithBackground(plugin, file),
withTheme: forceTheme ? true : plugin.settings.exportWithTheme,
isMask: isMaskFile(plugin, file),
};
const theme =
@@ -389,8 +400,9 @@ const createImgElement = async (
fname: fileSource,
fwidth: imgOrDiv.getAttribute("w"),
fheight: imgOrDiv.getAttribute("h"),
style: imgOrDiv.getAttribute("class"),
style: [...Array.from(imgOrDiv.classList)],
}, onCanvas);
if(!newImg) return;
parent.empty();
if(!onCanvas) {
newImg.style.maxHeight = imgMaxHeigth;
@@ -400,7 +412,19 @@ const createImgElement = async (
parent.append(newImg);
});
const cssClasses = getFileCSSClasses(attr.file);
cssClasses.forEach((cssClass) => imgOrDiv.addClass(cssClass));
cssClasses.forEach((cssClass) => {
if(imgOrDiv.hasClass(cssClass)) return;
imgOrDiv.addClass(cssClass);
});
if(window?.ExcalidrawAutomate?.plugin?.settings?.canvasImmersiveEmbed) {
if(!imgOrDiv.hasClass("excalidraw-canvas-immersive")) {
imgOrDiv.addClass("excalidraw-canvas-immersive");
}
} else {
if(imgOrDiv.hasClass("excalidraw-canvas-immersive")) {
imgOrDiv.removeClass("excalidraw-canvas-immersive");
}
}
return imgOrDiv;
}
@@ -409,7 +433,7 @@ const createImageDiv = async (
onCanvas: boolean = false
): Promise<HTMLDivElement> => {
const img = await createImgElement(attr, onCanvas);
return createDiv(attr.style, (el) => el.append(img));
return createDiv(attr.style.join(" "), (el) => el.append(img));
};
const processReadingMode = async (
@@ -451,7 +475,7 @@ const processInternalEmbed = async (internalEmbedEl: Element, file: TFile ):Prom
fname: "",
fheight: "",
fwidth: "",
style: "",
style: [],
};
const src = internalEmbedEl.getAttribute("src");
@@ -468,7 +492,7 @@ const processInternalEmbed = async (internalEmbedEl: Element, file: TFile ):Prom
: getDefaultWidth(plugin);
attr.fheight = internalEmbedEl.getAttribute("height");
let alt = internalEmbedEl.getAttribute("alt");
attr.style = "excalidraw-svg";
attr.style = ["excalidraw-svg"];
processAltText(src.split("#")[0],alt,attr);
const fnameParts = getEmbeddedFilenameParts(src);
attr.fname = file?.path + (fnameParts.hasBlockref||fnameParts.hasSectionref?fnameParts.linkpartReference:"");
@@ -487,14 +511,14 @@ const processAltText = (
attr.fwidth = parts[2] ?? attr.fwidth;
attr.fheight = parts[3] ?? attr.fheight;
if (parts[4] && !parts[4].startsWith(fname)) {
attr.style = `excalidraw-svg${`-${parts[4]}`}`;
attr.style = [`excalidraw-svg${`-${parts[4]}`}`];
}
if (
(!parts[4] || parts[4]==="") &&
(!parts[2] || parts[2]==="") &&
parts[0] && parts[0] !== ""
) {
attr.style = `excalidraw-svg${`-${parts[0]}`}`;
attr.style = [`excalidraw-svg${`-${parts[0]}`}`];
}
}
}
@@ -552,7 +576,7 @@ const tmpObsidianWYSIWYG = async (
fname: ctx.sourcePath,
fheight: "",
fwidth: getDefaultWidth(plugin),
style: "excalidraw-svg",
style: ["excalidraw-svg"],
};
attr.file = file;
@@ -607,7 +631,7 @@ const tmpObsidianWYSIWYG = async (
//timer to avoid the image flickering when the user is typing
let timer: NodeJS.Timeout = null;
const observer = new MutationObserver((m) => {
const markdownObserverFn: MutationCallback = (m) => {
if (!["alt", "width", "height"].contains(m[0]?.attributeName)) {
return;
}
@@ -620,7 +644,10 @@ const tmpObsidianWYSIWYG = async (
const imgDiv = await processInternalEmbed(internalEmbedDiv,file);
internalEmbedDiv.appendChild(imgDiv);
}, 500);
});
}
const observer = isDebugMode
? new CustomMutationObserver(markdownObserverFn, "markdowPostProcessorObserverFn")
: new MutationObserver(markdownObserverFn);
observer.observe(internalEmbedDiv, {
attributes: true, //configure it to listen to attribute changes
});
@@ -672,13 +699,16 @@ export const hoverEvent = (e: any) => {
};
//monitoring for div.popover.hover-popover.file-embed.is-loaded to be added to the DOM tree
export const observer = new MutationObserver(async (m) => {
if (m.length == 0) {
const legacyExcalidrawPopoverObserverFn: MutationCallback = async (m) => {
if (m.length === 0) {
return;
}
if (!plugin.hover.linkText) {
return;
}
if (!plugin.hover.linkText.endsWith("excalidraw")) {
return;
}
const file = metadataCache.getFirstLinkpathDest(
plugin.hover.linkText,
plugin.hover.sourcePath ? plugin.hover.sourcePath : "",
@@ -715,9 +745,7 @@ export const observer = new MutationObserver(async (m) => {
return;
}
if (
//@ts-ignore
!m[0].addedNodes[0].classNames !=
"popover hover-popover file-embed is-loaded"
(m[0].addedNodes[0] as HTMLElement).className !== "popover hover-popover"
) {
return;
}
@@ -731,7 +759,7 @@ export const observer = new MutationObserver(async (m) => {
fname: file.path,
fwidth: "300",
fheight: null,
style: "excalidraw-svg",
style: ["excalidraw-svg"],
});
const div = createDiv("", async (el) => {
el.appendChild(img);
@@ -748,5 +776,9 @@ export const observer = new MutationObserver(async (m) => {
});
});
node.appendChild(div);
});
};
export const legacyExcalidrawPopoverObserver = isDebugMode
? new CustomMutationObserver(legacyExcalidrawPopoverObserverFn, "legacyExcalidrawPopoverObserverFn")
: new MutationObserver(legacyExcalidrawPopoverObserverFn);

View File

@@ -5,7 +5,7 @@ import {
TFile,
WorkspaceLeaf,
} from "obsidian";
import { PLUGIN_ID, VIEW_TYPE_EXCALIDRAW } from "./constants";
import { PLUGIN_ID, VIEW_TYPE_EXCALIDRAW } from "./constants/constants";
import ExcalidrawView from "./ExcalidrawView";
import ExcalidrawPlugin from "./main";
import { ButtonDefinition, GenericInputPrompt, GenericSuggester } from "./dialogs/Prompt";
@@ -43,6 +43,7 @@ export class ScriptEngine {
this.loadScript(scriptFile);
}
};
const deleteEventHandler = async (file: TFile) => {
if (!(file instanceof TFile)) {
return;
@@ -104,7 +105,7 @@ export class ScriptEngine {
public getListofScripts(): TFile[] {
this.scriptPath = this.plugin.settings.scriptFolderPath;
if (!app.vault.getAbstractFileByPath(this.scriptPath)) {
this.scriptPath = null;
//this.scriptPath = null;
return;
}
return app.vault
@@ -262,7 +263,7 @@ export class ScriptEngine {
new Notice(t("SCRIPT_EXECUTION_ERROR"), 4000);
errorlog({ script: this.plugin.ea.activeScript, error: e });
}*/
ea.activeScript = null;
//ea.activeScript = null;
return result;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
import { customAlphabet } from "nanoid";
import { DeviceType } from "./types";
import { ExcalidrawLib } from "./ExcalidrawLib";
import { DeviceType } from "../types";
import { ExcalidrawLib } from "../ExcalidrawLib";
import { moment } from "obsidian";
//This is only for backward compatibility because an early version of obsidian included an encoding to avoid fantom links from littering Obsidian graph view
declare const PLUGIN_VERSION:string;
@@ -139,12 +139,14 @@ export const REG_BLOCK_REF_CLEAN = /[!"#$%&()*+,.:;<=>?@^`{|}~\/\[\]\\\r\n]/g;
// /[!"#$%&()*+,.:;<=>?@^`{|}~\/\[\]\\]/g;
// https://discord.com/channels/686053708261228577/989603365606531104/1000128926619816048
// /\+|\/|~|=|%|\(|\)|{|}|,|&|\.|\$|!|\?|;|\[|]|\^|#|\*|<|>|&|@|\||\\|"|:|\s/g;
export const IMAGE_TYPES = ["jpeg", "jpg", "png", "gif", "svg", "webp", "bmp", "ico"];
export const IMAGE_TYPES = ["jpeg", "jpg", "png", "gif", "svg", "webp", "bmp", "ico", "jtif", "tif"];
export const ANIMATED_IMAGE_TYPES = ["gif", "webp", "apng", "svg"];
export const EXPORT_TYPES = ["svg", "dark.svg", "light.svg", "png", "dark.png", "light.png"];
export const MAX_IMAGE_SIZE = 500;
export const FRONTMATTER_KEY = "excalidraw-plugin";
export const FRONTMATTER_KEY_EXPORT_TRANSPARENT =
"excalidraw-export-transparent";
export const FRONTMATTER_KEY_MASK = "excalidraw-mask";
export const FRONTMATTER_KEY_EXPORT_DARK = "excalidraw-export-dark";
export const FRONTMATTER_KEY_EXPORT_SVGPADDING = "excalidraw-export-svgpadding"; //depricated
export const FRONTMATTER_KEY_EXPORT_PADDING = "excalidraw-export-padding";

View File

@@ -0,0 +1,116 @@
/*
#exclude
```js*/
/**
* If set, this callback is triggered when the user closes an Excalidraw view.
* onViewUnloadHook: (view: ExcalidrawView) => void = null;
*/
//ea.onViewUnloadHook = (view) => {};
/**
* If set, this callback is triggered, when the user changes the view mode.
* You can use this callback in case you want to do something additional when the user switches to view mode and back.
* onViewModeChangeHook: (isViewModeEnabled:boolean, view: ExcalidrawView, ea: ExcalidrawAutomate) => void = null;
*/
//ea.onViewModeChangeHook = (isViewModeEnabled, view, ea) => {};
/**
* If set, this callback is triggered, when the user hovers a link in the scene.
* You can use this callback in case you want to do something additional when the onLinkHover event occurs.
* This callback must return a boolean value.
* In case you want to prevent the excalidraw onLinkHover action you must return false, it will stop the native excalidraw onLinkHover management flow.
* onLinkHoverHook: (
* element: NonDeletedExcalidrawElement,
* linkText: string,
* view: ExcalidrawView,
* ea: ExcalidrawAutomate
* ) => boolean = null;
*/
//ea.onLinkHoverHook = (element, linkText, view, ea) => {};
/**
* If set, this callback is triggered, when the user clicks a link in the scene.
* You can use this callback in case you want to do something additional when the onLinkClick event occurs.
* This callback must return a boolean value.
* In case you want to prevent the excalidraw onLinkClick action you must return false, it will stop the native excalidraw onLinkClick management flow.
* onLinkClickHook:(
* element: ExcalidrawElement,
* linkText: string,
* event: MouseEvent,
* view: ExcalidrawView,
* ea: ExcalidrawAutomate
* ) => boolean = null;
*/
//ea.onLinkClickHook = (element,linkText,event, view, ea) => {};
/**
* If set, this callback is triggered, when Excalidraw receives an onDrop event.
* You can use this callback in case you want to do something additional when the onDrop event occurs.
* This callback must return a boolean value.
* In case you want to prevent the excalidraw onDrop action you must return false, it will stop the native excalidraw onDrop management flow.
* onDropHook: (data: {
* ea: ExcalidrawAutomate;
* event: React.DragEvent<HTMLDivElement>;
* draggable: any; //Obsidian draggable object
* type: "file" | "text" | "unknown";
* payload: {
* files: TFile[]; //TFile[] array of dropped files
* text: string; //string
* };
* excalidrawFile: TFile; //the file receiving the drop event
* view: ExcalidrawView; //the excalidraw view receiving the drop
* pointerPosition: { x: number; y: number }; //the pointer position on canvas at the time of drop
* }) => boolean = null;
*/
//ea.onDropHook = (data) => {};
/**
* If set, this callback is triggered, when Excalidraw receives an onPaste event.
* You can use this callback in case you want to do something additional when the
* onPaste event occurs.
* This callback must return a boolean value.
* In case you want to prevent the excalidraw onPaste action you must return false,
* it will stop the native excalidraw onPaste management flow.
* onPasteHook: (data: {
* ea: ExcalidrawAutomate;
* payload: ClipboardData;
* event: ClipboardEvent;
* excalidrawFile: TFile; //the file receiving the paste event
* view: ExcalidrawView; //the excalidraw view receiving the paste
* pointerPosition: { x: number; y: number }; //the pointer position on canvas
* }) => boolean = null;
*/
//ea.onPasteHook = (data) => {};
/**
* if set, this callback is triggered, when an Excalidraw file is opened
* You can use this callback in case you want to do something additional when the file is opened.
* This will run before the file level script defined in the `excalidraw-onload-script` frontmatter.
* onFileOpenHook: (data: {
* ea: ExcalidrawAutomate;
* excalidrawFile: TFile; //the file being loaded
* view: ExcalidrawView;
* }) => Promise<void>;
*/
//ea.onFileOpenHook = (data) => {};
/**
* if set, this callback is triggered, when an Excalidraw file is created
* see also: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1124
* onFileCreateHook: (data: {
* ea: ExcalidrawAutomate;
* excalidrawFile: TFile; //the file being created
* view: ExcalidrawView;
* }) => Promise<void>;
*/
//ea.onFileCreateHook = (data) => {};
/**
* If set, this callback is triggered whenever the active canvas color changes
* onCanvasColorChangeHook: (
* ea: ExcalidrawAutomate,
* view: ExcalidrawView, //the excalidraw view
* color: string,
* ) => void = null;
*/
//ea.onCanvasColorChangeHook = (ea, view, color) => {};

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +1,13 @@
import { NonDeletedExcalidrawElement } from "@zsviczian/excalidraw/types/element/types";
import { NonDeletedExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import ExcalidrawView from "./ExcalidrawView";
import { Notice, WorkspaceLeaf, WorkspaceSplit } from "obsidian";
import * as React from "react";
import { ConstructableWorkspaceSplit, getContainerForDocument, isObsidianThemeDark } from "./utils/ObsidianUtils";
import { DEVICE, EXTENDED_EVENT_TYPES, KEYBOARD_EVENT_TYPES } from "./constants";
import { ExcalidrawImperativeAPI, UIAppState } from "@zsviczian/excalidraw/types/types";
import { DEVICE, EXTENDED_EVENT_TYPES, KEYBOARD_EVENT_TYPES } from "./constants/constants";
import { ExcalidrawImperativeAPI, UIAppState } from "@zsviczian/excalidraw/types/excalidraw/types";
import { ObsidianCanvasNode } from "./utils/CanvasNodeFactory";
import { processLinkText, patchMobileView } from "./utils/CustomEmbeddableUtils";
import { EmbeddableMDCustomProps } from "./dialogs/EmbeddableSettings";
declare module "obsidian" {
interface Workspace {
@@ -18,13 +19,22 @@ declare module "obsidian" {
}
}
const getTheme = (view: ExcalidrawView, theme:string): string => view.excalidrawData.embeddableTheme === "dark"
? "theme-dark"
: view.excalidrawData.embeddableTheme === "light"
? "theme-light"
: view.excalidrawData.embeddableTheme === "auto"
? theme === "dark" ? "theme-dark" : "theme-light"
: isObsidianThemeDark() ? "theme-dark" : "theme-light";
//--------------------------------------------------------------------------------
//Render webview for anything other than Vimeo and Youtube
//Vimeo and Youtube are rendered by Excalidraw because of the window messaging
//required to control the video
//--------------------------------------------------------------------------------
export const renderWebView = (src: string, view: ExcalidrawView, id: string, appState: UIAppState):JSX.Element =>{
if(DEVICE.isDesktop) {
const isDataURL = src.startsWith("data:");
if(DEVICE.isDesktop && !isDataURL) {
return (
<webview
ref={(ref) => view.updateEmbeddableRef(id, ref)}
@@ -46,11 +56,12 @@ export const renderWebView = (src: string, view: ExcalidrawView, id: string, app
title="Excalidraw Embedded Content"
allowFullScreen={true}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
src={src}
src={isDataURL ? null : src}
style={{
overflow: "hidden",
borderRadius: "var(--embeddable-radius)",
}}
srcDoc={isDataURL ? atob(src.split(',')[1]) : null}
/>
);
}
@@ -59,13 +70,15 @@ export const renderWebView = (src: string, view: ExcalidrawView, id: string, app
//Render WorkspaceLeaf or CanvasNode
//--------------------------------------------------------------------------------
function RenderObsidianView(
{ element, linkText, view, containerRef, appState, theme }:{
{ mdProps, element, linkText, view, containerRef, activeEmbeddable, theme, canvasColor }:{
mdProps: EmbeddableMDCustomProps;
element: NonDeletedExcalidrawElement;
linkText: string;
view: ExcalidrawView;
containerRef: React.RefObject<HTMLDivElement>;
appState: UIAppState;
activeEmbeddable: {element: NonDeletedExcalidrawElement; state: string};
theme: string;
canvasColor: string;
}): JSX.Element {
const { subpath, file } = processLinkText(linkText, view);
@@ -79,8 +92,19 @@ function RenderObsidianView(
const leafRef = react.useRef<{leaf: WorkspaceLeaf; node?: ObsidianCanvasNode} | null>(null);
const isEditingRef = react.useRef(false);
const isActiveRef = react.useRef(false);
const themeRef = react.useRef(theme);
const elementRef = react.useRef(element);
// Update themeRef when theme changes
react.useEffect(() => {
themeRef.current = theme;
}, [theme]);
// Update elementRef when element changes
react.useEffect(() => {
elementRef.current = element;
}, [element]);
//--------------------------------------------------------------------------------
//block propagation of events to the parent if the iframe element is active
//--------------------------------------------------------------------------------
@@ -192,6 +216,7 @@ function RenderObsidianView(
//This runs only when the file is added, thus should not be a major performance issue
await leafRef.current.leaf.setViewState({state: {file:null}})
leafRef.current.node = view.canvasNodeFactory.createFileNote(file, subpath, containerRef.current, element.id);
setColors(containerRef.current, element, mdProps, canvasColor);
} else {
const workspaceLeaf:HTMLDivElement = rootSplit.containerEl.querySelector("div.workspace-leaf");
if(workspaceLeaf) workspaceLeaf.style.borderRadius = "var(--embeddable-radius)";
@@ -205,9 +230,84 @@ function RenderObsidianView(
return () => {}; //cleanup on unmount
}, [linkText, subpath, containerRef]);
const setColors = (canvasNode: HTMLDivElement, element: NonDeletedExcalidrawElement, mdProps: EmbeddableMDCustomProps, canvasColor: string) => {
if(!mdProps) return;
if (!leafRef.current?.hasOwnProperty("node")) return;
const canvasNodeContainer = containerRef.current?.firstElementChild as HTMLElement;
if(mdProps.useObsidianDefaults) {
canvasNode?.style.removeProperty("--canvas-background");
canvasNodeContainer?.style.removeProperty("background-color");
canvasNode?.style.removeProperty("--canvas-border");
canvasNodeContainer?.style.removeProperty("border-color");
return;
}
const ea = view.plugin.ea;
if(mdProps.backgroundMatchElement) {
const opacity = (mdProps?.backgroundOpacity ?? 50)/100;
const color = element?.backgroundColor
? (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
? (canvasColor.toLowerCase() === "transparent"
? "transparent"
: ea.getCM(canvasColor).alphaTo(opacity).stringHEX())
: ea.getCM(mdProps.backgroundColor).alphaTo((mdProps.backgroundOpacity??100)/100).stringHEX();
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
? (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);
}
}
react.useEffect(() => {
if(!containerRef.current) {
return;
}
const element = elementRef.current;
const canvasNode = containerRef.current;
if(!canvasNode.hasClass("canvas-node")) return;
setColors(canvasNode, element, mdProps, canvasColor);
}, [
mdProps,
elementRef.current,
containerRef.current,
canvasColor,
])
react.useEffect(() => {
if(isEditingRef.current) {
if(leafRef.current?.node) {
containerRef.current?.addClasses(["is-editing", "is-focused"]);
view.canvasNodeFactory.stopEditing(leafRef.current.node);
}
isEditingRef.current = false;
@@ -242,10 +342,12 @@ function RenderObsidianView(
patchMobileView(view);
} else if (leafRef.current?.node) {
//Handle canvas node
view.canvasNodeFactory.startEditing(leafRef.current.node, theme);
const newTheme = getTheme(view, themeRef.current);
containerRef.current?.addClasses(["is-editing", "is-focused"]);
view.canvasNodeFactory.startEditing(leafRef.current.node, newTheme);
}
}
}, [leafRef.current?.leaf, element.id]);
}, [leafRef.current?.leaf, element.id, view, themeRef.current]);
//--------------------------------------------------------------------------------
// Set isActiveRef and switch to preview mode when the iframe is not active
@@ -256,7 +358,7 @@ function RenderObsidianView(
}
const previousIsActive = isActiveRef.current;
isActiveRef.current = (appState.activeEmbeddable?.element.id === element.id) && (appState.activeEmbeddable?.state === "active");
isActiveRef.current = (activeEmbeddable?.element.id === element.id) && (activeEmbeddable?.state === "active");
if (previousIsActive === isActiveRef.current) {
return;
@@ -278,20 +380,17 @@ function RenderObsidianView(
}
} else if (leafRef.current?.node) {
//Handle canvas node
containerRef.current?.removeClasses(["is-editing", "is-focused"]);
view.canvasNodeFactory.stopEditing(leafRef.current.node);
}
}, [
containerRef,
leafRef,
isActiveRef,
appState.activeEmbeddable?.element,
appState.activeEmbeddable?.state,
activeEmbeddable?.element,
activeEmbeddable?.state,
element,
view,
linkText,
subpath,
file,
theme,
isEditingRef,
view.canvasNodeFactory
]);
@@ -299,16 +398,12 @@ function RenderObsidianView(
return null;
};
export const CustomEmbeddable: React.FC<{element: NonDeletedExcalidrawElement; view: ExcalidrawView; appState: UIAppState; linkText: string}> = ({ element, view, appState, linkText }) => {
const react = view.plugin.getPackage(view.ownerWindow).react;
const containerRef: React.RefObject<HTMLDivElement> = react.useRef(null);
const theme = view.excalidrawData.embeddableTheme === "dark"
? "theme-dark"
: view.excalidrawData.embeddableTheme === "light"
? "theme-light"
: view.excalidrawData.embeddableTheme === "auto"
? appState.theme === "dark" ? "theme-dark" : "theme-light"
: isObsidianThemeDark() ? "theme-dark" : "theme-light";
const theme = getTheme(view, appState.theme);
const mdProps: EmbeddableMDCustomProps = element.customData?.mdProps || null;
return (
<div
@@ -319,15 +414,19 @@ export const CustomEmbeddable: React.FC<{element: NonDeletedExcalidrawElement; v
borderRadius: "var(--embeddable-radius)",
color: `var(--text-normal)`,
}}
className={theme}
className={`${theme} canvas-node ${
mdProps?.filenameVisible && !mdProps.useObsidianDefaults ? "" : "excalidraw-mdEmbed-hideFilename"}`}
>
<RenderObsidianView
mdProps={mdProps}
element={element}
linkText={linkText}
view={view}
containerRef={containerRef}
appState={appState}
theme={theme}/>
activeEmbeddable={appState.activeEmbeddable}
theme={appState.theme}
canvasColor={appState.viewBackgroundColor}
/>
</div>
)
}

View File

@@ -0,0 +1,179 @@
import { Setting, ToggleComponent } from "obsidian";
import { EmbeddableMDCustomProps } from "./EmbeddableSettings";
import { fragWithHTML } from "src/utils/Utils";
import { t } from "src/lang/helpers";
export class EmbeddalbeMDFileCustomDataSettingsComponent {
constructor (
private contentEl: HTMLElement,
private mdCustomData: EmbeddableMDCustomProps,
private update?: Function,
private isMDFile: boolean = true,
) {
if(!update) this.update = () => {};
}
render() {
let detailsDIV: HTMLDivElement;
new Setting(this.contentEl)
.setName(t("ES_USE_OBSIDIAN_DEFAULTS"))
.addToggle(toggle =>
toggle
.setValue(this.mdCustomData.useObsidianDefaults)
.onChange(value => {
this.mdCustomData.useObsidianDefaults = value;
detailsDIV.style.display = value ? "none" : "block";
this.update();
})
);
this.contentEl.createEl("hr", { cls: "excalidraw-setting-hr" });
detailsDIV = this.contentEl.createDiv();
detailsDIV.style.display = this.mdCustomData.useObsidianDefaults ? "none" : "block";
const contentEl = detailsDIV
if(this.isMDFile) {
new Setting(contentEl)
.setName(t("ES_FILENAME_VISIBLE"))
.addToggle(toggle =>
toggle
.setValue(this.mdCustomData.filenameVisible)
.onChange(value => {
this.mdCustomData.filenameVisible = value;
})
);
}
contentEl.createEl("h4",{text: t("ES_BACKGROUND_HEAD")});
let bgSetting: Setting;
let bgMatchElementToggle: ToggleComponent;
let bgMatchCanvasToggle: ToggleComponent;
new Setting(contentEl)
.setName(t("ES_BACKGROUND_MATCH_ELEMENT"))
.addToggle(toggle => {
bgMatchElementToggle = toggle;
toggle
.setValue(this.mdCustomData.backgroundMatchElement)
.onChange(value => {
this.mdCustomData.backgroundMatchElement = value;
if(value) {
bgSetting.settingEl.style.display = "none";
if(this.mdCustomData.backgroundMatchCanvas) {
bgMatchCanvasToggle.setValue(false);
}
} else {
if(!this.mdCustomData.backgroundMatchCanvas) {
bgSetting.settingEl.style.display = "";
}
}
this.update();
})
});
new Setting(contentEl)
.setName(t("ES_BACKGROUND_MATCH_CANVAS"))
.addToggle(toggle => {
bgMatchCanvasToggle = toggle;
toggle
.setValue(this.mdCustomData.backgroundMatchCanvas)
.onChange(value => {
this.mdCustomData.backgroundMatchCanvas = value;
if(value) {
bgSetting.settingEl.style.display = "none";
if(this.mdCustomData.backgroundMatchElement) {
bgMatchElementToggle.setValue(false);
}
} else {
if(!this.mdCustomData.backgroundMatchElement) {
bgSetting.settingEl.style.display = "";
}
}
this.update();
})
});
if(this.mdCustomData.backgroundMatchElement && this.mdCustomData.backgroundMatchCanvas) {
bgMatchCanvasToggle.setValue(false);
}
bgSetting = new Setting(contentEl)
.setName(t("ES_BACKGROUND_COLOR"))
.addColorPicker(colorPicker =>
colorPicker
.setValue(this.mdCustomData.backgroundColor)
.onChange((value) => {
this.mdCustomData.backgroundColor = value;
this.update();
})
);
bgSetting.settingEl.style.display = (this.mdCustomData.backgroundMatchElement || this.mdCustomData.backgroundMatchCanvas) ? "none" : "";
const opacity = (value:number):DocumentFragment => {
return fragWithHTML(`Current opacity is <b>${value}%</b>`);
}
const bgOpacitySetting = new Setting(contentEl)
.setName(t("ES_BACKGROUND_OPACITY"))
.setDesc(opacity(this.mdCustomData.backgroundOpacity))
.addSlider(slider =>
slider
.setLimits(0,100,5)
.setValue(this.mdCustomData.backgroundOpacity)
.onChange(value => {
this.mdCustomData.backgroundOpacity = value;
bgOpacitySetting.setDesc(opacity(value));
this.update();
})
);
if(this.isMDFile) {
contentEl.createEl("h4",{text: t("ES_BORDER_HEAD")});
let borderSetting: Setting;
new Setting(contentEl)
.setName(t("ES_BORDER_MATCH_ELEMENT"))
.addToggle(toggle =>
toggle
.setValue(this.mdCustomData.borderMatchElement)
.onChange(value => {
this.mdCustomData.borderMatchElement = value;
if(value) {
borderSetting.settingEl.style.display = "none";
} else {
borderSetting.settingEl.style.display = "";
}
this.update();
})
);
borderSetting = new Setting(contentEl)
.setName(t("ES_BORDER_COLOR"))
.addColorPicker(colorPicker =>
colorPicker
.setValue(this.mdCustomData.borderColor)
.onChange((value) => {
this.mdCustomData.borderColor = value;
this.update();
})
);
borderSetting.settingEl.style.display = this.mdCustomData.borderMatchElement ? "none" : "";
const borderOpacitySetting = new Setting(contentEl)
.setName(t("ES_BORDER_OPACITY"))
.setDesc(opacity(this.mdCustomData.borderOpacity))
.addSlider(slider =>
slider
.setLimits(0,100,5)
.setValue(this.mdCustomData.borderOpacity)
.onChange(value => {
this.mdCustomData.borderOpacity = value;
borderOpacitySetting.setDesc(opacity(value));
this.update();
})
);
}
}
}

View File

@@ -0,0 +1,220 @@
import { ExcalidrawEmbeddableElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import { Modal, Notice, Setting, TFile, ToggleComponent } from "obsidian";
import { getEA } from "src";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import ExcalidrawView from "src/ExcalidrawView";
import { t } from "src/lang/helpers";
import ExcalidrawPlugin from "src/main";
import { getNewUniqueFilepath, getPathWithoutExtension, splitFolderAndFilename } from "src/utils/FileUtils";
import { addAppendUpdateCustomData, fragWithHTML } from "src/utils/Utils";
import { getYouTubeStartAt, isValidYouTubeStart, isYouTube, updateYouTubeStartTime } from "src/utils/YoutTubeUtils";
import { EmbeddalbeMDFileCustomDataSettingsComponent } from "./EmbeddableMDFileCustomDataSettingsComponent";
import { isWinCTRLorMacCMD } from "src/utils/ModifierkeyHelper";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
export type EmbeddableMDCustomProps = {
useObsidianDefaults: boolean;
backgroundMatchCanvas: boolean;
backgroundMatchElement: boolean;
backgroundColor: string;
backgroundOpacity: number;
borderMatchElement: boolean;
borderColor: string;
borderOpacity: number;
filenameVisible: boolean;
}
export class EmbeddableSettings extends Modal {
private ea: ExcalidrawAutomate;
private updatedFilepath: string = null;
private zoomValue: number;
private isYouTube: boolean;
private youtubeStart: string = null;
private isMDFile: boolean;
private notExcalidrawIsInternal: boolean;
private isLocalURI: boolean;
private mdCustomData: EmbeddableMDCustomProps;
private onKeyDown: (ev: KeyboardEvent) => void;
constructor(
private plugin: ExcalidrawPlugin,
private view: ExcalidrawView,
private file: TFile,
private element: ExcalidrawEmbeddableElement
) {
super(plugin.app);
this.ea = getEA(this.view);
this.ea.copyViewElementsToEAforEditing([this.element]);
this.zoomValue = element.scale[0];
this.isYouTube = isYouTube(this.element.link);
this.notExcalidrawIsInternal = this.file && !this.view.plugin.isExcalidrawFile(this.file)
this.isMDFile = this.file && this.file.extension === "md" && !this.view.plugin.isExcalidrawFile(this.file);
this.isLocalURI = this.element.link.startsWith("file://");
if(isYouTube) this.youtubeStart = getYouTubeStartAt(this.element.link);
this.mdCustomData = element.customData?.mdProps ?? view.plugin.settings.embeddableMarkdownDefaults;
if(!element.customData?.mdProps) {
const bgCM = this.plugin.ea.getCM(element.backgroundColor);
this.mdCustomData.backgroundColor = bgCM.stringHEX({alpha: false});
this.mdCustomData.backgroundOpacity = element.opacity;
const borderCM = this.plugin.ea.getCM(element.strokeColor);
this.mdCustomData.borderColor = borderCM.stringHEX({alpha: false});
this.mdCustomData.borderOpacity = element.opacity;
}
}
onOpen(): void {
this.containerEl.classList.add("excalidraw-release");
//this.titleEl.setText(t("ES_TITLE"));
this.createForm();
}
onClose() {
this.containerEl.removeEventListener("keydown",this.onKeyDown);
}
async createForm() {
this.contentEl.createEl("h1",{text: t("ES_TITLE")});
if(this.file) {
new Setting(this.contentEl)
.setName(t("ES_RENAME"))
.addText(text =>
text
.setValue(getPathWithoutExtension(this.file))
.onChange(async (value) => {
this.updatedFilepath = value;
})
)
}
const zoomValue = ():DocumentFragment => {
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)
.setValue(this.zoomValue*100)
.onChange(value => {
this.zoomValue = value/100;
zoomSetting.setDesc(zoomValue());
})
)
if(this.isYouTube) {
new Setting(this.contentEl)
.setName(t("ES_YOUTUBE_START"))
.setDesc(t("ES_YOUTUBE_START_DESC"))
.addText(text =>
text
.setValue(this.youtubeStart)
.onChange(async (value) => {
this.youtubeStart = value;
})
)
}
if(this.isMDFile || this.notExcalidrawIsInternal) {
this.contentEl.createEl("h3",{text: t("ES_EMBEDDABLE_SETTINGS")});
new EmbeddalbeMDFileCustomDataSettingsComponent(this.contentEl,this.mdCustomData, undefined, this.isMDFile).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 onKeyDown = (ev: KeyboardEvent) => {
if(isWinCTRLorMacCMD(ev) && ev.key === "Enter") {
this.applySettings();
}
}
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

@@ -1,7 +1,7 @@
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
import { Modal, Setting, TFile } from "obsidian";
import { getEA } from "src";
import { DEVICE } from "src/constants";
import { DEVICE } from "src/constants/constants";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import ExcalidrawView from "src/ExcalidrawView";
import ExcalidrawPlugin from "src/main";

View File

@@ -1,8 +1,10 @@
import { App, FuzzySuggestModal, TFile } from "obsidian";
import { REG_LINKINDEX_INVALIDCHARS } from "../constants";
import { REG_LINKINDEX_INVALIDCHARS } from "../constants/constants";
import ExcalidrawView from "../ExcalidrawView";
import { t } from "../lang/helpers";
import ExcalidrawPlugin from "../main";
import { getEA } from "src";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
export class ImportSVGDialog extends FuzzySuggestModal<TFile> {
public app: App;
@@ -38,12 +40,11 @@ export class ImportSVGDialog extends FuzzySuggestModal<TFile> {
async onChooseItem(item: TFile, event: KeyboardEvent): Promise<void> {
if(!item) return;
const ea = this.plugin.ea;
ea.reset();
ea.setView(this.view);
const ea = getEA(this.view) as ExcalidrawAutomate;
const svg = await app.vault.read(item);
if(!svg || svg === "") return;
ea.importSVG(svg);
ea.addToGroup(ea.getElements().map(el=>el.id));
ea.addElementsToView(true, true, true,true);
}

View File

@@ -1,5 +1,5 @@
import { App, FuzzySuggestModal, TFile } from "obsidian";
import { REG_LINKINDEX_INVALIDCHARS } from "../constants";
import { REG_LINKINDEX_INVALIDCHARS } from "../constants/constants";
import { t } from "../lang/helpers";
export class InsertCommandDialog extends FuzzySuggestModal<TFile> {

View File

@@ -1,7 +1,6 @@
import { App, FuzzySuggestModal, TFile } from "obsidian";
import { isALT, scaleToFullsizeModifier } from "src/utils/ModifierkeyHelper";
import { fileURLToPath } from "url";
import { DEVICE, IMAGE_TYPES, REG_LINKINDEX_INVALIDCHARS } from "../constants";
import { scaleToFullsizeModifier } from "src/utils/ModifierkeyHelper";
import { DEVICE, IMAGE_TYPES, REG_LINKINDEX_INVALIDCHARS } from "../constants/constants";
import ExcalidrawView from "../ExcalidrawView";
import { t } from "../lang/helpers";
import ExcalidrawPlugin from "../main";
@@ -60,6 +59,7 @@ export class InsertImageDialog extends FuzzySuggestModal<TFile> {
ea.canvas.theme = this.view.excalidrawAPI.getAppState().theme;
const scaleToFullsize = scaleToFullsizeModifier(event);
(async () => {
//this.view.currentPosition = this.position;
await ea.addImage(0, 0, item, !scaleToFullsize);
ea.addElementsToView(true, true, true);
})();

View File

@@ -1,15 +1,17 @@
import { App, FuzzySuggestModal, TFile } from "obsidian";
import { REG_LINKINDEX_INVALIDCHARS } from "../constants";
import { REG_LINKINDEX_INVALIDCHARS } from "../constants/constants";
import { t } from "../lang/helpers";
import ExcalidrawPlugin from "src/main";
import { getLink } from "src/utils/FileUtils";
export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
public app: App;
private addText: Function;
private drawingPath: string;
constructor(app: App) {
super(app);
this.app = app;
constructor(private plugin: ExcalidrawPlugin) {
super(plugin.app);
this.app = plugin.app;
this.limit = 20;
this.setInstructions([
{
@@ -45,7 +47,8 @@ export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
true,
);
}
this.addText(`[[${filepath + (item.alias ? `|${item.alias}` : "")}]]`, filepath, item.alias);
const link = getLink(this.plugin,{embed: false, path: filepath, alias: item.alias});
this.addText(getLink(this.plugin,{embed: false, path: filepath, alias: item.alias}), filepath, item.alias);
}
public start(drawingPath: string, addText: Function) {

View File

@@ -6,7 +6,7 @@ import { Modal, Setting, TextComponent } from "obsidian";
import { FileSuggestionModal } from "./FolderSuggester";
import { getEA } from "src";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
export class InsertPDFModal extends Modal {
private borderBox: boolean = true;

View File

@@ -17,6 +17,233 @@ 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.17":`
## Fixed
- Image cropping now supports dark mode
- Image cropping/carve out was not working reliably in some cases [#1546](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1546)
- Masking a mirrored image resulted in an off-positioned mask
## New
- Context menu action to convert SVG to Excalidraw strokes
- Updated Chinese translation (Thank you @tswwe)
`,
"2.0.16":`
## Fixed
- Image cropping did not work consistently with large image files on lower-powered devices [#1538](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1538).
- Mermaid editor was not working when Excalidraw was open in an Obsidian popout window [#1503](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1503)
`,
"2.0.15":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/uHFd0XoHRxE" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## New
- Crop and Mask Images in Excalidraw, Canvas, and Markdown. (Inspired by @bonecast [#4566](https://github.com/excalidraw/excalidraw/issues/4566))
- Draw metadata around images but hide it on the export.
## Fixed
- Freedraw closed circles (2nd attempt)
- Interactive Markdown embeddable border-color (setting did not have an effect)
`,
"2.0.14":`
## New
- Stylus button now activates the eraser function. Note: This feature is supported for styluses that comply with industry-standard button events. Unfortunately, Samsung SPEN and Apple Pencil do not support this functionality.
## Fixed
- Improved handwriting quality. I have resolved the long-standing issue of closing the loop when ends of the line are close, making an "u" into an "o" ([#1529](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1529) and [#6303](https://github.com/excalidraw/excalidraw/issues/6303)).
- Improved Excalidraw's full-screen mode behavior. Access it via the Obsidian Command Palette or the full-screen button on the Obsidian Tools Panel ([#1528](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1528)).
- Fixed color picker overlapping with the Obsidian mobile toolbar on Obsidian-Mobile.
- Corrected display issues with alternative font sizes (Fibonacci and Zoom relative) in the element properties panel when editing a text element (refer to [2.0.11 Release Notes](https://github.com/zsviczian/obsidian-excalidraw-plugin/releases/tag/2.0.11) for details about the font-size Easter Egg).
- Resolved the issue where Excalidraw SVG exports containing LaTeX were not loading correctly into Inkscape ([#1519](https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/1519)). Thanks to 🙏@HyunggyuJang for the contribution.
`,
"2.0.13":`
## Fixed
- Excalidraw crashes if you paste an image and right-click on canvas immediately after pasting.
`,
"2.0.12":`
## Fixed
- Stencil library not working [#1516](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1516), [#1517](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1517)
- The new convert image from URL to Local File feature did not work in two situations:
- When the embedded image is downloaded from a very slow server (e.g. OpenAIs temp image server)
- On Android
- The postToOpenAI function did not work in all situations on Android.
- ExcaliAI wireframe to code did not display correctly on Android
- Tooltips kept popping up on Android.
## New
- Added "Save image from URL to local file" to the right-click context menu
- Further [ExcaliAI](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/ExcaliAI.md) improvements including support for image editing with image mask
`,
"2.0.11":`
## Fixed
- Resolved an Obsidian performance issue caused by simultaneous installations of Excalidraw and the Minimal theme. Optimized Excalidraw CSS loading into Obsidian since April 2021, resulting in noticeable performance improvements. ([#1456](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1456))
- Removed default support for the [Sliding Panes Plugin](https://github.com/deathau/sliding-panes-obsidian) due to compatibility issues with Obsidian Workspaces. Obsidian's "Stack Tabs" feature now supersedes Sliding Panes. To re-enable sliding panes support, navigate to Compatibility Features in Plugin Settings.
- Sometimes images referenced with URLs did not show in exported scenes and when embedding Excalidraw into a markdown note. I hope all that is now resolved.
- ExcalidrawAutomate scripts sometimes were not able to save their settings.
## New
- Introduced an "Easter Egg" feature in font-size properties:
- Hold SHIFT while selecting font size to use scaled sizes (S, M, L, XL) based on the current canvas zoom, ensuring consistent sizes within zoom ranges.
- Hold ALT/OPT while selecting font size to use values based on the golden mean (s:16, m:26, l:42, xl:68). ALT+SHIFT scales font sizes based on canvas zoom.
- Scaled sizes are sticky; new text elements adjust font sizes relative to the canvas zoom. Deselect SHIFT to disable this feature.
- For more on the Golden Scale, watch [The Golden Opportunity](https://youtu.be/2SHn_ruax-s).
- Added two new Command Palette Actions:
- "Decompress current Excalidraw File" in Markdown View mode helps repair corrupted, compressed Excalidraw files manually.
- "Save image from URL to local file" saves referenced URL images to your Vault, replacing images in the drawing.
- Updated the ExcaliAI script to generate images using ExcaliAI.
## New in ExcalidrawAutomate
- Added additional documentation about functions to ea.suggester.
- Added ea.help(). You can use this function from Developer Console to print help information about functions. Usage: ${String.fromCharCode(96)}ea.help(ea.functionName)${String.fromCharCode(96)} or ${String.fromCharCode(96)}ea.help('propertyName')${String.fromCharCode(96)} - notice property name is in quotes.
`,
"2.0.10":`
One more minor tweak to support an updated ExcaliAI script - now available in the script store.
`,
"2.0.9":`
This release is very minor, and I apologize for the frequent updates in a short span. I chose not to delay this fix for 1-2 weeks, waiting for my larger release. The WireframeToAI feature wasn't working in 2.0.8, but now it does.
`,
"2.0.8":`
## New
- Mermaid Class Diagrams [#7381](https://github.com/excalidraw/excalidraw/pull/7381)
- New Scripts:
- Repeat Texts contributed by @soraliu [#1425](https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/1425)
- Relative Font Size Cycle [#1474](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1474)
- New setting to configure the URL used to reach the OpenAI API - for setting an OpenAI API compatible local LLM URL.
## Fixed
- web images with jpeg extension were not displayed. [#1486](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1486)
- MathJax was causing errors on the file in the active editor when starting Obsidian or starting the Excalidraw Plugin. I reworked the MathJax implementation from the ground up. [#1484](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1484), [#1473](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1473)
- Enhanced performance for resizing sticky notes (resize + ALT) on slower devices when centrally adjusting their size.
## New in ExcalidrawAutomate:
- New ArrowHead types. Currently only available programmatically and when converting Mermaid Class Diagrams into Excalidraw Objects:
${String.fromCharCode(96,96,96)}ts
addArrow(
points: [x: number, y: number][],
formatting?: {
startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
startObjectId?: string;
endObjectId?: string;
},
): string;
connectObjects(
objectA: string,
connectionA: ConnectionPoint | null,
objectB: string,
connectionB: ConnectionPoint | null,
formatting?: {
numberOfPoints?: number;
startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
padding?: number;
},
): string;
connectObjectWithViewSelectedElement(
objectA: string,
connectionA: ConnectionPoint | null,
connectionB: ConnectionPoint | null,
formatting?: {
numberOfPoints?: number;
startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
padding?: number;
},
): boolean;
${String.fromCharCode(96,96,96)}
`,
"2.0.7":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/kp1K7GRrE6E" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
# Fixed
- The Android and iOS crash with 2.0.5 😰. I can't apologize enough for releasing a version that I did not properly test on Android and iOS. That ought to teach me something about last-minute changes before hitting release.
- Scaled-resizing a sticky note (SHIFT+resize) caused Excalidraw to choke on slower devices
- Improved plugin performance focusing on minimizing Excalidraw's effect on Obsidian overall
- Images embedded with a URL often did not show up in image exports, hopefully, the issue will less frequently occur in the future.
- Local file URL now follows Obsidian standard - making it easier to navigate in Markdown view mode.
# New
- Bonus feature compared to 2.0.4: Second-order links when clicking embedded images. I use images to connect ideas. Clicking on an image and seeing all the connections immediately is very powerful.
- In plugin settings, under "Startup Script", the button now opens the startup script if it already exists.
- Partial support for animated GIFs (will not show up in image exports, but can be added as interactive embeddables)
- Configurable modifier keys for link click action and drag&drop actions.
- Improved support for drag&drop from your local drive and embedding of files external to Excalidraw.
`,
"2.0.5":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/kp1K7GRrE6E" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
# Fixed
- Scaled-resizing a sticky note (SHIFT+resize) caused Excalidraw to choke on slower devices
- Improved plugin performance focusing on minimizing Excalidraw's effect on Obsidian overall
- Images embedded with a URL often did not show up in image exports, hopefully, the issue will less frequently occur in the future.
- Local file URL now follows Obsidian standard - making it easier to navigate in Markdown view mode.
# New
- In plugin settings, under "Startup Script", the button now opens the startup script if it already exists.
- Partial support for animated GIFs (will not show up in image exports, but can be added as interactive embeddables)
- Configurable modifier keys for link click action and drag&drop actions.
- Improved support for drag&drop from your local drive and embedding of files external to Excalidraw.
`,
"2.0.4":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/A1vrSGBbWgo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## New
- ExcaliAI
- You can now add ${String.fromCharCode(96)}ex-md-font-hand-drawn${String.fromCharCode(96)} or ${String.fromCharCode(96)}ex-md-font-hand-drawn${String.fromCharCode(96)} to the ${String.fromCharCode(96)}cssclasses:${String.fromCharCode(96)} frontmatter property in embedded markdown nodes and their font face will match the respective Excalidraw fonts.
## Fixed
- Adding a script for the very first time (when the script folder did not yet exist) did not show up in the tools panel. Required an Obsidian restart.
- Performance improvements
## New and updated In Excalidraw Automate
- Added many new functions and some features to existing functions. See the [release notes](https://github.com/zsviczian/obsidian-excalidraw-plugin/releases/tag/2.0.3) for details
`,
"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>
</div></div>
## Fixed
- bug with cssclasses in frontmatter
- styling of help screen keyboard shortcuts [#1437](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1437)
`,
"2.0.0":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/JC1E-jeiWhI" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

View File

@@ -0,0 +1,97 @@
import { Setting } from "obsidian";
import { DEVICE } from "src/constants/constants";
import { t } from "src/lang/helpers";
import { ModifierKeySet, ModifierSetType, modifierKeyTooltipMessages } from "src/utils/ModifierkeyHelper";
type ModifierKeyCategories = Partial<{
[modifierSetType in ModifierSetType]: string;
}>;
const CATEGORIES: ModifierKeyCategories = {
WebBrowserDragAction: t("WEB_BROWSER_DRAG_ACTION"),
LocalFileDragAction: t("LOCAL_FILE_DRAG_ACTION"),
InternalDragAction: t("INTERNAL_DRAG_ACTION"),
LinkClickAction: t("PANE_TARGET"),
};
export class ModifierKeySettingsComponent {
private isMacOS: boolean;
constructor(
private contentEl: HTMLElement,
private modifierKeyConfig: {
Mac: Record<string, ModifierKeySet>;
Win: Record<string, ModifierKeySet>;
},
private update?: Function,
) {
this.isMacOS = (DEVICE.isMacOS || DEVICE.isIOS);
}
render() {
const platform = this.isMacOS ? "Mac" : "Win";
const modifierKeysConfig = this.modifierKeyConfig[platform];
Object.entries(CATEGORIES).forEach(([modifierSetType, label]) => {
const detailsEl = this.contentEl.createEl("details");
detailsEl.createEl("summary", {
text: label,
cls: "excalidraw-setting-h4",
});
const modifierKeys = modifierKeysConfig[modifierSetType];
detailsEl.createDiv({
//@ts-ignore
text: t("DEFAULT_ACTION_DESC") + modifierKeyTooltipMessages()[modifierSetType][modifierKeys.defaultAction],
cls: "setting-item-description"
});
Object.entries(modifierKeys.rules).forEach(([action, rule]) => {
const setting = new Setting(detailsEl)
//@ts-ignore
.setName(modifierKeyTooltipMessages()[modifierSetType][rule.result]);
setting.addToggle((toggle) =>
toggle
.setValue(rule.shift)
.setTooltip("SHIFT")
.onChange((value) => {
rule.shift = value;
this.update();
})
);
setting.addToggle((toggle) => {
toggle
.setValue(rule.ctrl_cmd)
.setTooltip(this.isMacOS ? "CMD" : "CTRL")
.onChange((value) => {
rule.ctrl_cmd = value;
this.update();
})
if(this.isMacOS && modifierSetType !== "LinkClickAction") {
toggle.setDisabled(true);
toggle.toggleEl.style.opacity = "0.5";
}
});
setting.addToggle((toggle) =>
toggle
.setValue(rule.alt_opt)
.setTooltip(this.isMacOS ? "OPT" : "ALT")
.onChange((value) => {
rule.alt_opt = value;
this.update();
})
);
setting.addToggle((toggle) =>
toggle
.setValue(rule.meta_ctrl)
.setTooltip(this.isMacOS ? "CTRL" : "META")
.onChange((value) => {
rule.meta_ctrl = value;
this.update();
})
);
});
});
}
}

View File

@@ -1,6 +1,6 @@
import { App, FuzzySuggestModal, TFile } from "obsidian";
import ExcalidrawPlugin from "../main";
import { EMPTY_MESSAGE } from "../constants";
import { EMPTY_MESSAGE } from "../constants/constants";
import { t } from "../lang/helpers";
export enum openDialogAction {

View File

@@ -1,6 +1,6 @@
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
import { ColorComponent, Modal, Setting, SliderComponent, TextComponent, ToggleComponent } from "obsidian";
import { COLOR_NAMES, VIEW_TYPE_EXCALIDRAW } from "src/constants";
import { COLOR_NAMES, VIEW_TYPE_EXCALIDRAW } from "src/constants/constants";
import ExcalidrawView from "src/ExcalidrawView";
import ExcalidrawPlugin from "src/main";
import { setPen } from "src/menu/ObsidianMenu";

View File

@@ -14,11 +14,11 @@ import ExcalidrawPlugin from "../main";
import { escapeRegExp, sleep } from "../utils/Utils";
import { getLeaf } from "../utils/ObsidianUtils";
import { checkAndCreateFolder, splitFolderAndFilename } from "src/utils/FileUtils";
import { KeyEvent, isCTRL } from "src/utils/ModifierkeyHelper";
import { KeyEvent, isWinCTRLorMacCMD } from "src/utils/ModifierkeyHelper";
import { t } from "src/lang/helpers";
import { ExcalidrawElement, getEA } from "src";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import { MAX_IMAGE_SIZE } from "src/constants";
import { MAX_IMAGE_SIZE } from "src/constants/constants";
export type ButtonDefinition = { caption: string; tooltip?:string; action: Function };
@@ -342,11 +342,11 @@ export class GenericInputPrompt extends Modal {
private cancelClickCallback = () => this.cancel();
private keyDownCallback = (evt: KeyboardEvent) => {
if ((evt.key === "Enter" && this.lines === 1) || (isCTRL(evt) && evt.key === "Enter")) {
if ((evt.key === "Enter" && this.lines === 1) || (isWinCTRLorMacCMD(evt) && evt.key === "Enter")) {
evt.preventDefault();
this.submit();
}
if (this.displayEditorButtons && evt.key === "k" && isCTRL(evt)) {
if (this.displayEditorButtons && evt.key === "k" && isWinCTRLorMacCMD(evt)) {
evt.preventDefault();
this.linkBtnClickCallback();
}

View File

@@ -88,7 +88,8 @@ export class ScriptInstallPrompt extends Modal {
this.close();
return;
}
await MarkdownRenderer.renderMarkdown(
await MarkdownRenderer.render(
this.plugin.app,
source,
this.contentDiv,
"",

View File

@@ -5,7 +5,17 @@ type SuggesterInfo = {
after: string;
};
const hyperlink = (url: string, text: string) => {
return `<a onclick='window.open("${url}")'>${text}</a>`;
}
export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
{
field: "help",
code: "help(target: Function | string)",
desc: "Utility function that provides help about ExcalidrawAutomate functions and properties. I recommend calling this function from Developer Console to print out help to the console.",
after: "",
},
{
field: "plugin",
code: null,
@@ -27,13 +37,13 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
{
field: "style.strokeColor",
code: "[string]",
desc: "A valid css color. See <a onclick='window.open(\"https://www.w3schools.com/colors/default.asp\")'>W3 School Colors</a> for more.",
desc: `A valid css color. See ${hyperlink("https://www.w3schools.com/colors/default.asp", "W3 School Colors")} for more.`,
after: "",
},
{
field: "style.backgroundColor",
code: "[string]",
desc: "A valid css color. See <a onclick='window.open(\"https://www.w3schools.com/colors/default.asp\")'>W3 School Colors</a> for more.",
desc: `A valid css color. See ${hyperlink("https://www.w3schools.com/colors/default.asp","W3 School Colors")} for more.`,
after: "",
},
{
@@ -123,7 +133,7 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
{
field: "canvas.viewBackgroundColor",
code: "[string]",
desc: "A valid css color.\nSee <a onclick='window.open(\"https://www.w3schools.com/colors/default.asp\")'>W3 School Colors</a> for more.",
desc: `A valid css color.\nSee ${hyperlink("https://www.w3schools.com/colors/default.asp","W3 School Colors")} for more.`,
after: "",
},
{
@@ -170,20 +180,26 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "create",
code: 'create(params?: {filename?: string, foldername?: string, templatePath?: string, onNewPane?: boolean, silent?: boolean, frontmatterKeys?: { "excalidraw-plugin"?: "raw" | "parsed", "excalidraw-link-prefix"?: string, "excalidraw-link-brackets"?: boolean, "excalidraw-url-prefix"?: string,},}): Promise<string>;',
code: 'async create(params?: {filename?: string, foldername?: string, templatePath?: string, onNewPane?: boolean, silent?: boolean, frontmatterKeys?: { "excalidraw-plugin"?: "raw" | "parsed", "excalidraw-link-prefix"?: string, "excalidraw-link-brackets"?: boolean, "excalidraw-url-prefix"?: string,},}): Promise<string>;',
desc: "Create a drawing and save it to filename.\nIf filename is null: default filename as defined in Excalidraw settings.\nIf folder is null: default folder as defined in Excalidraw settings\nReturns the path to the created file",
after: "",
},
{
field: "createSVG",
code: "createSVG(templatePath?: string, embedFont?: boolean, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string,): Promise<SVGSVGElement>;",
code: "async createSVG(templatePath?: string, embedFont?: boolean, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string,): Promise<SVGSVGElement>;",
desc: "Use ExcalidrawAutomate.getExportSettings(boolean,boolean) to create an ExportSettings object.\nUse ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?) to create an EmbeddedFilesLoader object.",
after: "",
},
{
field: "createPNG",
code: "createPNG(templatePath?: string, scale?: number, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string,): Promise<any>;",
desc: "Use ExcalidrawAutomate.getExportSettings(boolean,boolean) to create an ExportSettings object.\nUse ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?) to create an EmbeddedFilesLoader object.",
code: "async createPNG(templatePath?: string, scale?: number, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string,padding?: number): Promise<any>;",
desc: "Create an image based on the objects in ea.getElements(). The elements in ea will be merged with the elements from the provided template file - if any. Use ExcalidrawAutomate.getExportSettings(boolean,boolean) to create an ExportSettings object.\nUse ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?) to create an EmbeddedFilesLoader object.",
after: "",
},
{
field: "createPNGBase64",
code: "async craetePNGBase64(templatePath?: string, scale?: number, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string,padding?: number): Promise<string>;",
desc: "The same as createPNG but returns a base64 encoded string instead of a file.",
after: "",
},
{
@@ -237,12 +253,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
{
field: "addArrow",
code: "addArrow(points: [[x: number, y: number]], formatting?: { startArrowHead?: string; endArrowHead?: string; startObjectId?: string; endObjectId?: string;},): string;",
desc: null,
desc: `valid values for startArrowHead and endArrowHead are: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null`,
after: "",
},
{
field: "addImage",
code: "addImage(topX: number, topY: number, imageFile: TFile, scale?: boolean, anchor?: boolean): Promise<string>;",
code: "async addImage(topX: number, topY: number, imageFile: TFile, scale?: boolean, anchor?: boolean): Promise<string>;",
desc: "set scale to false if you want to embed the image at 100% of its original size. Default is true which will insert a scaled image. anchor will only be evaluated if scale is false. anchor true will add |100% to the end of the filename, resulting in an image that will always pop back to 100% when the source file is updated or when the Excalidraw file is reopened. ",
after: "",
},
@@ -252,16 +268,25 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: "Adds an iframe/webview (depending on content and platform) to the drawing. If url is not null then the iframe/webview 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/webview will be loaded from the file. Both the url and the file may not be null.",
after: "",
},
{
field: "addMermaid",
code: "async addMermaid(diagram: string, groupElements: boolean = true,): Promise<string[]|string>;",
desc: "Creates a mermaid diagram and returns the ids of the created elements as a string[]. " +
"The elements will be added to ea. To add them to the canvas you'll need to use addElementsToView. " +
"Depending on the diagram type the result will be either a single SVG image, or a number of excalidraw elements.<br>" +
"If there is an error, the function returns a string with the error message.",
after: "",
},
{
field: "addLaTex",
code: "addLaTex(topX: number, topY: number, tex: string): Promise<string>;",
desc: null,
code: "async addLaTex(topX: number, topY: number, tex: string): Promise<string>;",
desc: "This is an async function, you need to avait the results. Adds a LaTex element to the drawing. The tex string is the LaTex code. The function returns the id of the created element.",
after: "",
},
{
field: "connectObjects",
code: "connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?: {numberOfPoints?: number; startArrowHead?: string; endArrowHead?: string; padding?: number;},): string;",
desc: 'type ConnectionPoint = "top" | "bottom" | "left" | "right" | null\nWhen null is passed as ConnectionPoint then Excalidraw will automatically decide\nnumberOfPoints is the number of points on the line. Default is 0 i.e. line will only have a start and end point.\nArrowHead: "triangle"|"dot"|"arrow"|"bar"|null',
desc: 'type ConnectionPoint = "top" | "bottom" | "left" | "right" | null\nWhen null is passed as ConnectionPoint then Excalidraw will automatically decide\nnumberOfPoints is the number of points on the line. Default is 0 i.e. line will only have a start and end point.\nArrowHead: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null',
after: "",
},
{
@@ -303,7 +328,7 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
{
field: "getExcalidrawAPI",
code: "getExcalidrawAPI(): any;",
desc: "<a onclick='window.open(\"https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw#ref\")'>Excalidraw API</a>",
desc: `${hyperlink("https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw#ref","Excalidraw API")}`,
after: "",
},
{
@@ -338,8 +363,8 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "copyViewElementsToEAforEditing",
code: "copyViewElementsToEAforEditing(elements: ExcalidrawElement[]): void;",
desc: "Copies elements from view to elementsDict for editing",
code: "copyViewElementsToEAforEditing(elements: ExcalidrawElement[], copyImages: boolean = false): void;",
desc: "Copies elements from view to elementsDict for editing. If copyImages is true, then relevant entries from scene.files will also be copied. This is required if you want to generate a PNG for a subset of the elements in the drawing (e.g. for AI generation)",
after: "",
},
{
@@ -356,7 +381,7 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "addElementsToView",
code: "addElementsToView(repositionToCursor?: boolean, save?: boolean, newElementsOnTop?: boolean,shouldRestoreElements?: boolean,): Promise<boolean>;",
code: "async addElementsToView(repositionToCursor?: boolean, save?: boolean, newElementsOnTop?: boolean,shouldRestoreElements?: boolean,): Promise<boolean>;",
desc: "Adds elements from elementsDict to the current view\nrepositionToCursor: default is false\nsave: default is true\nnewElementsOnTop: default is false, i.e. the new elements get to the bottom of the stack\nnewElementsOnTop controls whether elements created with ExcalidrawAutomate are added at the bottom of the stack or the top of the stack of elements already in the view\nNote that elements copied to the view with copyViewElementsToEAforEditing retain their position in the stack of elements in the view even if modified using EA",
after: "",
},
@@ -423,19 +448,19 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
{
field: "activeScript",
code: "activeScript: string;",
desc: "Mandatory to set before calling the get and set ScriptSettings functions. Set automatically by the ScriptEngine\nSee for more details: <a onclick='window.open(\"https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html\")'>Script Engine Help</a>",
desc: `Mandatory to set before calling the get and set ScriptSettings functions. Set automatically by the ScriptEngine\nSee for more details: ${hyperlink("https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html","Script Engine Help")}`,
after: "",
},
{
field: "getScriptSettings",
code: "getScriptSettings(): {};",
desc: "Returns script settings. Saves settings in plugin settings, under the activeScript key. See for more details: <a onclick='window.open(\"https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html\")'>Script Engine Help</a>",
desc: `Returns script settings. Saves settings in plugin settings, under the activeScript key. See for more details: ${hyperlink("https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html","Script Engine Help")}`,
after: "",
},
{
field: "setScriptSettings",
code: "setScriptSettings(settings: any): Promise<void>;",
desc: "Sets script settings.\nSee for more details: <a onclick='window.open(\"https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html\")'>Script Engine Help</a>",
code: "async setScriptSettings(settings: any): Promise<void>;",
desc: `Sets script settings.\nSee for more details: ${hyperlink("https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html","Script Engine Help")}`,
after: "",
},
{
@@ -513,13 +538,107 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
{
field: "obsidian",
code: "obsidian",
desc: "Access functions and objects available on the <a onclick='window.open(\"https://github.com/obsidianmd/obsidian-api/blob/master/obsidian.d.ts\")'>Obsidian Module</a>",
desc: `Access functions and objects available on the ${hyperlink("https://github.com/obsidianmd/obsidian-api/blob/master/obsidian.d.ts","Obsidian Module")}`,
after: "",
},
{
field: "getAttachmentFilepath",
code: "async getAttachmentFilepath(filename: string): Promise<string>",
desc: "This asynchronous function should be awaited. It retrieves the filepath to a new file, taking into account the attachments preference settings in Obsidian. If the attachment folder doesn't exist, it creates it. The function returns the complete path to the file. If the provided filename already exists, the function will append '_[number]' before the extension to generate a unique filename.",
desc: "This asynchronous function should be awaited. It retrieves the filepath to a new file, taking into account the attachments preference settings in Obsidian. If the attachment folder doesn't exist, it creates it. The function returns the complete path to the file. If the provided filename already exists, the function will append '_[number]' before the extension to generate a unique filename." +
"Prompts the user with a dialog to select new file action.<br>" +
" - create markdown file<br>" +
" - create excalidraw file<br>" +
" - cancel action<br>" +
"The new file will be relative to this.targetView.file.path, unless parentFile is provided. " +
"If shouldOpenNewFile is true, the new file will be opened in a workspace leaf. " +
"targetPane controls which leaf will be used for the new file.<br>" +
"Returns the TFile for the new file or null if the user cancelled the action.<br>" +
'<code>type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";</code>',
after: "",
},
{
field: "getActiveEmbeddableViewOrEditor",
code: "getActiveEmbeddableViewOrEditor(view?: ExcalidrawView);",
desc: "Returns the editor or leaf.view of the currently active embedded obsidian file.<br>" +
"If view is not provided, ea.targetView is used.<br>" +
"If the embedded file is a markdown document the function will return<br>" +
"<code>{file:TFile, editor:Editor}</code> otherwise it will return {view:any}. You can check view type with view.getViewType();",
after: "",
},
{
field: "getViewLastPointerPosition",
code: "getViewLastPointerPosition(): {x: number, y: number};",
desc: "@returns the last recorded pointer position on the Excalidraw canvas",
after: "",
},
{
field: "getleaf",
code: "getLeaf(origo: WorkspaceLeaf, targetPane?: PaneTarget): WorkspaceLeaf;",
desc: "Generates a new Obsidian Leaf following Excalidraw plugin settings such as open in Main Workspace or not, open in adjacent pane if avaialble, etc.<br>" +
"@param origo: the currently active leaf, the origin of the new leaf<br>" +
'@param targetPane: <code>type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";',
after: "",
},
{
field: "newFilePrompt",
code: "async newFilePrompt(newFileNameOrPath: string, shouldOpenNewFile: boolean, targetPane?: PaneTarget, parentFile?: TFile): Promise<TFile | null>;",
desc: "",
after: "",
},
{
field: "DEVICE",
code: "get DEVICE(): DeviceType;",
desc: "Returns the current device type. Possible values are: <br>" +
"<code>type DeviceType = {<br>" +
" isDesktop: boolean,<br>" +
" isPhone: boolean,<br>" +
" isTablet: boolean,<br>" +
" isMobile: boolean,<br>" +
" isLinux: boolean,<br>" +
" isMacOS: boolean,<br>" +
" isWindows: boolean,<br>" +
" isIOS: boolean,<br>" +
" isAndroid: boolean<br>" +
"};",
after: "",
},
{
field: "checkAndCreateFolder",
code: "async checkAndCreateFolder(folderpath: string): Promise<TFolder>",
desc: "Checks if the folder exists, if not, creates it.",
after: "",
},
{
field: "getNewUniqueFilepath",
code: "getNewUniqueFilepath(filename: string, folderpath: string): string",
desc: "Checks if the filepath already exists, if so, returns a new filepath with a number appended to the filename else returns the filepath as provided.",
after: "",
},
{
field: "extractCodeBlocks",
code: "extractCodeBlocks(markdown: string): { data: string, type: string }[]",
desc: "Grabs the codeblock content from the supplied markdown string. Returns an array of dictionaries with the codeblock content and type",
after: "",
},
{
field: "postOpenAI",
code: "async postOpenAI(requst: AIRequest): Promise<RequestUrlResponse>",
desc:
"This asynchronous function should be awaited. It posts the supplied request to the OpenAI API and returns the response.<br>" +
"The response is a dictionary with the following keys:<br><code>{image, text, instruction, systemPrompt, responseType}</code><br>"+
"<b>image</b> should be a dataURL - use ea.createPNGBase64()<br>"+
"<b>systemPrompt</b>: if <code>undefined</code> the message to OpenAI will not include a system prompt<br>"+
"<b>text</b> is the actual user prompt, a request must have either an image or a text<br>"+
"<b>instruction</b> is a user prompt sent as a separate element in the message - I use it to reinforce the type of response I am seeing (e.g. mermaid in a codeblock)<br>"+
`<b>imageGenerationProperties</b> if provided then the dall-e model will be used. <code> imageGenerationProperties?: {size?: string, quality?: "standard" | "hd"; n?: number; mask?: string; }</code><br>` +
"Different openAI models accept different parameters fr size, quality, n and mask. Consult the API documenation for more information.<br>" +
`RequestUrlResponse is defined in the ${hyperlink("https://github.com/obsidianmd/obsidian-api/blob/master/obsidian.d.ts","Obsidian API")}`,
after: "",
},
{
field: "convertStringToDataURL",
code: 'async convertStringToDataURL (data:string, type: string = "text/html"):Promise<string>',
desc: "Converts a string to a DataURL.",
after: "",
},
{
@@ -685,6 +804,10 @@ export const FRONTMATTER_KEYS_INFO: SuggesterInfo[] = [
desc: "Override iFrame theme plugin-settings for this file. 'match' will match the Excalidraw theme, 'default' will match the obsidian theme. Valid values are\ndark\nlight\nauto\ndefault",
after: ": auto",
},
{
field: "mask",
code: null,
desc: "If this key is present the drawing will be handled as a mask to crop an image.",
after: ": true",
},
];

View File

@@ -3,11 +3,11 @@ import ExcalidrawView from "../ExcalidrawView";
import ExcalidrawPlugin from "../main";
import { Modal, Setting, TextComponent } from "obsidian";
import { FileSuggestionModal } from "./FolderSuggester";
import { IMAGE_TYPES, sceneCoordsToViewportCoords, viewportCoordsToSceneCoords, MAX_IMAGE_SIZE } from "src/constants";
import { IMAGE_TYPES, sceneCoordsToViewportCoords, viewportCoordsToSceneCoords, MAX_IMAGE_SIZE, ANIMATED_IMAGE_TYPES } from "src/constants/constants";
import { insertEmbeddableToView, insertImageToView } from "src/utils/ExcalidrawViewUtils";
import { getEA } from "src";
import { InsertPDFModal } from "./InsertPDFModal";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import { cleanSectionHeading } from "src/utils/ObsidianUtils";
@@ -80,6 +80,7 @@ export class UniversalInsertFileModal extends Modal {
const ea = this.plugin.ea;
const isMarkdown = file && file.extension === "md" && !ea.isExcalidrawFile(file);
const isImage = file && (IMAGE_TYPES.contains(file.extension) || ea.isExcalidrawFile(file));
const isAnimatedImage = file && ANIMATED_IMAGE_TYPES.contains(file.extension);
const isIFrame = file && !isImage;
const isPDF = file && file.extension === "pdf";
const isExcalidraw = file && ea.isExcalidrawFile(file);
@@ -116,7 +117,7 @@ export class UniversalInsertFileModal extends Modal {
actionImage.buttonEl.style.display = "none";
}
if (isIFrame) {
if (isIFrame || isAnimatedImage) {
actionIFrame.buttonEl.style.display = "block";
} else {
actionIFrame.buttonEl.style.display = "none";

View File

@@ -2,8 +2,8 @@ import "obsidian";
//import { ExcalidrawAutomate } from "./ExcalidrawAutomate";
//export ExcalidrawAutomate from "./ExcalidrawAutomate";
//export {ExcalidrawAutomate} from "./ExcaildrawAutomate";
export type { ExcalidrawBindableElement, ExcalidrawElement, FileId, FillStyle, StrokeRoundness, StrokeStyle } from "@zsviczian/excalidraw/types/element/types";
export type { Point } from "@zsviczian/excalidraw/types/types";
export type { ExcalidrawBindableElement, ExcalidrawElement, FileId, FillStyle, StrokeRoundness, StrokeStyle } from "@zsviczian/excalidraw/types/excalidraw/element/types";
export type { Point } from "@zsviczian/excalidraw/types/excalidraw/types";
export const getEA = (view?:any): any => {
try {
return window.ExcalidrawAutomate.getAPI(view);

View File

@@ -25,7 +25,7 @@ import ru from "./locale/ru";
import tr from "./locale/tr";
import zhCN from "./locale/zh-cn";
import zhTW from "./locale/zh-tw";
import { LOCALE } from "src/constants";
import { LOCALE } from "src/constants/constants";
const localeMap: { [k: string]: Partial<typeof en> } = {
ar,

View File

@@ -3,13 +3,17 @@ import {
FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS,
FRONTMATTER_KEY_CUSTOM_PREFIX,
FRONTMATTER_KEY_CUSTOM_URL_PREFIX,
} from "src/constants";
} from "src/constants/constants";
import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/utils/ModifierkeyHelper";
// English
export default {
// main.ts
CONVERT_URL_TO_FILE: "Save image from URL to local file",
UNZIP_CURRENT_FILE: "Decompress current Excalidraw file",
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",
@@ -62,6 +66,7 @@ export default {
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)",
IMPORT_SVG_CONTEXTMENU: "Convert SVG to strokes - with limitations",
INSERT_MD: "Insert markdown file from vault",
INSERT_PDF: "Insert PDF file from vault",
UNIVERSAL_ADD_FILE: "Insert ANY file",
@@ -72,18 +77,20 @@ export default {
RUN_OCR: "OCR: Grab text from freedraw scribble and pictures to clipboard",
TRAY_MODE: "Toggle property-panel tray-mode",
SEARCH: "Search for text in drawing",
CROP_IMAGE: "Crop and mask image",
RESET_IMG_TO_100: "Set selected image element size to 100% of original",
TEMPORARY_DISABLE_AUTOSAVE: "Disable autosave until next time Obsidian starts (only set this if you know what you are doing)",
TEMPORARY_ENABLE_AUTOSAVE: "Enable autosave",
//ExcalidrawView.ts
MASK_FILE_NOTICE: "This is a mask file. It is used to crop images and mask out parts of the image. Press and hold notice to open the help video.",
INSTALL_SCRIPT_BUTTON: "Install or update Excalidraw Scripts",
OPEN_AS_MD: "Open as Markdown",
EXPORT_IMAGE: `Export Image`,
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:
@@ -138,6 +145,37 @@ export default {
"You can access your scripts from Excalidraw via the Obsidian Command Palette. Assign " +
"hotkeys to your favorite scripts just like to any other Obsidian command. " +
"The folder may not be the root folder of your Vault. ",
AI_HEAD: "AI Settings - Experimental",
AI_DESC: `In the "AI" settings, you can configure options for using OpenAI's GPT API. ` +
`While the OpenAI API is in beta, its use is strictly limited — as such we require you use your own API key. ` +
`You can create an OpenAI account, add a small credit (5 USD minimum), and generate your own API key. ` +
`Once API key is set, you can use the AI tools in Excalidraw.`,
AI_OPENAI_TOKEN_NAME: "OpenAI API key",
AI_OPENAI_TOKEN_DESC:
"You can get your OpenAI API key from your <a href='https://platform.openai.com/api-keys'>OpenAI account</a>.",
AI_OPENAI_TOKEN_PLACEHOLDER: "Enter your OpenAI API key here",
AI_OPENAI_DEFAULT_MODEL_NAME: "Default AI model",
AI_OPENAI_DEFAULT_MODEL_DESC:
"The default AI model to use when generating text. This is a freetext field, so you can enter any valid OpenAI model name. " +
"Find out more about the available models on the <a href='https://platform.openai.com/docs/models'>OpenAI website</a>.",
AI_OPENAI_DEFAULT_MODEL_PLACEHOLDER: "Enter your default AI model here. e.g.: gpt-3.5-turbo-1106",
AI_OPENAI_DEFAULT_IMAGE_MODEL_NAME: "Default Image Generation AI model",
AI_OPENAI_DEFAULT_IMAGE_MODEL_DESC:
"The default AI model to use when generating images. Image editing and variations are only supported by dall-e-2 at this time by OpenAI, " +
"for this reason dall-e-2 will automatically be used in such cases regardless of this setting.<br>" +
"This is a freetext field, so you can enter any valid OpenAI model name. " +
"Find out more about the available models on the <a href='https://platform.openai.com/docs/models'>OpenAI website</a>.",
AI_OPENAI_DEFAULT_IMAGE_MODEL_PLACEHOLDER: "Enter your default Image Generation AI model here e.g.: dall-e-3",
AI_OPENAI_DEFAULT_VISION_MODEL_NAME: "Default AI vision model",
AI_OPENAI_DEFAULT_VISION_MODEL_DESC:
"The default AI vision model to use when generating text from images. This is a freetext field, so you can enter any valid OpenAI model name. " +
"Find out more about the available models on the <a href='https://platform.openai.com/docs/models'>OpenAI website</a>.",
AI_OPENAI_DEFAULT_API_URL_NAME: "OpenAI API URL",
AI_OPENAI_DEFAULT_API_URL_DESC:
"The default OpenAI API URL. This is a freetext field, so you can enter any valid OpenAI API compatible URL. " +
"Excalidraw will use this URL when posting API requests to OpenAI. I am not doing any error handling on this field, so make sure you enter a valid URL and only change this if you know what you are doing. ",
AI_OPENAI_DEFAULT_IMAGE_API_URL_NAME: "OpenAI Image Generation API URL",
AI_OPENAI_DEFAULT_VISION_MODEL_PLACEHOLDER: "Enter your default AI vision model here. e.g.: gpt-4-vision-preview",
SAVING_HEAD: "Saving",
SAVING_DESC: "In the 'Saving' section of Excalidraw Settings, you can configure how your drawings are saved. This includes options for compressing Excalidraw JSON in Markdown, setting autosave intervals for both desktop and mobile, defining filename formats, and choosing whether to use the .excalidraw.md or .md file extension. ",
COMPRESS_NAME: "Compress Excalidraw JSON in Markdown",
@@ -256,6 +294,12 @@ FILENAME_HEAD: "Filename",
"the plugin will open it in a browser. " +
"When Obsidian files change, the matching <code>[[link]]</code> in your drawings will also change. " +
"If you don't want text accidentally changing in your drawings use <code>[[links|with aliases]]</code>.",
DRAG_MODIFIER_NAME: "Link Click and Drag&Drop Modifier Keys",
DRAG_MODIFIER_DESC: "Modifier key behavior when clicking links and dragging and dropping elements. " +
"Excalidraw will not validate your configuration... pay attention to avoid conflicting settings. " +
"These settings are different for Apple and non-Apple. If you use Obsidian on multiple platforms, you'll need to make the settings separately. "+
"The toggles follow the order of " +
(DEVICE.isIOS || DEVICE.isMacOS ? "SHIFT, CMD, OPT, CONTROL." : "SHIFT, CTRL, ALT, META (Windows key)."),
ADJACENT_PANE_NAME: "Reuse adjacent pane",
ADJACENT_PANE_DESC:
`When ${labelCTRL()}+${labelALT()} clicking a link in Excalidraw, by default the plugin will open the link in a new pane. ` +
@@ -324,8 +368,11 @@ FILENAME_HEAD: "Filename",
PDF_TO_IMAGE_SCALE_NAME: "PDF to Image conversion scale",
PDF_TO_IMAGE_SCALE_DESC: "Sets the resolution of the image that is generated from the PDF page. Higher resolution will result in bigger images in memory and consequently a higher load on your system (slower performance), but sharper imagee. " +
"Additionally, if you want to copy PDF pages (as images) to Excalidraw.com, the bigger image size may result in exceeding the 2MB limit on Excalidraw.com.",
EMBED_TOEXCALIDRAW_HEAD: "Embed files into Excalidraw",
EMBED_TOEXCALIDRAW_DESC: "In the Embed Files section of Excalidraw Settings, you can configure how various files are embedded into Excalidraw. This includes options for embedding interactive markdown files, PDFs, and markdown files as images.",
MD_HEAD: "Embed markdown into Excalidraw as image",
MD_HEAD_DESC: `In the "Embed markdown as image settings," you can configure various options for how markdown documents are embedded as images within Excalidraw. These settings allow you to control the default width and maximum height of embedded markdown files, choose the font typeface, font color, and border color for embedded markdown content. Additionally, you can specify a custom CSS file to style the embedded markdown content. Note you can also embed markdown documents as interactive frames. The color setting of frames is under the Display Settings section.`,
MD_EMBED_CUSTOMDATA_HEAD_NAME: "Interactive Markdown Files",
MD_EMBED_CUSTOMDATA_HEAD_DESC: `These settings will only effect future embeds. Current embeds remain unchanged. The theme setting of embedded frames is under the "Excalidraw appearance and behavior" section.`,
MD_TRANSCLUDE_WIDTH_NAME: "Default width of a transcluded markdown document",
MD_TRANSCLUDE_WIDTH_DESC:
"The width of the markdown page. This affects the word wrapping when transcluding longer paragraphs, and the width of " +
@@ -364,6 +411,11 @@ FILENAME_HEAD: "Filename",
EMBED_HEAD: "Embedding Excalidraw into your Notes and Exporting",
EMBED_DESC: `In the "Embed & Export" settings, you can configure how images and Excalidraw drawings are embedded and exported within your documents. Key settings include choosing the image type for markdown preview (such as Native SVG or PNG), specifying the type of file to insert into the document (original Excalidraw, PNG, or SVG), and managing image caching for embedding in markdown. You can also control image sizing, whether to embed drawings using wiki links or markdown links, and adjust settings related to image themes, background colors, and Obsidian integration.
Additionally, there are settings for auto-export, which automatically generates SVG and/or PNG files to match the title of your Excalidraw drawings, keeping them in sync with file renames and deletions.`,
EMBED_CANVAS: "Obsidian Canvas support",
EMBED_CANVAS_NAME: "Immersive embedding",
EMBED_CANVAS_DESC:
"Hide canvas node border and background when embedding an Excalidraw drawing to Canvas. " +
"Note that for a full transparent background for your image, you will still need to configure Excalidraw to export images with transparent background.",
EMBED_CACHING: "Image caching",
EXPORT_SUBHEAD: "Export Settings",
EMBED_SIZING: "Image sizing",
@@ -449,6 +501,13 @@ FILENAME_HEAD: "Filename",
"Double files will be exported both if auto-export SVG or PNG (or both) are enabled, as well as when clicking export on a single image.",
COMPATIBILITY_HEAD: "Compatibility features",
COMPATIBILITY_DESC: "You should only enable these features if you have a strong reason for wanting to work with excalidraw.com files instead of markdown files. Many of the plugin features are not supported on legacy files. Typical usecase would be if you use set your vault up on top of a Visual Studio Code project folder and you have .excalidraw drawings you want to access from Visual Studio Code as well. Another usecase might be using Excalidraw in Logseq and Obsidian in parallel.",
SLIDING_PANES_NAME: "Sliding panes plugin support",
SLIDING_PANES_DESC:
"Need to restart Obsidian for this change to take effect.<br>" +
"If you use the <a href='https://github.com/deathau/sliding-panes-obsidian' target='_blank'>Sliding Panes plugin</a> " +
"you can enable this setting to make Excalidraw drawings work with the Sliding Panes plugin.<br>" +
"Note, that Excalidraw Sliding Panes support causes compatibility issues with Obsidian Workspaces.<br>" +
"Note also, that the 'Stack Tabs' feature is now available in Obsidian, providing native support for most of the Sliding Panes functionality.",
EXPORT_EXCALIDRAW_NAME: "Auto-export Excalidraw",
EXPORT_EXCALIDRAW_DESC: "Same as the auto-export SVG, but for *.Excalidraw",
SYNC_EXCALIDRAW_NAME:
@@ -463,10 +522,10 @@ FILENAME_HEAD: "Filename",
"and the file explorer are going to be all legacy *.excalidraw files. This setting will also turn off the reminder message " +
"when you open a legacy file for editing.",
MATHJAX_NAME: "MathJax (LaTeX) javascript library host",
MATHJAX_DESC: "If you are using LaTeX equiations in Excalidraw then the plugin needs to load a javascript library for that. " +
"Some users are unable to access certain host servers. If you are experiencing issues try changing the host here. You may need to "+
MATHJAX_DESC: "If you are using LaTeX equations in Excalidraw, then the plugin needs to load a javascript library for that. " +
"Some users are unable to access certain host servers. If you are experiencing issues, try changing the host here. You may need to "+
"restart Obsidian after closing settings, for this change to take effect.",
LATEX_DEFAULT_NAME: "Default LaTeX formual for new equations",
LATEX_DEFAULT_NAME: "Default LaTeX formula for new equations",
LATEX_DEFAULT_DESC: "Leave empty if you don't want a default formula. You can add default formatting here such as <code>\\color{white}</code>.",
NONSTANDARD_HEAD: "Non-Excalidraw.com supported features",
NONSTANDARD_DESC: `These settings in the "Non-Excalidraw.com Supported Features" section provide customization options beyond the default Excalidraw.com features. These features are not available on excalidraw.com. When exporting the drawing to Excalidraw.com these features will appear different.
@@ -476,10 +535,24 @@ FILENAME_HEAD: "Filename",
CUSTOM_PEN_DESC: "You will see these pens next to the Obsidian Menu on the canvas. You can customize the pens on the canvas by long-pressing the pen button.",
EXPERIMENTAL_HEAD: "Miscellaneous features",
EXPERIMENTAL_DESC: `These miscellaneous features in Excalidraw include options for setting default LaTeX formulas for new equations, enabling a Field Suggester for autocompletion, displaying type indicators for Excalidraw files, enabling immersive image embedding in live preview editing mode, and experimenting with Taskbone Optical Character Recognition for text extraction from images and drawings. Users can also enter a Taskbone API key for extended usage of the OCR service.`,
EA_HEAD: "Excalidraw Automate",
EA_DESC:
"ExcalidrawAutomate is a scripting and automation API for Excalidraw. Unfortunately, the documentation of the API is sparse. " +
"I recommend reading the <a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/docs/API/ExcalidrawAutomate.d.ts'>ExcalidrawAutomate.d.ts</a> file, " +
"visiting the <a href='https://zsviczian.github.io/obsidian-excalidraw-plugin/'>ExcalidrawAutomate How-to</a> page - though the information " +
"here has not been updated for a long while -, and finally to enable the field suggester below. The field suggester will show you the available " +
"functions, their parameters and short description as you type. The field suggester is the most up-to-date documentation of the API.",
FIELD_SUGGESTER_NAME: "Enable Field Suggester",
FIELD_SUGGESTER_DESC:
"Field Suggester borrowed from Breadcrumbs and Templater plugins. The Field Suggester will show an autocomplete menu " +
"when you type <code>excalidraw-</code> or <code>ea.</code> with function description as hints on the individual items in the list.",
STARTUP_SCRIPT_NAME: "Startup script",
STARTUP_SCRIPT_DESC:
"If set, excalidraw will execute the script at plugin startup. This is useful if you want to set any of the Excalidraw Automate hooks. The startup script is a markdown file " +
"that should contain the javascript code you want to execute when Excalidraw starts.",
STARTUP_SCRIPT_BUTTON_CREATE: "Create startup script",
STARTUP_SCRIPT_BUTTON_OPEN: "Open startup script",
STARTUP_SCRIPT_EXISTS: "Startup script file already exists",
FILETYPE_NAME: "Display type (✏️) for excalidraw.md files in File Explorer",
FILETYPE_DESC:
"Excalidraw files will receive an indicator using the emoji or text defined in the next setting.",
@@ -563,6 +636,30 @@ FILENAME_HEAD: "Filename",
ZOOM_TO_FIT: "Zoom to fit",
RELOAD: "Reload original link",
OPEN_IN_BROWSER: "Open current link in browser",
PROPERTIES: "Properties",
COPYCODE: "Copy source to clipboard",
//EmbeddableSettings.tsx
ES_TITLE: "Embeddable Element Settings",
ES_RENAME: "Rename File",
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",
ES_FILENAME_VISIBLE: "Filename Visible",
ES_BACKGROUND_HEAD: "Embedded note background color",
ES_BACKGROUND_MATCH_ELEMENT: "Match Element Background Color",
ES_BACKGROUND_MATCH_CANVAS: "Match Canvas Background Color",
ES_BACKGROUND_COLOR: "Background Color",
ES_BORDER_HEAD: "Embedded note border color",
ES_BORDER_COLOR: "Border Color",
ES_BORDER_MATCH_ELEMENT: "Match Element Border Color",
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?",
@@ -583,5 +680,11 @@ FILENAME_HEAD: "Filename",
PROMPT_BUTTON_INSERT_SPACE: "Insert space",
PROMPT_BUTTON_INSERT_LINK: "Insert markdown link to file",
PROMPT_BUTTON_UPPERCASE: "Uppercase",
//ModifierKeySettings
WEB_BROWSER_DRAG_ACTION: "Web Browser Drag Action",
LOCAL_FILE_DRAG_ACTION: "OS Local File Drag Action",
INTERNAL_DRAG_ACTION: "Obsidian Internal Drag Action",
PANE_TARGET: "Link click behavior",
DEFAULT_ACTION_DESC: "In case none of the combinations apply the default action for this group is: ",
};

View File

@@ -3,12 +3,18 @@ import {
FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS,
FRONTMATTER_KEY_CUSTOM_PREFIX,
FRONTMATTER_KEY_CUSTOM_URL_PREFIX,
} from "src/constants";
} from "src/constants/constants";
import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/utils/ModifierkeyHelper";
// 简体中文
export default {
// main.ts
CONVERT_URL_TO_FILE: "从 URL 下载图像到本地",
UNZIP_CURRENT_FILE: "解压当前 Excalidraw 文件",
PUBLISH_SVG_CHECK: "Obsidian Publish: 搜索过期的 SVG 和 PNG 导出文件",
EMBEDDABLE_PROPERTIES: "Embeddable 元素设置",
EMBEDDABLE_RELATIVE_ZOOM: "使元素的缩放等级等于当前画布的缩放等级",
OPEN_IMAGE_SOURCE: "打开 Excalidraw 绘图文件",
INSTALL_SCRIPT: "安装此脚本",
UPDATE_SCRIPT: "有可用更新 - 点击安装",
CHECKING_SCRIPT:
@@ -43,7 +49,7 @@ export default {
"新建绘图 - 于当前面板 - 并将其嵌入(形如 ![[drawing]])到当前 Markdown 文档中",
NEW_IN_POPOUT_WINDOW_EMBED: "新建绘图 - 于新窗口 - 并将其嵌入(形如 ![[drawing]])到当前 Markdown 文档中",
TOGGLE_LOCK: "文本元素原文模式RAW⟺ 预览模式PREVIEW",
DELETE_FILE: "从库中删除所选图像或 MD-Embed 的源文件",
DELETE_FILE: "从库中删除所选图像(或以图像形式嵌入绘图中的 Markdown的源文件",
INSERT_LINK_TO_ELEMENT:
`复制所选元素为内部链接(形如 [[file#^id]] )。\n按住 ${labelCTRL()} 可复制元素所在分组为内部链接(形如 [[file#^group=id]] )。\n按住 ${labelSHIFT()} 可复制所选元素所在区域为内部链接(形如 [[file#^area=id]] )。\n按住 ${labelALT()} 可观看视频演示。`,
INSERT_LINK_TO_ELEMENT_GROUP:
@@ -57,23 +63,26 @@ export default {
INSERT_LINK_TO_ELEMENT_ERROR: "未选择画布里的单个元素",
INSERT_LINK_TO_ELEMENT_READY: "链接已生成并复制到剪贴板",
INSERT_LINK: "插入任意文件(以内部链接形式嵌入,形如 [[drawing]] )到当前绘图中",
INSERT_COMMAND: "插入 Obsidian 命令(以内部链接形式嵌入)到当前绘图中",
INSERT_IMAGE: "插入图像或 Excalidraw 绘图(以图像形式嵌入)到当前绘图中",
IMPORT_SVG: "从 SVG 文件导入图形元素到当前绘图中(暂不支持文本元素)",
INSERT_MD: "插入 Markdown 文档(以图像形式嵌入)到当前绘图中",
INSERT_PDF: "插入 PDF 文档(以图像形式嵌入)到当前绘图中",
UNIVERSAL_ADD_FILE: "插入任意文件(以 Embeddable 形式嵌入)到当前绘图中",
UNIVERSAL_ADD_FILE: "插入任意文件(以交互形式嵌入,或者以图像形式嵌入)到当前绘图中",
INSERT_LATEX:
`插入 LaTeX 公式到当前绘图。按住 ${labelALT()} 可观看视频演示。`,
`插入 LaTeX 公式到当前绘图`,
ENTER_LATEX: "输入 LaTeX 表达式",
READ_RELEASE_NOTES: "阅读本插件的更新说明",
RUN_OCR: "OCR识别涂鸦和图片里的文本并复制到剪贴板",
TRAY_MODE: "绘图工具属性页:面板模式 ⟺ 托盘模式",
SEARCH: "搜索文本",
CROP_IMAGE: "裁剪与蒙版",
RESET_IMG_TO_100: "重设图像元素的尺寸为 100%",
TEMPORARY_DISABLE_AUTOSAVE: "临时禁用自动保存功能,直到本次 Obsidian 退出(小白慎用!)",
TEMPORARY_ENABLE_AUTOSAVE: "启用自动保存功能",
//ExcalidrawView.ts
MASK_FILE_NOTICE: "这是一个蒙版图像。长按本提示来观看视频讲解。",
INSTALL_SCRIPT_BUTTON: "安装或更新 Excalidraw 脚本",
OPEN_AS_MD: "打开为 Markdown 文档",
EXPORT_IMAGE: `导出为图像`,
@@ -98,7 +107,8 @@ export default {
OBSIDIAN_TOOLS_PANEL: "Obsidian 工具面板",
ERROR_SAVING_IMAGE: "获取图像时发生未知错误",
WARNING_PASTING_ELEMENT_AS_TEXT: "你不能将 Excalidraw 元素粘贴为文本元素!",
USE_INSERT_FILE_MODAL: "使用“插入任意文件(以 iFrame 形式嵌入)”功能来嵌入 Markdown 文档",
USE_INSERT_FILE_MODAL: "使用“插入任意文件”功能来嵌入 Markdown 文档",
CONVERT_TO_MARKDOWN: "转存为 Markdown 文档(并嵌入为 MD-Embeddable",
//settings.ts
RELEASE_NOTES_NAME: "显示更新说明",
@@ -110,6 +120,8 @@ export default {
"<b>开启:</b>当本插件存在可用更新时,显示通知。<br>" +
"<b>关闭:</b>您需要手动检查本插件的更新(设置 - 第三方插件 - 检查更新)。",
BASIC_HEAD: "基本",
BASIC_DESC: `包括:更新说明,更新提示,新绘图文件、模板文件、脚本文件的存储路径等的设置。`,
FOLDER_NAME: "Excalidraw 文件夹",
FOLDER_DESC:
"新绘图的默认存储路径。若为空,将在库的根目录中创建新绘图。",
@@ -118,10 +130,10 @@ export default {
FOLDER_EMBED_DESC:
"在命令面板中执行“新建绘图”系列命令时," +
"新建的绘图文件的存储路径。<br>" +
"<b>开启:</b>使用 Excalidraw 文件夹。 <br><b>关闭:</b>使用 Obsidian 设置的新附件默认位置。",
"<b>开启:</b>使用上面的 Excalidraw 文件夹。 <br><b>关闭:</b>使用 Obsidian 设置的新附件默认位置。",
TEMPLATE_NAME: "Excalidraw 模板文件",
TEMPLATE_DESC:
"Excalidraw 模板文件的完整路径。<br>" +
"Excalidraw 模板文件的存储路径。<br>" +
"如果您的模板在默认的 Excalidraw 文件夹中且文件名是 " +
"Template.md则此项应设为 Excalidraw/Template.md也可省略 .md 扩展名,即 Excalidraw/Template。<br>" +
"如果您在兼容模式下使用 Excalidraw那么您的模板文件也必须是旧的 *.excalidraw 格式," +
@@ -132,7 +144,39 @@ export default {
"您可以在 Obsidian 命令面板中执行这些脚本," +
"还可以为喜欢的脚本分配快捷键,就像为其他 Obsidian 命令分配快捷键一样。<br>" +
"该项不能设为库的根目录。",
AI_HEAD: "AI实验性",
AI_DESC: `OpenAI GPT API 的设置。 ` +
`目前 OpenAI API 还处于测试中,您需要在自己的。` +
`OpenAI 账户中充值至少 5 美元后才能生成 API key` +
`然后就可以在 Excalidraw 中配置并使用 AI。`,
AI_OPENAI_TOKEN_NAME: "OpenAI API key",
AI_OPENAI_TOKEN_DESC:
"您可以访问您的<a href='https://platform.openai.com/api-keys'> OpenAI 账户</a>来获取自己的 OpenAI API key。",
AI_OPENAI_TOKEN_PLACEHOLDER: "OpenAI API key",
AI_OPENAI_DEFAULT_MODEL_NAME: "默认的文本 AI 模型",
AI_OPENAI_DEFAULT_MODEL_DESC:
"使用哪个 AI 模型来生成文本。请填写有效的 OpenAI 模型名称。" +
"您可访问<a href='https://platform.openai.com/docs/models'> OpenAI 网站</a>了解更多模型信息。",
AI_OPENAI_DEFAULT_MODEL_PLACEHOLDER: "gpt-3.5-turbo-1106",
AI_OPENAI_DEFAULT_IMAGE_MODEL_NAME: "默认的图像 AI 模型",
AI_OPENAI_DEFAULT_IMAGE_MODEL_DESC:
"使用哪个 AI 模型来生成图像(在编辑和调整图像时会强制使用 dall-e-2 模型," +
"因为目前只有该模型支持编辑和调整图像)。" +
"请填写有效的 OpenAI 模型名称。" +
"您可访问<a href='https://platform.openai.com/docs/models'>OpenAI 网站</a>了解更多模型信息。",
AI_OPENAI_DEFAULT_IMAGE_MODEL_PLACEHOLDER: "dall-e-3",
AI_OPENAI_DEFAULT_VISION_MODEL_NAME: "默认的 AI 视觉模型",
AI_OPENAI_DEFAULT_VISION_MODEL_DESC:
"根据文本生成图像时,使用哪个 AI 视觉模型。请填写有效的 OpenAI 模型名称。" +
"您可访问<a href='https://platform.openai.com/docs/models'> OpenAI 网站</a>了解更多模型信息。",
AI_OPENAI_DEFAULT_API_URL_NAME: "OpenAI API URL",
AI_OPENAI_DEFAULT_API_URL_DESC:
"默认的 OpenAI API URL。请填写有效的 OpenAI API URL。" +
"Excalidraw 会通过该 URL 发送 API 请求给 OpenAI。我没有对此选项做任何错误处理请谨慎修改。",
AI_OPENAI_DEFAULT_IMAGE_API_URL_NAME: "OpenAI Image Generation API URL",
AI_OPENAI_DEFAULT_VISION_MODEL_PLACEHOLDER: "gpt-4-vision-preview",
SAVING_HEAD: "保存",
SAVING_DESC: "包括:压缩,自动保存的时间间隔,文件的命名格式和扩展名等的设置。",
COMPRESS_NAME: "压缩 Excalidraw JSON",
COMPRESS_DESC:
"Excalidraw 绘图文件默认将元素记录为 JSON 格式。开启此项,可将元素的 JSON 数据以 BASE64 编码" +
@@ -180,19 +224,20 @@ FILENAME_HEAD: "文件名",
FILENAME_EXCALIDRAW_EXTENSION_DESC:
"该选项在兼容模式(即非 Excalidraw 专用 Markdown 文件)下不会生效。<br>" +
"<b>开启:</b>使用 .excalidraw.md 作为扩展名。<br><b>关闭:</b>使用 .md 作为扩展名。",
DISPLAY_HEAD: "显示",
DISPLAY_HEAD: "界面 & 行为",
DISPLAY_DESC: "包括:左手模式,主题匹配,缩放,激光笔工具,修饰键等的设置。",
DYNAMICSTYLE_NAME: "动态样式",
DYNAMICSTYLE_DESC:
"根据画布颜色调节 Excalidraw 界面颜色",
"根据画布颜色自动调节 Excalidraw 界面颜色",
LEFTHANDED_MODE_NAME: "左手模式",
LEFTHANDED_MODE_DESC:
"目前只在托盘模式下生效。若开启此项,则托盘(绘图工具属性页)将位于右侧。" +
"<br><b>开启:</b>左手模式。<br><b>关闭:</b>右手模式。",
IFRAME_MATCH_THEME_NAME: "使 MD-Embed 匹配 Excalidraw 主题",
IFRAME_MATCH_THEME_NAME: "使 Embeddable 匹配 Excalidraw 主题",
IFRAME_MATCH_THEME_DESC:
"<b>开启:</b>当你的 Obsidian 和 Excalidraw 一个使用黑暗主题、一个使用明亮主题时," +
"开启此项MD-Embed 将会匹配 Excalidraw 主题。<br>" +
"<b>关闭:</b>如果想要 MD-Embed 匹配 Obsidian 主题,请关闭此项。",
"<b>开启:</b>当 Obsidian 和 Excalidraw 一个使用黑暗主题、一个使用明亮主题时," +
"开启此项以交互形式嵌入到绘图中的元素Embeddable 将会匹配 Excalidraw 主题。<br>" +
"<b>关闭:</b>如果想要 Embeddable 匹配 Obsidian 主题,请关闭此项。",
MATCH_THEME_NAME: "使新建的绘图匹配 Obsidian 主题",
MATCH_THEME_DESC:
"如果 Obsidian 使用黑暗主题,新建的绘图文件也将使用黑暗主题。<br>" +
@@ -213,7 +258,8 @@ FILENAME_HEAD: "文件名",
DEFAULT_PEN_MODE_NAME: "触控笔模式Pen mode",
DEFAULT_PEN_MODE_DESC:
"打开绘图时,是否自动开启触控笔模式?",
THEME_HEAD: "主题和样式",
ZOOM_HEAD: "缩放",
DEFAULT_PINCHZOOM_NAME: "允许在触控笔模式下进行双指缩放",
DEFAULT_PINCHZOOM_DESC:
"在触控笔模式下使用自由画笔工具时,双指缩放可能造成干扰。<br>" +
@@ -232,7 +278,14 @@ FILENAME_HEAD: "文件名",
ZOOM_TO_FIT_MAX_LEVEL_NAME: "自动缩放的最高级别",
ZOOM_TO_FIT_MAX_LEVEL_DESC:
"自动缩放画布时,允许放大的最高级别。该值不能低于 0.550%)且不能超过 101000%)。",
LINKS_HEAD: "链接Links & 以内部链接形式嵌入到绘图中的 Markdown 文档Transclusion",
LASER_HEAD: "激光笔工具More Tools > Laser pointer",
LASER_COLOR: "激光笔颜色",
LASER_DECAY_TIME_NAME: "激光笔消失时间",
LASER_DECAY_TIME_DESC: "单位是毫秒,默认是 1000即 1 秒)。",
LASER_DECAY_LENGTH_NAME: "激光笔轨迹长度",
LASER_DECAY_LENGTH_DESC: "默认是 50。",
LINKS_HEAD: "链接 & 以内部链接形式嵌入到绘图中的 Markdown 文档MD-Transclusion& 待办任务Todo",
LINKS_HEAD_DESC: "包括链接的打开和显示MD-Transclusion 的显示Todo 的显示等设置。",
LINKS_DESC:
`按住 ${labelCTRL()} 并点击包含 <code>[[链接]]</code> 的文本元素可以打开其中的链接。` +
"如果所选文本元素包含多个 <code>[[有效的内部链接]]</code> ,只会打开第一个链接;" +
@@ -240,6 +293,12 @@ FILENAME_HEAD: "文件名",
"插件会在浏览器中打开链接。<br>" +
"链接的源文件被重命名时,绘图中相应的 <code>[[内部链接]]</code> 也会同步更新。" +
"若您不愿绘图中的链接外观因此而变化,可使用 <code>[[内部链接|别名]]</code>。",
DRAG_MODIFIER_NAME: "修饰键",
DRAG_MODIFIER_DESC: "在您按住点击链接或拖放元素时,可以触发某些行为。您可以为这些行为添加修饰键。" +
"Excalidraw 不会检查您的设置是否合理,因此请谨慎设置,避免冲突。" +
"以下选项在苹果和非苹果设备上区别很大,如果您在多个硬件平台上使用 Obsidian需要分别进行设置。"+
"选项里的 4 个开关依次代表 " +
(DEVICE.isIOS || DEVICE.isMacOS ? "SHIFT, CMD, OPT, CONTROL." : "SHIFT, CTRL, ALT, META (Win 键)。"),
ADJACENT_PANE_NAME: "在相邻面板中打开",
ADJACENT_PANE_DESC:
`按住 ${labelCTRL()}+${labelSHIFT()} 并点击绘图里的内部链接时,插件默认会在新面板中打开该链接。<br>` +
@@ -284,31 +343,35 @@ FILENAME_HEAD: "文件名",
LINK_CTRL_CLICK_DESC:
"如果此功能影响到您使用某些原版 Excalidraw 功能,可将其关闭。" +
"关闭后,您只能通过绘图面板标题栏中的链接按钮来打开链接。",
TRANSCLUSION_WRAP_NAME: "Transclusion 的折行方式",
TRANSCLUSION_WRAP_NAME: "MD-Transclusion 的折行方式",
TRANSCLUSION_WRAP_DESC:
"中的 number 表示嵌入的文本溢出时,在第几个字符处进行折行。<br>" +
"此开关控制具体的折行方式。若开启,则严格在 number 处折行,禁止溢出;" +
"若关闭,则允许在 number 位置后最近的空格处折行。",
TRANSCLUSION_DEFAULT_WRAP_NAME: "Transclusion 的默认折行位置",
TRANSCLUSION_DEFAULT_WRAP_NAME: "MD-Transclusion 的默认折行位置",
TRANSCLUSION_DEFAULT_WRAP_DESC:
"除了通过 <code>![[doc#^block]]{number}</code> 中的 number 来控制折行位置,您也可以在此设置 number 的默认值。<br>" +
"一般设为 0 即可,表示不设置固定的默认值,这样当您需要嵌入文档到便签中时," +
"Excalidraw 能更好地帮您自动处理。",
PAGE_TRANSCLUSION_CHARCOUNT_NAME: "Transclusion 的最大显示字符数",
PAGE_TRANSCLUSION_CHARCOUNT_NAME: "MD-Transclusion 的最大显示字符数",
PAGE_TRANSCLUSION_CHARCOUNT_DESC:
"以 <code>![[内部链接]]</code> 或 <code>![](内部链接)</code> 的形式将文档以文本形式嵌入到绘图中时," +
"该文档在绘图中可显示的最大字符数量。",
QUOTE_TRANSCLUSION_REMOVE_NAME: "隐藏 Transclusion 行首的引用符号",
QUOTE_TRANSCLUSION_REMOVE_DESC: "不显示 Transclusion 中每一行行首的 > 符号,以提高纯文本 Transclusion 的可读性。<br>" +
QUOTE_TRANSCLUSION_REMOVE_NAME: "隐藏 MD-Transclusion 行首的引用符号",
QUOTE_TRANSCLUSION_REMOVE_DESC: "不显示 MD-Transclusion 中每一行行首的 > 符号,以提高纯文本 MD-Transclusion 的可读性。<br>" +
"<b>开启:</b>隐藏 > 符号<br><b>关闭:</b>不隐藏 > 符号(注意,由于 Obsidian API 的原因,首行行首的 > 符号不会被隐藏)",
GET_URL_TITLE_NAME: "使用 iframly 获取页面标题",
GET_URL_TITLE_DESC:
"拖放链接到 Excalidraw 时,使用 <code>http://iframely.server.crestify.com/iframely?url=</code> 来获取页面的标题。",
PDF_TO_IMAGE: "以图像形式嵌入到绘图中的 PDF 文档",
PDF_TO_IMAGE_SCALE_NAME: "分辨率",
PDF_TO_IMAGE_SCALE_DESC: "分辨率越高,图像越清晰,但内存占用也越大。" +
"此外,如果您想要复制这些图像到 Excalidraw.com可能会超出其 2MB 大小的限制。",
EMBED_TOEXCALIDRAW_HEAD: "嵌入到绘图中的文件",
EMBED_TOEXCALIDRAW_DESC: "包括:以图像形式嵌入到绘图中的 PDF 文档、以交互形式嵌入到绘图中的 Markdown 文档MD-Embeddable、以图像形式嵌入的 Markdown 文档MD-Embed等。",
MD_HEAD: "以图像形式嵌入到绘图中的 Markdown 文档MD-Embed",
MD_HEAD_DESC:
"除了 Transclusion您还可以将 Markdown 文档以图像形式嵌入到绘图中。" +
`方法是按住 ${labelCTRL()} 并从文件管理器中把文档拖入绘图,或者执行“以图像形式嵌入”系列命令。`,
MD_EMBED_CUSTOMDATA_HEAD_NAME: "以交互形式嵌入到绘图中的 Markdown 文档MD-Embeddable",
MD_EMBED_CUSTOMDATA_HEAD_DESC: `这些选项不会影响到已存在的 MD-Embeddable。MD-Embeddable 的主题风格在“显示 & 行为”小节设置。`,
MD_TRANSCLUDE_WIDTH_NAME: "MD-Embed 的默认宽度",
MD_TRANSCLUDE_WIDTH_DESC:
"MD-Embed 的宽度。该选项会影响到折行,以及图像元素的宽度。<br>" +
@@ -344,16 +407,24 @@ FILENAME_HEAD: "文件名",
"此外,在 CSS 中不能任意地设置字体,您一般只能使用系统默认的标准字体(详见 README" +
"但可以通过上面的设置来额外添加一个自定义字体。<br>" +
"您可为某个 MD-Embed 单独设置此项,方法是在其源文件的 frontmatter 中添加形如 <code>excalidraw-css: 库中的CSS文件或CSS片段</code> 的键值对。",
EMBED_HEAD: "嵌入到 Markdown 文档中的绘图 & 导出",
EMBED_CACHING: "启用预览图",
EMBED_SIZING: "预览图的尺寸",
EMBED_THEME_BACKGROUND: "预览图的主题和背景色",
EMBED_IMAGE_CACHE_NAME: "为嵌入到 Markdown 文档中的绘图创建预览图",
EMBED_IMAGE_CACHE_DESC: "为嵌入到文档中的绘图创建预览图。可提高下次嵌入的速度。" +
EMBED_HEAD: "嵌入到 Markdown 文档中的绘图",
EMBED_DESC: `包括:嵌入到 Markdown 文档中的绘图的预览图类型SVG、PNG、源文件类型Excalidraw 绘图文件、SVG、PNG、缓存、图像大小、图像主题以及嵌入的语法等。
此外,还有自动导出 SVG 或 PNG 文件并保持与绘图文件状态同步的设置。`,
EMBED_CANVAS: "Obsidian 白板支持",
EMBED_CANVAS_NAME: "沉浸式嵌入",
EMBED_CANVAS_DESC:
"当嵌入绘图到 Obsidian 白板中时,隐藏元素的边界和背景。" +
"注意:如果想要背景完全透明,您依然需要在 Excalidraw 中设置“导出的图像不包含背景”。",
EMBED_CACHING: "预览图缓存",
EXPORT_SUBHEAD: "导出",
EMBED_SIZING: "图像尺寸",
EMBED_THEME_BACKGROUND: "图像的主题和背景色",
EMBED_IMAGE_CACHE_NAME: "为嵌入到 Markdown 文档中的绘图创建预览图缓存",
EMBED_IMAGE_CACHE_DESC: "可提高下次嵌入的速度。" +
"但如果绘图中又嵌入了子绘图,当子绘图改变时,您需要打开子绘图并手动保存,才能够更新父绘图的预览图。",
EMBED_IMAGE_CACHE_CLEAR: "清除预览图",
EMBED_IMAGE_CACHE_CLEAR: "清除缓存",
BACKUP_CACHE_CLEAR: "清除备份",
BACKUP_CACHE_CLEAR_CONFIRMATION: "该操作将删除所有绘图文件的备份。备份是绘图文件损坏时的一种补救手段。每次您打开 Obsidian 时,本插件会自动清理无用的备份。您确定要删除所有备份吗?",
BACKUP_CACHE_CLEAR_CONFIRMATION: "该操作将删除所有绘图文件的备份。备份是绘图文件损坏时的一种补救手段。每次您打开 Obsidian 时,本插件会自动清理无用的备份。您确定要现在删除所有备份吗?",
EMBED_REUSE_EXPORTED_IMAGE_NAME:
"将之前已导出的图像作为预览图",
EMBED_REUSE_EXPORTED_IMAGE_DESC:
@@ -368,7 +439,7 @@ FILENAME_HEAD: "文件名",
"<b>关闭:</b>为嵌入到 Markdown 文档中的绘图生成 <a href='' target='_blank'>PNG</a> 格式的预览图。注意PNG 格式预览图不支持某些 <a href='https://www.youtube.com/watch?v=yZQoJg2RCKI&t=633s' target='_blank'>绘图元素的块引用特性</a>。",*/
EMBED_PREVIEW_IMAGETYPE_NAME: "预览图的格式",
EMBED_PREVIEW_IMAGETYPE_DESC:
"<b>原始 SVG</b>高品质、可交互。<br>" +
"<b>Native SVG</b>高品质、可交互。<br>" +
"<b>SVG</b>高品质、不可交互。<br>" +
"<b>PNG</b>高性能、<a href='https://www.youtube.com/watch?v=yZQoJg2RCKI&t=633s' target='_blank'>不可交互</a>。",
PREVIEW_MATCH_OBSIDIAN_NAME: "预览图匹配 Obsidian 主题",
@@ -386,6 +457,11 @@ FILENAME_HEAD: "文件名",
"如果您想选择 PNG 或 SVG 副本,需要先开启下方的“自动导出 PNG 副本”或“自动导出 SVG 副本”。<br>" +
"如果您选择了 PNG 或 SVG 副本,当副本不存在时,该命令将会插入一条损坏的链接,您需要打开绘图文件并手动导出副本才能修复 —— " +
"也就是说,该选项不会自动帮您生成 PNG/SVG 副本,而只会引用已有的 PNG/SVG 副本。",
EMBED_MARKDOWN_COMMENT_NAME: "Embed link to drawing as comment",
EMBED_MARKDOWN_COMMENT_DESC:
"Embed the link to the original Excalidraw file as a markdown link under the image, e.g.:<code>%%[[drawing.excalidraw]]%%</code>.<br>" +
"Instead of adding a markdown comment you may also select the embedded SVG or PNG line and use the command palette action: " +
"'<code>Excalidraw: Open Excalidraw drawing</code>' to open the drawing.",
EMBED_WIKILINK_NAME: "“嵌入绘图到当前 Markdown 文档中”系列命令产生的内部链接类型",
EMBED_WIKILINK_DESC:
"<b>开启:</b>将产生 <code>![[Wiki 链接]]</code>。<b>关闭:</b>将产生 <code>![](Markdown 链接)</code>。",
@@ -423,6 +499,14 @@ FILENAME_HEAD: "文件名",
EXPORT_BOTH_DARK_AND_LIGHT_DESC: "若开启Excalidraw 将导出两个文件filename.dark.png或 filename.dark.svg和 filename.light.png或 filename.light.svg。<br>"+
"该选项可作用于“自动导出 SVG 副本”、“自动导出 PNG 副本”,以及其他的手动的导出命令。",
COMPATIBILITY_HEAD: "兼容性设置",
COMPATIBILITY_DESC: "如果没有特殊原因(例如您想同时在 VSCode / Logseq 和 Obsidian 中使用 Excalidraw建议您使用 markdown 格式的绘图文件,而不是旧的 excalidraw.com 格式,因为本插件的很多功能在旧格式中无法使用。",
SLIDING_PANES_NAME: "Sliding panes 插件支持",
SLIDING_PANES_DESC:
"设置此项后需要重启 Obsidian 才能生效。<br>" +
"如果您使用 <a href='https://github.com/deathau/sliding-panes-obsidian' target='_blank'>Sliding Panes 插件</a>" +
"您可以开启此项来使 Excalidraw 绘图兼容此插件。<br>" +
"注意,开启后会产生一些与 Obsidian 工作空间的兼容性问题。<br>" +
"另外Obsidian 现在已经原生支持 Stack Tabs堆叠标签基本实现了 Sliding Panes 插件的功能。",
EXPORT_EXCALIDRAW_NAME: "自动导出 Excalidraw 旧格式副本",
EXPORT_EXCALIDRAW_DESC: "和“自动导出 SVG 副本”类似,但是导出格式为 *.excalidraw。",
SYNC_EXCALIDRAW_NAME:
@@ -432,6 +516,7 @@ FILENAME_HEAD: "文件名",
"则根据旧格式文件的内容来更新新格式文件。",
COMPATIBILITY_MODE_NAME: "以旧格式创建新绘图",
COMPATIBILITY_MODE_DESC:
"⚠️ 慎用99.9% 的情况下您不需要开启此项。" +
"开启此功能后,您通过功能区按钮、命令面板、" +
"文件浏览器等创建的绘图都将是旧格式(*.excalidraw。" +
"此外,您打开旧格式绘图文件时将不再收到警告消息。",
@@ -442,16 +527,31 @@ FILENAME_HEAD: "文件名",
LATEX_DEFAULT_NAME: "插入 LaTeX 时的默认表达式",
LATEX_DEFAULT_DESC: "允许留空。允许使用类似 <code>\\color{white}</code> 的格式化表达式。",
NONSTANDARD_HEAD: "非 Excalidraw.com 官方支持的特性",
NONSTANDARD_DESC: "这些特性不受 Excalidraw.com 官方支持。当导出绘图到 Excalidraw.com ,这些特性将会发生变化。",
CUSTOM_PEN_NAME: "自定义画笔的数量",
CUSTOM_PEN_DESC: "在画布上的 Obsidian 菜单旁边切换自定义画笔。长按画笔按钮可以修改其样式。",
EXPERIMENTAL_HEAD: "实验性功能",
EXPERIMENTAL_DESC:
"以下部分设置不会立即生效,需要刷新文件资源管理器或者重启 Obsidian 才会生效。",
NONSTANDARD_DESC: `这些特性不受 Excalidraw.com 官方支持。如果以 Excalidraw.com 格式导出绘图,这些特性将会发生不可预知的变化。
包括:自定义画笔工具的数量,自定义字体等。`,
CUSTOM_PEN_HEAD: "自定义画笔",
CUSTOM_PEN_NAME: "自定义画笔工具的数量",
CUSTOM_PEN_DESC: "在画布上的 Obsidian 菜单按钮旁边切换自定义画笔。长按画笔按钮可以修改其样式。",
EXPERIMENTAL_HEAD: "杂项",
EXPERIMENTAL_DESC: `包括:默认的 LaTeX 公式字段建议绘图文件的类型标识符OCR 等设置。`,
EA_HEAD: "Excalidraw 自动化",
EA_DESC:
"ExcalidrawAutomate 是用于 Excalidraw 自动化脚本的 API但是目前说明文档还不够完善" +
"建议阅读 <a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/docs/API/ExcalidrawAutomate.d.ts'>ExcalidrawAutomate.d.ts</a> 文件源码," +
"参考 <a href='https://zsviczian.github.io/obsidian-excalidraw-plugin/'>ExcalidrawAutomate How-to</a> 网页(不过该网页" +
"有一段时间未更新了),并开启下方的字段建议。字段建议功能能够在您键入时提示可用的" +
"函数及相应的参数,而且附带描述,相当于最新的“文档”。",
FIELD_SUGGESTER_NAME: "开启字段建议",
FIELD_SUGGESTER_DESC:
"开启后,当您在编辑器中输入 <code>excalidraw-</code> 或者 <code>ea.</code> 时,会弹出一个带有函数说明的自动补全提示菜单。<br>" +
"该功能借鉴了 Breadcrumbs 和 Templater 插件。",
STARTUP_SCRIPT_NAME: "起动脚本",
STARTUP_SCRIPT_DESC:
"插件启动时将自动执行该脚本。可用于为您的 Excalidraw 自动化脚本设置钩子。" +
"起动脚本请用 javascript 代码编写,并保存为 Markdown 格式。",
STARTUP_SCRIPT_BUTTON_CREATE: "创建起动脚本",
STARTUP_SCRIPT_BUTTON_OPEN: "打开起动脚本",
STARTUP_SCRIPT_EXISTS: "起动脚本已存在",
FILETYPE_NAME: "在文件浏览器中为 excalidraw.md 文件添加类型标识符(如 ✏️)",
FILETYPE_DESC:
"可通过下一项设置来自定义类型标识符。",
@@ -463,6 +563,7 @@ FILENAME_HEAD: "文件名",
"开启此项,则可在 Obsidian 实时预览模式的编辑视图下,用形如 <code>![[绘图|宽度|样式]]</code> 的语法来嵌入绘图。<br>" +
"该选项不会在已打开的文档中立刻生效 —— " +
"你需要重新打开此文档来使其生效。",
CUSTOM_FONT_HEAD: "自定义字体",
ENABLE_FOURTH_FONT_NAME: "为文本元素启用本地字体",
ENABLE_FOURTH_FONT_DESC:
"开启此项后,文本元素的属性面板里会多出一个本地字体按钮。<br>" +
@@ -474,13 +575,14 @@ FILENAME_HEAD: "文件名",
"选择库文件夹中的一个 .ttf, .woff 或 .woff2 字体文件作为本地字体文件。" +
"若未选择文件,则使用默认的 Virgil 字体。",
SCRIPT_SETTINGS_HEAD: "已安装脚本的设置",
SCRIPT_SETTINGS_DESC: "有些 Excalidraw 自动化脚本包含设置项,当执行这些脚本时,它们会在该列表下添加设置项。",
TASKBONE_HEAD: "Taskbone OCR光学符号识别",
TASKBONE_DESC: "这是一个将 OCR 融入 Excalidraw 的实验性功能。请注意Taskbone 是一项独立的外部服务,而不是由 Excalidraw 或 Obsidian-excalidraw-plugin 项目提供的。" +
"OCR 能够对画布上用自由画笔工具写下的涂鸦或者嵌入的图像进行文本识别,并将识别出来的文本写入绘图文件的 frontmatter同时复制到剪贴板。" +
"之所以要写入 frontmatter 是为了便于您在 Obsidian 中能够搜索到这些文本。" +
"注意,识别的过程不是在本地进行的,而是通过在线 API图像会被上传到 taskbone 的服务器(仅用于识别目的)。如果您介意,请不要使用这个功能。",
TASKBONE_ENABLE_NAME: "启用 Taskbone",
TASKBONE_ENABLE_DESC: "启用这个功能意味着同意 Taskbone <a href='https://www.taskbone.com/legal/terms/' target='_blank'>条款及细则</a> 以及 " +
TASKBONE_ENABLE_DESC: "启用意味着同意 Taskbone <a href='https://www.taskbone.com/legal/terms/' target='_blank'>条款及细则</a> 以及 " +
"<a href='https://www.taskbone.com/legal/privacy/' target='_blank'>隐私政策</a>.",
TASKBONE_APIKEY_NAME: "Taskbone API Key",
TASKBONE_APIKEY_DESC: "Taskbone 的免费 API key 提供了一定数量的每月识别次数。如果您非常频繁地使用此功能,或者想要支持 " +
@@ -489,9 +591,12 @@ FILENAME_HEAD: "文件名",
//openDrawings.ts
SELECT_FILE: "选择一个文件后按回车。",
SELECT_COMMAND: "选择一个命令后按回车。",
SELECT_FILE_WITH_OPTION_TO_SCALE: `选择一个文件后按回车,或者 ${labelSHIFT()}+${labelMETA()}+ENTER 以 100% 尺寸插入。`,
NO_MATCH: "查询不到匹配的文件。",
NO_MATCHING_COMMAND: "查询不到匹配的命令。",
SELECT_FILE_TO_LINK: "选择要插入(以内部链接形式嵌入)到当前绘图中的文件。",
SELECT_COMMAND_PLACEHOLDER: "选择要插入到当前绘图中的命令。",
SELECT_DRAWING: "选择要插入(以图像形式嵌入)到当前绘图中的图像或绘图文件。",
TYPE_FILENAME: "键入要选择的绘图名称。",
SELECT_FILE_OR_TYPE_NEW:
@@ -528,8 +633,32 @@ FILENAME_HEAD: "文件名",
NARROW_TO_BLOCK: "缩放至块",
SHOW_ENTIRE_FILE: "显示全部",
ZOOM_TO_FIT: "缩放至合适大小",
RELOAD: "重载",
RELOAD: "重载链接",
OPEN_IN_BROWSER: "在浏览器中打开",
PROPERTIES: "属性",
COPYCODE: "复制源文件",
//EmbeddableSettings.tsx
ES_TITLE: "Embeddable 元素设置",
ES_RENAME: "重命名",
ES_ZOOM: "缩放",
ES_YOUTUBE_START: "YouTube 起始时间",
ES_YOUTUBE_START_DESC: "ss, mm:ss, hh:mm:ss",
ES_YOUTUBE_START_INVALID: "YouTube 起始时间无效。请检查格式并重试",
ES_FILENAME_VISIBLE: "显示文件名",
ES_BACKGROUND_HEAD: "背景色",
ES_BACKGROUND_MATCH_ELEMENT: "匹配元素背景色",
ES_BACKGROUND_MATCH_CANVAS: "匹配画布背景色",
ES_BACKGROUND_COLOR: "背景色",
ES_BORDER_HEAD: "边框颜色",
ES_BORDER_COLOR: "边框颜色",
ES_BORDER_MATCH_ELEMENT: "匹配元素边框颜色",
ES_BACKGROUND_OPACITY: "背景透明度",
ES_BORDER_OPACITY: "边框透明度",
ES_EMBEDDABLE_SETTINGS: "MD-Embeddable 设置",
ES_USE_OBSIDIAN_DEFAULTS: "使用 Obsidian 默认设置",
ES_ZOOM_100_RELATIVE_DESC: "使元素的缩放等级等于当前画布的缩放等级",
ES_ZOOM_100: "Relative 100%",
//Prompts.ts
PROMPT_FILE_DOES_NOT_EXIST: "文件不存在。要创建吗?",
@@ -538,7 +667,11 @@ FILENAME_HEAD: "文件名",
PROMPT_TITLE_NEW_FILE: "新建文件",
PROMPT_TITLE_CONFIRMATION: "确认",
PROMPT_BUTTON_CREATE_EXCALIDRAW: "创建 Excalidraw 绘图",
PROMPT_BUTTON_CREATE_EXCALIDRAW_ARIA: "创建 Excalidraw 绘图并在新页签中打开",
PROMPT_BUTTON_CREATE_MARKDOWN: "创建 Markdown 文档",
PROMPT_BUTTON_CREATE_MARKDOWN_ARIA: "创建 Markdown 文档并在新页签中打开",
PROMPT_BUTTON_EMBED_MARKDOWN: "嵌入",
PROMPT_BUTTON_EMBED_MARKDOWN_ARIA: "将所选元素替换为 MD-Embeddable",
PROMPT_BUTTON_NEVERMIND: "算了",
PROMPT_BUTTON_OK: "OK",
PROMPT_BUTTON_CANCEL: "取消",
@@ -547,4 +680,10 @@ FILENAME_HEAD: "文件名",
PROMPT_BUTTON_INSERT_LINK: "插入内部链接",
PROMPT_BUTTON_UPPERCASE: "大写",
//ModifierKeySettings
WEB_BROWSER_DRAG_ACTION: "从浏览器拖进来时",
LOCAL_FILE_DRAG_ACTION: "从本地文件系统拖进来时",
INTERNAL_DRAG_ACTION: "在 Obsidian 内部拖放时",
PANE_TARGET: "点击链接时",
DEFAULT_ACTION_DESC: "无修饰键时的行为:",
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
import { Globe, RotateCcw, Scan } from "lucide-react";
import { Copy, Crop, Globe, RotateCcw, Scan, Settings } from "lucide-react";
import * as React from "react";
import { PenStyle } from "src/PenTypes";
@@ -27,8 +27,11 @@ export const ICONS = {
</svg>
),
Reload: (<RotateCcw />),
Copy: (<Copy /> ),
Globe: (<Globe />),
Crop: (<Crop />),
ZoomToSelectedElement: (<Scan />),
Properties: (<Settings />),
ZoomToSection: (
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,16 +1,17 @@
import { TFile } from "obsidian";
import * as React from "react";
import ExcalidrawView from "../ExcalidrawView";
import { ExcalidrawElement, ExcalidrawEmbeddableElement } from "@zsviczian/excalidraw/types/element/types";
import { AppState, ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
import { ExcalidrawElement, ExcalidrawEmbeddableElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { AppState, ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
import { ActionButton } from "./ActionButton";
import { ICONS } from "./ActionIcons";
import { t } from "src/lang/helpers";
import { ScriptEngine } from "src/Scripts";
import { ROOTELEMENTSIZE, mutateElement, nanoid, sceneCoordsToViewportCoords } from "src/constants";
import { ROOTELEMENTSIZE, mutateElement, nanoid, sceneCoordsToViewportCoords } from "src/constants/constants";
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "src/ExcalidrawData";
import { processLinkText, useDefaultExcalidrawFrame } from "src/utils/CustomEmbeddableUtils";
import { cleanSectionHeading } from "src/utils/ObsidianUtils";
import { EmbeddableSettings } from "src/dialogs/EmbeddableSettings";
export class EmbeddableMenu {
@@ -77,21 +78,26 @@ export class EmbeddableMenu {
if(!link) return null;
const isExcalidrawiFrame = useDefaultExcalidrawFrame(element);
let isObsidianiFrame = element.link?.match(REG_LINKINDEX_HYPERLINK);
let isObsidianiFrame = Boolean(element.link?.match(REG_LINKINDEX_HYPERLINK));
if(!isExcalidrawiFrame && !isObsidianiFrame) {
const res = REGEX_LINK.getRes(element.link).next();
if(!res || (!res.value && res.done)) {
return null;
if(link.startsWith("data:text/html")) {
isObsidianiFrame = true;
} else {
const res = REGEX_LINK.getRes(element.link).next();
if(!res || (!res.value && res.done)) {
return null;
}
link = REGEX_LINK.getLink(res);
isObsidianiFrame = Boolean(link.match(REG_LINKINDEX_HYPERLINK));
}
link = REGEX_LINK.getLink(res);
isObsidianiFrame = link.match(REG_LINKINDEX_HYPERLINK);
if(!isObsidianiFrame) {
const { subpath, file } = processLinkText(link, view);
if(!file || file.extension!=="md") return null;
if(!file) return;
const isMD = file.extension==="md";
const { x, y } = sceneCoordsToViewportCoords( { sceneX: element.x, sceneY: element.y }, appState);
const top = `${y-2.5*ROOTELEMENTSIZE-appState.offsetTop}px`;
const left = `${x-appState.offsetLeft}px`;
@@ -116,79 +122,93 @@ export class EmbeddableMenu {
display: "block",
}}
>
<ActionButton
key={"MarkdownSection"}
title={t("NARROW_TO_HEADING")}
action={async () => {
const sections = (await app.metadataCache.blockCache
.getForFile({ isCancelled: () => false },file))
.blocks.filter((b: any) => b.display && b.node?.type === "heading");
const values = [""].concat(
sections.map((b: any) => `#${cleanSectionHeading(b.display)}`)
);
const display = [t("SHOW_ENTIRE_FILE")].concat(
sections.map((b: any) => b.display)
);
const newSubpath = await ScriptEngine.suggester(
app, display, values, "Select section from document"
);
if(!newSubpath && newSubpath!=="") return;
if (newSubpath !== subpath) {
this.updateElement(newSubpath, element, file);
}
}}
icon={ICONS.ZoomToSection}
view={view}
/>
<ActionButton
key={"MarkdownBlock"}
title={t("NARROW_TO_BLOCK")}
action={async () => {
if(!file) return;
const paragrphs = (await app.metadataCache.blockCache
.getForFile({ isCancelled: () => false },file))
.blocks.filter((b: any) => b.display && b.node?.type === "paragraph");
const values = ["entire-file"].concat(paragrphs);
const display = [t("SHOW_ENTIRE_FILE")].concat(
paragrphs.map((b: any) => `${b.node?.id ? `#^${b.node.id}: ` : ``}${b.display.trim()}`));
const selectedBlock = await ScriptEngine.suggester(
app, display, values, "Select section from document"
);
if(!selectedBlock) return;
{isMD && (
<ActionButton
key={"MarkdownSection"}
title={t("NARROW_TO_HEADING")}
action={async () => {
const sections = (await app.metadataCache.blockCache
.getForFile({ isCancelled: () => false },file))
.blocks.filter((b: any) => b.display && b.node?.type === "heading");
const values = [""].concat(
sections.map((b: any) => `#${cleanSectionHeading(b.display)}`)
);
const display = [t("SHOW_ENTIRE_FILE")].concat(
sections.map((b: any) => b.display)
);
const newSubpath = await ScriptEngine.suggester(
app, display, values, "Select section from document"
);
if(!newSubpath && newSubpath!=="") return;
if (newSubpath !== subpath) {
this.updateElement(newSubpath, element, file);
}
}}
icon={ICONS.ZoomToSection}
view={view}
/>
)}
{isMD && (
<ActionButton
key={"MarkdownBlock"}
title={t("NARROW_TO_BLOCK")}
action={async () => {
if(!file) return;
const paragrphs = (await app.metadataCache.blockCache
.getForFile({ isCancelled: () => false },file))
.blocks.filter((b: any) => b.display && b.node?.type === "paragraph");
const values = ["entire-file"].concat(paragrphs);
const display = [t("SHOW_ENTIRE_FILE")].concat(
paragrphs.map((b: any) => `${b.node?.id ? `#^${b.node.id}: ` : ``}${b.display.trim()}`));
const selectedBlock = await ScriptEngine.suggester(
app, display, values, "Select section from document"
);
if(!selectedBlock) return;
if(selectedBlock==="entire-file") {
if(subpath==="") return;
this.updateElement("", element, file);
return;
}
let blockID = selectedBlock.node.id;
if(blockID && (`#^${blockID}` === subpath)) return;
if (!blockID) {
const offset = selectedBlock.node?.position?.end?.offset;
if(!offset) return;
blockID = nanoid();
const fileContents = await app.vault.cachedRead(file);
if(!fileContents) return;
await app.vault.modify(file, fileContents.slice(0, offset) + ` ^${blockID}` + fileContents.slice(offset));
await sleep(200); //wait for cache to update
}
this.updateElement(`#^${blockID}`, element, file);
}}
icon={ICONS.ZoomToBlock}
view={view}
/>
if(selectedBlock==="entire-file") {
if(subpath==="") return;
this.updateElement("", element, file);
return;
}
let blockID = selectedBlock.node.id;
if(blockID && (`#^${blockID}` === subpath)) return;
if (!blockID) {
const offset = selectedBlock.node?.position?.end?.offset;
if(!offset) return;
blockID = nanoid();
const fileContents = await app.vault.cachedRead(file);
if(!fileContents) return;
await app.vault.modify(file, fileContents.slice(0, offset) + ` ^${blockID}` + fileContents.slice(offset));
await sleep(200); //wait for cache to update
}
this.updateElement(`#^${blockID}`, element, file);
}}
icon={ICONS.ZoomToBlock}
view={view}
/>
)}
<ActionButton
key={"ZoomToElement"}
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}
/>
<ActionButton
key={"Properties"}
title={t("PROPERTIES")}
action={() => {
if(!element) return;
new EmbeddableSettings(view.plugin,view,file,element).open();
}}
icon={ICONS.Properties}
view={view}
/>
</div>
</div>
);
@@ -256,6 +276,28 @@ export class EmbeddableMenu {
icon={ICONS.ZoomToSelectedElement}
view={view}
/>
<ActionButton
key={"Properties"}
title={t("PROPERTIES")}
action={() => {
if(!element) return;
new EmbeddableSettings(view.plugin,view,null,element).open();
}}
icon={ICONS.Properties}
view={view}
/>
{link?.startsWith("data:text/html") && (
<ActionButton
key={"CopyCode"}
title={t("COPYCODE")}
action={() => {
if(!element) return;
navigator.clipboard.writeText(atob(link.split(",")[1]));
}}
icon={ICONS.Copy}
view={view}
/>
)}
</div>
</div>
);

View File

@@ -1,8 +1,8 @@
import { AppState, ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
import { AppState, ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
import clsx from "clsx";
import { TFile } from "obsidian";
import * as React from "react";
import { VIEW_TYPE_EXCALIDRAW } from "src/constants";
import { DEVICE, VIEW_TYPE_EXCALIDRAW } from "src/constants/constants";
import { PenSettingsModal } from "src/dialogs/PenSettingsModal";
import ExcalidrawView from "src/ExcalidrawView";
import { PenStyle } from "src/PenTypes";
@@ -142,7 +142,7 @@ export class ObsidianMenu {
>
<div
className="ToolIcon__icon"
aria-label={pen.type}
aria-label={DEVICE.isDesktop ? pen.type : undefined}
style={{
...appState.activeTool.type === "freedraw" && appState.currentStrokeOptions === pen.penOptions
? {background: "var(--color-primary)"}
@@ -225,7 +225,10 @@ export class ObsidianMenu {
prevClickTimestamp = now;
}}
>
<div className="ToolIcon__icon" aria-label={name}>
<div
className="ToolIcon__icon"
aria-label={DEVICE.isDesktop ? name : undefined}
>
{icon}
</div>
</label>
@@ -265,6 +268,7 @@ export class ObsidianMenu {
},
)}
onClick={() => {
this.view.setCurrentPositionToCenter();
const insertFileModal = new UniversalInsertFileModal(this.plugin, this.view);
insertFileModal.open();
}}

View File

@@ -3,16 +3,15 @@ import { Notice, TFile } from "obsidian";
import * as React from "react";
import { ActionButton } from "./ActionButton";
import { ICONS, saveIcon, stringToSVG } from "./ActionIcons";
import { DEVICE, SCRIPT_INSTALL_FOLDER, VIEW_TYPE_EXCALIDRAW } from "../constants";
import { DEVICE, SCRIPT_INSTALL_FOLDER, VIEW_TYPE_EXCALIDRAW } from "../constants/constants";
import { insertLaTeXToView, search } from "../ExcalidrawAutomate";
import ExcalidrawView, { TextMode } from "../ExcalidrawView";
import { t } from "../lang/helpers";
import { ReleaseNotes } from "../dialogs/ReleaseNotes";
import { ScriptIconMap } from "../Scripts";
import { getIMGFilename } from "../utils/FileUtils";
import { ScriptInstallPrompt } from "src/dialogs/ScriptInstallPrompt";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
import { isALT, isCTRL, isSHIFT, mdPropModifier } from "src/utils/ModifierkeyHelper";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
import { isWinALTorMacOPT, isWinCTRLorMacCMD, isSHIFT } from "src/utils/ModifierkeyHelper";
import { InsertPDFModal } from "src/dialogs/InsertPDFModal";
import { ExportDialog } from "src/dialogs/ExportDialog";
@@ -380,7 +379,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
new Notice("Taskbone OCR is not enabled. Please go to plugins settings to enable it.",4000);
return;
}
this.props.view.plugin.taskbone.getTextForView(this.props.view, isCTRL(e));
this.props.view.plugin.taskbone.getTextForView(this.props.view, isWinCTRLorMacCMD(e));
}}
icon={ICONS.ocr}
view={this.props.view}
@@ -505,7 +504,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
key={"latex"}
title={t("INSERT_LATEX")}
action={(e) => {
if(isALT(e)) {
if(isWinALTorMacOPT(e)) {
this.props.view.openExternalLink("https://youtu.be/r08wk-58DPk");
return;
}
@@ -522,7 +521,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
this.props.centerPointer();
this.props.view.plugin.insertLinkDialog.start(
this.props.view.file.path,
this.props.view.addText,
(text: string, fontFamily?: 1 | 2 | 3 | 4, save?: boolean) => this.props.view.addText (text, fontFamily, save),
);
}}
icon={ICONS.insertLink}
@@ -532,12 +531,12 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
key={"link-to-element"}
title={t("INSERT_LINK_TO_ELEMENT")}
action={(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
if(isALT(e)) {
if(isWinALTorMacOPT(e)) {
this.props.view.openExternalLink("https://youtu.be/yZQoJg2RCKI");
return;
}
this.props.view.copyLinkToSelectedElementToClipboard(
isCTRL(e) ? "group=" : (isSHIFT(e) ? "area=" : "")
isWinCTRLorMacCMD(e) ? "group=" : (isSHIFT(e) ? "area=" : "")
);
}}
icon={ICONS.copyElementLink}
@@ -552,6 +551,16 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
icon={ICONS.importSVG}
view={this.props.view}
/>
<ActionButton
key={"crop-image"}
title={t("CROP_IMAGE")}
action={(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
// @ts-ignore
this.props.view.app.commands.executeCommandById("obsidian-excalidraw-plugin:crop-image")
}}
icon={ICONS.Crop}
view={this.props.view}
/>
</div>
</fieldset>
{this.renderScriptButtons(false)}

View File

@@ -4,7 +4,7 @@ import ExcalidrawPlugin from "../main"
import {log} from "../utils/Utils"
import ExcalidrawView, { ExportSettings } from "../ExcalidrawView"
import FrontmatterEditor from "src/utils/Frontmatter";
import { ExcalidrawElement, ExcalidrawImageElement } from "@zsviczian/excalidraw/types/element/types";
import { ExcalidrawElement, ExcalidrawImageElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { EmbeddedFilesLoader } from "src/EmbeddedFileLoader";
import { blobToBase64 } from "src/utils/FileUtils";
@@ -75,6 +75,7 @@ export default class Taskbone {
const exportSettings: ExportSettings = {
withBackground: true,
withTheme: true,
isMask: false,
};
const img =

View File

@@ -1,5 +1,6 @@
import {
App,
ButtonComponent,
DropdownComponent,
normalizePath,
PluginSettingTab,
@@ -7,7 +8,7 @@ import {
TextComponent,
TFile,
} from "obsidian";
import { GITHUB_RELEASES, VIEW_TYPE_EXCALIDRAW } from "./constants";
import { GITHUB_RELEASES, VIEW_TYPE_EXCALIDRAW } from "./constants/constants";
import ExcalidrawView from "./ExcalidrawView";
import { t } from "./lang/helpers";
import type ExcalidrawPlugin from "./main";
@@ -27,7 +28,11 @@ import {
} from "./utils/Utils";
import { imageCache } from "./utils/ImageCache";
import { ConfirmationPrompt } from "./dialogs/Prompt";
import de from "./lang/locale/de";
import { EmbeddableMDCustomProps } from "./dialogs/EmbeddableSettings";
import { EmbeddalbeMDFileCustomDataSettingsComponent } from "./dialogs/EmbeddableMDFileCustomDataSettingsComponent";
import { startupScript } from "./constants/starutpscript";
import { ModifierKeySet, ModifierSetType } from "./utils/ModifierkeyHelper";
import { ModifierKeySettingsComponent } from "./dialogs/ModifierKeySettings";
export interface ExcalidrawSettings {
folder: string;
@@ -149,6 +154,22 @@ export interface ExcalidrawSettings {
DECAY_LENGTH: number,
COLOR: string,
};
embeddableMarkdownDefaults: EmbeddableMDCustomProps;
canvasImmersiveEmbed: boolean,
startupScriptPath: string,
openAIAPIToken: string,
openAIDefaultTextModel: string,
openAIDefaultVisionModel: string,
openAIDefaultImageGenerationModel: string,
openAIURL: string,
openAIImageGenerationURL: string,
openAIImageEditsURL: string,
openAIImageVariationURL: string,
modifierKeyConfig: {
Mac: Record<ModifierSetType, ModifierKeySet>,
Win: Record<ModifierSetType, ModifierKeySet>,
},
slidingPanesSupport: boolean;
}
declare const PLUGIN_VERSION:string;
@@ -278,7 +299,109 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
DECAY_LENGTH: 50,
DECAY_TIME: 1000,
COLOR: "#ff0000",
}
},
embeddableMarkdownDefaults: {
useObsidianDefaults: false,
backgroundMatchCanvas: false,
backgroundMatchElement: true,
backgroundColor: "#fff",
backgroundOpacity: 60,
borderMatchElement: true,
borderColor: "#fff",
borderOpacity: 0,
filenameVisible: false,
},
canvasImmersiveEmbed: true,
startupScriptPath: "",
openAIAPIToken: "",
openAIDefaultTextModel: "gpt-3.5-turbo-1106",
openAIDefaultVisionModel: "gpt-4-vision-preview",
openAIDefaultImageGenerationModel: "dall-e-3",
openAIURL: "https://api.openai.com/v1/chat/completions",
openAIImageGenerationURL: "https://api.openai.com/v1/images/generations",
openAIImageEditsURL: "https://api.openai.com/v1/images/edits",
openAIImageVariationURL: "https://api.openai.com/v1/images/variations",
modifierKeyConfig: {
Mac: {
LocalFileDragAction:{
defaultAction: "image-import",
rules: [
{ shift: false, ctrl_cmd: false, alt_opt: false, meta_ctrl: false, result: "image-import" },
{ shift: true , ctrl_cmd: false, alt_opt: true , meta_ctrl: false, result: "link" },
{ shift: true , ctrl_cmd: false, alt_opt: false, meta_ctrl: false, result: "image-url" },
{ shift: false, ctrl_cmd: false, alt_opt: true , meta_ctrl: false, result: "embeddable" },
],
},
WebBrowserDragAction: {
defaultAction: "image-url",
rules: [
{ shift: false, ctrl_cmd: false, alt_opt: false, meta_ctrl: false, result: "image-url" },
{ shift: true , ctrl_cmd: false, alt_opt: true , meta_ctrl: false, result: "link" },
{ shift: false, ctrl_cmd: false, alt_opt: true , meta_ctrl: false, result: "embeddable" },
{ shift: true , ctrl_cmd: false, alt_opt: false, meta_ctrl: false, result: "image-import" },
],
},
InternalDragAction: {
defaultAction: "link",
rules: [
{ shift: false, ctrl_cmd: false, alt_opt: false, meta_ctrl: false, result: "link" },
{ shift: false, ctrl_cmd: false, alt_opt: false, meta_ctrl: true , result: "embeddable" },
{ shift: true , ctrl_cmd: false, alt_opt: false, meta_ctrl: false, result: "image" },
{ shift: true , ctrl_cmd: false, alt_opt: false, meta_ctrl: true , result: "image-fullsize" },
],
},
LinkClickAction: {
defaultAction: "new-tab",
rules: [
{ shift: false, ctrl_cmd: false, alt_opt: false, meta_ctrl: false, result: "active-pane" },
{ shift: false, ctrl_cmd: true , alt_opt: false, meta_ctrl: false, result: "new-tab" },
{ shift: false, ctrl_cmd: true , alt_opt: true , meta_ctrl: false, result: "new-pane" },
{ shift: true , ctrl_cmd: true , alt_opt: true , meta_ctrl: false, result: "popout-window" },
{ shift: false, ctrl_cmd: true , alt_opt: false, meta_ctrl: true , result: "md-properties" },
],
},
},
Win: {
LocalFileDragAction:{
defaultAction: "image-import",
rules: [
{ shift: false, ctrl_cmd: false, alt_opt: false, meta_ctrl: false, result: "image-import" },
{ shift: false, ctrl_cmd: true , alt_opt: false, meta_ctrl: false, result: "link" },
{ shift: true , ctrl_cmd: false, alt_opt: false, meta_ctrl: false, result: "image-url" },
{ shift: true , ctrl_cmd: true , alt_opt: false, meta_ctrl: false, result: "embeddable" },
],
},
WebBrowserDragAction: {
defaultAction: "image-url",
rules: [
{ shift: false, ctrl_cmd: false, alt_opt: false, meta_ctrl: false, result: "image-url" },
{ shift: false, ctrl_cmd: true , alt_opt: false, meta_ctrl: false, result: "link" },
{ shift: true , ctrl_cmd: true , alt_opt: false, meta_ctrl: false, result: "embeddable" },
{ shift: true , ctrl_cmd: false, alt_opt: false, meta_ctrl: false, result: "image-import" },
],
},
InternalDragAction: {
defaultAction: "link",
rules: [
{ shift: false, ctrl_cmd: false, alt_opt: false, meta_ctrl: false, result: "link" },
{ shift: true , ctrl_cmd: true , alt_opt: false, meta_ctrl: false, result: "embeddable" },
{ shift: true , ctrl_cmd: false, alt_opt: false, meta_ctrl: false, result: "image" },
{ shift: false, ctrl_cmd: true , alt_opt: true , meta_ctrl: false, result: "image-fullsize" },
],
},
LinkClickAction: {
defaultAction: "new-tab",
rules: [
{ shift: false, ctrl_cmd: false, alt_opt: false, meta_ctrl: false, result: "active-pane" },
{ shift: false, ctrl_cmd: true , alt_opt: false, meta_ctrl: false, result: "new-tab" },
{ shift: false, ctrl_cmd: true , alt_opt: true , meta_ctrl: false, result: "new-pane" },
{ shift: true , ctrl_cmd: true , alt_opt: true , meta_ctrl: false, result: "popout-window" },
{ shift: false, ctrl_cmd: true , alt_opt: false, meta_ctrl: true , result: "md-properties" },
],
},
},
},
slidingPanesSupport: false,
};
export class ExcalidrawSettingTab extends PluginSettingTab {
@@ -287,7 +410,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
private requestReloadDrawings: boolean = false;
private requestUpdatePinnedPens: boolean = false;
private requestUpdateDynamicStyling: boolean = false;
private reloadMathJax: boolean = false;
//private reloadMathJax: boolean = false;
//private applyDebounceTimer: number = 0;
constructor(app: App, plugin: ExcalidrawPlugin) {
@@ -341,9 +464,9 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.plugin.triggerEmbedUpdates();
}
this.plugin.scriptEngine.updateScriptPath();
if(this.reloadMathJax) {
/* if(this.reloadMathJax) {
this.plugin.loadMathJax();
}
}*/
}
async display() {
@@ -407,7 +530,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setDesc(fragWithHTML(t("FOLDER_DESC")))
.addText((text) =>
text
.setPlaceholder("Excalidraw")
.setPlaceholder("e.g.: Excalidraw")
.setValue(this.plugin.settings.folder)
.onChange(async (value) => {
this.plugin.settings.folder = value;
@@ -432,7 +555,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setDesc(fragWithHTML(t("TEMPLATE_DESC")))
.addText((text) =>
text
.setPlaceholder("Excalidraw/Template")
.setPlaceholder("e.g.: Excalidraw/Template")
.setValue(this.plugin.settings.templateFilePath)
.onChange(async (value) => {
this.plugin.settings.templateFilePath = value;
@@ -446,7 +569,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setDesc(fragWithHTML(t("SCRIPT_FOLDER_DESC")))
.addText((text) =>
text
.setPlaceholder("Excalidraw/Scripts")
.setPlaceholder("e.g.: Excalidraw/Scripts")
.setValue(this.plugin.settings.scriptFolderPath)
.onChange(async (value) => {
this.plugin.settings.scriptFolderPath = value;
@@ -548,7 +671,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setDesc(fragWithHTML(t("FILENAME_PREFIX_DESC")))
.addText((text) =>
text
.setPlaceholder("Drawing ")
.setPlaceholder("e.g.: Drawing ")
.setValue(this.plugin.settings.drawingFilenamePrefix)
.onChange(async (value) => {
this.plugin.settings.drawingFilenamePrefix = value.replaceAll(
@@ -624,6 +747,82 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
);
//------------------------------------------------
// AI Settings
//------------------------------------------------
containerEl.createEl("hr", { cls: "excalidraw-setting-hr" });
containerEl.createDiv({ text: t("AI_DESC"), cls: "setting-item-description" });
detailsEl = this.containerEl.createEl("details");
const aiDetailsEl = detailsEl;
detailsEl.createEl("summary", {
text: t("AI_HEAD"),
cls: "excalidraw-setting-h1",
});
new Setting(detailsEl)
.setName(t("AI_OPENAI_TOKEN_NAME"))
.setDesc(fragWithHTML(t("AI_OPENAI_TOKEN_DESC")))
.addText((text) =>
text
.setPlaceholder(t("AI_OPENAI_TOKEN_PLACEHOLDER"))
.setValue(this.plugin.settings.openAIAPIToken)
.onChange(async (value) => {
this.plugin.settings.openAIAPIToken = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("AI_OPENAI_DEFAULT_MODEL_NAME"))
.setDesc(fragWithHTML(t("AI_OPENAI_DEFAULT_MODEL_DESC")))
.addText((text) =>
text
.setPlaceholder(t("AI_OPENAI_DEFAULT_MODEL_PLACEHOLDER"))
.setValue(this.plugin.settings.openAIDefaultTextModel)
.onChange(async (value) => {
this.plugin.settings.openAIDefaultTextModel = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("AI_OPENAI_DEFAULT_VISION_MODEL_NAME"))
.setDesc(fragWithHTML(t("AI_OPENAI_DEFAULT_VISION_MODEL_DESC")))
.addText((text) =>
text
.setPlaceholder(t("AI_OPENAI_DEFAULT_VISION_MODEL_PLACEHOLDER"))
.setValue(this.plugin.settings.openAIDefaultVisionModel)
.onChange(async (value) => {
this.plugin.settings.openAIDefaultVisionModel = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("AI_OPENAI_DEFAULT_IMAGE_MODEL_NAME"))
.setDesc(fragWithHTML(t("AI_OPENAI_DEFAULT_IMAGE_MODEL_DESC")))
.addText((text) =>
text
.setPlaceholder(t("AI_OPENAI_DEFAULT_IMAGE_MODEL_PLACEHOLDER"))
.setValue(this.plugin.settings.openAIDefaultImageGenerationModel)
.onChange(async (value) => {
this.plugin.settings.openAIDefaultImageGenerationModel = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("AI_OPENAI_DEFAULT_API_URL_NAME"))
.setDesc(fragWithHTML(t("AI_OPENAI_DEFAULT_API_URL_DESC")))
.addText((text) =>
text
.setPlaceholder("e.g.: https://api.openai.com/v1/chat/completions")
.setValue(this.plugin.settings.openAIURL)
.onChange(async (value) => {
this.plugin.settings.openAIURL = value;
this.applySettingsUpdate();
}),
);
// ------------------------------------------------
// Display
@@ -737,6 +936,11 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.matchThemeTrigger)
.onChange(async (value) => {
this.plugin.settings.matchThemeTrigger = value;
if(value) {
this.plugin.addThemeObserver();
} else {
this.plugin.removeThemeObserver();
}
this.applySettingsUpdate();
}),
);
@@ -898,6 +1102,18 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
el.innerText = ` ${this.plugin.settings.laserSettings.DECAY_LENGTH.toString()}`;
});
detailsEl = displayDetailsEl.createEl("details");
detailsEl.createEl("summary", {
text: t("DRAG_MODIFIER_NAME"),
cls: "excalidraw-setting-h3",
});
detailsEl.createDiv({ text: t("DRAG_MODIFIER_DESC"), cls: "setting-item-description" });
new ModifierKeySettingsComponent(
detailsEl,
this.plugin.settings.modifierKeyConfig,
this.applySettingsUpdate,
).render();
// ------------------------------------------------
// Links and Transclusions
@@ -1257,6 +1473,24 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}),
);
detailsEl = embedDetailsEl.createEl("details");
detailsEl.createEl("summary", {
text: t("EMBED_CANVAS"),
cls: "excalidraw-setting-h3",
});
new Setting(detailsEl)
.setName(t("EMBED_CANVAS_NAME"))
.setDesc(fragWithHTML(t("EMBED_CANVAS_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.canvasImmersiveEmbed)
.onChange(async (value) => {
this.plugin.settings.canvasImmersiveEmbed = value;
this.applySettingsUpdate();
}),
);
detailsEl = embedDetailsEl.createEl("details");
detailsEl.createEl("summary", {
text: t("EMBED_CACHING"),
@@ -1324,7 +1558,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setDesc(fragWithHTML(t("EMBED_WIDTH_DESC")))
.addText((text) =>
text
.setPlaceholder("400")
.setPlaceholder("e.g.: 400")
.setValue(this.plugin.settings.width)
.onChange(async (value) => {
this.plugin.settings.width = value;
@@ -1500,9 +1734,22 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.plugin.settings.autoExportLightAndDark = value;
this.applySettingsUpdate();
}),
);
);
detailsEl = embedDetailsEl.createEl("details");
// ------------------------------------------------
// Embedding settings
// ------------------------------------------------
containerEl.createEl("hr", { cls: "excalidraw-setting-hr" });
containerEl.createDiv({ text: t("EMBED_TOEXCALIDRAW_DESC"), cls: "setting-item-description" });
detailsEl = this.containerEl.createEl("details");
const embedFilesDetailsEl = detailsEl;
detailsEl.createEl("summary", {
text: t("EMBED_TOEXCALIDRAW_HEAD"),
cls: "excalidraw-setting-h1",
});
detailsEl = embedFilesDetailsEl.createEl("details");
detailsEl.createEl("summary", {
text: t("PDF_TO_IMAGE"),
cls: "excalidraw-setting-h3",
@@ -1527,20 +1774,28 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.applySettingsUpdate();
}),
);
// ------------------------------------------------
// Markdown embedding settings
// ------------------------------------------------
containerEl.createEl("hr", { cls: "excalidraw-setting-hr" });
containerEl.createDiv({ text: t("MD_HEAD_DESC"), cls: "setting-item-description" });
detailsEl = this.containerEl.createEl("details");
detailsEl = embedFilesDetailsEl.createEl("details");
detailsEl.createEl("summary", {
text: t("MD_EMBED_CUSTOMDATA_HEAD_NAME"),
cls: "excalidraw-setting-h3",
});
detailsEl.createEl("span", {text: t("MD_EMBED_CUSTOMDATA_HEAD_DESC")});
new EmbeddalbeMDFileCustomDataSettingsComponent(
detailsEl,
this.plugin.settings.embeddableMarkdownDefaults,
this.applySettingsUpdate,
).render();
detailsEl = embedFilesDetailsEl.createEl("details");
detailsEl.createEl("summary", {
text: t("MD_HEAD"),
cls: "excalidraw-setting-h1",
cls: "excalidraw-setting-h3",
});
new Setting(detailsEl)
.setName(t("MD_TRANSCLUDE_WIDTH_NAME"))
.setDesc(fragWithHTML(t("MD_TRANSCLUDE_WIDTH_DESC")))
@@ -1735,7 +1990,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.requestReloadDrawings = true;
this.plugin.settings.experimantalFourthFont = value;
this.applySettingsUpdate(true);
this.plugin.initializeFourthFont();
this.plugin.initializeFonts();
},
);
});
@@ -1753,23 +2008,6 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
cls: "excalidraw-setting-h1",
});
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/628
/*new Setting(detailsEl)
.setName(t("MATHJAX_NAME"))
.setDesc(t("MATHJAX_DESC"))
.addDropdown((dropdown) => {
dropdown
.addOption("https://cdn.jsdelivr.net/npm/mathjax@3.2.1/es5/tex-svg.js", "jsdelivr")
.addOption("https://unpkg.com/mathjax@3.2.1/es5/tex-svg.js", "unpkg")
.addOption("https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.1/es5/tex-svg-full.min.js","cdnjs")
.setValue(this.plugin.settings.mathjaxSourceURL)
.onChange((value)=> {
this.plugin.settings.mathjaxSourceURL = value;
this.reloadMathJax = true;
this.applySettingsUpdate();
})
})*/
addIframe(detailsEl, "r08wk-58DPk");
new Setting(detailsEl)
.setName(t("LATEX_DEFAULT_NAME"))
@@ -1783,18 +2021,6 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}),
);
new Setting(detailsEl)
.setName(t("FIELD_SUGGESTER_NAME"))
.setDesc(fragWithHTML(t("FIELD_SUGGESTER_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.fieldSuggester)
.onChange(async (value) => {
this.plugin.settings.fieldSuggester = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("FILETYPE_NAME"))
.setDesc(fragWithHTML(t("FILETYPE_DESC")))
@@ -1877,6 +2103,77 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}
);
// ------------------------------------------------
// ExcalidrawAutomate
// ------------------------------------------------
containerEl.createEl("hr", { cls: "excalidraw-setting-hr" });
containerEl.createDiv( { cls: "setting-item-description" }, (el)=>{
el.innerHTML = t("EA_DESC");
});
detailsEl = containerEl.createEl("details");
const eaDetailsEl = detailsEl;
detailsEl.createEl("summary", {
text: t("EA_HEAD"),
cls: "excalidraw-setting-h1",
});
new Setting(detailsEl)
.setName(t("FIELD_SUGGESTER_NAME"))
.setDesc(fragWithHTML(t("FIELD_SUGGESTER_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.fieldSuggester)
.onChange(async (value) => {
this.plugin.settings.fieldSuggester = value;
this.applySettingsUpdate();
}),
);
//STARTUP_SCRIPT_NAME
//STARTUP_SCRIPT_BUTTON
let startupScriptPathText: TextComponent;
let startupScriptButton: ButtonComponent;
const scriptExists = () => {
const startupPath = normalizePath(this.plugin.settings.startupScriptPath.endsWith(".md")
? this.plugin.settings.startupScriptPath
: this.plugin.settings.startupScriptPath + ".md");
return Boolean(this.app.vault.getAbstractFileByPath(startupPath));
}
new Setting(detailsEl)
.setName(t("STARTUP_SCRIPT_NAME"))
.setDesc(fragWithHTML(t("STARTUP_SCRIPT_DESC")))
.addText((text) => {
startupScriptPathText = text;
text
.setValue(this.plugin.settings.startupScriptPath)
.onChange( (value) => {
this.plugin.settings.startupScriptPath = value;
startupScriptButton.setButtonText(scriptExists() ? t("STARTUP_SCRIPT_BUTTON_OPEN") : t("STARTUP_SCRIPT_BUTTON_CREATE"));
this.applySettingsUpdate();
});
})
.addButton((button) => {
startupScriptButton = button;
startupScriptButton
.setButtonText(scriptExists() ? t("STARTUP_SCRIPT_BUTTON_OPEN") : t("STARTUP_SCRIPT_BUTTON_CREATE"))
.onClick(async () => {
if(this.plugin.settings.startupScriptPath === "") {
this.plugin.settings.startupScriptPath = normalizePath(normalizePath(this.plugin.settings.folder) + "/ExcalidrawStartup");
startupScriptPathText.setValue(this.plugin.settings.startupScriptPath);
this.applySettingsUpdate();
}
const startupPath = normalizePath(this.plugin.settings.startupScriptPath.endsWith(".md")
? this.plugin.settings.startupScriptPath
: this.plugin.settings.startupScriptPath + ".md");
let f = this.app.vault.getAbstractFileByPath(startupPath);
if(!f) {
f = await this.app.vault.create(startupPath, startupScript());
}
startupScriptButton.setButtonText(t("STARTUP_SCRIPT_BUTTON_OPEN"));
this.app.workspace.openLinkText(f.path,"",true);
this.hide();
})
});
// ------------------------------------------------
@@ -1890,6 +2187,19 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
cls: "excalidraw-setting-h1",
});
new Setting(detailsEl)
.setName(t("SLIDING_PANES_NAME"))
.setDesc(fragWithHTML(t("SLIDING_PANES_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.slidingPanesSupport)
.onChange((value) => {
this.plugin.settings.slidingPanesSupport = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("COMPATIBILITY_MODE_NAME"))
.setDesc(fragWithHTML(t("COMPATIBILITY_MODE_DESC")))
@@ -2039,6 +2349,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.addTextArea((text) => {
text.inputEl.style.minHeight = textAreaHeight(scriptName, variableName);
text.inputEl.style.minWidth = "400px";
text.inputEl.style.width = "100%";
text
.setValue(getValue(scriptName, variableName))
.onChange(async (value) => {

View File

@@ -1,6 +1,6 @@
import { randomId, randomInteger } from "../utils";
import { ExcalidrawLinearElement, FillStyle, GroupId, RoundnessType, StrokeStyle } from "@zsviczian/excalidraw/types/element/types";
import { ExcalidrawLinearElement, FillStyle, GroupId, RoundnessType, StrokeStyle } from "@zsviczian/excalidraw/types/excalidraw/element/types";
export type Point = [number, number];

View File

@@ -1,4 +1,4 @@
import { GITHUB_RELEASES } from "src/constants";
import { GITHUB_RELEASES } from "src/constants/constants";
import { ExcalidrawGenericElement } from "./ExcalidrawElement";
declare const PLUGIN_VERSION:string;

View File

@@ -1,4 +1,4 @@
import { ExcalidrawElement, ExcalidrawLinearElement, ExcalidrawTextElement, FillStyle, GroupId, RoundnessType, StrokeStyle } from "@zsviczian/excalidraw/types/element/types";
import { ExcalidrawElement, ExcalidrawLinearElement, ExcalidrawTextElement, FillStyle, GroupId, RoundnessType, StrokeStyle } from "@zsviczian/excalidraw/types/excalidraw/element/types";
export type PathCommand = {
type: string;

View File

@@ -25,7 +25,7 @@ import {
import { getTransformMatrix, transformPoints } from "./transform";
import { pointsOnPath } from "points-on-path";
import { randomId, getWindingOrder } from "./utils";
import { ROUNDNESS } from "../constants";
import { ROUNDNESS } from "../constants/constants";
const SUPPORTED_TAGS = [
"svg",

12
src/types.d.ts vendored
View File

@@ -49,4 +49,16 @@ declare module "obsidian" {
ctx?: any,
): EventRef;
}
interface DataAdapter {
url: {
pathToFileURL(path: string): URL;
},
basePath: string;
}
interface Editor {
insertText(data: string): void;
}
interface MetadataCache {
getBacklinksForFile(file: TFile): any;
}
}

252
src/utils/AIUtils.ts Normal file
View File

@@ -0,0 +1,252 @@
import { DEVICE } from "../constants/constants";
import { Notice, RequestUrlResponse, requestUrl } from "obsidian";
import ExcalidrawPlugin from "src/main";
type MessageContent =
| string
| (string | { type: "image_url"; image_url: string })[];
export type GPTCompletionRequest = {
model: string;
messages?: {
role?: "system" | "user" | "assistant" | "function";
content?: MessageContent;
name?: string | undefined;
}[];
functions?: any[] | undefined;
function_call?: any | undefined;
stream?: boolean | undefined;
temperature?: number | undefined;
top_p?: number | undefined;
max_tokens?: number | undefined;
n?: number | undefined;
best_of?: number | undefined;
frequency_penalty?: number | undefined;
presence_penalty?: number | undefined;
logit_bias?:
| {
[x: string]: number;
}
| undefined;
stop?: (string[] | string) | undefined;
size?: string;
quality?: "standard" | "hd";
prompt?: string;
image?: string;
mask?: string;
};
export type AIRequest = {
image?: string;
text?: string;
instruction?: string;
systemPrompt?: string;
imageGenerationProperties?: {
size?: string; //depends on model
quality?: "standard" | "hd"; //depends on model
n?: number; //dall-e-3 only accepts 1
mask?: string; //dall-e-2 only (image editing)
};
};
const handleImageEditPrompt = async (request: AIRequest) : Promise<RequestUrlResponse> => {
const plugin: ExcalidrawPlugin = window.ExcalidrawAutomate.plugin;
const {
openAIAPIToken,
openAIImageEditsURL,
} = plugin.settings;
const { image, text, imageGenerationProperties} = request;
const body = new FormData();
body.append("model", "dall-e-2");
text.trim() !== "" && body.append("prompt", text);
if (image) {
const imageBlob = await fetch(image).then((res) => res.blob());
body.append('image', imageBlob, 'image.png');
}
if (imageGenerationProperties.mask) {
const maskBlob = await fetch(imageGenerationProperties.mask).then((res) => res.blob());
body.append('mask', maskBlob, 'masik.png');
}
imageGenerationProperties.size && body.append("size", imageGenerationProperties.size);
imageGenerationProperties.n && body.append("n", String(imageGenerationProperties.n));
try {
//https://platform.openai.com/docs/api-reference/images
const resp = await fetch(
openAIImageEditsURL,
{
method: "post",
body,
headers: {
Authorization: `Bearer ${openAIAPIToken}`,
},
}
);
if(!resp) return null;
return {
status: resp.status,
headers: resp.headers as any,
text: null,
json: await resp.json(),
arrayBuffer: null,
};
} catch (e) {
console.log(e);
}
return null;
}
const handleGenericPrompt = async (request: AIRequest) : Promise<RequestUrlResponse> => {
const plugin: ExcalidrawPlugin = window.ExcalidrawAutomate.plugin;
const {
openAIAPIToken,
openAIDefaultTextModel,
openAIDefaultVisionModel,
openAIURL,
openAIImageGenerationURL,
openAIDefaultImageGenerationModel,
} = plugin.settings;
const { image, text, instruction, systemPrompt, imageGenerationProperties} = request;
const isImageGeneration = Boolean(imageGenerationProperties);
const requestType = isImageGeneration ? "dall-e" : (image ? "image" : "text");
let body: GPTCompletionRequest;
switch (requestType) {
case "text":
body = {
model: openAIDefaultTextModel,
max_tokens: 4096,
messages: [
...(systemPrompt && systemPrompt.trim() !=="" ? [{role: "system" as const,content: systemPrompt}] : []),
{
role: "user",
content: text,
},
...(instruction && instruction.trim() !=="" ? [{role: "user" as const,content: instruction}] : []),
],
};
break;
case "image":
body = {
model: openAIDefaultVisionModel,
max_tokens: 4096,
messages: [
...(systemPrompt && systemPrompt.trim() !=="" ? [{role: "system" as const,content: systemPrompt}] : []),
{
role: "user",
content: [
{
type: "image_url",
image_url: image,
},
...(text ? [text] : []),
...(instruction && instruction.trim() !== "" ? [instruction] : []),
],
}
],
};
break;
case "dall-e":
body = {
model: openAIDefaultImageGenerationModel,
prompt: text,
...imageGenerationProperties
};
break;
default:
return null;
}
try {
//https://platform.openai.com/docs/api-reference/images
const resp = await fetch (isImageGeneration ? openAIImageGenerationURL : openAIURL, {
method: "post",
//@ts-ignore
contentType: "application/json",
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${openAIAPIToken}`,
}
});
if(!resp) return null;
return {
status: resp.status,
headers: resp.headers as any,
text: null,
json: await resp.json(),
arrayBuffer: null,
};
} catch (e) {
console.log(e);
}
return null;
/*
//does not seem to work on Android :(
try {
//https://platform.openai.com/docs/api-reference/images
const resp = await requestUrl ({
url: isImageGeneration ? openAIImageGenerationURL : openAIURL,
method: "post",
contentType: "application/json",
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${openAIAPIToken}`,
},
throw: false
});
return resp;
} catch (e) {
console.log(e);
}
return null;*/
}
export const postOpenAI = async (request: AIRequest) : Promise<RequestUrlResponse> => {
const plugin: ExcalidrawPlugin = window.ExcalidrawAutomate.plugin;
const { openAIAPIToken } = plugin.settings;
const { image, imageGenerationProperties} = request;
const isImageGeneration = Boolean(imageGenerationProperties);
const isImageVariationOrEditing = isImageGeneration && (Boolean(imageGenerationProperties.mask) || Boolean(image));
if(openAIAPIToken === "") {
new Notice("OpenAI API Token is not set. Please set it in plugin settings.");
return null;
}
if(isImageVariationOrEditing) {
return await handleImageEditPrompt(request);
}
return await handleGenericPrompt(request);
}
/**
* Grabs the codeblock contents from the supplied markdown string.
* @param markdown
* @param codeblockType
* @returns an array of dictionaries with the codeblock contents and type
*/
export const extractCodeBlocks = (markdown: string): { data: string, type: string }[] => {
if (!markdown) return [];
markdown = markdown.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
const result: { data: string, type: string }[] = [];
const regex = /```([a-zA-Z0-9]*)\n([\s\S]+?)```/g;
let match;
while ((match = regex.exec(markdown)) !== null) {
const codeblockType = match[1]??"";
const codeblockString = match[2].trim();
result.push({ data: codeblockString, type: codeblockType });
}
return result;
}

View File

@@ -11,6 +11,7 @@ container.appendChild(node.contentEl)
import { TFile, WorkspaceLeaf, WorkspaceSplit } from "obsidian";
import ExcalidrawView from "src/ExcalidrawView";
import { getContainerForDocument, ConstructableWorkspaceSplit, isObsidianThemeDark } from "./ObsidianUtils";
import { CustomMutationObserver, isDebugMode } from "./DebugHelper";
declare module "obsidian" {
interface Workspace {
@@ -72,8 +73,9 @@ export class CanvasNodeFactory {
const node = this.canvas.createFileNode({pos: {x:0,y:0}, file, subpath, save: false});
node.setFilePath(file.path,subpath);
node.render();
containerEl.style.background = "var(--background-primary)";
containerEl.appendChild(node.contentEl)
//containerEl.style.background = "var(--background-primary)";
node.containerEl.querySelector(".canvas-node-content-blocker")?.remove();
containerEl.appendChild(node.containerEl)
this.nodes.set(elementId, node);
return node;
}
@@ -93,8 +95,8 @@ export class CanvasNodeFactory {
if (!node.child.editor?.containerEl?.parentElement?.parentElement) return;
node.child.editor.containerEl.parentElement.parentElement.classList.remove(obsidianTheme);
node.child.editor.containerEl.parentElement.parentElement.classList.add(theme);
const observer = new MutationObserver((mutationsList) => {
const nodeObserverFn: MutationCallback = (mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const targetElement = mutation.target as HTMLElement;
@@ -104,7 +106,10 @@ export class CanvasNodeFactory {
}
}
}
});
};
const observer = isDebugMode
? new CustomMutationObserver(nodeObserverFn, "CanvasNodeFactory")
: new MutationObserver(nodeObserverFn);
observer.observe(node.child.editor.containerEl.parentElement.parentElement, { attributes: true });
})();

163
src/utils/CarveOut.ts Normal file
View File

@@ -0,0 +1,163 @@
import { ExcalidrawFrameElement, ExcalidrawImageElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import { getEA } from "src";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import { splitFolderAndFilename } from "./FileUtils";
import { Notice, TFile } from "obsidian";
import ExcalidrawView from "src/ExcalidrawView";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
export const CROPPED_PREFIX = "cropped_";
export const carveOutImage = async (sourceEA: ExcalidrawAutomate, viewImageEl: ExcalidrawImageElement) => {
if(!viewImageEl?.fileId) return;
if(!sourceEA?.targetView) return;
const targetEA = getEA(sourceEA.targetView) as ExcalidrawAutomate;
targetEA.copyViewElementsToEAforEditing([viewImageEl],true);
const {height, width} = await sourceEA.getOriginalImageSize(viewImageEl);
if(!height || !width || height === 0 || width === 0) return;
const newImage = targetEA.getElement(viewImageEl.id) as Mutable<ExcalidrawImageElement>;
newImage.x = 0;
newImage.y = 0;
newImage.width = width;
newImage.height = height;
const scale = newImage.scale;
newImage.scale = [1,1];
const ef = sourceEA.targetView.excalidrawData.getFile(viewImageEl.fileId);
let imageLink = "";
let fname = "";
if(ef.file) {
fname = CROPPED_PREFIX + ef.file.basename;
imageLink = `[[${ef.file.path}]]`;
} else {
const imagename = ef.hyperlink?.match(/^.*\/([^?]*)\??.*$/)?.[1];
imageLink = ef.hyperlink;
fname = viewImageEl
? CROPPED_PREFIX + imagename.substring(0,imagename.lastIndexOf("."))
: CROPPED_PREFIX + "_image";
}
const attachmentPath = await sourceEA.getAttachmentFilepath(fname + ".md");
const {folderpath: foldername, filename} = splitFolderAndFilename(attachmentPath);
const file = await createImageCropperFile(targetEA, newImage.id, imageLink, foldername, filename);
if(!file) return;
//console.log(await app.vault.read(file));
sourceEA.clear();
sourceEA.copyViewElementsToEAforEditing([viewImageEl]);
const sourceImageEl = sourceEA.getElement(viewImageEl.id) as Mutable<ExcalidrawImageElement>;
sourceImageEl.isDeleted = true;
const replacingImageID = await sourceEA.addImage(sourceImageEl.x, sourceImageEl.y, file, true);
const replacingImage = sourceEA.getElement(replacingImageID) as Mutable<ExcalidrawImageElement>;
replacingImage.width = sourceImageEl.width;
replacingImage.height = sourceImageEl.height;
replacingImage.scale = scale;
sourceEA.addElementsToView(false, true, true);
}
export const createImageCropperFile = async (targetEA: ExcalidrawAutomate, imageID: string, imageLink:string, foldername: string, filename: string): Promise<TFile> => {
const workspace = targetEA.plugin.app.workspace;
const vault = targetEA.plugin.app.vault;
const newImage = targetEA.getElement(imageID) as Mutable<ExcalidrawImageElement>;
const { width, height } = newImage;
newImage.opacity = 100;
newImage.locked = true;
const frameID = targetEA.addFrame(0,0,width,height,"Adjust frame to crop image. Add elements for mask: White shows, Black hides.");
const frame = targetEA.getElement(frameID) as Mutable<ExcalidrawFrameElement>;
frame.link = imageLink;
newImage.frameId = frameID;
targetEA.style.opacity = 50;
targetEA.style.fillStyle = "solid";
targetEA.style.strokeStyle = "solid";
targetEA.style.strokeColor = "black";
targetEA.style.backgroundColor = "black";
targetEA.style.roughness = 0;
targetEA.style.roundness = null;
targetEA.canvas.theme = "light";
targetEA.canvas.viewBackgroundColor = "#3d3d3d";
const templateFile = app.vault.getAbstractFileByPath(targetEA.plugin.settings.templateFilePath);
if(templateFile && templateFile instanceof TFile) {
const {appState} = await targetEA.getSceneFromFile(templateFile);
if(appState) {
targetEA.style.fontFamily = appState.currentItemFontFamily;
targetEA.style.fontSize = appState.currentItemFontSize;
}
}
const newPath = await targetEA.create ({
filename,
foldername,
onNewPane: true,
frontmatterKeys: {
"excalidraw-mask": true,
"excalidraw-export-dark": false,
"excalidraw-export-padding": 0,
"excalidraw-export-transparent": true,
}
});
//console.log({newPath});
//wait for file to be created/indexed by Obsidian
let file = vault.getAbstractFileByPath(newPath);
let counter = 0;
while((!file || !targetEA.isExcalidrawFile(file as TFile)) && counter < 50) {
await sleep(100);
file = vault.getAbstractFileByPath(newPath);
counter++;
}
//console.log({counter, file});
if(!file || !(file instanceof TFile)) {
new Notice("File not found. NewExcalidraw Drawing is taking too long to create. Please try again.");
return;
}
/*
//wait for the new ExcalidrawView to open and initialize
counter = 0;
let newView = workspace.getActiveViewOfType(ExcalidrawView) as ExcalidrawView;
while(
(workspace.getActiveFile() !== file ||
newView?.file !== file ||
!newView?.isLoaded ||
!Boolean(newView?.excalidrawAPI)) &&
counter < 100
) {
await sleep(100);
newView = workspace.getActiveViewOfType(ExcalidrawView) as ExcalidrawView;
counter++;
}
//console.log({counter});
if(newView?.file !== file || !newView?.isLoaded ||!Boolean(newView?.excalidrawAPI)) {
new Notice("View did not initialize. NewExcalidraw Drawing is taking too long to open. Please try again.");
return;
}
//wait for the image to load to the new view
const api = newView.excalidrawAPI as ExcalidrawImperativeAPI;
counter = 0;
while(Object.keys(api.getFiles()).length === 0 && counter < 100) {
await sleep(100);
counter++;
}
if(Object.keys(api.getFiles()).length === 0) {
new Notice("Image did not load to the view. NewExcalidraw Drawing is taking too long to load. Please try again.");
return;
}
*/
//console.log({counter, path: workspace.getActiveFile()?.path, newView, files: api.getFiles()});
return file;
}

183
src/utils/CropImage.ts Normal file
View File

@@ -0,0 +1,183 @@
import { ExcalidrawElement, FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { BinaryFileData } from "@zsviczian/excalidraw/types/excalidraw/types";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import { Notice } from "obsidian";
import { getEA } from "src";
import { ExcalidrawAutomate, cloneElement } from "src/ExcalidrawAutomate";
import { ExportSettings } from "src/ExcalidrawView";
import { embedFontsInSVG } from "./Utils";
import { nanoid } from "src/constants/constants";
export class CropImage {
private imageEA: ExcalidrawAutomate;
private maskEA: ExcalidrawAutomate;
private bbox: {topX: number, topY: number, width: number, height: number};
constructor (
private elements: ExcalidrawElement[],
files: Map<FileId,BinaryFileData>,
) {
const imageEA = getEA() as ExcalidrawAutomate;
this.imageEA = imageEA;
const maskEA = getEA() as ExcalidrawAutomate;
this.maskEA = maskEA;
this.bbox = imageEA.getBoundingBox(elements);
//this makes both the image and the mask the same size
//Adding the bounding element first so it is at the bottom of the layers - does not override the image.
this.setBoundingEl(imageEA, "transparent");
this.setBoundingEl(maskEA, "white"); //the bbox should not mask the image. White lets everything through.
elements.forEach(el => {
const newEl = cloneElement(el) as Mutable<ExcalidrawElement>;
if(el.type !== "image" && el.type !== "frame") {
newEl.opacity = 100;
maskEA.elementsDict[el.id] = newEl;
}
if(el.type === "image") {
imageEA.elementsDict[el.id] = newEl;
}
})
Object.values(files).forEach(file => {
imageEA.imagesDict[file.id] = file;
})
}
private setBoundingEl(ea: ExcalidrawAutomate, bgColor: string) {
const {topX, topY, width, height} = this.bbox;
ea.style.backgroundColor = bgColor;
ea.style.strokeColor = "transparent";
//@ts-ignore: Setting this to string "0" will produce a rectangle with zero stroke width
ea.style.strokeWidth = "0";
ea.style.strokeStyle = "solid";
ea.style.fillStyle = "solid";
ea.style.roughness = 0;
ea.addRect(topX, topY, width, height);
}
private getViewBoxAndSize(): {viewBox: string, vbWidth: number, vbHeight: number, width: number, height: number} {
const frames = this.elements.filter(el=>el.type === "frame");
if(frames.length > 1) {
new Notice("Multiple frames are not supported for image cropping. Discarding frames from mask.");
}
const images = this.imageEA.getElements().filter(el=>el.type === "image");
const {x: frameX, y: frameY, width: frameWidth, height: frameHeight} = frames.length === 1
? frames[0]
: mapToXY(this.imageEA.getBoundingBox(images));
const {topX, topY, width, height} = this.bbox;
return {
viewBox: `${frameX-topX} ${frameY-topY} ${frameWidth} ${frameHeight}`,
vbWidth: frameWidth,
vbHeight: frameHeight,
width,
height,
}
}
private async getMaskSVG():Promise<{style: string, mask: string}> {
const exportSettings:ExportSettings = {
withBackground: false,
withTheme: false,
isMask: false,
}
const maskSVG = await this.maskEA.createSVG(null,false,exportSettings,null,null,0);
const defs = maskSVG.querySelector("defs");
const styleEl = maskSVG.querySelector("style");
const style = styleEl ? styleEl.outerHTML : "";
defs.parentElement.removeChild(defs);
return {style, mask:maskSVG.innerHTML};
}
private async getImage() {
const exportSettings:ExportSettings = {
withBackground: false,
withTheme: false,
isMask: false,
}
const images = Object.values(this.imageEA.imagesDict);
if(images.length === 1) {
return images[0].dataURL;
}
return await this.imageEA.createPNGBase64(null,1,exportSettings,null,null,0);
const imageSVG = await this.imageEA.createSVG(null,false,exportSettings,null,null,0);
const svgData = new XMLSerializer().serializeToString(imageSVG);
return `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svgData)))}`;
// const blob = new Blob([svgString], { type: 'image/svg+xml' });
// return `data:image/svg+xml;base64,${await blobToBase64(blob)}`;
}
private async buildSVG(): Promise<SVGSVGElement> {
if(this.imageEA.getElements().filter(el=>el.type === "image").length === 0) {
new Notice("No image found. Cannot crop.");
return;
}
const maskID = nanoid();
const imageID = nanoid();
const {viewBox, vbWidth, vbHeight, width, height} = this.getViewBoxAndSize();
const parser = new DOMParser();
const {style, mask} = await this.getMaskSVG();
const svgString = `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="${viewBox}" width="${vbWidth}" height="${vbHeight}">\n` +
`<symbol id="${imageID}"><image width="100%" height="100%" href="${await this.getImage()}"/></symbol>\n` +
`<defs>${style}\n<mask id="${maskID}" x="0" y="0" width="${width}" height="${height}" maskUnits="userSpaceOnUse">\n${mask}\n</mask>\n</defs>\n` +
`<use x="0" y="0" width="${width}" height="${height}" mask="url(#${maskID})" mask-type="alpha" href="#${imageID}"/>\n</svg>`;
return parser.parseFromString(
svgString,
"image/svg+xml",
).firstElementChild as SVGSVGElement
}
async getCroppedPNG(): Promise<Blob> {
//@ts-ignore
const PLUGIN = app.plugins.plugins["obsidian-excalidraw-plugin"];
const svg = embedFontsInSVG(await this.buildSVG(), PLUGIN);
return new Promise((resolve, reject) => {
const svgData = new XMLSerializer().serializeToString(svg);
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) {
reject('Unable to get 2D context');
return;
}
canvas.width = svg.width.baseVal.value;
canvas.height = svg.height.baseVal.value;
const image = new Image();
image.onload = () => {
context.clearRect(0, 0, canvas.width, canvas.height);
context.drawImage(image, 0, 0);
canvas.toBlob(
(blob) => {
if (blob) {
resolve(blob);
} else {
reject(new Error('Failed to convert to PNG'));
}
},
'image/png',
1 // image quality (0 - 1)
);
};
image.src = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svgData)))}`;
});
}
async getCroppedSVG() {
return await this.buildSVG();
}
}
const mapToXY = ({topX, topY, width, height}: {topX: number, topY: number, width: number, height: number}): {x: number, y: number, width: number, height: number} => {
return {
x: topX,
y: topY,
width,
height,
}
}

View File

@@ -1,12 +1,12 @@
import { NonDeletedExcalidrawElement } from "@zsviczian/excalidraw/types/element/types";
import { DEVICE, REG_LINKINDEX_INVALIDCHARS } from "src/constants";
import { NonDeletedExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { DEVICE, REG_LINKINDEX_INVALIDCHARS } from "src/constants/constants";
import { getParentOfClass } from "./ObsidianUtils";
import { TFile, WorkspaceLeaf } from "obsidian";
import { getLinkParts } from "./Utils";
import ExcalidrawView from "src/ExcalidrawView";
export const useDefaultExcalidrawFrame = (element: NonDeletedExcalidrawElement) => {
return !element.link.startsWith("["); // && !element.link.match(TWITTER_REG);
return !(element.link.startsWith("[") || element.link.startsWith("file:") || element.link.startsWith("data:")); // && !element.link.match(TWITTER_REG);
}
export const leafMap = new Map<string, WorkspaceLeaf>();

38
src/utils/DebugHelper.ts Normal file
View File

@@ -0,0 +1,38 @@
export const isDebugMode = false;
export const durationTreshold = 0; //0.05; //ms
export class CustomMutationObserver {
private originalCallback: MutationCallback;
private observer: MutationObserver | null;
private name: string;
constructor(callback: MutationCallback, name: string) {
this.originalCallback = callback;
this.observer = null;
this.name = name;
}
observe(target: Node, options: MutationObserverInit) {
const wrappedCallback: MutationCallback = async (mutationsList, observer) => {
const startTime = performance.now(); // Get start time
await this.originalCallback(mutationsList, observer); // Invoke the original callback
const endTime = performance.now(); // Get end time
const executionTime = endTime - startTime;
if (executionTime > durationTreshold) {
console.log(`Excalidraw ${this.name} MutationObserver callback took ${executionTime}ms to execute`);
}
};
this.observer = new MutationObserver(wrappedCallback);
// Start observing with the modified callback
this.observer.observe(target, options);
}
disconnect() {
if (this.observer) {
this.observer.disconnect();
this.observer = null;
}
}
}

View File

@@ -1,12 +1,12 @@
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
import { ColorMaster } from "colormaster";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import ExcalidrawView from "src/ExcalidrawView";
import { DynamicStyle } from "src/types";
import { cloneElement } from "src/ExcalidrawAutomate";
import { ExcalidrawFrameElement } from "@zsviczian/excalidraw/types/element/types";
import { ExcalidrawFrameElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { addAppendUpdateCustomData } from "./Utils";
import { mutateElement } from "src/constants";
import { mutateElement } from "src/constants/constants";
export const setDynamicStyle = (
ea: ExcalidrawAutomate,

View File

@@ -1,7 +1,8 @@
import { MAX_IMAGE_SIZE, IMAGE_TYPES } from "src/constants";
import { MAX_IMAGE_SIZE, IMAGE_TYPES, ANIMATED_IMAGE_TYPES } from "src/constants/constants";
import { TFile } from "obsidian";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "src/ExcalidrawData";
export const insertImageToView = async (
ea: ExcalidrawAutomate,
@@ -33,7 +34,7 @@ export const insertEmbeddableToView = async (
ea.clear();
ea.style.strokeColor = "transparent";
ea.style.backgroundColor = "transparent";
if(file && IMAGE_TYPES.contains(file.extension) || ea.isExcalidrawFile(file)) {
if(file && (IMAGE_TYPES.contains(file.extension) || ea.isExcalidrawFile(file)) && !ANIMATED_IMAGE_TYPES.contains(file.extension)) {
return await insertImageToView(ea, position, file);
} else {
const id = ea.addEmbeddable(
@@ -47,4 +48,17 @@ export const insertEmbeddableToView = async (
await ea.addElementsToView(false, true, true);
return id;
}
}
export const getLinkTextFromLink = (text: string): string => {
if (!text) return;
if (text.match(REG_LINKINDEX_HYPERLINK)) return;
const parts = REGEX_LINK.getRes(text).next();
if (!parts.value) return;
const linktext = REGEX_LINK.getLink(parts); //parts.value[2] ? parts.value[2]:parts.value[6];
if (linktext.match(REG_LINKINDEX_HYPERLINK)) return;
return linktext;
}

View File

@@ -1,19 +1,22 @@
import { DataURL } from "@zsviczian/excalidraw/types/types";
import { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
import { loadPdfJs, normalizePath, Notice, requestUrl, RequestUrlResponse, TAbstractFile, TFile, TFolder, Vault } from "obsidian";
import { URLFETCHTIMEOUT } from "src/constants";
import { MimeType } from "src/EmbeddedFileLoader";
import { DEVICE, URLFETCHTIMEOUT } from "src/constants/constants";
import { IMAGE_MIME_TYPES, MimeType } from "src/EmbeddedFileLoader";
import { ExcalidrawSettings } from "src/settings";
import { errorlog, getDataURL } from "./Utils";
import ExcalidrawPlugin from "src/main";
/**
* Splits a full path including a folderpath and a filename into separate folderpath and filename components
* @param filepath
*/
type ImageExtension = keyof typeof IMAGE_MIME_TYPES;
export function splitFolderAndFilename(filepath: string): {
folderpath: string;
filename: string;
basename: string;
extension: string;
} {
const lastIndex = filepath.lastIndexOf("/");
const filename = lastIndex == -1 ? filepath : filepath.substring(lastIndex + 1);
@@ -21,6 +24,7 @@ export function splitFolderAndFilename(filepath: string): {
folderpath: normalizePath(filepath.substring(0, lastIndex)),
filename,
basename: filename.replace(/\.[^/.]+$/, ""),
extension: filename.substring(filename.lastIndexOf(".") + 1),
};
}
@@ -134,7 +138,7 @@ export function getEmbedFilename(
* Open or create a folderpath if it does not exist
* @param folderpath
*/
export async function checkAndCreateFolder(folderpath: string) {
export async function checkAndCreateFolder(folderpath: string):Promise<TFolder> {
const vault = app.vault;
folderpath = normalizePath(folderpath);
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/658
@@ -146,7 +150,7 @@ export async function checkAndCreateFolder(folderpath: string) {
if (folder && folder instanceof TFile) {
new Notice(`The folder cannot be created because it already exists as a file: ${folderpath}.`)
}
await vault.createFolder(folderpath);
return await vault.createFolder(folderpath);
}
export const getURLImageExtension = (url: string):string => {
@@ -155,15 +159,10 @@ export const getURLImageExtension = (url: string):string => {
}
export const getMimeType = (extension: string):MimeType => {
if(IMAGE_MIME_TYPES.hasOwnProperty(extension)) {
return IMAGE_MIME_TYPES[extension as ImageExtension];
};
switch (extension) {
case "png": return "image/png";
case "jpeg": return "image/jpeg";
case "jpg": return "image/jpeg";
case "gif": return "image/gif";
case "webp": return "image/webp";
case "bmp": return "image/bmp";
case "ico": return "image/x-icon";
case "svg": return "image/svg+xml";
case "md": return "image/svg+xml";
default: return "application/octet-stream";
}
@@ -172,14 +171,18 @@ export const getMimeType = (extension: string):MimeType => {
// using fetch API
const getFileFromURL = async (url: string, mimeType: MimeType, timeout: number = URLFETCHTIMEOUT): Promise<RequestUrlResponse> => {
try {
const timeoutPromise = new Promise<Response>((resolve) =>
setTimeout(() => resolve(null), timeout)
);
const response = await Promise.race([
fetch(url),
new Promise<Response>((resolve) => setTimeout(() => resolve(null), timeout))
fetch(url, { mode: 'no-cors' }), //cors error cannot be caught
timeoutPromise,
]);
if (!response) {
new Notice(`URL did not load within the timeout period of ${timeout}ms.\n\nTry force-saving again in a few seconds.\n\n${url}`,8000);
throw new Error(`URL did not load within the timeout period of ${timeout}ms`);
errorlog({ where: getFileFromURL, message: `URL did not load within the timeout period of ${timeout}ms.\n\nTry force-saving again in a few seconds.\n\n${url}`, url: url });
return null;
}
const arrayBuffer = await response.arrayBuffer();
@@ -192,8 +195,8 @@ const getFileFromURL = async (url: string, mimeType: MimeType, timeout: number =
text: null,
};
} catch (e) {
errorlog({ where: getFileFromURL, message: e.message, url: url });
return undefined;
//errorlog({ where: getFileFromURL, message: e.message, url: url });
return null;
}
};
@@ -201,19 +204,23 @@ const getFileFromURL = async (url: string, mimeType: MimeType, timeout: number =
// https://firebasestorage.googleapis.com/v0/b/firescript-577a2.appspot.com/o/imgs%2Fapp%2FJSG%2FfTMP6WGQRC.png?alt=media&token=6d2993b4-e629-46b6-98d1-133af7448c49
const getFileFromURLFallback = async (url: string, mimeType: MimeType, timeout: number = URLFETCHTIMEOUT):Promise<RequestUrlResponse> => {
try {
const timeoutPromise = new Promise<RequestUrlResponse | null>((resolve) =>
setTimeout(() => resolve(null), timeout)
);
return await Promise.race([
(async () => new Promise<RequestUrlResponse>((resolve) => setTimeout(()=>resolve(null), timeout)))(),
requestUrl({url: url, method: "get", contentType: mimeType, throw: false })
timeoutPromise,
requestUrl({url: url, throw: false }), //if method: "get" is added it won't load images on Android, contentType: mimeType,
])
} catch (e) {
errorlog({where: getFileFromURL, message: `URL did not load within timeout period of ${timeout}ms`, url: url});
return undefined;
errorlog({where: getFileFromURLFallback, message: `URL did not load within timeout period of ${timeout}ms`, url: url});
return null;
}
}
export const getDataURLFromURL = async (url: string, mimeType: MimeType, timeout: number = URLFETCHTIMEOUT): Promise<DataURL> => {
let response = await getFileFromURL(url, mimeType, timeout);
if(response && response.status !== 200) {
if(!response || response?.status !== 200) {
response = await getFileFromURLFallback(url, mimeType, timeout);
}
return response && response.status === 200
@@ -272,9 +279,9 @@ export const getDataURLFromURL = async (
export const blobToBase64 = async (blob: Blob): Promise<string> => {
const arrayBuffer = await blob.arrayBuffer()
const bytes = new Uint8Array(arrayBuffer)
var binary = '';
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
let binary = '';
let len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
@@ -288,6 +295,7 @@ export const getPDFDoc = async (f: TFile): Promise<any> => {
}
export const readLocalFile = async (filePath:string): Promise<string> => {
if (!DEVICE.isDesktop) return null;
return new Promise((resolve, reject) => {
//@ts-ignore
app.vault.adapter.fs.readFile(filePath, 'utf8', (err:any, data:any) => {
@@ -301,6 +309,7 @@ export const readLocalFile = async (filePath:string): Promise<string> => {
}
export const readLocalFileBinary = async (filePath:string): Promise<ArrayBuffer> => {
if (!DEVICE.isDesktop) return null;
return new Promise((resolve, reject) => {
const path = decodeURI(filePath);
//@ts-ignore
@@ -313,4 +322,52 @@ export const readLocalFileBinary = async (filePath:string): Promise<ArrayBuffer>
}
});
});
}
export const getPathWithoutExtension = (f:TFile): string => {
if(!f) return null;
return f.path.substring(0, f.path.lastIndexOf("."));
}
const VAULT_BASE_URL = DEVICE.isDesktop
? app.vault.adapter.url.pathToFileURL(app.vault.adapter.basePath).toString()
: "";
export const getInternalLinkOrFileURLLink = (
path: string, plugin:ExcalidrawPlugin, alias?: string, sourceFile?: TFile
):{link: string, isInternal: boolean, file?: TFile, url?: string} => {
if(!DEVICE.isDesktop) {
//I've not tested this... don't even know if external drag and drop works on mobile
//Added this for safety
return {link: `[${alias??""}](${path})`, isInternal: false, url: path};
}
const vault = plugin.app.vault;
const fileURLString = vault.adapter.url.pathToFileURL(path).toString();
if (fileURLString.startsWith(VAULT_BASE_URL)) {
const internalPath = normalizePath(fileURLString.substring(VAULT_BASE_URL.length));
const file = vault.getAbstractFileByPath(internalPath);
if(file && file instanceof TFile) {
const link = plugin.app.metadataCache.fileToLinktext(
file,
sourceFile?.path,
true,
);
return {link: getLink(plugin, { embed: false, path: link, alias}), isInternal: true, file};
};
}
return {link: `[${alias??""}](${fileURLString})`, isInternal: false, url: fileURLString};
}
/**
* get markdown or wiki link
* @param plugin
* @param param1: { embed = true, path, alias }
* @returns
*/
export const getLink = (
plugin: ExcalidrawPlugin,
{ embed = true, path, alias }: { embed?: boolean; path: string; alias?: string }
):string => {
return plugin.settings.embedWikiLink
? `${embed ? "!" : ""}[[${path}${alias ? `|${alias}` : ""}]]`
: `${embed ? "!" : ""}[${alias ?? ""}](${encodeURI(path)})`
}

View File

@@ -1,4 +1,4 @@
import { ExcalidrawElement, ExcalidrawImageElement, ExcalidrawTextElement } from "@zsviczian/excalidraw/types/element/types";
import { ExcalidrawElement, ExcalidrawImageElement, ExcalidrawTextElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "src/ExcalidrawData";
import ExcalidrawView, { TextMode } from "src/ExcalidrawView";
import { rotatedDimensions } from "./Utils";

View File

@@ -1,4 +1,4 @@
import { ExcalidrawElement, ExcalidrawImageElement } from "@zsviczian/excalidraw/types/element/types";
import { ExcalidrawElement, ExcalidrawImageElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { requireApiVersion } from "obsidian";
export const getMermaidImageElements = (elements: ExcalidrawElement[]):ExcalidrawImageElement[] =>

View File

@@ -1,19 +1,88 @@
import { DEVICE, isDarwin } from "src/constants";
import { DEVICE } from "src/constants/constants";
import { ExcalidrawSettings } from "src/settings";
export type ModifierKeys = {shiftKey:boolean, ctrlKey: boolean, metaKey: boolean, altKey: 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 WebBrowserDragAction = "link"|"image-url"|"image-import"|"embeddable";
export type LocalFileDragAction = "link"|"image-url"|"image-import"|"embeddable";
export type InternalDragAction = "link"|"image"|"image-fullsize"|"embeddable";
export type ModifierSetType = "WebBrowserDragAction" | "LocalFileDragAction" | "InternalDragAction" | "LinkClickAction";
type ModifierKey = {
shift: boolean;
ctrl_cmd: boolean;
alt_opt: boolean;
meta_ctrl: boolean;
result: WebBrowserDragAction | LocalFileDragAction | InternalDragAction | PaneTarget;
};
export type ModifierKeySet = {
defaultAction: WebBrowserDragAction | LocalFileDragAction | InternalDragAction | PaneTarget;
rules: ModifierKey[];
};
export type ModifierKeyTooltipMessages = Partial<{
[modifierSetType in ModifierSetType]: Partial<{
[action in WebBrowserDragAction | LocalFileDragAction | InternalDragAction | PaneTarget]: string;
}>;
}>;
export const modifierKeyTooltipMessages = ():ModifierKeyTooltipMessages => {
return {
WebBrowserDragAction: {
"image-import": "Import Image to Vault",
"image-url": `Insert Image or YouTube Thumbnail with URL`,
"link": "Insert Link",
"embeddable": "Insert Interactive-Frame",
// Add more messages for WebBrowserDragAction as needed
},
LocalFileDragAction: {
"image-import": "Insert Image: import external or reuse existing if path in Vault",
"image-url": `Insert Image: with local URI or internal-link if from Vault`,
"link": "Insert Link: local URI or internal-link if from Vault",
"embeddable": "Insert Interactive-Frame: local URI or internal-link if from Vault",
},
InternalDragAction: {
"image": "Insert Image",
"image-fullsize": "Insert Image @100%",
"link": `Insert Link`,
"embeddable": "Insert Interactive-Frame",
},
LinkClickAction: {
"active-pane": "Open in current active window",
"new-pane": "Open in a new adjacent window",
"popout-window": "Open in a popout window",
"new-tab": "Open in a new tab",
"md-properties": "Show the Markdown image-properties dialog (only relevant if you have embedded a markdown document as an image)",
},
}
};
const processModifiers = (ev: KeyEvent, modifierType: ModifierSetType): WebBrowserDragAction | LocalFileDragAction | InternalDragAction | PaneTarget => {
const settings:ExcalidrawSettings = window.ExcalidrawAutomate.plugin.settings;
const keySet = ((DEVICE.isMacOS || DEVICE.isIOS) ? settings.modifierKeyConfig.Mac : settings.modifierKeyConfig.Win)[modifierType];
for (const rule of keySet.rules) {
const { shift, ctrl_cmd, alt_opt, meta_ctrl, result } = rule;
if (
(isSHIFT(ev) === shift) &&
(isWinCTRLorMacCMD(ev) === ctrl_cmd) &&
(isWinALTorMacOPT(ev) === alt_opt) &&
(isWinMETAorMacCTRL(ev) === meta_ctrl)
) {
return result;
}
}
return keySet.defaultAction;
}
export const labelCTRL = () => DEVICE.isIOS || DEVICE.isMacOS ? "CMD" : "CTRL";
export const labelALT = () => DEVICE.isIOS || DEVICE.isMacOS ? "OPT" : "ALT";
export const labelMETA = () => DEVICE.isIOS || DEVICE.isMacOS ? "CTRL" : (DEVICE.isWindows ? "WIN" : "META");
export const labelSHIFT = () => "SHIFT";
export const isCTRL = (e:KeyEvent) => DEVICE.isIOS || DEVICE.isMacOS ? e.metaKey : e.ctrlKey;
export const isALT = (e:KeyEvent) => e.altKey;
export const isMETA = (e:KeyEvent) => DEVICE.isIOS || DEVICE.isMacOS ? e.ctrlKey : e.metaKey;
export const isWinCTRLorMacCMD = (e:KeyEvent) => DEVICE.isIOS || DEVICE.isMacOS ? e.metaKey : e.ctrlKey;
export const isWinALTorMacOPT = (e:KeyEvent) => e.altKey;
export const isWinMETAorMacCTRL = (e:KeyEvent) => DEVICE.isIOS || DEVICE.isMacOS ? e.ctrlKey : e.metaKey;
export const isSHIFT = (e:KeyEvent) => e.shiftKey;
export const setCTRL = (e:ModifierKeys, value: boolean): ModifierKeys => {
@@ -39,50 +108,38 @@ export const setSHIFT = (e:ModifierKeys, value: boolean): ModifierKeys => {
return e;
}
export const mdPropModifier = (ev: KeyEvent): boolean => !isSHIFT(ev) && isCTRL(ev) && !isALT(ev) && isMETA(ev);
export const scaleToFullsizeModifier = (ev: KeyEvent) =>
( isSHIFT(ev) && !isCTRL(ev) && !isALT(ev) && isMETA(ev)) ||
(!isSHIFT(ev) && isCTRL(ev) && isALT(ev) && !isMETA(ev));
export const linkClickModifierType = (ev: KeyEvent):PaneTarget => {
if(isCTRL(ev) && !isALT(ev) && isSHIFT(ev) && !isMETA(ev)) return "active-pane";
if(isCTRL(ev) && !isALT(ev) && !isSHIFT(ev) && !isMETA(ev)) return "new-tab";
if(isCTRL(ev) && isALT(ev) && !isSHIFT(ev) && !isMETA(ev)) return "new-pane";
if(DEVICE.isDesktop && isCTRL(ev) && isALT(ev) && isSHIFT(ev) && !isMETA(ev) ) return "popout-window";
if(isCTRL(ev) && isALT(ev) && isSHIFT(ev) && !isMETA(ev)) return "new-tab";
if(mdPropModifier(ev)) return "md-properties";
return "active-pane";
export const mdPropModifier = (ev: KeyEvent): boolean => !isSHIFT(ev) && isWinCTRLorMacCMD(ev) && !isWinALTorMacOPT(ev) && isWinMETAorMacCTRL(ev);
export const scaleToFullsizeModifier = (ev: KeyEvent) => {
const settings:ExcalidrawSettings = window.ExcalidrawAutomate.plugin.settings;
const keySet = ((DEVICE.isMacOS || DEVICE.isIOS) ? settings.modifierKeyConfig.Mac : settings.modifierKeyConfig.Win )["InternalDragAction"];
const rule = keySet.rules.find(r => r.result === "image-fullsize");
if(!rule) return false;
const { shift, ctrl_cmd, alt_opt, meta_ctrl, result } = rule;
return (
(isSHIFT(ev) === shift) &&
(isWinCTRLorMacCMD(ev) === ctrl_cmd) &&
(isWinALTorMacOPT(ev) === alt_opt) &&
(isWinMETAorMacCTRL(ev) === meta_ctrl)
);
}
export const externalDragModifierType = (ev: KeyEvent):ExternalDragAction => {
if(DEVICE.isWindows && isSHIFT(ev) && isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "embeddable";
if(DEVICE.isMacOS && !isSHIFT(ev) && !isCTRL(ev) && isALT(ev) && !isMETA(ev)) return "embeddable";
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-url";
export const linkClickModifierType = (ev: KeyEvent):PaneTarget => {
const action = processModifiers(ev, "LinkClickAction") as PaneTarget;
if(!DEVICE.isDesktop && action === "popout-window") return "active-pane";
return action;
}
export const webbrowserDragModifierType = (ev: KeyEvent):WebBrowserDragAction => {
return processModifiers(ev, "WebBrowserDragAction") as WebBrowserDragAction;
}
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";
return processModifiers(ev, "LocalFileDragAction") as LocalFileDragAction;
}
//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";
if( (DEVICE.isIOS || DEVICE.isMacOS) && !isSHIFT(ev) && !isCTRL(ev) && !isALT(ev) && isMETA(ev)) return "embeddable";
if( isSHIFT(ev) && !isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "image";
if(!isSHIFT(ev) && isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "image";
if(scaleToFullsizeModifier(ev)) return "image-fullsize";
return "link";
return processModifiers(ev, "InternalDragAction") as InternalDragAction;
}
export const emulateCTRLClickForLinks = (e:KeyEvent) => {
@@ -97,27 +154,23 @@ export const emulateCTRLClickForLinks = (e:KeyEvent) => {
export const emulateKeysForLinkClick = (action: PaneTarget): ModifierKeys => {
const ev = {shiftKey: false, ctrlKey: false, metaKey: false, altKey: false};
if(!action) return ev;
switch(action) {
case "active-pane":
setCTRL(ev, true);
setSHIFT(ev, true);
break;
case "new-pane":
setCTRL(ev, true);
setALT(ev, true);
break;
case "popout-window":
setCTRL(ev, true);
setALT(ev, true);
setSHIFT(ev, true);
break;
case "new-tab":
setCTRL(ev, true);
break;
case "md-properties":
setCTRL(ev, true);
setMETA(ev, true);
break;
const platform = DEVICE.isMacOS || DEVICE.isIOS ? "Mac" : "Win";
const settings:ExcalidrawSettings = window.ExcalidrawAutomate.plugin.settings;
const modifierKeyConfig = settings.modifierKeyConfig;
const config = modifierKeyConfig[platform]?.LinkClickAction;
if (config) {
const rule = config.rules.find(rule => rule.result === action);
if (rule) {
setCTRL(ev, rule.ctrl_cmd);
setALT(ev, rule.alt_opt);
setMETA(ev, rule.meta_ctrl);
setSHIFT(ev, rule.shift);
} else {
const defaultAction = config.defaultAction as PaneTarget;
return emulateKeysForLinkClick(defaultAction);
}
}
return ev;
}

View File

@@ -5,14 +5,14 @@ import {
import ExcalidrawPlugin from "../main";
import { checkAndCreateFolder, splitFolderAndFilename } from "./FileUtils";
import { linkClickModifierType, ModifierKeys } from "./ModifierkeyHelper";
import { REG_BLOCK_REF_CLEAN, REG_SECTION_REF_CLEAN } from "src/constants";
import { REG_BLOCK_REF_CLEAN, REG_SECTION_REF_CLEAN } from "src/constants/constants";
export const getParentOfClass = (element: Element, cssClass: string):HTMLElement | null => {
let parent = element.parentElement;
while (
parent &&
!(parent instanceof window.HTMLBodyElement) &&
!parent.classList.contains(cssClass)
!parent.classList.contains(cssClass) &&
!(parent instanceof window.HTMLBodyElement)
) {
parent = parent.parentElement;
}
@@ -244,7 +244,8 @@ export const getFileCSSClasses = (
file: TFile,
): string[] => {
if (file) {
const plugin = window.ExcalidrawAutomate.plugin;
const plugin = window?.ExcalidrawAutomate?.plugin;
if(!plugin) return [];
const fileCache = plugin.app.metadataCache.getFileCache(file);
if(!fileCache?.frontmatter) return [];
const x = parseFrontMatterEntry(fileCache.frontmatter, "cssclasses");

View File

@@ -11,6 +11,8 @@ export class StylesManager {
private styleDark: string;
private plugin: ExcalidrawPlugin;
constructor(plugin: ExcalidrawPlugin) {
this.plugin = plugin;
plugin.app.workspace.onLayoutReady(async () => {

View File

@@ -1,19 +1,17 @@
//import Excalidraw from "@zsviczian/excalidraw";
import {
App,
Notice,
parseFrontMatterEntry,
request,
requestUrl,
TFile,
} from "obsidian";
import { Random } from "roughjs/bin/math";
import { BinaryFileData, DataURL} from "@zsviczian/excalidraw/types/types";
import { BinaryFileData, DataURL} from "@zsviczian/excalidraw/types/excalidraw/types";
import {
ASSISTANT_FONT,
CASCADIA_FONT,
VIRGIL_FONT,
} from "src/constFonts";
} from "src/constants/constFonts";
import {
FRONTMATTER_KEY_EXPORT_DARK,
FRONTMATTER_KEY_EXPORT_TRANSPARENT,
@@ -22,22 +20,24 @@ import {
FRONTMATTER_KEY_EXPORT_PADDING,
exportToSvg,
exportToBlob,
IMAGE_TYPES
} from "../constants";
IMAGE_TYPES,
FRONTMATTER_KEY_MASK
} from "../constants/constants";
import ExcalidrawPlugin from "../main";
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/element/types";
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ExportSettings } from "../ExcalidrawView";
import { compressToBase64, decompressFromBase64 } from "lz-string";
import { getDataURLFromURL, getIMGFilename, getMimeType, getURLImageExtension } from "./FileUtils";
import { generateEmbeddableLink } from "./CustomEmbeddableUtils";
import ExcalidrawScene from "src/svgToExcalidraw/elements/ExcalidrawScene";
import { FILENAMEPARTS } from "./UtilTypes";
import { Mutable } from "@zsviczian/excalidraw/types/utility-types";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import { cleanBlockRef, cleanSectionHeading, getFileCSSClasses } from "./ObsidianUtils";
import { updateElementLinksToObsidianLinks } from "src/ExcalidrawAutomate";
import { CropImage } from "./CropImage";
declare const PLUGIN_VERSION:string;
declare var LZString: any;
declare module "obsidian" {
interface Workspace {
@@ -273,26 +273,34 @@ export const getSVG = async (
});
}
elements = srcFile
? updateElementLinksToObsidianLinks({
elements,
hostFile: srcFile,
})
: elements;
try {
const svg = await exportToSvg({
elements: srcFile
? updateElementLinksToObsidianLinks({
elements,
hostFile: srcFile,
})
: elements,
appState: {
exportBackground: exportSettings.withBackground,
exportWithDarkMode: exportSettings.withTheme
? scene.appState?.theme != "light"
: false,
...scene.appState,
},
files: scene.files,
exportPadding: padding,
exportingFrame: null,
renderEmbeddables: true,
});
let svg: SVGSVGElement;
if(exportSettings.isMask) {
const cropObject = new CropImage(elements, scene.files);
svg = await cropObject.getCroppedSVG();
} else {
svg = await exportToSvg({
elements,
appState: {
exportBackground: exportSettings.withBackground,
exportWithDarkMode: exportSettings.withTheme
? scene.appState?.theme != "light"
: false,
...scene.appState,
},
files: scene.files,
exportPadding: padding,
exportingFrame: null,
renderEmbeddables: true,
});
}
if(svg) {
svg.addClass("excalidraw-svg");
if(srcFile instanceof TFile) {
@@ -323,8 +331,13 @@ export const getPNG = async (
exportSettings: ExportSettings,
padding: number,
scale: number = 1,
) => {
): Promise<Blob> => {
try {
if(exportSettings.isMask) {
const cropObject = new CropImage(scene.elements, scene.files);
return await cropObject.getCroppedPNG();
}
return await exportToBlob({
elements: scene.elements,
appState: {
@@ -386,10 +399,15 @@ export const embedFontsInSVG = (
svg.querySelector("text[font-family^='LocalFont']") != null;
const defs = svg.querySelector("defs");
if (defs && (includesCascadia || includesVirgil || includesLocalFont || includesAssistant)) {
defs.innerHTML = `<style>${includesVirgil ? VIRGIL_FONT : ""}${
let style = defs.querySelector("style");
if (!style) {
style = document.createElement("style");
defs.appendChild(style);
}
style.innerHTML = `${includesVirgil ? VIRGIL_FONT : ""}${
includesCascadia ? CASCADIA_FONT : ""}${
includesAssistant ? ASSISTANT_FONT : ""
}${includesLocalFont ? plugin.fourthFontDef : ""}</style>`;
}${includesLocalFont ? plugin.fourthFontDef : ""}`;
}
return svg;
};
@@ -515,11 +533,27 @@ export const getLinkParts = (fname: string, file?: TFile): LinkParts => {
};
export const compress = (data: string): string => {
return compressToBase64(data).replace(/(.{64})/g, "$1\n\n");
return LZString.compressToBase64(data).replace(/(.{64})/g, "$1\n\n");
};
export const decompress = (data: string): string => {
return decompressFromBase64(data.replaceAll("\n", "").replaceAll("\r", ""));
return LZString.decompressFromBase64(data.replaceAll("\n", "").replaceAll("\r", ""));
};
export const isMaskFile = (
plugin: ExcalidrawPlugin,
file: TFile,
): boolean => {
if (file) {
const fileCache = plugin.app.metadataCache.getFileCache(file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEY_MASK] != null
) {
return Boolean(fileCache.frontmatter[FRONTMATTER_KEY_MASK]);
}
}
return false;
};
export const hasExportTheme = (

View File

@@ -0,0 +1,62 @@
const REG_YOUTUBE = /^(?:http(?:s)?:\/\/)?(?:www\.)?youtu(?:be\.com|\.be)\/(embed\/|watch\?v=|shorts\/|playlist\?list=|embed\/videoseries\?list=)?([a-zA-Z0-9_-]+)(?:\?t=|.*&t=|\?start=|.*&start=)?([a-zA-Z0-9_-]+)?[^\s]*$/;
export const isYouTube = (url: string): boolean => {
return Boolean(
url.match(REG_YOUTUBE)
);
}
export const getYouTubeStartAt = (url: string): string => {
const ytLink = url.match(REG_YOUTUBE);
if (ytLink?.[2]) {
const time = ytLink[3] ? parseInt(ytLink[3]) : 0;
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 "";
};
export const isValidYouTubeStart = (value: string): boolean => {
if(/^[0-9]+$/.test(value)) return true; // Matches only numbers (seconds)
if(/^[0-9]+:[0-9]+$/.test(value)) return true; // Matches only numbers (minutes and seconds)
if(/^[0-9]+:[0-9]+:[0-9]+$/.test(value)) return true; // Matches only numbers (hours, minutes, and seconds
};
export const updateYouTubeStartTime = (link: string, startTime: string): string => {
const match = link.match(REG_YOUTUBE);
if (match?.[2]) {
const startTimeParam = startTime === ""
? ``
: `t=${timeStringToSeconds(startTime)}`;
let updatedLink = link;
if (match[3]) {
// If start time already exists, update it
updatedLink = link.replace(/([?&])t=[a-zA-Z0-9_-]+/, `$1${startTimeParam}`);
updatedLink = updatedLink.replace(/([?&])start=[a-zA-Z0-9_-]+/, `$1${startTimeParam}`);
} else {
// If no start time exists, add it to the link
updatedLink += (link.includes('?') ? '&' : '?') + startTimeParam;
}
return updatedLink;
}
return link;
};
const timeStringToSeconds = (time: string): number => {
const timeParts = time.split(':').map(Number);
const totalParts = timeParts.length;
if (totalParts === 1) {
return timeParts[0]; // Only seconds provided (ss)
} else if (totalParts === 2) {
return timeParts[0] * 60 + timeParts[1]; // Minutes and seconds provided (mm:ss)
} else if (totalParts === 3) {
return timeParts[0] * 3600 + timeParts[1] * 60 + timeParts[2]; // Hours, minutes, and seconds provided (hh:mm:ss)
}
return 0; // Invalid format, return 0 or handle accordingly
};

View File

@@ -7,6 +7,7 @@
height: 100%;
margin: 0px;
background-color: white;
position:relative;
}
.context-menu-option__shortcut {
@@ -345,7 +346,7 @@ label.color-input-container > input {
padding: 0;
}
.excalidraw-settings input {
.excalidraw-settings input:not([type="color"]) {
min-width: 10em;
}
@@ -371,10 +372,6 @@ div.excalidraw-draginfo {
background: initial;
}
.excalidraw .HelpDialog__key {
background-color: var(--color-gray-80) !important;
}
.excalidraw .embeddable-menu {
width: fit-content;
height: fit-content;
@@ -475,4 +472,78 @@ summary.excalidraw-setting-h4 {
hr.excalidraw-setting-hr {
margin: 1rem 0rem 0rem 0rem;
}
.excalidraw-mdEmbed-hideFilename .mod-header {
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 {
display: none;
}
background-color: transparent !important;
}
.canvas-node:not(.is-editing) .canvas-node-container:has(.excalidraw-canvas-immersive) {
border: unset;
box-shadow: unset;
}
.excalidraw .canvas-node .ex-md-font-hand-drawn {
--font-text: "Virgil";
}
.excalidraw .canvas-node .ex-md-font-code {
--font-text: "Cascadia";
}
.excalidraw__embeddable-container .workspace-leaf,
.excalidraw__embeddable-container .workspace-leaf .view-content {
::-webkit-scrollbar,
::-webkit-scrollbar-horizontal {
display: none;
}
background-color: transparent !important;
}
.excalidraw__embeddable-container .workspace-leaf-content .view-content {
padding-left: 2px;
padding-right: 2px;
padding-top: 0px;
padding-bottom: 0px;
}
.excalidraw__embeddable-container .workspace-leaf .view-content {
display: flex;
align-items: center;
justify-content: center;
}
.excalidraw__embeddable-container .workspace-leaf-content .image-container,
.excalidraw__embeddable-container .workspace-leaf-content .audio-container,
.excalidraw__embeddable-container .workspace-leaf-content .video-container {
display: flex;
}
.excalidraw__embeddable-container .canvas-node-container {
border: 2px solid var(--canvas-color);
}
.excalidraw__embeddable-container .canvas-node {
--shadow-border-themed-inset: inset 0 0 0 1px rgb(var(--canvas-color));;
--shadow-border-themed: 0 0 0 2px rgb(var(--canvas-color));
}
.excalidraw__embeddable-container .canvas-node.is-selected.is-themed .canvas-node-container,
.excalidraw__embeddable-container .canvas-node.is-focused.is-themed .canvas-node-container {
border-color: var(--canvas-color);
}

View File

@@ -1,8 +1,8 @@
{
"compilerOptions": {
"baseUrl": ".",
"sourceMap": true,
"module": "es2015",
"sourceMap": false,
"module": "ES2015",
"target": "es2017", //es2017 because script engine requires for async execution
"allowJs": true,
"noImplicitAny": true,
@@ -17,7 +17,8 @@
"esnext",
"DOM.Iterable"
],
"jsx": "react"
"jsx": "react",
"inlineSourceMap": true
},
"include": [
"**/*.ts",