Compare commits

..

35 Commits

Author SHA1 Message Date
zsviczian
68180db2aa 2.13.0, 0.18.0-25
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-07-01 20:49:40 +02:00
zsviczian
63505d17e9 MultiOptionConfirmationPrompt 2025-07-01 18:02:50 +00:00
zsviczian
0851b45977 Merge pull request #2397 from dmscode/zh-2025-06-30
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
Update zh-cn.ts to 8240d87
2025-06-30 18:54:54 +02:00
dmscode
16332a3e83 Update zh-cn.ts to 8240d87 2025-06-30 14:07:31 +08:00
zsviczian
8240d87fa8 2.13.0 code, but not yet released. Manifest version not updated
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-06-29 22:56:02 +02:00
zsviczian
a7db044715 Merge pull request #2378 from zsviczian/dependabot/npm_and_yarn/master/multi-2bdc9b2f63
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
Bump chroma-js and @types/chroma-js
2025-06-24 21:10:45 +02:00
dependabot[bot]
339c274f1b Bump chroma-js and @types/chroma-js
Bumps [chroma-js](https://github.com/gka/chroma.js) and [@types/chroma-js](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/chroma-js). These dependencies needed to be updated together.

Updates `chroma-js` from 2.6.0 to 3.1.2
- [Release notes](https://github.com/gka/chroma.js/releases)
- [Changelog](https://github.com/gka/chroma.js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/gka/chroma.js/compare/v2.6.0...v3.1.2)

Updates `@types/chroma-js` from 2.4.5 to 3.1.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/chroma-js)

---
updated-dependencies:
- dependency-name: chroma-js
  dependency-version: 3.1.2
  dependency-type: direct:production
  update-type: version-update:semver-major
- dependency-name: "@types/chroma-js"
  dependency-version: 3.1.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-23 20:14:35 +00:00
zsviczian
d5a19cbc09 Merge branch 'master' of https://github.com/zsviczian/obsidian-excalidraw-plugin
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-06-23 22:13:16 +02:00
zsviczian
400cffcd01 fix hover preview, bump excalidraw to 0.18.0-24 2025-06-23 22:13:13 +02:00
zsviczian
e5438c1e56 fixed determineFocusDistance function signature preventing ExcaliBrain from rendering
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-06-16 11:52:12 +00:00
zsviczian
7e2ef3f115 Merge pull request #2383 from dmscode/zh-2025-06-16
Update zh-cn.ts to 32d0301
2025-06-16 11:06:54 +02:00
dmscode
ad091df4d9 Update zh-cn.ts to 32d0301 2025-06-16 06:52:20 +08:00
dmscode
0837c017a1 Update zh-cn.ts to 32d0301 2025-06-15 06:48:05 +08:00
zsviczian
32d0301366 2.12.3, 0.18.0-23 2025-06-14 23:36:37 +02:00
zsviczian
5accd657d9 npm version, fix gpt-4-vision 2025-06-12 19:19:14 +00:00
zsviczian
8da97a63e0 Update bug_report.yml 2025-06-04 21:26:22 +02:00
zsviczian
a5d7731533 localgraph loadFile does not exist if localGraph is not yet visible (leaf not loaded) 2025-05-30 08:18:47 +02:00
zsviczian
acb83fd697 2.12.2 0.18.0-18
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-05-28 07:22:11 +00:00
zsviczian
271f21f85a 0.18.0-18 2025-05-28 06:30:26 +02:00
zsviczian
371fb54787 2.12.1, 0.18.0-18
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-05-26 22:50:40 +02:00
zsviczian
3f19f9771a updated boolean operations 2025-05-26 18:57:20 +02:00
zsviczian
5a8596d113 Merge pull request #2359 from zsviczian/move-input
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
feat: Implement draggable inputPrompt
2025-05-26 17:52:24 +02:00
zsviczian
6edd8b9a4e Implement draggable inputPrompt 2025-05-26 15:51:34 +00:00
zsviczian
778346b0dd Merge pull request #2358 from dmscode/zh-language-update/2025-05-26
Update zh-cn.ts to ff404e4
2025-05-26 11:36:06 +02:00
dmscode
85ac633263 Update zh-cn.ts to ff404e4 2025-05-26 05:56:02 +08:00
zsviczian
ff404e4dd6 text to path minor changes
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-05-25 23:01:00 +02:00
zsviczian
d0845a7d68 text to path updated 2025-05-25 22:56:20 +02:00
zsviczian
954eaefe29 2.12.0, 0.18.0-17 2025-05-25 22:00:12 +02:00
zsviczian
175b202a6f text to path 2025-05-25 19:51:17 +02:00
zsviczian
b77b4df56d text to path 2025-05-25 19:24:20 +02:00
zsviczian
dd7abe2547 0.18.0-17 - Text to Path 2025-05-25 19:18:00 +02:00
zsviczian
cac27fb936 test script update alert 2025-05-25 18:09:49 +02:00
zsviczian
d9aef84e13 Merge pull request #2353 from dmscode/zh-2025-05-20
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
Update zh-cn.ts to 6824a1a
2025-05-21 07:56:09 +02:00
dmscode
c6a81bef24 Update zh-cn.ts to 6824a1a 2025-05-20 13:28:52 +08:00
zsviczian
6824a1aa68 2.12.0-beta-2
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-05-18 18:49:02 +02:00
51 changed files with 5147 additions and 1988 deletions

View File

@@ -0,0 +1,17 @@
{
"name": "Node.js 20",
"image": "mcr.microsoft.com/devcontainers/javascript-node:20",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "20"
}
},
"postCreateCommand": "npm install",
"customizations": {
"vscode": {
"extensions": [
"ms-vscode.vscode-typescript-next"
]
}
}
}

View File

@@ -13,7 +13,7 @@ body:
1. **Review recent [release notes](https://github.com/zsviczian/obsidian-excalidraw-plugin/releases)** maybe there is already an answer.
2. **[Search issues](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues) (including closed ones)** to see if there is anything similar.
3. **[Watch the Feature Walkthrough Video](https://youtu.be/P_Q6avJGoWI)**: As it infact answers 90% of the typical questions I receive
4. **[Consult NotebookLM with your question](https://excalidraw-obsidian.online/WIKI/09+Video+Transcripts/Videos/Turn+any+YouTube+Channel+into+your+AI+Mentor+-+Obsidian+is+the+ultimate+automation+workbench+for+PKM)**
4. **[Consult NotebookLM with your question](https://excalidraw-obsidian.online/WIKI/09+Video+Transcripts/Videos/Turn+any+YouTube+Channel+into+your+AI+Mentor+-+Obsidian+is+the+ultimate+automation+workbench+for+PKM)** Here's the [direct link to a preloaded NotebookLM](https://notebooklm.google.com/notebook/42d76a2f-c11d-4002-9286-1683c43d0ab0)
- type: markdown
attributes:

7
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
target-branch: "master"

2
.nvmrc
View File

@@ -1 +1 @@
18
20

View File

@@ -23,8 +23,8 @@ const elements = ea.getViewSelectedElements().filter(
el.groupIds.some(id => id.startsWith(ShadowGroupMarker)) ||
(["line", "arrow"].includes(el.type) && el.roundness === null)
);
if(elements.length === 0) {
new Notice ("Select ellipses, rectangles or diamonds");
if(elements.length < 2) {
new Notice ("Select ellipses, rectangles, diamonds; or lines and arrows with sharp edges");
return;
}

View File

@@ -109,9 +109,18 @@ async function editExistingTextElement(elements) {
ea.copyViewElementsToEAforEditing(elements);
const el = ea.getElements()[0];
ea.style.strokeColor = el.strokeColor;
const text = await utils.inputPrompt(
"Edit text","",elements[0].rawText,undefined,5,true,customControls,true,true
);
const text = await utils.inputPrompt({
header: "Edit text",
placeholder: "",
value: elements[0].rawText,
//buttons: undefined,
lines: 5,
displayEditorButtons: true,
customComponents: customControls,
blockPointerInputOutsideModal: true,
controlsOnTop: true
});
windowOpen = false;
if(!text) return;

View File

@@ -1,49 +0,0 @@
/*
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/text-arch.jpg)
Fit a text to the arch of a circle. The script will prompt you for the radius of the circle and then split your text to individual letters and place each letter to the arch defined by the radius. Setting a lower radius value will increase the arching of the text. Note that the arched-text will no longer be editable as a text element and it will no longer function as a markdown link. Emojis are currently not supported.
```javascript
*/
el = ea.getViewSelectedElement();
if(!el || el.type!=="text") {
new Notice("Please select a text element");
return;
}
ea.style.fontSize = el.fontSize;
ea.style.fontFamily = el.fontFamily;
ea.style.strokeColor = el.strokeColor;
ea.style.opacity = el.opacity;
const r = parseInt (await utils.inputPrompt("The radius of the arch you'd like to fit the text to","number","150"));
const archAbove = await utils.suggester(["Arch above","Arch below"],[true,false]);
if(isNaN(r)) {
new Notice("The radius is not a number");
return;
}
circlePoint = (angle) => archAbove
? [
r * Math.sin(angle),
-r * Math.cos(angle)
]
: [
-r * Math.sin(angle),
r * Math.cos(angle)
];
let rot = (archAbove ? -0.5 : 0.5) * ea.measureText(el.text).width/r;
let objectIDs = [];
for(i=0;i<el.text.length;i++) {
const character = el.text.substring(i,i+1);
const width = ea.measureText(character).width;
ea.style.angle = rot;
const [x,y] = circlePoint(rot);
rot += (archAbove ? 1 : -1) *width / r;
objectIDs.push(ea.addText(x,y,character));
}
ea.addToGroup(objectIDs);
ea.addElementsToView(true, false, true);

1078
ea-scripts/Text to Path.md Normal file

File diff suppressed because it is too large Load Diff

View File

Before

Width:  |  Height:  |  Size: 327 B

After

Width:  |  Height:  |  Size: 327 B

476
ea-scripts/To Line.md Normal file
View File

@@ -0,0 +1,476 @@
/**
* Converts an ellipse element to a line element
* @param {Object} ellipse - The ellipse element to convert
* @param {number} pointDensity - Optional number of points to generate (defaults to 64)
* @returns {string} The ID of the created line element
```js*/
function ellipseToLine(ellipse, pointDensity = 64) {
if (!ellipse || ellipse.type !== "ellipse") {
throw new Error("Input must be an ellipse element");
}
// Calculate points along the ellipse perimeter
const stepSize = (Math.PI * 2) / pointDensity;
const points = drawEllipse(
ellipse.x,
ellipse.y,
ellipse.width,
ellipse.height,
ellipse.angle,
0,
Math.PI * 2,
stepSize
);
// Save original styling to apply to the new line
const originalStyling = {
strokeColor: ellipse.strokeColor,
strokeWidth: ellipse.strokeWidth,
backgroundColor: ellipse.backgroundColor,
fillStyle: ellipse.fillStyle,
roughness: ellipse.roughness,
strokeSharpness: ellipse.strokeSharpness,
frameId: ellipse.frameId,
groupIds: [...ellipse.groupIds],
opacity: ellipse.opacity
};
// Use current style
const prevStyle = {...ea.style};
// Apply ellipse styling to the line
ea.style.strokeColor = originalStyling.strokeColor;
ea.style.strokeWidth = originalStyling.strokeWidth;
ea.style.backgroundColor = originalStyling.backgroundColor;
ea.style.fillStyle = originalStyling.fillStyle;
ea.style.roughness = originalStyling.roughness;
ea.style.strokeSharpness = originalStyling.strokeSharpness;
ea.style.opacity = originalStyling.opacity;
// Create the line and close it
const lineId = ea.addLine(points);
const line = ea.getElement(lineId);
// Make it a polygon to close the path
line.polygon = true;
// Transfer grouping and frame information
line.frameId = originalStyling.frameId;
line.groupIds = originalStyling.groupIds;
// Restore previous style
ea.style = prevStyle;
return lineId;
// Helper function from the Split Ellipse script
function drawEllipse(x, y, width, height, angle = 0, start = 0, end = Math.PI*2, step = Math.PI/32) {
const ellipse = (t) => {
const spanningVector = rotateVector([width/2*Math.cos(t), height/2*Math.sin(t)], angle);
const baseVector = [x+width/2, y+height/2];
return addVectors([baseVector, spanningVector]);
}
if(end <= start) end = end + Math.PI*2;
let points = [];
const almostEnd = end - step/2;
for (let t = start; t < almostEnd; t = t + step) {
points.push(ellipse(t));
}
points.push(ellipse(end));
return points;
}
function rotateVector(vec, ang) {
var cos = Math.cos(ang);
var sin = Math.sin(ang);
return [vec[0] * cos - vec[1] * sin, vec[0] * sin + vec[1] * cos];
}
function addVectors(vectors) {
return vectors.reduce((acc, vec) => [acc[0] + vec[0], acc[1] + vec[1]], [0, 0]);
}
}
/**
* Converts a rectangle element to a line element
* @param {Object} rectangle - The rectangle element to convert
* @param {number} pointDensity - Optional number of points to generate for curved segments (defaults to 16)
* @returns {string} The ID of the created line element
*/
function rectangleToLine(rectangle, pointDensity = 16) {
if (!rectangle || rectangle.type !== "rectangle") {
throw new Error("Input must be a rectangle element");
}
// Save original styling to apply to the new line
const originalStyling = {
strokeColor: rectangle.strokeColor,
strokeWidth: rectangle.strokeWidth,
backgroundColor: rectangle.backgroundColor,
fillStyle: rectangle.fillStyle,
roughness: rectangle.roughness,
strokeSharpness: rectangle.strokeSharpness,
frameId: rectangle.frameId,
groupIds: [...rectangle.groupIds],
opacity: rectangle.opacity
};
// Use current style
const prevStyle = {...ea.style};
// Apply rectangle styling to the line
ea.style.strokeColor = originalStyling.strokeColor;
ea.style.strokeWidth = originalStyling.strokeWidth;
ea.style.backgroundColor = originalStyling.backgroundColor;
ea.style.fillStyle = originalStyling.fillStyle;
ea.style.roughness = originalStyling.roughness;
ea.style.strokeSharpness = originalStyling.strokeSharpness;
ea.style.opacity = originalStyling.opacity;
// Calculate points for the rectangle perimeter
const points = generateRectanglePoints(rectangle, pointDensity);
// Create the line and close it
const lineId = ea.addLine(points);
const line = ea.getElement(lineId);
// Make it a polygon to close the path
line.polygon = true;
// Transfer grouping and frame information
line.frameId = originalStyling.frameId;
line.groupIds = originalStyling.groupIds;
// Restore previous style
ea.style = prevStyle;
return lineId;
// Helper function to generate rectangle points with optional rounded corners
function generateRectanglePoints(rectangle, pointDensity) {
const { x, y, width, height, angle = 0 } = rectangle;
const centerX = x + width / 2;
const centerY = y + height / 2;
// If no roundness, create a simple rectangle
if (!rectangle.roundness) {
const corners = [
[x, y], // top-left
[x + width, y], // top-right
[x + width, y + height], // bottom-right
[x, y + height], // bottom-left
[x,y] //origo
];
// Apply rotation if needed
if (angle !== 0) {
return corners.map(point => rotatePoint(point, [centerX, centerY], angle));
}
return corners;
}
// Handle rounded corners
const points = [];
// Calculate corner radius using Excalidraw's algorithm
const cornerRadius = getCornerRadius(Math.min(width, height), rectangle);
const clampedRadius = Math.min(cornerRadius, width / 2, height / 2);
// Corner positions
const topLeft = [x + clampedRadius, y + clampedRadius];
const topRight = [x + width - clampedRadius, y + clampedRadius];
const bottomRight = [x + width - clampedRadius, y + height - clampedRadius];
const bottomLeft = [x + clampedRadius, y + height - clampedRadius];
// Add top-left corner arc
points.push(...createArc(
topLeft[0], topLeft[1], clampedRadius, Math.PI, Math.PI * 1.5, pointDensity));
// Add top edge
points.push([x + clampedRadius, y], [x + width - clampedRadius, y]);
// Add top-right corner arc
points.push(...createArc(
topRight[0], topRight[1], clampedRadius, Math.PI * 1.5, Math.PI * 2, pointDensity));
// Add right edge
points.push([x + width, y + clampedRadius], [x + width, y + height - clampedRadius]);
// Add bottom-right corner arc
points.push(...createArc(
bottomRight[0], bottomRight[1], clampedRadius, 0, Math.PI * 0.5, pointDensity));
// Add bottom edge
points.push([x + width - clampedRadius, y + height], [x + clampedRadius, y + height]);
// Add bottom-left corner arc
points.push(...createArc(
bottomLeft[0], bottomLeft[1], clampedRadius, Math.PI * 0.5, Math.PI, pointDensity));
// Add left edge
points.push([x, y + height - clampedRadius], [x, y + clampedRadius]);
// Apply rotation if needed
if (angle !== 0) {
return points.map(point => rotatePoint(point, [centerX, centerY], angle));
}
return points;
}
// Helper function to create an arc of points
function createArc(centerX, centerY, radius, startAngle, endAngle, pointDensity) {
const points = [];
const angleStep = (endAngle - startAngle) / pointDensity;
for (let i = 0; i <= pointDensity; i++) {
const angle = startAngle + i * angleStep;
const x = centerX + radius * Math.cos(angle);
const y = centerY + radius * Math.sin(angle);
points.push([x, y]);
}
return points;
}
// Helper function to rotate a point around a center
function rotatePoint(point, center, angle) {
const sin = Math.sin(angle);
const cos = Math.cos(angle);
// Translate point to origin
const x = point[0] - center[0];
const y = point[1] - center[1];
// Rotate point
const xNew = x * cos - y * sin;
const yNew = x * sin + y * cos;
// Translate point back
return [xNew + center[0], yNew + center[1]];
}
}
function getCornerRadius(x, element) {
const fixedRadiusSize = element.roundness?.value ?? 32;
const CUTOFF_SIZE = fixedRadiusSize / 0.25;
if (x <= CUTOFF_SIZE) {
return x * 0.25;
}
return fixedRadiusSize;
}
/**
* Converts a diamond element to a line element
* @param {Object} diamond - The diamond element to convert
* @param {number} pointDensity - Optional number of points to generate for curved segments (defaults to 16)
* @returns {string} The ID of the created line element
*/
function diamondToLine(diamond, pointDensity = 16) {
if (!diamond || diamond.type !== "diamond") {
throw new Error("Input must be a diamond element");
}
// Save original styling to apply to the new line
const originalStyling = {
strokeColor: diamond.strokeColor,
strokeWidth: diamond.strokeWidth,
backgroundColor: diamond.backgroundColor,
fillStyle: diamond.fillStyle,
roughness: diamond.roughness,
strokeSharpness: diamond.strokeSharpness,
frameId: diamond.frameId,
groupIds: [...diamond.groupIds],
opacity: diamond.opacity
};
// Use current style
const prevStyle = {...ea.style};
// Apply diamond styling to the line
ea.style.strokeColor = originalStyling.strokeColor;
ea.style.strokeWidth = originalStyling.strokeWidth;
ea.style.backgroundColor = originalStyling.backgroundColor;
ea.style.fillStyle = originalStyling.fillStyle;
ea.style.roughness = originalStyling.roughness;
ea.style.strokeSharpness = originalStyling.strokeSharpness;
ea.style.opacity = originalStyling.opacity;
// Calculate points for the diamond perimeter
const points = generateDiamondPoints(diamond, pointDensity);
// Create the line and close it
const lineId = ea.addLine(points);
const line = ea.getElement(lineId);
// Make it a polygon to close the path
line.polygon = true;
// Transfer grouping and frame information
line.frameId = originalStyling.frameId;
line.groupIds = originalStyling.groupIds;
// Restore previous style
ea.style = prevStyle;
return lineId;
function generateDiamondPoints(diamond, pointDensity) {
const { x, y, width, height, angle = 0 } = diamond;
const cx = x + width / 2;
const cy = y + height / 2;
// Diamond corners
const top = [cx, y];
const right = [x + width, cy];
const bottom = [cx, y + height];
const left = [x, cy];
if (!diamond.roundness) {
const corners = [top, right, bottom, left, top];
if (angle !== 0) {
return corners.map(pt => rotatePoint(pt, [cx, cy], angle));
}
return corners;
}
// Clamp radius
const r = Math.min(
getCornerRadius(Math.min(width, height) / 2, diamond),
width / 2,
height / 2
);
// For a diamond, the rounded corner is a *bezier* between the two adjacent edge points, not a circular arc.
// Excalidraw uses a quadratic bezier for each corner, with the control point at the corner itself.
// Calculate edge directions
function sub(a, b) { return [a[0] - b[0], a[1] - b[1]]; }
function add(a, b) { return [a[0] + b[0], a[1] + b[1]]; }
function norm([x, y]) {
const len = Math.hypot(x, y);
return [x / len, y / len];
}
function scale([x, y], s) { return [x * s, y * s]; }
// For each corner, move along both adjacent edges by r to get arc endpoints
// Order: top, right, bottom, left
const corners = [top, right, bottom, left];
const next = [right, bottom, left, top];
const prev = [left, top, right, bottom];
// For each corner, calculate the two points where the straight segments meet the arc
const arcPoints = [];
for (let i = 0; i < 4; ++i) {
const c = corners[i];
const n = next[i];
const p = prev[i];
const toNext = norm(sub(n, c));
const toPrev = norm(sub(p, c));
arcPoints.push([
add(c, scale(toPrev, r)), // start of arc (from previous edge)
add(c, scale(toNext, r)), // end of arc (to next edge)
c // control point for bezier
]);
}
// Helper: quadratic bezier between p0 and p2 with control p1
function bezier(p0, p1, p2, density) {
const pts = [];
for (let i = 0; i <= density; ++i) {
const t = i / density;
const mt = 1 - t;
pts.push([
mt*mt*p0[0] + 2*mt*t*p1[0] + t*t*p2[0],
mt*mt*p0[1] + 2*mt*t*p1[1] + t*t*p2[1]
]);
}
return pts;
}
// Build path: for each corner, straight line to arc start, then bezier to arc end using corner as control
let pts = [];
for (let i = 0; i < 4; ++i) {
const prevArc = arcPoints[(i + 3) % 4];
const arc = arcPoints[i];
if (i === 0) {
pts.push(arc[0]);
} else {
pts.push(arc[0]);
}
// Quadratic bezier from arc[0] to arc[1] with control at arc[2] (the corner)
pts.push(...bezier(arc[0], arc[2], arc[1], pointDensity));
}
pts.push(arcPoints[0][0]); // close
if (angle !== 0) {
return pts.map(pt => rotatePoint(pt, [cx, cy], angle));
}
return pts;
}
// Helper function to create an arc between two points
function createArcBetweenPoints(startPoint, endPoint, centerX, centerY, pointDensity) {
const startAngle = Math.atan2(startPoint[1] - centerY, startPoint[0] - centerX);
const endAngle = Math.atan2(endPoint[1] - centerY, endPoint[0] - centerX);
// Ensure angles are in correct order for arc drawing
let adjustedEndAngle = endAngle;
if (endAngle < startAngle) {
adjustedEndAngle += 2 * Math.PI;
}
const points = [];
const angleStep = (adjustedEndAngle - startAngle) / pointDensity;
// Start with the straight line to arc start
points.push(startPoint);
// Create arc points
for (let i = 1; i < pointDensity; i++) {
const angle = startAngle + i * angleStep;
const distance = Math.hypot(startPoint[0] - centerX, startPoint[1] - centerY);
const x = centerX + distance * Math.cos(angle);
const y = centerY + distance * Math.sin(angle);
points.push([x, y]);
}
// Add the end point of the arc
points.push(endPoint);
return points;
}
// Helper function to rotate a point around a center
function rotatePoint(point, center, angle) {
const sin = Math.sin(angle);
const cos = Math.cos(angle);
// Translate point to origin
const x = point[0] - center[0];
const y = point[1] - center[1];
// Rotate point
const xNew = x * cos - y * sin;
const yNew = x * sin + y * cos;
// Translate point back
return [xNew + center[0], yNew + center[1]];
}
}
const el = ea.getViewSelectedElement();
switch (el.type) {
case "rectangle":
rectangleToLine(el);
break;
case "ellipse":
ellipseToLine(el);
break;
case "diamond":
diamondToLine(el);
break;
}
ea.addElementsToView();

File diff suppressed because one or more lines are too long

View File

@@ -73,8 +73,8 @@ 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/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]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Split%20text%20by%20lines.svg"/></div>|[[#Split text by lines]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20Arch.svg"/></div>|[[#Text Arch]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20Aura.svg"/></div>|[[#Text Aura]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20to%20Path.svg"/></div>|[[#Text to Path]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20to%20Sticky%20Notes.svg"/></div>|[[#Text to Sticky Notes]]|
## Styling and Appearance
@@ -596,18 +596,24 @@ 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/Split%20text%20by%20lines.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Split lines of text into separate text elements for easier reorganization<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-split-lines.jpg'></td></tr></table>
## Text Arch
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20Arch.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/Text%20Arch.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Fit a text to the arch of a circle. The script will prompt you for the radius of the circle and then split your text to individual letters and place each letter to the arch defined by the radius. Setting a lower radius value will increase the arching of the text. Note that the arched-text will no longer be editable as a text element and it will no longer function as a markdown link. Emojis are currently not supported.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/text-arch.jpg'></td></tr></table>
## Text Aura
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20Aura.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/Text%20Aura.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Select a single text element, or a text element in a container. The container must have a transparent background.<br>The script will add an aura to the text by adding 4 copies of the text each with the inverted stroke color of the original text element and with a very small X and Y offset. The resulting 4 + 1 (original) text elements or containers will be grouped.<br>If you copy a color string on the clipboard before running the script, the script will use that color instead of the inverted color.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-text-aura.jpg'></td></tr></table>
## Text to Path
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20to%20Path.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/Text%20to%20Path.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script allows you to fit a text element along a selected path: line, arrow, freedraw, ellipse, rectangle, or diamond. You can select either a path or a text element, or both:<br><br>
- If only a path is selected, you will be prompted to provide the text.<br>
- If only a text element is selected and it was previously fitted to a path, the script will use the original path if it is still present in the scene.<br>
- If both a text and a path are selected, the script will fit the text to the selected path.<br><br>
If the path is a perfect circle, you will be prompted to choose whether to fit the text above or below the circle.<br><br>
After fitting, the text will no longer be editable as a standard text element or function as a markdown link. Emojis are not supported.<br>
<img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-text-to-path.jpg'></td></tr></table>
## Toggle Grid
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Toggle%20Grid.md

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

View File

@@ -1,8 +1,8 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "2.12.0-beta-1",
"minAppVersion": "1.1.6",
"version": "2.13.0",
"minAppVersion": "1.5.7",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",
"authorUrl": "https://excalidraw-obsidian.online",

View File

@@ -1,12 +1,12 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "2.11.1",
"minAppVersion": "1.1.6",
"version": "2.13.0",
"minAppVersion": "1.5.7",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",
"authorUrl": "https://excalidraw-obsidian.online",
"fundingUrl": "https://ko-fi.com/zsolt",
"helpUrl": "https://github.com/zsviczian/obsidian-excalidraw-plugin#readme",
"isDesktopOnly": false
}
}

3400
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,10 +21,10 @@
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {
"dependencies": {
"@popperjs/core": "^2.11.8",
"@zsviczian/excalidraw": "0.18.0-15",
"chroma-js": "^2.4.2",
"@zsviczian/excalidraw": "0.18.0-25",
"chroma-js": "^3.1.2",
"clsx": "^2.0.0",
"@zsviczian/colormaster": "^1.2.2",
"gl-matrix": "^3.4.3",
@@ -59,7 +59,7 @@
"@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-typescript": "^12.1.2",
"@rollup/plugin-json": "^6.1.0",
"@types/chroma-js": "^2.4.0",
"@types/chroma-js": "^3.1.1",
"@types/js-beautify": "^1.14.0",
"@types/js-yaml": "^4.0.9",
"@types/node": "^20.10.5",
@@ -89,5 +89,9 @@
"resolutions": {
"@typescript-eslint/typescript-estree": "5.3.0"
},
"prettier": "@excalidraw/prettier-config"
"prettier": "@excalidraw/prettier-config",
"engines": {
"node": ">=20.0.0",
"npm": ">=10.0.0"
}
}

View File

@@ -125,20 +125,148 @@
* @returns {string} - The new filepath for the image including full vault path and extension.
*
* Example usage:
* ```
* onImageFilePathHook: (data) => {
* const { currentImageName, drawingFilePath } = data;
* const ext = currentImageName.split('.').pop();
* // Generate a new filepath based on the drawing file name and other criteria
* return `${drawingFileName} - ${currentImageName || 'image'}.${ext}`;
* }
* ```
* onImageFilePathHook: (data: {
*
* Signiture:
* onImageFilePathHook: (data: {
* currentImageName: string; // Excalidraw generated name of the image, or the name received from the file system.
* drawingFilePath: string; // The full filepath of the Excalidraw file where the image is being used.
* }) => string = null;
*/
//ea.onImageFilePathHook = (data) => {};
// ea.onImageFilePathHook = (data) => { console.log(data); };
/**
* If set, this callback is triggered when the Excalidraw image is being exported to
* .svg, .png, or .excalidraw.
* You can use this callback to customize the naming and path of the images. This allows
* you to place images into an assets folder.
*
* If the function returns null or undefined, the normal Excalidraw operation will continue
* with the currentImageName and in the same folder as the Excalidraw file
* If a filepath is returned, that will be used. Include the full Vault filepath and filename
* with the file extension.
* !!!! If an image already exists on the path, that will be overwritten. When returning
* your own image path, you must take care of unique filenames (if that is a requirement) !!!!
* The current image name is the name generated by Excalidraw:
* - my-drawing.png
* - my-drawing.svg
* - my-drawing.excalidraw
* - my-drawing.dark.svg
* - my-drawing.light.svg
* - my-drawing.dark.png
* - my-drawing.light.png
*
* @param data - An object containing the following properties:
* @property {string} exportFilepath - Default export filepath for the image.
* @property {string} excalidrawFile - TFile: The Excalidraw file being exported.
* @property {string} exportExtension - The file extension of the export (e.g., .dark.svg, .png, .excalidraw).
* @property {string} oldExcalidrawPath - If action === "move" The old path of the Excalidraw file, else undefined
* @property {string} action - The action being performed:
* "export" | "move" | "delete"
* move and delete reference the change to the Excalidraw file.
*
* @returns {string} - The new filepath for the image including full vault path and extension.
*
* action === "move" || action === "delete" is only possible if "keep in sync" is enabled
* in plugin export settings
*
* Example usage:
* onImageFilePathHook: (data) => {
* const { currentImageName, drawingFilePath, frontmatter } = data;
* // Generate a new filepath based on the drawing file name and other criteria
* const ext = currentImageName.split('.').pop();
* if(frontmatter && frontmatter["my-custom-field"]) {
* }
* return `${drawingFileName} - ${currentImageName || 'image'}.${ext}`;
* }
*
*/
/*ea.onImageExportPathHook = (data) => {
//debugger; //remove comment to debug using Developer Console
let {excalidrawFile, exportFilepath, exportExtension, oldExcalidrawPath, action} = data;
const frontmatter = app.metadataCache.getFileCache(excalidrawFile)?.frontmatter;
//console.log(data, frontmatter);
const excalidrawFilename = action === "move"
? ea.splitFolderAndFilename(excalidrawFile.name).filename
: excalidrawFile.name
if(excalidrawFilename.match(/^icon - /i)) {
const {folderpath, filename, basename, extension} = ea.splitFolderAndFilename(exportFilepath);
exportFilepath = "assets/icons/" + filename;
return exportFilepath;
}
if(excalidrawFilename.match(/^stickfigure - /i)) {
const {folderpath, filename, basename, extension} = ea.splitFolderAndFilename(exportFilepath);
exportFilepath = "assets/stickfigures/" + filename;
return exportFilepath;
}
if(excalidrawFilename.match(/^logo - /i)) {
const {folderpath, filename, basename, extension} = ea.splitFolderAndFilename(exportFilepath);
exportFilepath = "assets/logos/" + filename;
return exportFilepath;
}
// !!!! frontmatter will be undefined when action === "delete"
// this means if you base your logic on frontmatter properties, then
// plugin settings keep files in sync will break for those files when
// deleting the Excalidraw file. The images will not be deleted, or worst
// your logic might result in deleting other files. This hook gives you
// powerful control, but the hook function logic requires careful testing
// on your part.
//if(frontmatter && frontmatter["is-asset"]) { //custom frontmatter property
exportFilepath = ea.obsidian.normalizePath("assets/" + exportFilepath);
return exportFilepath;
//}
return exportFilepath;
};*/
/**
* Excalidraw supports auto-export of Excalidraw files to .png, .svg, and .excalidraw formats.
*
* Auto-export of Excalidraw files can be controlled at multiple levels.
* 1) In plugin settings where you can set up default auto-export applicable to all your Excalidraw files.
* 2) However, if you do not want to auto-export every file, you can also control auto-export
* at the file level using the 'excalidraw-autoexport' frontmatter property.
* 3) This hook gives you an additional layer of control over the auto-export process.
*
* This hook is triggered when an Excalidraw file is being saved.
*
* interface AutoexportConfig {
* png: boolean; // Whether to auto-export to PNG
* svg: boolean; // Whether to auto-export to SVG
* excalidraw: boolean; // Whether to auto-export to Excalidraw format
* theme: "light" | "dark" | "both"; // The theme to use for the export
* }
*
* @param {Object} data - The data for the hook.
* @param {AutoexportConfig} data.autoexportConfig - The current autoexport configuration.
* @param {TFile} data.excalidrawFile - The Excalidraw file being auto-exported.
* @returns {AutoexportConfig | null} - Return a modified AutoexportConfig to override the export behavior, or null to use the default.
*/
/*ea.onTriggerAutoexportHook = (data) => {
let {autoexportConfig, excalidrawFile} = data;
const frontmatter = app.metadataCache.getFileCache(excalidrawFile)?.frontmatter;
//console.log(data, frontmatter);
//logic based on filepath and frontmatter
if(excalidrawFile.name.match(/^(?:icon|stickfigure|logo) - /i)) {
autoexportConfig.theme = "light";
autoexportConfig.svg = true;
autoexportConfig.png = false;
autoexportConfig.excalidraw = false;
return autoexportConfig;
}
return autoexportConfig;
};*/
/**
* If set, this callback is triggered whenever the active canvas color changes
@@ -147,5 +275,5 @@
* view: ExcalidrawView, //the excalidraw view
* color: string,
* ) => void = null;
*/
//ea.onCanvasColorChangeHook = (ea, view, color) => {};
*/
//ea.onCanvasColorChangeHook = (ea, view, color) => {};

View File

@@ -1,8 +1,8 @@
import { customAlphabet } from "nanoid";
import { ExcalidrawLib } from "../types/excalidrawLib";
import { moment } from "obsidian";
import ExcalidrawPlugin from "src/core/main";
import { DeviceType } from "src/types/types";
import { errorHandler } from "../utils/ErrorHandler";
//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;
export let EXCALIDRAW_PLUGIN: ExcalidrawPlugin = null;
@@ -106,33 +106,65 @@ export let {
getCSSFontDefinition,
loadSceneFonts,
loadMermaid,
syncInvalidIndices,
} = excalidrawLib;
export function updateExcalidrawLib() {
({
sceneCoordsToViewportCoords,
viewportCoordsToSceneCoords,
determineFocusDistance,
intersectElementWithLine,
getCommonBoundingBox,
getMaximumGroups,
measureText,
getLineHeight,
wrapText,
getFontString,
getBoundTextMaxWidth,
exportToSvg,
exportToBlob,
mutateElement,
restore,
mermaidToExcalidraw,
getFontFamilyString,
getContainerElement,
refreshTextDimensions,
getCSSFontDefinition,
loadSceneFonts,
loadMermaid,
} = excalidrawLib);
try {
// First validate that excalidrawLib exists and has the expected methods
if (!excalidrawLib) {
throw new Error("excalidrawLib is undefined");
}
// Check that critical functions exist before assigning them
const requiredFunctions = [
'sceneCoordsToViewportCoords',
'viewportCoordsToSceneCoords',
'determineFocusDistance',
'intersectElementWithLine',
'getCommonBoundingBox',
'measureText',
'getLineHeight',
'restore'
];
for (const fnName of requiredFunctions) {
if (!(fnName in excalidrawLib) || typeof excalidrawLib[fnName as keyof typeof excalidrawLib] !== 'function') {
throw new Error(`Required function ${fnName} is missing from excalidrawLib`);
}
}
// If validation passes, update the exported functions
({
sceneCoordsToViewportCoords,
viewportCoordsToSceneCoords,
determineFocusDistance,
intersectElementWithLine,
getCommonBoundingBox,
getMaximumGroups,
measureText,
getLineHeight,
wrapText,
getFontString,
getBoundTextMaxWidth,
exportToSvg,
exportToBlob,
mutateElement,
restore,
mermaidToExcalidraw,
getFontFamilyString,
getContainerElement,
refreshTextDimensions,
getCSSFontDefinition,
loadSceneFonts,
loadMermaid,
syncInvalidIndices,
} = excalidrawLib);
} catch (error) {
errorHandler.handleError(error, "updateExcalidrawLib", true);
// Don't throw here - we'll try to continue with potentially stale functions
// but at least we won't crash
}
}
export const FONTS_STYLE_ID = "excalidraw-custom-fonts";

File diff suppressed because one or more lines are too long

View File

@@ -44,7 +44,7 @@ import { initExcalidrawAutomate } from "src/utils/excalidrawAutomateUtils";
import { around, dedupe } from "monkey-around";
import { t } from "../lang/helpers";
import {
checkAndCreateFolder,
createOrOverwriteFile,
fileShouldDefaultAsExcalidraw,
getDrawingFilename,
getIMGFilename,
@@ -566,7 +566,7 @@ export default class ExcalidrawPlugin extends Plugin {
}
}
this.packageManager.getPackageMap().forEach(({excalidrawLib}) => {
(excalidrawLib as typeof ExcalidrawLib).registerLocalFont({metrics: fontMetrics as any, icon: null}, fourthFontDataURL);
(excalidrawLib as typeof ExcalidrawLib).registerLocalFont({metrics: fontMetrics as any}, fourthFontDataURL);
});
// Add fonts to open Obsidian documents
for(const ownerDocument of this.getOpenObsidianDocuments()) {
@@ -738,13 +738,7 @@ export default class ExcalidrawPlugin extends Plugin {
if (!data || data.startsWith("404: Not Found")) {
return null;
}
if (file) {
await this.app.vault.modify(file as TFile, data);
} else {
await checkAndCreateFolder(folder);
file = await this.app.vault.create(localPath, data);
}
return file;
return await createOrOverwriteFile(this.app, file?.path ?? localPath, data);
};
try {
@@ -894,7 +888,8 @@ export default class ExcalidrawPlugin extends Plugin {
normalizePath(file.path.substring(0, file.path.lastIndexOf(file.name))),
);
log(fname);
const result = await this.app.vault.create(
const result = await createOrOverwriteFile(
this.app,
fname,
FRONTMATTER + (await this.fileManager.exportSceneToMD(data, false)),
);

View File

@@ -34,6 +34,7 @@ import { insertLaTeXToView, search } from "src/utils/excalidrawAutomateUtils";
import { templatePromt } from "../../shared/Dialogs/Prompt";
import { t } from "../../lang/helpers";
import {
createOrOverwriteFile,
getAliasWithSize,
getAnnotationFileNameAndFolder,
getCropFileNameAndFolder,
@@ -69,7 +70,6 @@ import { carveOutImage, carveOutPDF, createImageCropperFile } from "../../utils/
import { showFrameSettings } from "../../shared/Dialogs/FrameSettings";
import { insertImageToView } from "../../utils/excalidrawViewUtils";
import ExcalidrawPlugin from "src/core/main";
import { get } from "http";
declare const PLUGIN_VERSION:string;
@@ -258,7 +258,7 @@ export class CommandManager {
new Notice("The compressed string is corrupted. Unable to decompress data.");
return;
}
await this.app.vault.modify(activeFile,header + decompressed + "\n```\n%%" + compressed[1]);
await createOrOverwriteFile(this.app, activeFile.path,header + decompressed + "\n```\n%%" + compressed[1]);
})();
}
@@ -355,7 +355,16 @@ export class CommandManager {
if(excalidrawFname.endsWith(".light.md")) {
excalidrawFile = this.app.metadataCache.getFirstLinkpathDest(excalidrawFname.replace(/\.light\.md$/,".md"), view.file.path);
}
if(!excalidrawFile) return false;
//handles the case if the png or svg is not in the same folder as the excalidraw file
if(!excalidrawFile) {
const basename = imgFile.basename.replace(/(?:\.dark|\.light)$/,"");
const candidates = this.app.vault.getMarkdownFiles().filter(f=>f.basename === basename);
if(candidates.length === 1) {
excalidrawFile = candidates[0];
} else {
return false;
}
}
}
if(checking) return true;
this.plugin.openDrawing(excalidrawFile, "new-tab", true);
@@ -1823,10 +1832,7 @@ export class CommandManager {
const template = await this.plugin.getBlankDrawing();
const target = await this.app.vault.read(activeFile);
const mergedTarget = mergeMarkdownFiles(template, target);
await this.app.vault.modify(
activeFile,
mergedTarget,
);
await createOrOverwriteFile(this.app, activeFile.path, mergedTarget);
setExcalidrawView(activeView.leaf);
})();
},

View File

@@ -6,10 +6,11 @@ import { changeThemeOfExcalidrawMD, ExcalidrawData, getMarkdownDrawingSection }
import ExcalidrawView, { getTextMode } from "src/view/ExcalidrawView";
import ExcalidrawPlugin from "src/core/main";
import { DEBUGGING } from "src/utils/debugHelper";
import { checkAndCreateFolder, download, getIMGFilename, getLink, getListOfTemplateFiles, getNewUniqueFilepath } from "src/utils/fileUtils";
import { checkAndCreateFolder, createFileAndAwaitMetacacheUpdate, download, getIMGFilename, getLink, getListOfTemplateFiles, getNewUniqueFilepath } from "src/utils/fileUtils";
import { PaneTarget } from "src/utils/modifierkeyHelper";
import { getExcalidrawViews, getNewOrAdjacentLeaf, isObsidianThemeDark, openLeaf } from "src/utils/obsidianUtils";
import { errorlog, getExportTheme } from "src/utils/utils";
import { imageCache } from "src/shared/ImageCache";
export class PluginFileManager {
private plugin: ExcalidrawPlugin;
@@ -172,28 +173,71 @@ export class PluginFileManager {
? ""
: theme + ".";
const imageRelativePath = getIMGFilename(
excalidrawRelativePath,
theme+this.settings.embedType.toLowerCase(),
);
const imageFullpath = getIMGFilename(
const exportExtension = theme+this.settings.embedType.toLowerCase();
let imageFullpath = getIMGFilename(
file.path,
theme+this.settings.embedType.toLowerCase(),
exportExtension,
);
if(this.plugin.ea?.onImageExportPathHook) {
try {
imageFullpath = this.plugin.ea.onImageExportPathHook({
exportFilepath: imageFullpath,
exportExtension,
excalidrawFile: file,
action: "export",
}) ?? imageFullpath;
} catch (e) {
errorlog({where: "FileManager.embedDrawing", fn: this.plugin.ea.onImageExportPathHook, error: e});
}
}
const createFile = async (path: string):Promise<TFile> => {
return await createFileAndAwaitMetacacheUpdate(this.app, path,
this.settings.embedType === "SVG"
? `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="0" height="0"></svg>`
: new Uint8Array([
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
0x00, 0x00, 0x00, 0x0D, // IHDR chunk length
0x49, 0x48, 0x44, 0x52, // IHDR
0x00, 0x00, 0x00, 0x01, // width: 1
0x00, 0x00, 0x00, 0x01, // height: 1
0x08, 0x06, 0x00, 0x00, 0x00, // bit depth: 8, color type: 6 (RGBA), compression: 0, filter: 0, interlace: 0
0x1F, 0x15, 0xC4, 0x89, // IHDR CRC
0x00, 0x00, 0x00, 0x0B, // IDAT chunk length
0x49, 0x44, 0x41, 0x54, // IDAT
0x78, 0x9C, 0x62, 0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, // compressed data (1x1 transparent pixel)
0x0A, 0x2D, 0xB4, // IDAT CRC
0x00, 0x00, 0x00, 0x00, // IEND chunk length
0x49, 0x45, 0x4E, 0x44, // IEND
0xAE, 0x42, 0x60, 0x82 // IEND CRC
]).buffer
);
}
let imgFile = this.app.vault.getFileByPath(imageFullpath);
if (!imgFile) {
imgFile = await createFile(imageFullpath);
}
const imageRelativePath = this.app.metadataCache.fileToLinktext(
imgFile,
activeView.file.path,
false,
);
//will hold incorrect value if theme==="", however in that case it won't be used
const otherTheme = theme === "dark." ? "light." : "dark.";
const otherImageRelativePath = theme === ""
//if the hook tinkers with the extension, then I cannot predict the other theme's extension
//it would become a messy heuristic to try to guess the other theme's extension
const otherImageRelativePath = ((theme === "") || !imageRelativePath.endsWith(exportExtension))
? null
: getIMGFilename(
excalidrawRelativePath,
otherTheme+this.settings.embedType.toLowerCase(),
);
: (imageRelativePath.substring(0, imageRelativePath.lastIndexOf(exportExtension)) + otherTheme+this.settings.embedType.toLowerCase());
const imgFile = this.app.vault.getAbstractFileByPath(imageFullpath);
if (!imgFile) {
await this.app.vault.create(imageFullpath, "");
await sleep(200); //wait for metadata cache to update
if(otherImageRelativePath) {
await createFile(
imgFile.path.substring(0, imgFile.path.lastIndexOf(exportExtension)) + otherTheme+this.settings.embedType.toLowerCase()
);
}
const inclCom = this.settings.embedMarkdownCommentLinks;
@@ -367,17 +411,43 @@ export class PluginFileManager {
if (!this.isExcalidrawFile(file)) {
return;
}
this.moveBAKFile(oldPath, file.path);
if (!this.settings.keepInSync) {
return;
}
[EXPORT_TYPES, "excalidraw"].flat().forEach(async (ext: string) => {
const oldIMGpath = getIMGFilename(oldPath, ext);
const imgFile = this.app.vault.getAbstractFileByPath(
normalizePath(oldIMGpath),
const imgMap = new Map<string, {oldImgPath: string, newImgPath: string}>();
[EXPORT_TYPES, "excalidraw"].flat().forEach(ext => {
let oldImgPath = getIMGFilename(oldPath, ext);
let newImgPath = getIMGFilename(file.path, ext);
if(this.plugin.ea?.onImageExportPathHook) {
try {
oldImgPath = this.plugin.ea.onImageExportPathHook({
exportFilepath: oldImgPath,
exportExtension: ext,
excalidrawFile: file,
oldExcalidrawPath: oldPath,
action: "move",
}) ?? oldImgPath;
newImgPath = this.plugin.ea.onImageExportPathHook({
exportFilepath: newImgPath,
exportExtension: ext,
excalidrawFile: file,
action: "export",
}) ?? newImgPath;
} catch (e) {
errorlog({where: "FileManager.renameEventHandler", fn: this.plugin.ea.onImageExportPathHook, error: e});
}
}
imgMap.set(ext, { oldImgPath, newImgPath });
});
imgMap.forEach((path, ext) => {
const imgFile = this.app.vault.getFileByPath(
normalizePath(path.oldImgPath),
);
if (imgFile && imgFile instanceof TFile) {
const newIMGpath = getIMGFilename(file.path, ext);
await this.app.fileManager.renameFile(imgFile, newIMGpath);
if (imgFile) {
this.app.fileManager.renameFile(imgFile, normalizePath(path.newImgPath));
}
});
}
@@ -453,6 +523,34 @@ export class PluginFileManager {
});
}
private async removeBAKFromCache(path: string) {
//this will not work in a short period when Obsidian is starting up, however
//because there is housekeeping in ImageCache at each startup to delete
//BAK files, this is not a major issue.
if(!imageCache.isReady() || !path) {
return;
}
await imageCache.removeBAKFromCache(path);
}
private async moveBAKFile(oldPath: string, newPath: string) {
if(!oldPath || !newPath) {
return;
}
//this will not work in the short period when Obsidian is starting up, however
//this will only effect a very few files, statistically unlikely to cause
//much/any real user impact.
//a proper queuing feels overkill for this.
if(!imageCache.isReady()) {
return;
}
const backup = await imageCache.getBAKFromCache(oldPath);
if(backup) {
await imageCache.addBAKToCache(newPath, `${backup}`);
await this.removeBAKFromCache(oldPath);
}
}
/**
* watch file delete and delete corresponding .svg and .png
* @param file
@@ -473,7 +571,7 @@ export class PluginFileManager {
//close excalidraw view where this file is open
const excalidrawViews = getExcalidrawViews(this.app);
for (const excalidrawView of excalidrawViews) {
if (excalidrawView.file.path === file.path) {
if (file?.path && excalidrawView?.file?.path === file.path) {
await excalidrawView.leaf.setViewState({
type: VIEW_TYPE_EXCALIDRAW,
state: { file: null },
@@ -481,16 +579,35 @@ export class PluginFileManager {
}
}
this.removeBAKFromCache(file.path);
//delete PNG and SVG files as well
if (this.settings.keepInSync) {
const imgMap = new Map<string, string>();
[EXPORT_TYPES, "excalidraw"].flat().forEach(ext => {
let imgPath = getIMGFilename(file.path, ext);
if(this.plugin.ea?.onImageExportPathHook) {
try {
imgPath = this.plugin.ea.onImageExportPathHook({
exportFilepath: imgPath,
exportExtension: ext,
excalidrawFile: file,
action: "delete",
}) ?? imgPath;
} catch (e) {
errorlog({where: "FileManager.deleteEventHandler", fn: this.plugin.ea.onImageExportPathHook, error: e});
}
}
imgMap.set(ext, imgPath);
});
window.setTimeout(() => {
[EXPORT_TYPES, "excalidraw"].flat().forEach(async (ext: string) => {
const imgPath = getIMGFilename(file.path, ext);
const imgFile = this.app.vault.getAbstractFileByPath(
imgMap.forEach((imgPath: string, ext: string) => {
const imgFile = this.app.vault.getFileByPath(
normalizePath(imgPath),
);
if (imgFile && imgFile instanceof TFile) {
await this.app.vault.delete(imgFile);
if (imgFile) {
this.app.vault.delete(imgFile);
}
});
}, 500);

View File

@@ -4,77 +4,202 @@ import { Packages } from "../../types/types";
import { debug, DEBUGGING } from "../../utils/debugHelper";
import { Notice } from "obsidian";
import ExcalidrawPlugin from "src/core/main";
import { errorHandler } from "../../utils/ErrorHandler";
import React from "react";
import ReactDOM from "react-dom";
declare let REACT_PACKAGES:string;
declare let react:any;
declare let reactDOM:any;
declare let react: typeof React;
declare let reactDOM:typeof ReactDOM;
declare let excalidrawLib: typeof ExcalidrawLib;
declare const unpackExcalidraw: Function;
export class PackageManager {
private packageMap: Map<Window, Packages> = new Map<Window, Packages>();
private EXCALIDRAW_PACKAGE: string;
private plugin: ExcalidrawPlugin;
private fallbackPackage: Packages | null = null;
constructor(plugin: ExcalidrawPlugin) {
this.plugin = plugin;
try {
this.EXCALIDRAW_PACKAGE = unpackExcalidraw();
excalidrawLib = window.eval.call(window,`(function() {${this.EXCALIDRAW_PACKAGE};return ExcalidrawLib;})()`);
// Use safe evaluation for unpacking the Excalidraw package
excalidrawLib = errorHandler.safeEval(
`(function() {${this.EXCALIDRAW_PACKAGE};return ExcalidrawLib;})()`,
"PackageManager constructor - excalidrawLib initialization",
window
);
if (!excalidrawLib) {
throw new Error("Failed to initialize excalidrawLib");
}
// Update the exported functions
updateExcalidrawLib();
this.setPackage(window,{react, reactDOM, excalidrawLib});
// Create a package with the loaded libraries
const initialPackage = {react, reactDOM, excalidrawLib};
// Validate the package before storing
if (this.validatePackage(initialPackage)) {
this.setPackage(window, initialPackage);
this.fallbackPackage = initialPackage; // Store a valid package as fallback
} else {
throw new Error("Invalid initial package");
}
} catch (e) {
new Notice("Error loading the Excalidraw package", 6000);
errorHandler.handleError(e, "PackageManager constructor");
new Notice("Error loading the Excalidraw package. Some features may not work correctly.", 10000);
console.error("Error loading the Excalidraw package", e);
}
plugin.logStartupEvent("Excalidraw package unpacked");
}
/**
* Validates that a package contains all required components
*/
private validatePackage(pkg: Packages): boolean {
if (!pkg) return false;
// Check that all components exist
if (!pkg.react || !pkg.reactDOM || !pkg.excalidrawLib) {
return false;
}
// Verify that excalidrawLib has essential methods
const lib = pkg.excalidrawLib;
return (
typeof lib === 'object' &&
lib !== null &&
typeof lib.restore === 'function' &&
typeof lib.exportToSvg === 'function'
);
}
/**
* Store a package for a specific window
*/
public setPackage(window: Window, pkg: Packages) {
this.packageMap.set(window, pkg);
if (this.validatePackage(pkg)) {
this.packageMap.set(window, pkg);
// Update fallback if we don't have one
if (!this.fallbackPackage) {
this.fallbackPackage = pkg;
}
} else {
errorHandler.handleError(
"Attempted to set invalid package",
"PackageManager.setPackage"
);
}
}
public getPackageMap() {
return this.packageMap;
}
public getPackage(win:Window):Packages {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getPackage, `ExcalidrawPlugin.getPackage`, win);
/**
* Gets a package for a window, creating it if necessary
* with robust error handling
*/
public getPackage(win: Window): Packages {
try {
if ((process.env.NODE_ENV === 'development') && DEBUGGING) {
debug(this.getPackage, `PackageManager.getPackage`, win);
}
if(this.packageMap.has(win)) {
return this.packageMap.get(win);
// Return existing package if available
if (this.packageMap.has(win)) {
const pkg = this.packageMap.get(win);
if (this.validatePackage(pkg)) {
return pkg;
}
// If package exists but is invalid, delete it so we can recreate it
this.packageMap.delete(win);
}
// Create new package
return errorHandler.wrapWithTryCatch(() => {
// Use safe evaluation to load packages in the window context
const evalResult = errorHandler.safeEval<{react: typeof React, reactDOM: typeof ReactDOM, excalidrawLib: typeof ExcalidrawLib}>(
`(function() {
${REACT_PACKAGES + this.EXCALIDRAW_PACKAGE};
return {react: React, reactDOM: ReactDOM, excalidrawLib: ExcalidrawLib};
})()`,
"PackageManager.getPackage - package evaluation",
win
);
if (!evalResult || !this.validatePackage(evalResult)) {
throw new Error("Failed to create valid package");
}
const newPackage = {
react: evalResult.react,
reactDOM: evalResult.reactDOM,
excalidrawLib: evalResult.excalidrawLib
};
this.packageMap.set(win, newPackage);
return newPackage;
}, "PackageManager.getPackage", this.fallbackPackage);
} catch (error) {
errorHandler.handleError(error, "PackageManager.getPackage");
// Return fallback package if available to prevent data loss
if (this.fallbackPackage) {
return this.fallbackPackage;
}
// If no fallback, throw error to prevent undefined behavior
throw new Error("Failed to get package and no fallback available");
}
const {react:r, reactDOM:rd, excalidrawLib:e} = win.eval.call(win,
`(function() {
${REACT_PACKAGES + this.EXCALIDRAW_PACKAGE};
return {react:React,reactDOM:ReactDOM,excalidrawLib:ExcalidrawLib};
})()`);
this.packageMap.set(win,{react:r, reactDOM:rd, excalidrawLib:e});
return {react:r, reactDOM:rd, excalidrawLib:e};
}
public deletePackage(win: Window) {
const { react, reactDOM, excalidrawLib } = this.getPackage(win);
try {
const pkg = this.packageMap.get(win);
if (!pkg) return;
if (win.ExcalidrawLib === excalidrawLib) {
excalidrawLib.destroyObsidianUtils();
delete win.ExcalidrawLib;
const { react, reactDOM, excalidrawLib } = pkg;
if (win.ExcalidrawLib === excalidrawLib) {
// Safely clean up resources
errorHandler.wrapWithTryCatch(() => {
if (excalidrawLib && typeof excalidrawLib.destroyObsidianUtils === 'function') {
excalidrawLib.destroyObsidianUtils();
}
delete win.ExcalidrawLib;
}, "PackageManager.deletePackage - cleanup ExcalidrawLib");
}
if (win.React === react) {
errorHandler.wrapWithTryCatch(() => {
Object.keys(win.React || {}).forEach((key) => {
delete win.React[key];
});
delete win.React;
}, "PackageManager.deletePackage - cleanup React");
}
if (win.ReactDOM === reactDOM) {
errorHandler.wrapWithTryCatch(() => {
Object.keys(win.ReactDOM || {}).forEach((key) => {
delete win.ReactDOM[key];
});
delete win.ReactDOM;
}, "PackageManager.deletePackage - cleanup ReactDOM");
}
this.packageMap.delete(win);
} catch (error) {
errorHandler.handleError(error, "PackageManager.deletePackage");
}
if (win.React === react) {
Object.keys(win.React).forEach((key) => {
delete win.React[key];
});
delete win.React;
}
if (win.ReactDOM === reactDOM) {
Object.keys(win.ReactDOM).forEach((key) => {
delete win.ReactDOM[key];
});
delete win.ReactDOM;
}
this.packageMap.delete(win);
}
public setExcalidrawPackage(pkg: string) {
@@ -82,16 +207,22 @@ export class PackageManager {
}
public destroy() {
REACT_PACKAGES = "";
Object.values(this.packageMap).forEach((p: Packages) => {
delete p.excalidrawLib;
delete p.reactDOM;
delete p.react;
});
this.packageMap.clear();
this.EXCALIDRAW_PACKAGE = "";
react = null;
reactDOM = null;
excalidrawLib = null;
try {
REACT_PACKAGES = "";
Array.from(this.packageMap.entries()).forEach(([win, p]) => {
this.deletePackage(win);
});
this.packageMap.clear();
this.EXCALIDRAW_PACKAGE = "";
this.fallbackPackage = null;
react = null;
reactDOM = null;
excalidrawLib = null;
} catch (error) {
errorHandler.handleError(error, "PackageManager.destroy");
}
}
}

View File

@@ -18,6 +18,7 @@ import { DynamicStyle, GridSettings } from "src/types/types";
import { PreviewImageType } from "src/types/utilTypes";
import { setDynamicStyle } from "src/utils/dynamicStyling";
import {
createOrOverwriteFile,
getDrawingFilename,
getEmbedFilename,
} from "src/utils/fileUtils";
@@ -28,7 +29,7 @@ import {
setLeftHandedMode,
} from "src/utils/utils";
import { imageCache } from "src/shared/ImageCache";
import { ConfirmationPrompt } from "src/shared/Dialogs/Prompt";
import { MultiOptionConfirmationPrompt } from "src/shared/Dialogs/Prompt";
import { EmbeddableMDCustomProps } from "src/shared/Dialogs/EmbeddableSettings";
import { EmbeddalbeMDFileCustomDataSettingsComponent } from "src/shared/Dialogs/EmbeddableMDFileCustomDataSettingsComponent";
import { startupScript } from "src/constants/starutpscript";
@@ -43,7 +44,6 @@ import { HotkeyEditor } from "src/shared/Dialogs/HotkeyEditor";
import { getExcalidrawViews } from "src/utils/obsidianUtils";
import { createSliderWithText } from "src/utils/sliderUtils";
import { PDFExportSettingsComponent, PDFExportSettings } from "src/shared/Dialogs/PDFExportSettingsComponent";
import de from "src/lang/locale/de";
export interface ExcalidrawSettings {
disableDoubleClickTextEditing: boolean;
@@ -69,7 +69,9 @@ export interface ExcalidrawSettings {
drawingFilnameEmbedPostfix: string;
drawingFilenameDateTime: string;
useExcalidrawExtension: boolean;
cropSuffix: string;
cropPrefix: string;
annotateSuffix: string;
annotatePrefix: string;
annotatePreserveSize: boolean;
displaySVGInPreview: boolean; //No longer used since 1.9.13
@@ -252,7 +254,9 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
drawingFilnameEmbedPostfix: " ",
drawingFilenameDateTime: "YYYY-MM-DD HH.mm.ss",
useExcalidrawExtension: true,
cropSuffix: "",
cropPrefix: CROPPED_PREFIX,
annotateSuffix: "",
annotatePrefix: ANNOTATED_PREFIX,
annotatePreserveSize: false,
displaySVGInPreview: undefined,
@@ -320,7 +324,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
experimentalFileTag: "✏️",
experimentalLivePreview: true,
fadeOutExcalidrawMarkup: false,
loadPropertySuggestions: true,
loadPropertySuggestions: false,
experimentalEnableFourthFont: false,
experimantalFourthFont: "Virgil",
addDummyTextElement: false,
@@ -956,7 +960,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setDesc(fragWithHTML(t("CROP_PREFIX_DESC")))
.addText((text) =>
text
.setPlaceholder("e.g.: Cropped_ ")
.setPlaceholder("e.g.: cropped_")
.setValue(this.plugin.settings.cropPrefix)
.onChange(async (value) => {
this.plugin.settings.cropPrefix = value.replaceAll(
@@ -968,12 +972,29 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}),
);
new Setting(detailsEl)
.setName(t("CROP_SUFFIX_NAME"))
.setDesc(fragWithHTML(t("CROP_SUFFIX_DESC")))
.addText((text) =>
text
.setPlaceholder("e.g.: _cropped")
.setValue(this.plugin.settings.cropSuffix)
.onChange(async (value) => {
this.plugin.settings.cropSuffix = value.replaceAll(
/[<>:"/\\|?*]/g,
"_",
);
text.setValue(this.plugin.settings.cropSuffix);
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("ANNOTATE_PREFIX_NAME"))
.setDesc(fragWithHTML(t("ANNOTATE_PREFIX_DESC")))
.addText((text) =>
text
.setPlaceholder("e.g.: Annotated_ ")
.setPlaceholder("e.g.: annotated_")
.setValue(this.plugin.settings.annotatePrefix)
.onChange(async (value) => {
this.plugin.settings.annotatePrefix = value.replaceAll(
@@ -984,7 +1005,24 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("ANNOTATE_SUFFIX_NAME"))
.setDesc(fragWithHTML(t("ANNOTATE_SUFFIX_DESC")))
.addText((text) =>
text
.setPlaceholder("e.g.: _annotated")
.setValue(this.plugin.settings.annotateSuffix)
.onChange(async (value) => {
this.plugin.settings.annotateSuffix = value.replaceAll(
/[<>:"/\\|?*]/g,
"_",
);
text.setValue(this.plugin.settings.annotateSuffix);
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("ANNOTATE_PRESERVE_SIZE_NAME"))
.setDesc(fragWithHTML(t("ANNOTATE_PRESERVE_SIZE_DESC")))
@@ -2048,7 +2086,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
button
.setButtonText(t("BACKUP_CACHE_CLEAR"))
.onClick(() => {
const confirmationPrompt = new ConfirmationPrompt(this.plugin,t("BACKUP_CACHE_CLEAR_CONFIRMATION"));
const confirmationPrompt = new MultiOptionConfirmationPrompt(this.plugin,t("BACKUP_CACHE_CLEAR_CONFIRMATION"));
confirmationPrompt.waitForClose.then((confirmed) => {
if (confirmed) {
imageCache.clearBackupCache();
@@ -2876,7 +2914,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
: this.plugin.settings.startupScriptPath + ".md");
let f = this.app.vault.getAbstractFileByPath(startupPath);
if(!f) {
f = await this.app.vault.create(startupPath, startupScript());
f = await createOrOverwriteFile(this.app, startupPath, startupScript());
}
startupScriptButton.setButtonText(t("STARTUP_SCRIPT_BUTTON_OPEN"));
this.app.workspace.openLinkText(f.path,"",true);

View File

@@ -158,7 +158,10 @@ export default {
CONVERT_FILE: "Convert to new format",
BACKUP_AVAILABLE: "We encountered an error while loading your drawing. This might have occurred if Obsidian unexpectedly closed during a save operation. For example, if you accidentally closed Obsidian on your mobile device while saving.<br><br><b>GOOD NEWS:</b> Fortunately, a local backup is available. However, please note that if you last modified this drawing on a different device (e.g., tablet) and you are now on your desktop, that other device likely has a more recent backup.<br><br>I recommend trying to open the drawing on your other device first and restore the backup from its local storage.<br><br>Would you like to load the backup?",
BACKUP_RESTORED: "Backup restored",
CACHE_NOT_READY: "I apologize for the inconvenience, but an error occurred while loading your file.<br><br><mark>Having a little patience can save you a lot of time...</mark><br><br>The plugin has a backup cache, but it appears that you have just started Obsidian. Initializing the Backup Cache may take some time, usually up to a minute or more depending on your device's performance. You will receive a notification in the top right corner when the cache initialization is complete.<br><br>Please press OK to attempt loading the file again and check if the cache has finished initializing. If you see a completely empty file behind this message, I recommend waiting until the backup cache is ready before proceeding. Alternatively, you can choose Cancel to manually correct your file.<br>",
BACKUP_SAVE_AS_FILE: "This drawing is empty. A non-empty backup is avalable. Would you like to resture it as a new file and open it in a new tab?",
BACKUP_SAVE: "Restore",
BACKUP_DELETE: "Delete Backup",
BACKUP_CANCEL: "Cancel", CACHE_NOT_READY: "I apologize for the inconvenience, but an error occurred while loading your file.<br><br><mark>Having a little patience can save you a lot of time...</mark><br><br>The plugin has a backup cache, but it appears that you have just started Obsidian. Initializing the Backup Cache may take some time, usually up to a minute or more depending on your device's performance. You will receive a notification in the top right corner when the cache initialization is complete.<br><br>Please press OK to attempt loading the file again and check if the cache has finished initializing. If you see a completely empty file behind this message, I recommend waiting until the backup cache is ready before proceeding. Alternatively, you can choose Cancel to manually correct your file.<br>",
OBSIDIAN_TOOLS_PANEL: "Obsidian Tools Panel",
ERROR_SAVING_IMAGE: "Unknown error occurred while fetching the image. It could be that for some reason the image is not available or rejected the fetch request from Obsidian",
WARNING_PASTING_ELEMENT_AS_TEXT: "PASTING EXCALIDRAW ELEMENTS AS A TEXT ELEMENT IS NOT ALLOWED",
@@ -203,14 +206,22 @@ export default {
FOLDER_NAME: "Excalidraw folder (CAsE sEnsITive!)",
FOLDER_DESC:
"Default location for new drawings. If empty, drawings will be created in the Vault root.",
CROP_SUFFIX_NAME: "Crop file suffix",
CROP_SUFFIX_DESC:
"The last part of the filename for new drawings created when cropping an image. " +
"Leave empty if you don't need a sufix.",
CROP_PREFIX_NAME: "Crop file prefix",
CROP_PREFIX_DESC:
"The first part of the filename for new drawings created when cropping an image. " +
"If empty the default 'cropped_' will be used.",
"Leave empty if you don't need a prefix.",
ANNOTATE_SUFFIX_NAME: "Annotation file suffix",
ANNOTATE_SUFFIX_DESC:
"The last part of the filename for new drawings created when annotating an image. " +
"Leave empty if you don't need a suffix.",
ANNOTATE_PREFIX_NAME: "Annotation file prefix",
ANNOTATE_PREFIX_DESC:
"The first part of the filename for new drawings created when annotating an image. " +
"If empty the default 'annotated_' will be used.",
"Leave empty if you don't need a prefix.",
ANNOTATE_PRESERVE_SIZE_NAME: "Preserve image size when annotating",
ANNOTATE_PRESERVE_SIZE_DESC:
"When annotating an image in markdown the replacment image link will include the width of the original image.",
@@ -749,7 +760,7 @@ FILENAME_HEAD: "Filename",
"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 " +
"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:
@@ -991,6 +1002,7 @@ FILENAME_HEAD: "Filename",
//Utils.ts
UPDATE_AVAILABLE: `A newer version of Excalidraw is available in Community Plugins.\n\nYou are using ${PLUGIN_VERSION}.\nThe latest is`,
SCRIPT_UPDATES_AVAILABLE: `Script updates available - check the script store.\n\n${DEVICE.isDesktop ? `This message is available in console.log (${DEVICE.isMacOS ? "CMD+OPT+i" : "CTRL+SHIFT+i"})\n\n` : ""}If you have organized scripts into subfolders under the script store folder and have multiple copies of the same script, you may need to clean up unused versions to clear this alert. For private copies of scripts that should not be updated, store them outside the script store folder.`,
ERROR_PNG_TOO_LARGE: "Error exporting PNG - PNG too large, try a smaller resolution",
//modifierkeyHelper.ts

View File

@@ -44,7 +44,7 @@ export default {
TRANSCLUDE_MOST_RECENT: "嵌入最近编辑过的绘图(形如 ![[drawing]])到当前 Markdown 文档中",
TOGGLE_LEFTHANDED_MODE: "切换为左手模式",
TOGGLE_SPLASHSCREEN: "在新绘图中显示启动画面",
FLIP_IMAGE: "打开当前所选 excalidraw 图像的“背景笔记”",
FLIP_IMAGE: "在弹出窗口中打开当前所选图像的“背景笔记”",
NEW_IN_NEW_PANE: "新建绘图 - 于新面板",
NEW_IN_NEW_TAB: "新建绘图 - 于新页签",
NEW_IN_ACTIVE_PANE: "新建绘图 - 于当前面板",
@@ -158,6 +158,8 @@ export default {
CONVERT_FILE: "转换为新格式",
BACKUP_AVAILABLE: "加载绘图文件时出错,可能是由于 Obsidian 在上次保存时意外退出了(手机上更容易发生这种意外)。<br><br><b>好消息:</b>这台设备上存在备份。您是否想要恢复本设备上的备份?<br><br>(我建议您先尝试在最近使用过的其他设备上打开该绘图,以检查是否有更新的备份。)",
BACKUP_RESTORED: "已恢复备份",
BACKUP_SAVE_AS_FILE : "此绘图为空,但有一个较大的备份可用。您是否想将其另存为新文件,并在新标签页中打开?" ,
DO_YOU_WANT_TO_DELETE_THE_BACKUP : "该备份[未]作为恢复文件保存到您的存储库中。您是否想删除备份数据?" ,
CACHE_NOT_READY: "抱歉,加载绘图文件时出错。<br><br><mark>现在有耐心,将来更省心。</mark><br><br>该插件有备份机制,但您似乎刚刚打开 Obsidian需要等待一分钟或更长的时间来读取缓存。缓存读取完毕时您将会在右上角收到提示。<br><br>请点击 OK 并耐心等待缓存,或者选择点击取消后手动修复你的文件。<br>",
OBSIDIAN_TOOLS_PANEL: "Obsidian 工具面板",
ERROR_SAVING_IMAGE: "获取图像时发生未知错误。可能是由于某种原因,图像不可用或拒绝了 Obsidian 的获取请求。",
@@ -203,14 +205,22 @@ export default {
FOLDER_NAME: "Excalidraw 文件夹(區分大小寫!)",
FOLDER_DESC:
"新绘图的默认存储路径。若为空,将在库的根目录中创建新绘图。",
CROP_SUFFIX_NAME : "裁剪文件后缀" ,
CROP_SUFFIX_DESC :
"为裁剪图像时创建的新图纸文件名的最后部分。" +
"如果不需要后缀,请留空。" ,
CROP_PREFIX_NAME: "剪贴文件的前缀",
CROP_PREFIX_DESC:
"当剪贴图片进来时保存的文件名的前缀。 " +
"留空则使用 'cropped_'",
"如果不需要前缀,请留空。" ,
ANNOTATE_SUFFIX_NAME : "注释文件后缀" ,
ANNOTATE_SUFFIX_DESC :
"为注释图像时创建的新绘图文件名的最后部分。" +
"如果不需要后缀,请留空。" ,
ANNOTATE_PREFIX_NAME: "标注文件的前缀",
ANNOTATE_PREFIX_DESC:
"在标注图像时创建新绘图的文件名的第一部分。" +
"留空则使用'annotated_'",
"如果不需要前缀,请留空。" ,
ANNOTATE_PRESERVE_SIZE_NAME: "在标注时保留图像尺寸",
ANNOTATE_PRESERVE_SIZE_DESC:
"当在 Markdown 中标注图像时,替换后的图像链接将包含原始图像的宽度。",
@@ -463,7 +473,7 @@ FILENAME_HEAD: "文件名",
FOCUS_ON_EXISTING_TAB_NAME: "聚焦于当前标签页",
FOCUS_ON_EXISTING_TAB_DESC: "当打开一个链接时如果该文件已经打开Excalidraw 将会聚焦到现有的标签页上 " +
"启用这个设置会在文件已打开的情况下覆盖“重用相邻窗格”的设置。",
"启用此设置时,如果文件已打开,将覆盖“重用相邻窗格”,但“打开所选 Excalidraw 图像的背影笔记”命令面板操作除外。",
SECOND_ORDER_LINKS_NAME: "显示二级链接",
SECOND_ORDER_LINKS_DESC: "在 Excalidraw 中点击链接时显示链接。二级链接是指指向被点击链接的反向链接" +
"当使用图标连接相似的笔记时,二级链接可以让你直接到达相关笔记,而不需要两次点击。" +
@@ -991,6 +1001,7 @@ FILENAME_HEAD: "文件名",
//Utils.ts
UPDATE_AVAILABLE: `Excalidraw 的新版本已在社区插件中可用。\n\n您正在使用 ${PLUGIN_VERSION}\n最新版本是`,
SCRIPT_UPDATES_AVAILABLE : `脚本更新可用 - 请检查脚本存储。\n\n ${ DEVICE . isDesktop ? `此消息可在控制台日志中查看 ( ${ DEVICE . isMacOS ? "CMD+OPT+i" : "CTRL+SHIFT+i" } )\n\n` : "" } 如果您已将脚本组织到脚本存储文件夹下的子文件夹中,并且存在同一脚本的多个副本,可能需要清理未使用的版本以消除此警报。对于不需要更新的私人脚本副本,请将它们存储在脚本存储文件夹之外。` ,
ERROR_PNG_TOO_LARGE: "导出 PNG 时出错 - PNG 文件过大,请尝试较小的分辨率",
// ModifierkeyHelper.ts

View File

@@ -10,7 +10,7 @@ import { PageOrientation, PageSize, PDFPageAlignment, PDFPageMarginString, expor
import { t } from "src/lang/helpers";
import { PDFExportSettings, PDFExportSettingsComponent } from "./PDFExportSettingsComponent";
import { captureScreenshot } from "src/utils/screenshot";
import { createOrOverwriteFile, getIMGFilename } from "src/utils/fileUtils";
import { exportImageToFile, getIMGFilename } from "src/utils/fileUtils";
@@ -397,7 +397,7 @@ export class ExportDialog extends Modal {
});
bPNGVault.onclick = () => {
if(isScreenshot) {
//allow dialot to close before taking screenshot
//allow dialog to close before taking screenshot
setTimeout(async () => {
const png = await captureScreenshot(this.view, {
zoom: this.scale,
@@ -406,11 +406,11 @@ export class ExportDialog extends Modal {
theme: this.theme
});
if(png) {
createOrOverwriteFile(this.app, getIMGFilename(this.view.file.path,"png"), png);
exportImageToFile(this.view, getIMGFilename(this.view.file.path,"png"), png, ".png");
}
});
} else {
this.view.savePNG(this.view.getScene(this.isSelectedOnly));
this.view.savePNG({scene: this.view.getScene(this.isSelectedOnly)});
}
this.close();
};
@@ -466,7 +466,7 @@ export class ExportDialog extends Modal {
cls: "excalidraw-export-button"
});
bSVGVault.onclick = () => {
this.view.saveSVG(this.view.getScene(this.isSelectedOnly));
this.view.saveSVG({scene: this.view.getScene(this.isSelectedOnly)});
this.close();
};

View File

@@ -17,17 +17,100 @@ I build this plugin in my free time, as a labor of love. Curious about the philo
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://storage.ko-fi.com/cdn/kofi6.png?v=6" border="0" alt="Buy Me a Coffee at ko-fi.com" height=45></a></div>
`,
"2.12.0": `
## Fixed
- Dynamic styling was not working when there were frames in the scene.
- Minor fix for the screenshot feature. This fix also solves the long-standing issue of the window control buttons (close, minimize, maximize) being visible in full-screen mode.
- When ALT/OPT + dragging an Embeddable object, the object dragging was not working properly at times, resulting in an empty embeddable object (instead of dragging the element).
"2.13.0":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/QzhyQb4JF3Q" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## New
- Line polygons. Draw a closed line shape, and it will snap to a polygon. [#9477](https://github.com/excalidraw/excalidraw/pull/9477)
- I updated the Split Ellipse and Boolean Operations Scripts to support this new feature.
- If you enter line editor mode by CTRL/CMD + clicking on the line, the lock-point will be marked for easier editing. Look for the polygon action on the elements panel to break the polygon if required.
- Override "Focus on Existing Tab" setting for the "Open the back-of-the-note for the selected image in a popout window" action. "Open the back-of-the-note" will open a new popout every time.
- **Flexible Auto-Export Location:** Take control of where your auto-exported .png, .svg, and .excalidraw files are saved. Addressing a long-standing request, you can now define custom output paths using the new **Excalidraw Hooks**.
- Implement the \`onImageExportPathHook\` callback in your ExcalidrawAutomate startup script to control the *destination path*.
- Get the skeleton script via plugin settings or download it [here](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/refs/heads/master/src/constants/assets/startupScript.md).
- **Control Auto-Export Trigger:** Use the \`onTriggerAutoexportHook\` in your startup script to decide *if* and *how* auto-export runs for a file, based on its properties or frontmatter, *before* the export path is determined.
- **Improved "Open Excalidraw drawing":** The Command Palette command now searches the *entire Vault* for the matching Excalidraw file when used on an embedded .svg or .png, useful when exports are in different folders.
- **Placeholder Files for New Embeds:** When embedding a new drawing as PNG/SVG via the Command Palette, empty placeholder files are now created immediately based on your auto-export setting. This ensures Obsidian correctly updates links if you rename the file soon after creation (when "Keep filenames in sync" is on).
- **Paste Obsidian URLs into Excalidraw:** Pasting an Obsidian URL for an image or file into Excalidraw now inserts the associated image directly into the drawing.
- **\`onImageFilePathHook\` Drag & Drop Support:** The \`onImageFilePathHook\` (for controlling location/filename of *embedded* files) is now triggered when dragging and dropping files into Excalidraw from outside Obsidian, matching the existing behavior for pasting.
## New in ExcalidrawAutomate
\`\`\`ts
splitFolderAndFilename(filepath: string) : {
folderpath: string;
filename: string;
basename: string;
extension: string;
}
\`\`\`
`,
"2.12.4":`
## Fixed
- ExaliBrain did not render after the 2.12.3 update. [#2384](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2384)
`,
"2.12.3":`
## Minor fixes
- Includes all recent updates and fixes from excalidraw.com
- Fixed issue with line editor snapping out of edit mode
- Fixed long-standing issue with wireframe to code calling a deprecated OpenAI endpoint
- "Load Excalidraw Properties into Obsidian Suggester" setting now defaults to false for new installations. [#2380](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2380)
- Taskbone OCR result does not get saved to frontmatter in some cases [#1123](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1123)
## New
- If the cropped file or annotated file prefix is set to empty, there will now be no prefix added to the file name. Additionally, now you can also set a suffix to the file name. [#2370](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2370)
`,
"2.12.2": `
## Fixed
- BUG: Excalidraw theme changes to Light from Dark when clicking line element node [#2360](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2360)
`,
"2.12.1": `
## New
- "Text to Path" text input window is now draggable.
## Fixed
- Minor fixes to the Polygon line feature introduced in 2.12.0. [#9580](https://github.com/excalidraw/excalidraw/pull/9580)
- Fix new Improved Unlock UI, where if a lock element was over an unlocked element, the unlocked element was not selectable. [#9582](https://github.com/excalidraw/excalidraw/pull/9582)
- Fixed ghost point issue when moving a shape after dragging a point in the line editor [#9530](https://github.com/excalidraw/excalidraw/pull/9530)
## New in ExcalidrawAutomate
${String.fromCharCode(96,96,96)}js
untils.inputPrompt({
header: string,
placeholder?: string,
value?: string,
buttons?: { caption: string; tooltip?:string; action: Function }[],
lines?: number,
displayEditorButtons?: boolean,
customComponents?: (container: HTMLElement) => void,
blockPointerInputOutsideModal?: boolean,
controlsOnTop?: boolean,
draggable?: boolean,
});
${String.fromCharCode(96,96,96)}
`,
"2.12.0": `
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/-fldh3cE2gs" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## Fixed
- Dynamic styling was not working when frames were present in the scene.
- Minor fix to the screenshot feature. This also resolves the long-standing issue where window control buttons (close, minimize, maximize) appeared in full-screen mode.
- Fixed an issue where ALT/OPT + dragging an embeddable object sometimes failed, resulting in an empty object instead of dragging the element.
## New
- **Line Polygons**: Draw a closed line shape, and it will automatically snap into a polygon. [#9477](https://github.com/excalidraw/excalidraw/pull/9477)
- Updated the Split Ellipse and Boolean Operations scripts to support this feature.
- When entering line editor mode (CTRL/CMD + click), the lock point is now marked for easier editing. You can break the polygon using the polygon action in the elements panel.
- **Popout Override**: The "Open the back-of-the-note for the selected image in a popout window" action now overrides the "Focus on Existing Tab" setting and always opens a new popout.
- **Text Arch Enhancements**: The Text Arch script now supports fitting text to a wider range of paths and shapes. Text can also be edited and refitted to different paths.
- **Improved Unlock UI**: Single-clicking a locked element now shows an unlock button. [#9546](https://github.com/excalidraw/excalidraw/pull/9546)
- **Script Update Alerts**: On startup, Excalidraw will notify you if any installed scripts have available updates.
`,
"2.11.1": `
## Fixed:
@@ -218,160 +301,5 @@ function onImageFilePathHook: (data: {
drawingFilePath: string;
}) => string = null;
${String.fromCharCode(96,96,96)}
`,
"2.7.5":`
## Fixed
- PDF export scenario described in [#2184](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2184)
- Elbow arrows do not work within frames [#2187](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2187)
- Embedding images into Excalidraw with areaRef links did not work as expected due to conflicting SVG viewbox and width and height values
- Can't exit full-screen mode in popout windows using the Command Palette toggle action [#2188](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2188)
- If the image mask extended beyond the image in "Mask and Crop" image mode, the mask got misaligned from the image.
- PDF image embedding fixes that impacted some PDF files (not all):
- When cropping the PDF page in the scene (by double-clicking the image to crop), the size and position of the PDF cutout drifted.
- Using PDF++ there was a small offset in the position of the cutout in PDF++ and the image in Excalidraw.
- Updated a number of scripts including Split Ellipse, Select Similar Elements, and Concatenate Lines
## New in ExcalidrawAutomate
${String.fromCharCode(96,96,96)}
/**
* Add, modify, or delete keys in element.customData and preserve existing keys.
* Creates customData={} if it does not exist.
* Takes the element id for an element in ea.elementsDict and the newData to add or modify.
* To delete keys set key value in newData to undefined. So {keyToBeDeleted:undefined} will be deleted.
* @param id
* @param newData
* @returns undefined if element does not exist in elementsDict, returns the modified element otherwise.
*/
public addAppendUpdateCustomData(id:string, newData: Partial<Record<string, unknown>>);
${String.fromCharCode(96,96,96)}
`,
"2.7.4":`
## Fixed
- Regression from 2.7.3 where image fileId got overwritten in some cases
- White flash when opening a dark drawing [#2178](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2178)
`,
"2.7.3":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/ISuORbVKyhQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## Fixed
- Toggling image size anchoring on and off by modifying the image link did not update the image in the view until the user forced saved it or closed and opened the drawing again. This was a side-effect of the less frequent view save introduced in 2.7.1
## New
- **Shade Master Script**: A new script that allows you to modify the color lightness, hue, saturation, and transparency of selected Excalidraw elements, SVG images, and nested Excalidraw drawings. When a single image is selected, you can map colors individually. The original image remains unchanged, and a mapping table is added under ${String.fromCharCode(96)}## Embedded Files${String.fromCharCode(96)} for SVG and nested drawings. This helps maintain links between drawings while allowing different color themes.
- New Command Palette Command: "Duplicate selected image with a different image ID". Creates a copy of the selected image with a new image ID. This allows you to add multiple color mappings to the same image. In the scene, the image will be treated as if a different image, but loaded from the same file in the Vault.
## QoL Improvements
- New setting under ${String.fromCharCode(96)}Embedding Excalidraw into your notes and Exporting${String.fromCharCode(96)} > ${String.fromCharCode(96)}Image Caching and rendering optimization${String.fromCharCode(96)}. You can now set the number of concurrent workers that render your embedded images. Increasing the number will increase the speed but temporarily reduce the responsiveness of your system in case of large drawings.
- Moved pen-related settings under ${String.fromCharCode(96)}Excalidraw appearance and behavior${String.fromCharCode(96)} to their sub-heading called ${String.fromCharCode(96)}Pen${String.fromCharCode(96)}.
- Minor error fixing and performance optimizations when loading and updating embedded images.
- Color maps in ${String.fromCharCode(96)}## Embedded Files${String.fromCharCode(96)} may now include color keys "stroke" and "fill". If set, these will change the fill and stroke attributes of the SVG root element of the relevant file.
## New in ExcalidrawAutomate
${String.fromCharCode(96,96,96)}ts
// Updates the color map of an SVG image element in the view. If a ColorMap is provided, it will be used directly.
// If an SVGColorInfo is provided, it will be converted to a ColorMap.
// The view will be marked as dirty and the image will be reset using the color map.
updateViewSVGImageColorMap(
elements: ExcalidrawImageElement | ExcalidrawImageElement[],
colors: ColorMap | SVGColorInfo | ColorMap[] | SVGColorInfo[]
): Promise<void>;
// Retrieves the color map for an image element.
// The color map contains information about the mapping of colors used in the image.
// If the element already has a color map, it will be returned.
getColorMapForImageElement(el: ExcalidrawElement): ColorMap;
// Retrieves the color map for an SVG image element.
// The color map contains information about the fill and stroke colors used in the SVG.
// If the element already has a color map, it will be merged with the colors extracted from the SVG.
getColorMapForImgElement(el: ExcalidrawElement): Promise<SVGColorInfo>;
// Extracts the fill (background) and stroke colors from an Excalidraw file and returns them as an SVGColorInfo.
getColosFromExcalidrawFile(file:TFile, img: ExcalidrawImageElement): Promise<SVGColorInfo>;
// Extracts the fill and stroke colors from an SVG string and returns them as an SVGColorInfo.
getColorsFromSVGString(svgString: string): SVGColorInfo;
// upgraded the addImage function.
// 1. It now accepts an object as the input parameter, making your scripts more readable
// 2. AddImageOptions now includes colorMap as an optional parameter, this will only have an effect in case of SVGs and nested Excalidraws
// 3. The API function is backwards compatible, but I recommend new implementations to use the object based input
addImage(opts: AddImageOptions}): Promise<string>;
interface AddImageOptions {
topX: number;
topY: number;
imageFile: TFile | string;
scale?: boolean;
anchor?: boolean;
colorMap?: ColorMap;
}
type SVGColorInfo = Map<string, {
mappedTo: string;
fill: boolean;
stroke: boolean;
}>;
interface ColorMap {
[color: string]: string;
};
${String.fromCharCode(96,96,96)}
`,
"2.7.2":`
## Fixed
- The plugin did not load on **iOS 16 and older**. [#2170](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2170)
- Added empty line between ${String.fromCharCode(96)}# Excalidraw Data${String.fromCharCode(96)} and ${String.fromCharCode(96)}## Text Elements${String.fromCharCode(96)}. This will now follow **correct markdown linting**. [#2168](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2168)
- Adding an **embeddable** to view did not **honor the element background and element stroke colors**, even if it was configured in plugin settings. [#2172](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2172)
- **Deconstruct selected elements script** did not copy URLs and URIs for images embedded from outside Obsidian. Please update your script from the script library.
- When **rearranging tabs in Obsidian**, e.g. having two tabs side by side, and moving one of them to another location, if the tab was an Excalidraw tab, it appeared as non-responsive after the move, until the tab was resized.
## Source Code Refactoring
- Updated filenames, file locations, and file name letter-casing across the project
- Extracted onDrop, onDragover, etc. handlers to DropManger in ExcalidrawView
`,
"2.7.1":`
## Fixed
- Deleting excalidraw file from file system while it is open in fullscreen mode in Obsidian causes Obsidian to be stuck in full-screen view [#2161](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2161)
- Chinese fonts are not rendered in LaTeX statements [#2162](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2162)
- Since Electron 32 (newer Obsidian Desktop installers) drag and drop links from Finder or OS File Explorer did not work. [Electron breaking change](https://www.electronjs.org/docs/latest/breaking-changes#removed-filepath). This is now fixed
- Addressed unnecessary image reloads when changing windows in Obsidian
`,
"2.7.0":`
## Fixed
- Various Markdown embeddable "fuzziness":
- Fixed issues with appearance settings and edit mode toggling when single-click editing is enabled.
- Ensured embeddable file editing no longer gets interrupted unexpectedly.
- **Hover Preview**: Disabled hover preview for back-of-the-note cards to reduce distractions.
- **Settings Save**: Fixed an issue where plugin settings unnecessarily saved on every startup.
## New Features
- **Image Cropping Snaps to Objects**: When snapping is enabled in the scene, image cropping now aligns to nearby objects.
- **Session Persistence for Pen Mode**: Excalidraw remembers the last pen mode when switching between drawings within the same session.
## Refactoring
- **Mermaid Diagrams**: Excalidraw now uses its own Mermaid package, breaking future dependencies on Obsidian's Mermaid updates. This ensures stability and includes all fixes and improvements made to Excalidraw Mermaid since February 2024. The plugin file size has increased slightly, but this change significantly improves maintainability while remaining invisible to users.
- **MathJax Optimization**: MathJax (LaTeX equation SVG image generation) now loads only on demand, with the package compressed to minimize the startup and file size impact caused by the inclusion of Mermaid.
- **On-Demand Language Loading**: Non-English language files are now compressed and load only when needed, counterbalancing the increase in file size due to Mermaid and improving load speeds.
- **Codebase Restructuring**: Improved type safety by removing many ${String.fromCharCode(96)}//@ts-ignore${String.fromCharCode(96)} commands and enhancing modularity. Introduced new management classes: **CommandManager**, **EventManager**, **PluginFileManager**, **ObserverManager**, and **PackageManager**. Further restructuring is planned for upcoming releases to improve maintainability and stability.
`,
"2.6.8":`
## New
- **QoL improvements**:
- Obsidian-link search button in Element Link Editor.
- Add Any File now searches file aliases as well.
- Cosmetic changes to file search modals (display path, show file type icon).
- Text Element cursor-color matches the text color.
- New script in script store: [Image Occlusion](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Image%20Occlusion.md) by [@TrillStones](https://github.com/TrillStones) 🙏
## Fixed
- Excalidraw icon on the **ribbon menu kept reappearing** every time you reopen Obsidian [#2115](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2115)
- In pen mode, when **single-finger panning** is enabled, Excalidraw should still **allow actions with the mouse**.
- When **editing a drawing in split mode** (drawing is on one side, markdown view is on the other), editing the markdown note sometimes causes the drawing to re-zoom and jump away from the selected area.
- Hover-Editor compatibility resolved [2041](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2041)
- ${String.fromCharCode(96)}ExcalidrawAutomate.create() ${String.fromCharCode(96)} will now correctly include the markdown text in templates above Excalidraw Data and below YAML front matter. This also fixes the same issue with the **Deconstruct Selected Element script**.
`,
};

View File

@@ -13,7 +13,7 @@ import ExcalidrawView from "../../view/ExcalidrawView";
import ExcalidrawPlugin from "../../core/main";
import { escapeRegExp, getLinkParts, sleep } from "../../utils/utils";
import { getLeaf, openLeaf } from "../../utils/obsidianUtils";
import { checkAndCreateFolder, splitFolderAndFilename } from "src/utils/fileUtils";
import { createOrOverwriteFile } from "src/utils/fileUtils";
import { KeyEvent, isWinCTRLorMacCMD } from "src/utils/modifierkeyHelper";
import { t } from "src/lang/helpers";
import { ExcalidrawElement, getEA } from "src/core";
@@ -98,6 +98,8 @@ export class GenericInputPrompt extends Modal {
private customComponents: (container: HTMLElement) => void;
private blockPointerInputOutsideModal: boolean = false;
private controlsOnTop: boolean = false;
private draggable: boolean = false;
private cleanupDragListeners: (() => void) | null = null;
public static Prompt(
view: ExcalidrawView,
@@ -112,6 +114,7 @@ export class GenericInputPrompt extends Modal {
customComponents?: (container: HTMLElement) => void,
blockPointerInputOutsideModal?: boolean,
controlsOnTop?: boolean,
draggable?: boolean,
): Promise<string> {
const newPromptModal = new GenericInputPrompt(
view,
@@ -126,6 +129,7 @@ export class GenericInputPrompt extends Modal {
customComponents,
blockPointerInputOutsideModal,
controlsOnTop,
draggable,
);
return newPromptModal.waitForClose;
}
@@ -143,6 +147,7 @@ export class GenericInputPrompt extends Modal {
customComponents?: (container: HTMLElement) => void,
blockPointerInputOutsideModal?: boolean,
controlsOnTop?: boolean,
draggable?: boolean,
) {
super(app);
this.view = view;
@@ -155,6 +160,7 @@ export class GenericInputPrompt extends Modal {
this.customComponents = customComponents;
this.blockPointerInputOutsideModal = blockPointerInputOutsideModal ?? false;
this.controlsOnTop = controlsOnTop ?? false;
this.draggable = draggable ?? false;
this.waitForClose = new Promise<string>((resolve, reject) => {
this.resolvePromise = resolve;
@@ -473,12 +479,137 @@ export class GenericInputPrompt extends Modal {
super.onOpen();
this.inputComponent.inputEl.focus();
this.inputComponent.inputEl.select();
if (this.draggable) {
this.makeModalDraggable();
}
}
private makeModalDraggable() {
let isDragging = false;
let startX: number, startY: number, initialX: number, initialY: number;
let activeElement: HTMLElement | null = null;
let cursorPosition: { start: number; end: number } | null = null;
const modalEl = this.modalEl;
const header = modalEl.querySelector('.modal-titlebar') || modalEl.querySelector('.modal-title') || modalEl;
(header as HTMLElement).style.cursor = 'move';
// Track focus changes to store the last focused interactive element
const onFocusIn = (e: FocusEvent) => {
const target = e.target as HTMLElement;
if (target && (target.tagName === 'SELECT' || target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.tagName === 'BUTTON')) {
activeElement = target;
// Store cursor position for input/textarea elements (but not for number inputs)
if (target.tagName === 'TEXTAREA' ||
(target.tagName === 'INPUT' && (target as HTMLInputElement).type !== 'number')) {
const inputEl = target as HTMLInputElement | HTMLTextAreaElement;
cursorPosition = {
start: inputEl.selectionStart || 0,
end: inputEl.selectionEnd || 0
};
} else {
cursorPosition = null;
}
}
};
const onPointerDown = (e: PointerEvent) => {
const target = e.target as HTMLElement;
// Don't allow dragging if clicking on interactive controls
if (target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.tagName === 'BUTTON' ||
target.tagName === 'SELECT' ||
target.closest('button') ||
target.closest('input') ||
target.closest('textarea') ||
target.closest('select')) {
return;
}
// Allow dragging from header or modal content areas
if (!header.contains(target) && !modalEl.querySelector('.modal-content')?.contains(target)) {
return;
}
isDragging = true;
startX = e.clientX;
startY = e.clientY;
const rect = modalEl.getBoundingClientRect();
initialX = rect.left;
initialY = rect.top;
modalEl.style.position = 'absolute';
modalEl.style.margin = '0';
modalEl.style.left = `${initialX}px`;
modalEl.style.top = `${initialY}px`;
};
const onPointerMove = (e: PointerEvent) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
modalEl.style.left = `${initialX + dx}px`;
modalEl.style.top = `${initialY + dy}px`;
};
const onPointerUp = () => {
if (!isDragging) return;
isDragging = false;
// Restore focus and cursor position
if (activeElement && activeElement.isConnected) {
// Use setTimeout to ensure the pointer event is fully processed
setTimeout(() => {
activeElement.focus();
// Restore cursor position for input/textarea elements (but not for number inputs)
if (cursorPosition &&
(activeElement.tagName === 'TEXTAREA' ||
(activeElement.tagName === 'INPUT' && (activeElement as HTMLInputElement).type !== 'number'))) {
const inputEl = activeElement as HTMLInputElement | HTMLTextAreaElement;
inputEl.setSelectionRange(cursorPosition.start, cursorPosition.end);
}
}, 0);
}
};
// Initialize activeElement with the main input field
activeElement = this.inputComponent.inputEl;
cursorPosition = {
start: this.inputComponent.inputEl.selectionStart || 0,
end: this.inputComponent.inputEl.selectionEnd || 0
};
// Set up event listeners
modalEl.addEventListener('focusin', onFocusIn);
modalEl.addEventListener('pointerdown', onPointerDown);
document.addEventListener('pointermove', onPointerMove);
document.addEventListener('pointerup', onPointerUp);
// Store cleanup function for use in onClose
this.cleanupDragListeners = () => {
modalEl.removeEventListener('focusin', onFocusIn);
modalEl.removeEventListener('pointerdown', onPointerDown);
document.removeEventListener('pointermove', onPointerMove);
document.removeEventListener('pointerup', onPointerUp);
};
}
onClose() {
super.onClose();
this.resolveInput();
this.removeInputListener();
// Clean up drag listeners to prevent memory leaks
if (this.cleanupDragListeners) {
this.cleanupDragListeners();
this.cleanupDragListeners = null;
}
}
}
@@ -661,10 +792,7 @@ export class NewFileActions extends Modal {
if (!this.path.match(/\.md$/)) {
this.path = `${this.path}.md`;
}
const folderpath = splitFolderAndFilename(this.path).folderpath;
checkAndCreateFolder(folderpath);
const f = await this.app.vault.create(this.path, data);
return f;
return await createOrOverwriteFile(this.app, this.path, data);
};
if(this.sourceElement) {
@@ -726,17 +854,30 @@ export class NewFileActions extends Modal {
}
}
export class ConfirmationPrompt extends Modal {
public waitForClose: Promise<boolean>;
private resolvePromise: (value: boolean) => void;
export class MultiOptionConfirmationPrompt extends Modal {
public waitForClose: Promise<any>;
private resolvePromise: (value: any) => void;
private rejectPromise: (reason?: any) => void;
private didConfirm: boolean = false;
private selectedValue: any = null;
private readonly message: string;
private readonly buttons: Map<string, any>;
private ctaButtonLabel: string = null;
constructor(private plugin: ExcalidrawPlugin, message: string) {
constructor(private plugin: ExcalidrawPlugin, message: string, buttons?: Map<string, any>, ctaButtonLabel?: string) {
super(plugin.app);
this.message = message;
this.waitForClose = new Promise<boolean>((resolve, reject) => {
if (!buttons || buttons.size === 0) {
buttons = new Map<string, any>([
[t("PROMPT_BUTTON_CANCEL"), null],
[t("PROMPT_BUTTON_OK"), true],
]);
if( !ctaButtonLabel) {
ctaButtonLabel = t("PROMPT_BUTTON_OK");
}
}
this.ctaButtonLabel = ctaButtonLabel;
this.buttons = buttons;
this.waitForClose = new Promise<any>((resolve, reject) => {
this.resolvePromise = resolve;
this.rejectPromise = reject;
});
@@ -756,14 +897,35 @@ export class ConfirmationPrompt extends Modal {
const buttonContainer = this.contentEl.createDiv();
buttonContainer.style.display = "flex";
buttonContainer.style.justifyContent = "flex-end";
buttonContainer.style.flexWrap = "wrap";
// Convert Map to Array for easier iteration
const buttonEntries = Array.from(this.buttons.entries());
const cancelButton = this.createButton(buttonContainer, t("PROMPT_BUTTON_CANCEL"), this.cancelClickCallback.bind(this));
cancelButton.buttonEl.style.marginRight = "0.5rem";
// Add buttons in reverse order (last button will be on the right)
let ctaButton: HTMLButtonElement = null;
buttonEntries.reverse().forEach(([buttonText, value], index) => {
const button = this.createButton(buttonContainer, buttonText, () => {
this.selectedValue = value;
this.close();
});
if (buttonText === this.ctaButtonLabel) {
ctaButton = button.buttonEl;
button.setCta();
}
if (index < buttonEntries.length - 1) {
button.buttonEl.style.marginRight = "0.5rem";
}
});
const confirmButton = this.createButton(buttonContainer, t("PROMPT_BUTTON_OK"), this.confirmClickCallback.bind(this));
confirmButton.buttonEl.style.marginRight = "0";
cancelButton.buttonEl.focus();
// Set focus on the first button (visually last)
if(this.ctaButtonLabel) {
if (ctaButton) {
ctaButton.focus();
}
}
}
private createButton(container: HTMLElement, text: string, callback: (evt: MouseEvent) => void) {
@@ -772,16 +934,6 @@ export class ConfirmationPrompt extends Modal {
return button;
}
private cancelClickCallback() {
this.didConfirm = false;
this.close();
};
private confirmClickCallback() {
this.didConfirm = true;
this.close();
};
onOpen() {
super.onOpen();
this.contentEl.querySelector("button")?.focus();
@@ -789,11 +941,7 @@ export class ConfirmationPrompt extends Modal {
onClose() {
super.onClose();
if (!this.didConfirm) {
this.resolvePromise(false);
} else {
this.resolvePromise(true);
}
this.resolvePromise(this.selectedValue);
}
}

View File

@@ -575,27 +575,6 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
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: "",
},
{
field: "onDropHook",
code: 'onDropHook(data: {ea: ExcalidrawAutomate, event: React.DragEvent<HTMLDivElement>, draggable: any, type: "file" | "text" | "unknown", payload: {files: TFile[], text: string,}, excalidrawFile: TFile, view: ExcalidrawView, pointerPosition: { x: number, y: number},}): boolean;',
desc: "If set Excalidraw will call this function onDrop events.\nA return of true will stop the default onDrop processing in Excalidraw.\n\ndraggable is the Obsidian draggable object\nfiles is the array of dropped files\nexcalidrawFile is the file receiving the drop event\nview is the excalidraw view receiving the drop.\npointerPosition is the pointer position on canvas at the time of drop.",
after: "",
},
{
field: "onImageFilePathHook",
code: `onImageFilePathHook: (data: {currentImageName: string; drawingFilePath: string;}): string;`,
desc: "If set, this callback is triggered when an image is being saved in Excalidraw.\n"
+ "You can use this callback to customize the naming and path of pasted images to avoid\n"
+ 'default names like "Pasted image 123147170.png" being saved in the attachments folder,\n'
+ "and instead use more meaningful names based on the Excalidraw file or other criteria,\n"
+ "plus save the image in a different folder.\n\n"
+ "If the function returns null or undefined, the normal Excalidraw operation will continue\n"
+ "with the excalidraw generated name and default path.\n"
+ "If a filepath is returned, that will be used. Include the full Vault filepath and filename\n"
+ "with the file extension.\n"
+ "The currentImageName is the name of the image generated by excalidraw or provided during paste.",
after: "",
},
{
field: "mostRecentMarkdownSVG",
code: "mostRecentMarkdownSVG: SVGSVGElement;",
@@ -917,6 +896,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: "Sets Excalidraw in the targetView to view-mode",
after: "",
},
{
field: "splitFolderAndFilename",
code: "splitFolderAndFilename(filepath: string): { folderpath: string; filename: string; basename: string; extension: string; }",
desc: "Splits a file path into its components.",
after: "",
},
{
field: "viewUpdateScene",
code: "viewUpdateScene(scene:{elements?:ExcalidrawElement[],appState?: AppState,files?: BinaryFileData,captureUpdate?: 'IMMEDIATELY' | 'NEVER' | 'EVENTUALLY'},restore:boolean=false):void",
@@ -956,7 +941,7 @@ export const EXCALIDRAW_SCRIPTENGINE_INFO: SuggesterInfo[] = [
"controlsOnTop when set to true will move all the buttons to the top of the modal, leaving the text area at the bottom. This feature was developed for Scribble Helper script to avoid your palm pressing buttons while scribbling.\n"+
"buttons.action(input: string) => string\nThe button action function will receive the actual input string. If action returns null, input will be unchanged. If action returns a string, input will receive that value when the promise is resolved. " +
"example:\n<code>let fileType = '';\nconst filename = await utils.inputPrompt (\n 'Filename',\n '',\n '',\n, [\n {\n caption: 'Markdown',\n action: ()=>{fileType='md';return;}\n },\n {\n caption: 'Excalidraw',\n action: ()=>{fileType='ex';return;}\n }\n ]\n);</code>",
after: `({header: string, placeholder?: string, value?: string, buttons?: {caption:string, tooltip?:string, action:Function}[], lines?: number, displayEditorButtons?: boolean, customComponents?: (container: HTMLElement) => void, blockPointerInputOutsideModal?: boolean, controlsOnTop?: boolean})`,
after: `({\n header: "",\n placeholder: undefined, //string\n value: undefined, //string\n buttons: [{ //optional, may leave undefined\n caption: "", //string\n tooltip: undefined, //string\n action: (input)=>{} //Function\n }],\n lines: undefined, //number\n displayEditorButtons: undefined, //boolean\n customComponents: undefined, //(container: HTMLElement) => void\n blockPointerInputOutsideModal: undefined, //boolean\n controlsOnTop: undefined, //boolean\n draggable: undefined, //boolean\n});`,
},
{
field: "suggester",

View File

@@ -34,7 +34,7 @@ import {
mermaidToExcalidraw,
refreshTextDimensions,
} from "src/constants/constants";
import { blobToBase64, checkAndCreateFolder, getDrawingFilename, getExcalidrawEmbeddedFilesFiletree, getListOfTemplateFiles, getNewUniqueFilepath } from "src/utils/fileUtils";
import { blobToBase64, checkAndCreateFolder, getDrawingFilename, getExcalidrawEmbeddedFilesFiletree, getListOfTemplateFiles, getNewUniqueFilepath, splitFolderAndFilename } from "src/utils/fileUtils";
import {
//debug,
getImageSize,
@@ -87,6 +87,7 @@ import { _measureText, cloneElement, createPNG, createSVG, errorMessage, filterC
import { exportToPDF, getMarginValue, getPageDimensions, PageDimensions, PageOrientation, PageSize, PDFExportScale, PDFPageProperties } from "src/utils/exportUtils";
import { FrameRenderingOptions } from "src/types/utilTypes";
import { CaptureUpdateAction } from "src/constants/constants";
import { AutoexportConfig } from "src/types/excalidrawViewTypes";
extendPlugins([
HarmonyPlugin,
@@ -317,6 +318,19 @@ export class ExcalidrawAutomate {
return await checkAndCreateFolder(folderpath);
}
/**
* @param filepath - The file path to split into folder and filename.
* @returns object containing folderpath, filename, basename, and extension.
*/
public splitFolderAndFilename(filepath: string) : {
folderpath: string;
filename: string;
basename: string;
extension: string;
} {
return splitFolderAndFilename(filepath);
}
/**
* Generates a unique filepath by appending a number if file already exists.
* @param {string} filename - Base filename.
@@ -1715,6 +1729,7 @@ export class ExcalidrawAutomate {
id = id ?? nanoid();
const startPoint = points[0] as GlobalPoint;
const endPoint = points[points.length - 1] as GlobalPoint;
const elementsMap = arrayToMap(this.getElements());
this.elementsDict[id] = {
points: normalizeLinePoints(points),
lastCommittedPoint: null,
@@ -1723,6 +1738,7 @@ export class ExcalidrawAutomate {
focus: formatting?.startObjectId
? determineFocusDistance(
this.getElement(formatting?.startObjectId) as ExcalidrawBindableElement,
elementsMap,
endPoint,
startPoint,
)
@@ -1734,6 +1750,7 @@ export class ExcalidrawAutomate {
focus: formatting?.endObjectId
? determineFocusDistance(
this.getElement(formatting?.endObjectId) as ExcalidrawBindableElement,
elementsMap,
startPoint,
endPoint,
)
@@ -2164,7 +2181,7 @@ export class ExcalidrawAutomate {
};
targetView: ExcalidrawView = null; //the view currently edited
/**
* Sets the target view for EA. All the view operations and the access to Excalidraw API will be performend on this view.
* Sets the target view for EA. All the view operations and the access to Excalidraw API will be performed on this view.
* If view is null or undefined, the function will first try setView("active"), then setView("first").
* @param {ExcalidrawView | "first" | "active"} [view] - The view to set as target.
* @returns {ExcalidrawView} The target view.
@@ -2845,7 +2862,86 @@ export class ExcalidrawAutomate {
onImageFilePathHook: (data: {
currentImageName: string; // Excalidraw generated name of the image, or the name received from the file system.
drawingFilePath: string; // The full filepath of the Excalidraw file where the image is being used.
}) => string = null;
}) => string | null = null;
/**
* If set, this callback is triggered when the Excalidraw image is being exported to
* .svg, .png, or .excalidraw.
* You can use this callback to customize the naming and path of the images. This allows
* you to place images into an assets folder.
*
* If the function returns null or undefined, the normal Excalidraw operation will continue
* with the currentImageName and in the same folder as the Excalidraw file
* If a filepath is returned, that will be used. Include the full Vault filepath and filename
* with the file extension.
* If the new folder path does not exist, excalidraw will create it - you don't need to worry about that.
* ⚠If an image already exists on the path, that will be overwritten. When returning
* your own image path, you must take care of unique filenames (if that is a requirement) ⚠️⚠️
* The current image name is the name generated by Excalidraw:
* - my-drawing.png
* - my-drawing.svg
* - my-drawing.excalidraw
* - my-drawing.dark.svg
* - my-drawing.light.svg
* - my-drawing.dark.png
* - my-drawing.light.png
*
* @param data - An object containing the following properties:
* @property {string} exportFilepath - Default export filepath for the image.
* @property {string} exportExtension - The file extension of the export (e.g., .dark.svg, .png, .excalidraw).
* @property {string} excalidrawFile - TFile: The Excalidraw file being exported.
* @property {string} oldExcalidrawPath - If action === "move" The old path of the Excalidraw file, else undefined
* @property {string} action - The action being performed: "export", "move", or "delete". move and delete reference the change to the Excalidraw file.
*
* @returns {string} - The new filepath for the image including full vault path and extension.
*
* Example usage:
* ```
* onImageFilePathHook: (data) => {
* const { currentImageName, drawingFilePath, frontmatter } = data;
* // Generate a new filepath based on the drawing file name and other criteria
* const ext = currentImageName.split('.').pop();
* if(frontmatter && frontmatter["my-custom-field"]) {
* }
* return `${drawingFileName} - ${currentImageName || 'image'}.${ext}`;
* }
* ```
*/
onImageExportPathHook: (data: {
exportFilepath: string; // Default export filepath for the image.
exportExtension: string; // The file extension of the export (e.g., .dark.svg, .png, .excalidraw).
excalidrawFile: TFile; // The Excalidraw file being exported.
oldExcalidrawPath? : string; // The old path of the Excalidraw file, if it was moved/renamed.
action: "export" | "move" | "delete";
}) => string | null = null;
/**
* Excalidraw supports auto-export of Excalidraw files to .png, .svg, and .excalidraw formats.
*
* Auto-export of Excalidraw files can be controlled at multiple levels.
* 1) In plugin settings where you can set up default auto-export applicable to all your Excalidraw files.
* 2) However, if you do not want to auto-export every file, you can also control auto-export
* at the file level using the 'excalidraw-autoexport' frontmatter property.
* 3) This hook gives you an additional layer of control over the auto-export process.
*
* This hook is triggered when an Excalidraw file is being saved.
*
* interface AutoexportConfig {
* png: boolean; // Whether to auto-export to PNG
* svg: boolean; // Whether to auto-export to SVG
* excalidraw: boolean; // Whether to auto-export to Excalidraw format
* theme: "light" | "dark" | "both"; // The theme to use for the export
* }
*
* @param {Object} data - The data for the hook.
* @param {AutoexportConfig} data.autoexportConfig - The current autoexport configuration.
* @param {TFile} data.excalidrawFile - The Excalidraw file being auto-exported.
* @returns {AutoexportConfig | null} - Return a modified AutoexportConfig to override the export behavior, or null to use the default.
*/
onTriggerAutoexportHook: (data: {
autoexportConfig: AutoexportConfig;
excalidrawFile: TFile; // The Excalidraw file being auto-exported
}) => AutoexportConfig | null = null;
/**
* if set, this callback is triggered, when an Excalidraw file is opened

View File

@@ -47,16 +47,15 @@ import {
} from "@zsviczian/excalidraw/types/element/src/types";
import { BinaryFiles, DataURL, SceneData } from "@zsviczian/excalidraw/types/excalidraw/types";
import { EmbeddedFile, MimeType } from "./EmbeddedFileLoader";
import { ConfirmationPrompt } from "./Dialogs/Prompt";
import { MultiOptionConfirmationPrompt } from "./Dialogs/Prompt";
import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "../utils/mermaidUtils";
import { DEBUGGING, debug } from "../utils/debugHelper";
import { Mutable } from "@zsviczian/excalidraw/types/common/src/utility-types";
import { updateElementIdsInScene } from "../utils/excalidrawSceneUtils";
import { checkAndCreateFolder, getNewUniqueFilepath, splitFolderAndFilename } from "../utils/fileUtils";
import { importFileToVault } from "../utils/fileUtils";
import { t } from "../lang/helpers";
import { displayFontMessage } from "../utils/excalidrawViewUtils";
import { getPDFRect } from "../utils/PDFUtils";
import { create } from "domain";
type SceneDataWithFiles = SceneData & { files: BinaryFiles };
@@ -793,7 +792,7 @@ export class ExcalidrawData {
//once off migration of legacy scenes
if(this.scene?.elements?.some((el:any)=>el.type==="iframe" && !el.customData)) {
const prompt = new ConfirmationPrompt(
const prompt = new MultiOptionConfirmationPrompt(
this.plugin,
"This file contains embedded frames " +
"which will be migrated to a newer version for compatibility with " +
@@ -1547,37 +1546,15 @@ export class ExcalidrawData {
}
}
let hookFilepath:string;
const ea = this.view?.getHookServer();
if(ea?.onImageFilePathHook) {
hookFilepath = ea.onImageFilePathHook({
currentImageName: fname,
drawingFilePath: this.view?.file?.path,
})
}
let filepath:string;
if(hookFilepath) {
const {folderpath, filename} = splitFolderAndFilename(hookFilepath);
await checkAndCreateFolder(folderpath);
filepath = getNewUniqueFilepath(this.app.vault,filename,folderpath);
} else {
const x = await getAttachmentsFolderAndFilePath(this.app, this.file.path, fname);
filepath = getNewUniqueFilepath(this.app.vault,fname,x.folder);
}
const arrayBuffer = await getBinaryFileFromDataURL(dataURL);
if(!arrayBuffer) return null;
const file = await this.app.vault.createBinary(
filepath,
arrayBuffer,
);
const file = await importFileToVault(this.app, fname, arrayBuffer, this.file, this.view);
const embeddedFile = new EmbeddedFile(
this.plugin,
this.file.path,
filepath,
file.path,
);
embeddedFile.setImage({

View File

@@ -382,6 +382,25 @@ class ImageCache {
store.put(data, filepath);
}
public async removeBAKFromCache(filepath: string): Promise<void> {
if (!this.isReady()) {
return; // Database not initialized yet
}
const transaction = this.db.transaction(this.backupStoreName, "readwrite");
const store = transaction.objectStore(this.backupStoreName);
return new Promise<void>((resolve, reject) => {
const request = store.delete(filepath);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(new Error(`Failed to remove backup file with key: ${filepath}`));
};
});
}
public async clearImageCache(): Promise<void> {
if (!this.isReady()) {
return; // Database not initialized yet

View File

@@ -114,7 +114,7 @@ export default class Taskbone {
if(addToFrontmatter) {
fe.setKey("taskbone-ocr",text);
view.data = fe.data;
view.save(false);
view.save(false, true, true);
}
window.navigator.clipboard.writeText(text);
new Notice(`I placed the recognized text onto the system clipboard${addToFrontmatter?" and to document properties":""}.`);

View File

@@ -281,6 +281,7 @@ export class ScriptEngine {
customComponents?: (container: HTMLElement) => void,
blockPointerInputOutsideModal?: boolean,
controlsOnTop?: boolean,
draggable?: boolean,
) => {
if (typeof header === "object") {
const options = header as InputPromptOptions;
@@ -293,6 +294,7 @@ export class ScriptEngine {
customComponents = options.customComponents;
blockPointerInputOutsideModal = options.blockPointerInputOutsideModal;
controlsOnTop = options.controlsOnTop;
draggable = options.draggable;
}
return ScriptEngine.inputPrompt(
view,
@@ -307,6 +309,7 @@ export class ScriptEngine {
customComponents,
blockPointerInputOutsideModal,
controlsOnTop,
draggable
);
},
suggester: (
@@ -353,6 +356,7 @@ export class ScriptEngine {
customComponents?: (container: HTMLElement) => void,
blockPointerInputOutsideModal?: boolean,
controlsOnTop?: boolean,
draggable: boolean = false,
) {
try {
return await GenericInputPrompt.Prompt(
@@ -367,7 +371,8 @@ export class ScriptEngine {
displayEditorButtons,
customComponents,
blockPointerInputOutsideModal,
controlsOnTop
controlsOnTop,
draggable
);
} catch {
return undefined;

View File

@@ -1,8 +1,8 @@
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 { ElementsMap, ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameElement, ExcalidrawFrameLikeElement, ExcalidrawTextContainer, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, NonDeletedExcalidrawElement, Theme } from "@zsviczian/excalidraw/types/element/src/types";
import { FontMetadata } from "@zsviczian/excalidraw/types/excalidraw/fonts/FontMetadata";
import { BoundingBox } from "@zsviczian/excalidraw/types/element/src";
import { ElementsMap, ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameElement, ExcalidrawFrameLikeElement, ExcalidrawTextContainer, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, NonDeletedExcalidrawElement, OrderedExcalidrawElement, Theme } from "@zsviczian/excalidraw/types/element/src/types";
import { FontMetadata } from "@zsviczian/excalidraw/types/common/src";
import { AppState, BinaryFiles, DataURL, GenerateDiagramToCode, Zoom } from "@zsviczian/excalidraw/types/excalidraw/types";
import { Mutable } from "@zsviczian/excalidraw/types/common/src/utility-types";
import { GlobalPoint } from "@zsviczian/excalidraw/types/math/src/types";
@@ -52,7 +52,7 @@ type EmbeddedLink =
declare namespace ExcalidrawLib {
type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
Partial<TElement>,
"id" | "version" | "versionNonce"
"id" | "updated"
>;
type ExportOpts = {
@@ -108,6 +108,7 @@ declare namespace ExcalidrawLib {
function determineFocusDistance(
element: ExcalidrawBindableElement,
elementsMap: ElementsMap,
a: GlobalPoint,
b: GlobalPoint,
): number;
@@ -146,6 +147,15 @@ declare namespace ExcalidrawLib {
elementsMap: ElementsMap,
): ExcalidrawElement[][];
function getFontMetrics(fontFamily: ExcalidrawTextElement["fontFamily"], fontSize?:number): {
unitsPerEm: number,
ascender: number,
descender: number,
lineHeight: number,
baseline: number,
fontString: string
}
function measureText(
text: string,
font: FontString,
@@ -221,5 +231,7 @@ declare namespace ExcalidrawLib {
function safelyParseJSON (json: string): Record<string, any> | null;
function loadSceneFonts(elements: NonDeletedExcalidrawElement[]): Promise<void>;
function loadMermaid(): Promise<any>;
function syncInvalidIndices(elements: readonly ExcalidrawElement[]): OrderedExcalidrawElement[];
function syncMovedIndices(elements: readonly ExcalidrawElement[], movedElements: ElementsMap): OrderedExcalidrawElement[];
}

View File

@@ -22,11 +22,19 @@ export interface EmbeddableLeafRef {
editNode?: Function;
}
export interface AutoexportConfig {
png: boolean; // Whether to auto-export to PNG
svg: boolean; // Whether to auto-export to SVG
excalidraw: boolean; // Whether to auto-export to Excalidraw format
theme: "light" | "dark" | "both"; // The theme to use for the export
}
export interface ViewSemaphores {
warnAboutLinearElementLinkClick: boolean;
//flag to prevent overwriting the changes the user makes in an embeddable view editing the back side of the drawing
embeddableIsEditingSelf: boolean;
popoutUnload: boolean; //the unloaded Excalidraw view was the last leaf in the popout window
viewloaded: boolean; //onLayoutReady in view.onload has completed.
viewunload: boolean;
//first time initialization of the view
scriptsReady: boolean;

View File

@@ -10,4 +10,5 @@ export interface InputPromptOptions {
customComponents?: (container: HTMLElement) => void,
blockPointerInputOutsideModal?: boolean,
controlsOnTop?: boolean,
draggable?: boolean,
}

137
src/utils/ErrorHandler.ts Normal file
View File

@@ -0,0 +1,137 @@
import { Notice } from "obsidian";
import { debug, DEBUGGING } from "./debugHelper";
/**
* Centralized error handling for the Excalidraw plugin
*/
export class ErrorHandler {
private static instance: ErrorHandler;
private errorLog: Array<{error: Error, context: string, timestamp: number}> = [];
private errorNoticeTimeout: number = 10000; // 10 seconds
private maxLogEntries: number = 100;
private constructor() {}
/**
* Get singleton instance of ErrorHandler
*/
public static getInstance(): ErrorHandler {
if (!ErrorHandler.instance) {
ErrorHandler.instance = new ErrorHandler();
}
return ErrorHandler.instance;
}
/**
* Handles errors consistently across the plugin
* @param error The error object
* @param context Context information about where the error occurred
* @param showNotice Whether to show a user-facing notice
* @param timeout How long to show the notice (in ms)
*/
public handleError(
error: Error | string,
context: string,
showNotice = true,
timeout?: number
): void {
const errorObj = typeof error === 'string' ? new Error(error) : error;
// Log to console with better formatting
console.error(`[Excalidraw Error] in ${context}:`, errorObj);
// Add to error log with timestamp
this.errorLog.push({
error: errorObj,
context,
timestamp: Date.now()
});
// Trim log if it gets too large
if (this.errorLog.length > this.maxLogEntries) {
this.errorLog = this.errorLog.slice(this.errorLog.length - this.maxLogEntries);
}
// Show notice to user if required
if (showNotice) {
const formattedError = this.formatErrorForUser(errorObj, context);
new Notice(formattedError, timeout || this.errorNoticeTimeout);
}
// Debug output if debugging is enabled
if ((process.env.NODE_ENV === 'development') && DEBUGGING) {
debug(this.handleError, `ErrorHandler.handleError: ${context}`, errorObj);
}
}
/**
* Safely evaluates code with error handling
* @param code The code to evaluate
* @param context The context where evaluation is happening
* @param win The window object for evaluation context
* @param fallback Optional fallback value if evaluation fails
*/
public safeEval<T>(code: string, context: string, win: Window, fallback?: T): T {
try {
return win.eval.call(win, code) as T;
} catch (error) {
this.handleError(error, `SafeEval in ${context}`);
if (fallback !== undefined) {
return fallback;
}
throw error; // Re-throw if no fallback provided
}
}
/**
* Wraps a function with try/catch and error handling
* @param fn Function to wrap
* @param context Context for error reporting
* @param fallback Optional fallback value if function fails
*/
public wrapWithTryCatch<T>(fn: () => T, context: string, fallback?: T): T {
try {
return fn();
} catch (error) {
this.handleError(error, context);
if (fallback !== undefined) return fallback;
throw error; // Re-throw if no fallback provided
}
}
/**
* Format error message for user-facing notifications
*/
private formatErrorForUser(error: Error, context: string): string {
// Shorten and simplify the message for users
let message = error.message;
// Special handling for common error types
if (message.includes("Cannot read properties of undefined")) {
message = "A required object was not available. This might be due to a plugin loading issue.";
} else if (message.includes("is not a function")) {
message = "A required function was not available. This might be due to a plugin version mismatch.";
} else if (message.length > 100) {
// Truncate very long messages
message = message.substring(0, 100) + "...";
}
return `Excalidraw Error: ${message} (in ${context})`;
}
/**
* Get recent errors for debugging
*/
public getErrorLog(): Array<{error: Error, context: string, timestamp: number}> {
return [...this.errorLog];
}
/**
* Clear error log
*/
public clearErrorLog(): void {
this.errorLog = [];
}
}
export const errorHandler = ErrorHandler.getInstance();

View File

@@ -63,11 +63,12 @@ export function setFileToLocalGraph(app: App, file: TFile) {
if (l.view?.getViewType() === "localgraph") lgv = l.view;
});
try {
if (lgv) {
//@ts-ignore
lgv.loadFile(file);
//@ts-ignore
const loadFile = lgv?.loadFile;
if (loadFile) {
loadFile(file);
}
} catch (e) {
console.error(e);
}
}
}

View File

@@ -456,12 +456,16 @@ export const updateElementLinksToObsidianLinks = ({elements, hostFile}:{
}
let link = EXCALIDRAW_PLUGIN.app.getObsidianUrl(file);
if(window.ExcalidrawAutomate?.onUpdateElementLinkForExportHook) {
link = window.ExcalidrawAutomate.onUpdateElementLinkForExportHook({
originalLink: el.link,
obsidianLink: link,
linkedFile: file,
hostFile: hostFile
});
try {
link = window.ExcalidrawAutomate.onUpdateElementLinkForExportHook({
originalLink: el.link,
obsidianLink: link,
linkedFile: file,
hostFile: hostFile
}) ?? link;
} catch (e) {
errorlog({where: "excalidrawAutomateUtils.updateElementLinksToObsidianLinks", fn: window.ExcalidrawAutomate.onUpdateElementLinkForExportHook, error: e});
}
}
const newElement: Mutable<ExcalidrawElement> = cloneElement(el);
newElement.link = link;
@@ -698,14 +702,14 @@ export const getTextElementsMatchingQuery = (
el.type === "text" &&
query.some((q) => {
if (exactMatch) {
const text = el.rawText.toLowerCase().split("\n")[0].trim();
const text = el.customData?.text2Path?.text ?? el.rawText.toLowerCase().split("\n")[0].trim();
const m = text.match(/^#*(# .*)/);
if (!m || m.length !== 2) {
return false;
}
return m[1] === q.toLowerCase();
}
const text = el.rawText.toLowerCase().replaceAll("\n", " ").trim();
const text = el.customData?.text2Path?.text ?? el.rawText.toLowerCase().replaceAll("\n", " ").trim();
return text.match(q.toLowerCase()); //to distinguish between "# frame" and "# frame 1" https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
}));
}

View File

@@ -1,12 +1,12 @@
import { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
import { App, loadPdfJs, normalizePath, Notice, requestUrl, RequestUrlResponse, TAbstractFile, TFile, TFolder, Vault } from "obsidian";
import { App, loadPdfJs, MetadataCache, normalizePath, Notice, requestUrl, RequestUrlResponse, TAbstractFile, TFile, TFolder, Vault } from "obsidian";
import { DEVICE, EXCALIDRAW_PLUGIN, FRONTMATTER_KEYS, URLFETCHTIMEOUT } from "src/constants/constants";
import { IMAGE_MIME_TYPES, MimeType } from "../shared/EmbeddedFileLoader";
import { ExcalidrawSettings } from "src/core/settings";
import { errorlog, getDataURL } from "./utils";
import ExcalidrawPlugin from "src/core/main";
import { ANNOTATED_PREFIX, CROPPED_PREFIX } from "./carveout";
import { getAttachmentsFolderAndFilePath } from "./obsidianUtils";
import ExcalidrawView from "src/view/ExcalidrawView";
/**
* Splits a full path including a folderpath and a filename into separate folderpath and filename components
@@ -405,8 +405,8 @@ export const getAliasWithSize = (alias: string, size: string): string => {
export const getCropFileNameAndFolder = async (plugin: ExcalidrawPlugin, hostPath: string, baseNewFileName: string):Promise<{folderpath: string, filename: string}> => {
let prefix = plugin.settings.cropPrefix || "";
if(prefix.trim() === "") prefix = CROPPED_PREFIX;
const filename = prefix + baseNewFileName + ".md";
let suffix = plugin.settings.cropSuffix || "";
const filename = prefix + baseNewFileName + suffix + ".md";
if(!plugin.settings.cropFolder || plugin.settings.cropFolder.trim() === "") {
const folderpath = (await getAttachmentsFolderAndFilePath(plugin.app, hostPath, filename)).folder;
return {folderpath, filename};
@@ -418,8 +418,8 @@ export const getCropFileNameAndFolder = async (plugin: ExcalidrawPlugin, hostPat
export const getAnnotationFileNameAndFolder = async (plugin: ExcalidrawPlugin, hostPath: string, baseNewFileName: string):Promise<{folderpath: string, filename: string}> => {
let prefix = plugin.settings.annotatePrefix || "";
if(prefix.trim() === "") prefix = ANNOTATED_PREFIX;
const filename = prefix + baseNewFileName + ".md";
let suffix = plugin.settings.annotateSuffix || "";
const filename = prefix + baseNewFileName + suffix + ".md";
if(!plugin.settings.annotateFolder || plugin.settings.annotateFolder.trim() === "") {
const folderpath = (await getAttachmentsFolderAndFilePath(plugin.app, hostPath, filename)).folder;
return {folderpath, filename};
@@ -494,7 +494,55 @@ export const hasExcalidrawEmbeddedImagesTreeChanged = (sourceFile: TFile, mtime:
return fileList.some(f=>f.stat.mtime > mtime);
}
export async function exportImageToFile(view: ExcalidrawView, path: string, content: string | ArrayBuffer | Blob, extension: string): Promise<TFile> {
const ea = view?.getHookServer();
if(ea?.onImageExportPathHook) {
try {
path = ea.onImageExportPathHook({
exportFilepath: path,
exportExtension: extension,
excalidrawFile: view.file,
action: "export",
}) ?? path;
} catch (e) {
errorlog({where: "fileUtils.exportImageToFile", fn: ea.onImageExportPathHook, error: e});
}
}
return await createOrOverwriteFile(view.app, path, content);
}
export async function importFileToVault(app: App, fname: string, content: string | ArrayBuffer | Blob, excalidrawFile: TFile, view?: ExcalidrawView): Promise<TFile> {
let hookFilepath:string;
const ea = view?.getHookServer();
if(ea?.onImageFilePathHook) {
try {
hookFilepath = ea.onImageFilePathHook({
currentImageName: fname,
drawingFilePath: excalidrawFile.path,
})
} catch (e) {
errorlog({where: "fileUtils.importFileToVault", fn: ea.onImageFilePathHook, error: e});
}
}
let filepath:string;
if(hookFilepath) {
const {folderpath, filename} = splitFolderAndFilename(hookFilepath);
await checkAndCreateFolder(folderpath);
filepath = getNewUniqueFilepath(app.vault,filename,folderpath);
} else {
const {folder} = await getAttachmentsFolderAndFilePath(app, excalidrawFile.path, fname);
filepath = getNewUniqueFilepath(app.vault,fname,folder);
}
return await createOrOverwriteFile(app, filepath, content);
}
export async function createOrOverwriteFile(app: App, path: string, content: string | ArrayBuffer | Blob): Promise<TFile> {
const {folderpath} = splitFolderAndFilename(path);
if(folderpath && folderpath !== "/") {
await checkAndCreateFolder(folderpath);
}
const file = app.vault.getAbstractFileByPath(normalizePath(path));
if (content instanceof Blob) {
content = await content.arrayBuffer();
@@ -514,4 +562,41 @@ export async function createOrOverwriteFile(app: App, path: string, content: str
} else {
return await app.vault.create(path, content);
}
}
export async function createFileAndAwaitMetacacheUpdate(
app: App,
path: string,
content: string | ArrayBuffer | Blob,
) : Promise<TFile> {
path = normalizePath(path);
let ready = false;
const extension = path.substring(path.lastIndexOf(".") + 1);
//metadataCache.on("changed", (file:TFile) => void) does not fire for non-markdown files
if(extension === "md") {
const metaCache: MetadataCache = app.metadataCache;
const handler = (file:TFile) => {
if(file.path === path) {
metaCache.off("changed", handler);
ready = true;
}
}
metaCache.on("changed", handler);
const file = await createOrOverwriteFile(app, path, content);
if(!file) {
ready = true; //if file is null, it means it was not created, so we can skip waiting
metaCache.off("changed", handler);
}
let attempts = 0;
while (!ready && attempts++ < 15) await sleep(50);
if(!ready) {
metaCache.off("changed", handler); //if we timed out, remove the handler
}
return file;
}
return await createOrOverwriteFile(app, path, content);
}

View File

@@ -1,4 +1,4 @@
import { THEME } from "../constants/constants";
import { EXCALIDRAW_PLUGIN, THEME } from "../constants/constants";
import type { Theme } from "@zsviczian/excalidraw/types/element/src/types";
import type { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
import type { OpenAIInput, OpenAIOutput } from "@zsviczian/excalidraw/types/excalidraw/data/ai/types";
@@ -43,7 +43,7 @@ export async function diagramToHTML({
theme?: Theme;
}) {
const body: OpenAIInput.ChatCompletionCreateParamsBase = {
model: "gpt-4-vision-preview",
model: EXCALIDRAW_PLUGIN.settings.openAIDefaultVisionModel,
// 4096 are max output tokens allowed for `gpt-4-vision-preview` currently
max_tokens: 4096,
temperature: 0.1,

View File

@@ -16,6 +16,7 @@ import {
getCommonBoundingBox,
DEVICE,
getContainerElement,
SCRIPT_INSTALL_FOLDER,
} from "../constants/constants";
import ExcalidrawPlugin from "../core/main";
import { ExcalidrawElement, ExcalidrawImageElement, ExcalidrawTextElement, ImageCrop } from "@zsviczian/excalidraw/types/element/src/types";
@@ -33,6 +34,7 @@ import Pool from "es6-promise-pool";
import { FileData } from "../shared/EmbeddedFileLoader";
import { t } from "src/lang/helpers";
import ExcalidrawScene from "src/shared/svgToExcalidraw/elements/ExcalidrawScene";
import { log } from "./debugHelper";
declare const PLUGIN_VERSION:string;
declare var LZString: any;
@@ -82,6 +84,9 @@ export async function checkExcalidrawVersion() {
t("UPDATE_AVAILABLE") + ` ${latestVersion}`,
);
}
// Check for script updates
await checkScriptUpdates();
} catch (e) {
console.log({ where: "Utils/checkExcalidrawVersion", error: e });
}
@@ -91,6 +96,56 @@ export async function checkExcalidrawVersion() {
}, 28800000); //reset after 8 hours
};
async function checkScriptUpdates() {
try {
if (!EXCALIDRAW_PLUGIN?.settings?.scriptFolderPath) {
return;
}
const folder = `${EXCALIDRAW_PLUGIN.settings.scriptFolderPath}/${SCRIPT_INSTALL_FOLDER}/`;
const installedScripts = EXCALIDRAW_PLUGIN.app.vault.getFiles()
.filter(f => f.path.startsWith(folder) && f.extension === "md");
if (installedScripts.length === 0) {
return;
}
// Get directory info from GitHub
const files = new Map<string, number>();
const directoryInfo = JSON.parse(
await request({
url: "https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/directory-info.json",
}),
);
directoryInfo.forEach((f: any) => files.set(f.fname, f.mtime));
if (files.size === 0) {
return;
}
// Check if any installed scripts have updates
const updates:string[] = [];
let hasUpdates = false;
for (const scriptFile of installedScripts) {
const filename = scriptFile.name;
if (files.has(filename)) {
const mtime = files.get(filename);
if (mtime > scriptFile.stat.mtime) {
updates.push(scriptFile.path.split(folder)?.[1]?.split(".md")[0]);
hasUpdates = true;
}
}
}
if (hasUpdates) {
const message = `${t("SCRIPT_UPDATES_AVAILABLE")}\n\n${updates.sort().join("\n")}`;
new Notice(message,8000+updates.length*1000);
log(message);
}
} catch (e) {
console.log({ where: "Utils/checkScriptUpdates", error: e });
}
}
const random = new Random(Date.now());
export function randomInteger () {
@@ -911,6 +966,41 @@ export function hyperlinkIsImage (data: string):boolean {
return IMAGE_TYPES.contains(corelink.substring(corelink.lastIndexOf(".")+1));
}
export function getFilePathFromObsidianURL (data: string): string {
if(!data) return null;
if(!data.startsWith("obsidian://")) return null;
try {
const url = new URL(data);
const fileParam = url.searchParams.get("file");
if(!fileParam) return null;
return decodeURIComponent(fileParam);
} catch {
return null;
}
}
export function obsidianURLIsImage (data: string):boolean {
if(!data) return false;
if(!data.startsWith("obsidian://")) return false;
try {
const url = new URL(data);
const fileParam = url.searchParams.get("file");
if(!fileParam) return false;
const decodedFile = decodeURIComponent(fileParam);
const lastDotIndex = decodedFile.lastIndexOf(".");
if(lastDotIndex === -1) return false;
const extension = decodedFile.substring(lastDotIndex + 1);
return IMAGE_TYPES.contains(extension);
} catch {
return false;
}
}
export function hyperlinkIsYouTubeLink (link:string): boolean {
return isHyperLink(link) &&
(link.startsWith("https://youtu.be") || link.startsWith("https://www.youtube.com") || link.startsWith("https://youtube.com") || link.startsWith("https//www.youtu.be")) &&

View File

@@ -1,7 +1,6 @@
import {
TextFileView,
WorkspaceLeaf,
normalizePath,
TFile,
WorkspaceItem,
Notice,
@@ -56,6 +55,7 @@ import {
MD_EX_SECTIONS,
refreshTextDimensions,
getContainerElement,
syncInvalidIndices,
} from "../constants/constants";
import ExcalidrawPlugin from "../core/main";
import { ExcalidrawAutomate } from "../shared/ExcalidrawAutomate";
@@ -78,8 +78,10 @@ import {
} from "../shared/ExcalidrawData";
import {
checkAndCreateFolder,
createFileAndAwaitMetacacheUpdate,
createOrOverwriteFile,
download,
exportImageToFile,
getDataURLFromURL,
getIMGFilename,
getMimeType,
@@ -98,20 +100,19 @@ import {
getWithBackground,
hasExportTheme,
scaleLoadedImage,
svgToBase64,
hyperlinkIsImage,
getYouTubeThumbnailLink,
isContainer,
fragWithHTML,
isMaskFile,
shouldEmbedScene,
_getContainerElement,
arrayToMap,
addAppendUpdateCustomData,
getFilePathFromObsidianURL,
} from "../utils/utils";
import { cleanBlockRef, cleanSectionHeading, closeLeafView, getActivePDFPageNumberFromPDFView, getAttachmentsFolderAndFilePath, getLeaf, getParentOfClass, obsidianPDFQuoteWithRef, openLeaf, setExcalidrawView } from "../utils/obsidianUtils";
import { splitFolderAndFilename } from "../utils/fileUtils";
import { ConfirmationPrompt, GenericInputPrompt, NewFileActions, Prompt, linkPrompt } from "../shared/Dialogs/Prompt";
import { GenericInputPrompt, MultiOptionConfirmationPrompt, NewFileActions, Prompt, linkPrompt } from "../shared/Dialogs/Prompt";
import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard";
import { updateEquation } from "../shared/LaTeX";
import {
@@ -148,12 +149,13 @@ import React from "react";
import { diagramToHTML } from "../utils/matic";
import { IS_WORKER_SUPPORTED } from "../shared/Workers/compression-worker";
import { getPDFCropRect } from "../utils/PDFUtils";
import { Position, ViewSemaphores } from "../types/excalidrawViewTypes";
import { AutoexportConfig, Position, ViewSemaphores } from "../types/excalidrawViewTypes";
import { DropManager } from "./managers/DropManager";
import { ImageInfo } from "src/types/excalidrawAutomateTypes";
import { exportPNG, exportPNGToClipboard, exportSVG, exportToPDF, getMarginValue, getPageDimensions, PageOrientation, PageSize } from "src/utils/exportUtils";
import { FrameRenderingOptions } from "src/types/utilTypes";
import { CaptureUpdateAction } from "src/constants/constants";
import { Backpack } from "lucide-react";
const EMBEDDABLE_SEMAPHORE_TIMEOUT = 2000;
const PREVENT_RELOAD_TIMEOUT = 2000;
@@ -315,6 +317,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
warnAboutLinearElementLinkClick: true,
embeddableIsEditingSelf: false,
popoutUnload: false,
viewloaded: false,
viewunload: false,
scriptsReady: false,
justLoaded: false,
@@ -428,12 +431,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
0,
this.file.path.lastIndexOf(".md"),
)}.excalidraw`;
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
if (file && file instanceof TFile) {
this.app.vault.modify(file, JSON.stringify(scene, null, "\t"));
} else {
this.app.vault.create(filepath, JSON.stringify(scene, null, "\t"));
}
exportImageToFile(this, filepath, JSON.stringify(scene, null, "\t"), ".excalidraw");
}
public async exportExcalidraw(selectedOnly?: boolean) {
@@ -455,16 +453,18 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
filename = `${filename}.excalidraw`;
const folderpath = splitFolderAndFilename(this.file.path).folderpath;
await checkAndCreateFolder(folderpath); //create folder if it does not exist
const fname = getNewUniqueFilepath(
const path = getNewUniqueFilepath(
this.app.vault,
filename,
folderpath,
);
this.app.vault.create(
fname,
const file = await exportImageToFile(
this,
path,
JSON.stringify(this.getScene(), null, "\t"),
".excalidraw",
);
new Notice(`Exported to ${fname}`, 6000);
new Notice(`Exported to ${file?.name}`, 6000);
});
return;
}
@@ -548,7 +548,11 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
);
}
public async saveSVG(scene?: any, embedScene?: boolean) {
public async saveSVG(data:{scene?: any, embedScene?: boolean, autoexportConfig?: AutoexportConfig }) {
if(!data) {
data = {};
}
let { scene, embedScene, autoexportConfig } = data;
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.saveSVG, "ExcalidrawView.saveSVG", scene, embedScene);
if (!scene) {
if (!this.excalidrawAPI) {
@@ -564,14 +568,15 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
}
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2026
const svgString = svg.outerHTML;
await createOrOverwriteFile(this.app, filepath, svgString);
await exportImageToFile(this, filepath, svgString,
theme === "dark" ? ".dark.svg" : theme === "light" ? ".light.svg" : ".svg");
}
if(this.plugin.settings.autoExportLightAndDark) {
if(autoexportConfig?.theme ? autoexportConfig.theme === "both" : this.plugin.settings.autoExportLightAndDark) {
await exportImage(getIMGFilename(this.file.path, "dark.svg"),"dark");
await exportImage(getIMGFilename(this.file.path, "light.svg"),"light");
} else {
await exportImage(getIMGFilename(this.file.path, "svg"));
await exportImage(getIMGFilename(this.file.path, "svg"), autoexportConfig?.theme);
}
}
@@ -671,7 +676,11 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
);
}
public async savePNG(scene?: any, embedScene?: boolean) {
public async savePNG(data: {scene?: any, embedScene?: boolean, autoexportConfig?: AutoexportConfig}) {
if(!data) {
data = {};
}
let { scene, embedScene, autoexportConfig } = data;
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.savePNG, "ExcalidrawView.savePNG", scene, embedScene);
if (!scene) {
if (!this.excalidrawAPI) {
@@ -685,14 +694,15 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
if (!png) {
return;
}
await createOrOverwriteFile(this.app, filepath, png);
await exportImageToFile(this, filepath, png,
theme === "dark" ? ".dark.png" : theme === "light" ? ".light.png" : ".png");
}
if(this.plugin.settings.autoExportLightAndDark) {
if(autoexportConfig?.theme ? autoexportConfig.theme === "both" : this.plugin.settings.autoExportLightAndDark) {
await exportImage(getIMGFilename(this.file.path, "dark.png"),"dark");
await exportImage(getIMGFilename(this.file.path, "light.png"),"light");
} else {
await exportImage(getIMGFilename(this.file.path, "png"));
await exportImage(getIMGFilename(this.file.path, "png"), autoexportConfig?.theme);
}
}
@@ -842,8 +852,10 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
const plugin = this.plugin;
const file = this.file;
window.setTimeout(async ()=>{
if(!d) return;
await plugin.app.vault.modify(file,d);
await imageCache.addBAKToCache(file.path,d);
//this is a shady edge case, don't scrifice the BAK file in case the drawing is empty
//await imageCache.addBAKToCache(file.path,d);
},200)
this.semaphores.saving = false;
return;
@@ -859,7 +871,10 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
//saving to backup with a delay in case application closes in the meantime, I want to avoid both save and backup corrupted.
const path = this.file.path;
const data = this.lastSavedData;
window.setTimeout(()=>imageCache.addBAKToCache(path,data),50);
//if the scene is empty, do not save to BAK (this could be due to a crash when the BAK should not be updated)
if(scene && scene.elements && scene.elements.length > 0) {
window.setTimeout(()=>imageCache.addBAKToCache(path,data),50);
}
triggerReload = (this.lastSaveTimestamp === this.file.stat.mtime) &&
!preventReload && forcesave;
this.lastSaveTimestamp = this.file.stat.mtime;
@@ -877,22 +892,30 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1209 (added popout unload to the condition)
if (!triggerReload && !this.semaphores.autosaving && (!this.semaphores.viewunload || this.semaphores.popoutUnload)) {
const autoexportPreference = this.excalidrawData.autoexportPreference;
if (
(autoexportPreference === AutoexportPreference.inherit && this.plugin.settings.autoexportSVG) ||
autoexportPreference === AutoexportPreference.both || autoexportPreference === AutoexportPreference.svg
) {
this.saveSVG();
let autoexportConfig: AutoexportConfig = {
svg: (autoexportPreference === AutoexportPreference.inherit && this.plugin.settings.autoexportSVG) ||
autoexportPreference === AutoexportPreference.both || autoexportPreference === AutoexportPreference.svg,
png: (autoexportPreference === AutoexportPreference.inherit && this.plugin.settings.autoexportPNG) ||
autoexportPreference === AutoexportPreference.both || autoexportPreference === AutoexportPreference.png,
excalidraw: !this.compatibilityMode && this.plugin.settings.autoexportExcalidraw,
theme: this.plugin.settings.autoExportLightAndDark ? "both" : this.getViewExportTheme() as "dark" | "light",
}
if (
(autoexportPreference === AutoexportPreference.inherit && this.plugin.settings.autoexportPNG) ||
autoexportPreference === AutoexportPreference.both || autoexportPreference === AutoexportPreference.png
) {
this.savePNG();
if (this.getHookServer().onTriggerAutoexportHook) {
try {
autoexportConfig = this.getHookServer().onTriggerAutoexportHook({
excalidrawFile: this.file, autoexportConfig}) ?? autoexportConfig;
} catch (e) {
errorlog({where: "ExcalidrawView.save", fn: this.getHookServer().onTriggerAutoexportHook, error: e});
}
}
if (
!this.compatibilityMode &&
this.plugin.settings.autoexportExcalidraw
) {
if (autoexportConfig.svg) {
this.saveSVG({autoexportConfig});
}
if (autoexportConfig.png) {
this.savePNG({autoexportConfig});
}
if (autoexportConfig.excalidraw) {
this.saveExcalidraw();
}
}
@@ -1656,8 +1679,8 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
if(!Boolean(this?.plugin?.activeLeafChangeEventHandler)) return;
if (Boolean(this.plugin.activeLeafChangeEventHandler) && (this?.app?.workspace?.activeLeaf === this.leaf)) {
this.plugin.activeLeafChangeEventHandler(this.leaf);
}
this.canvasNodeFactory.initialize();
}
await this.canvasNodeFactory.initialize();
this.contentEl.addClass("excalidraw-view");
//https://github.com/zsviczian/excalibrain/issues/28
await this.addSlidingPanesListner(); //awaiting this because when using workspaces, onLayoutReady comes too early
@@ -1694,6 +1717,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
this.registerDomEvent(this.ownerWindow, "keyup", onKeyUp, false);
//this.registerDomEvent(this.contentEl, "mouseleave", onBlurOrLeave, false); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2004
this.registerDomEvent(this.ownerWindow, "blur", onBlurOrLeave, false);
this.semaphores.viewloaded = true;
});
this.setupAutosaveTimer();
@@ -2359,7 +2383,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
}
await this.plugin.awaitInit();
let counter = 0;
while ((!this.file || !this.plugin.fourthFontLoaded) && counter++<50) await sleep(50);
while ((!this.semaphores.viewloaded || !this.file || !this.plugin.fourthFontLoaded) && counter++<50) await sleep(50);
if(!this.file) return;
this.compatibilityMode = this.file.extension === "excalidraw";
await this.plugin.loadSettings();
@@ -2405,7 +2429,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
while (!imageCache.isReady() && confirmation) {
const message = `You've been now waiting for <b>${Math.round((Date.now()-timestamp)/1000)}</b> seconds. `
imageCache.initializationNotice = true;
const confirmationPrompt = new ConfirmationPrompt(plugin,
const confirmationPrompt = new MultiOptionConfirmationPrompt(plugin,
`${counter>0
? counter%4 === 0
? message + "The CACHE is still loading.<br><br>"
@@ -2431,7 +2455,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
);
return;
}
const confirmationPrompt = new ConfirmationPrompt(plugin,t("BACKUP_AVAILABLE"));
const confirmationPrompt = new MultiOptionConfirmationPrompt(plugin,t("BACKUP_AVAILABLE"));
confirmationPrompt.waitForClose.then(async (confirmed) => {
if (confirmed) {
await this.app.vault.modify(file, drawingBAK);
@@ -2446,6 +2470,33 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
return;
}
}
if(imageCache.isReady() && this.excalidrawData.scene && this.excalidrawData.scene.elements && this.excalidrawData.scene.elements.length === 0) {
const backup = await imageCache.getBAKFromCache(this.file.path);
if(backup && backup.length > data.length) {
setTimeout(async () => {
const confirmationPrompt = new MultiOptionConfirmationPrompt(
this.plugin,
t("BACKUP_SAVE_AS_FILE"),
new Map([
[t("BACKUP_CANCEL"), 0],
[t("BACKUP_DELETE"), 2],
[t("BACKUP_SAVE"), 1],
]),
t("BACKUP_SAVE"),
);
const result = await confirmationPrompt.waitForClose;
if(result === 1) {
const path = getNewUniqueFilepath(this.app.vault, `${this.file.basename}.restored.${this.file.extension}`, this.file.parent.path);
const backupFile = await createFileAndAwaitMetacacheUpdate(this.app,path, backup);
await imageCache.removeBAKFromCache(this.file.path);
this.plugin.openDrawing(backupFile,"new-tab");
} else if (result === 2) {
await imageCache.removeBAKFromCache(this.file.path);
}
});
}
}
await this.loadDrawing(true);
if(this.plugin.ea.onFileOpenHook) {
@@ -2516,6 +2567,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
if (!this.excalidrawAPI) {
return;
}
const loader = new EmbeddedFilesLoader(this.plugin);
const runLoader = (l: EmbeddedFilesLoader) => {
@@ -2703,7 +2755,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
if(this.getSceneVersion(inData.scene.elements) !== this.previousSceneVersion) {
this.setDirty(3);
}
this.updateScene({elements: sceneElements, storeAction: "capture"});
this.updateScene({elements: sceneElements, captureUpdate: CaptureUpdateAction.IMMEDIATELY});
if(reloadFiles.size>0) {
this.loadSceneFiles(false,reloadFiles);
}
@@ -2994,10 +3046,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
const text:string[] = [];
if(containerElement && containerElement.link) text.push(containerElement.link);
text.push(textElement.rawText);
const f = await this.app.vault.create(
fname,
text.join("\n"),
);
const f = await createOrOverwriteFile(this.app, fname, text.join("\n"));
if(f) {
const ea:ExcalidrawAutomate = getEA(this);
const elements = containerElement ? [textElement,containerElement] : [textElement];
@@ -3047,7 +3096,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
const ea = getEA(this) as ExcalidrawAutomate;
const mimeType = getMimeType(getURLImageExtension(link));
const dataURL = await getDataURLFromURL(link,mimeType,3000);
const fileId = await generateIdFromFile((new TextEncoder()).encode(dataURL as string))
const fileId = await generateIdFromFile((new TextEncoder()).encode(dataURL as string).buffer)
const file = await this.excalidrawData.saveDataURLtoVault(dataURL,mimeType,fileId);
if(!file) {
new Notice(t("ERROR_SAVING_IMAGE"));
@@ -3819,7 +3868,8 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
event: this.lastMouseEvent,
source: VIEW_TYPE_EXCALIDRAW,
hoverParent: this,
targetEl: this.hoverPreviewTarget, //null //0.15.0 hover editor!!
//https://discord.com/channels/686053708261228577/989603365606531104/1386783538795249715
//targetEl: this.hoverPreviewTarget, //null //0.15.0 hover editor!!
linktext: this.plugin.hover.linkText,
sourcePath: this.plugin.hover.sourcePath,
});
@@ -4061,15 +4111,19 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
.forEach(el=>(el as Mutable<ExcalidrawTextElement>).rawText = (el as ExcalidrawTextElement).originalText);
};
if(data && ea.onPasteHook) {
const res = ea.onPasteHook({
ea,
payload: data,
event,
excalidrawFile: this.file,
view: this,
pointerPosition: this.currentPosition,
});
if(typeof res === "boolean" && res === false) return false;
try {
const res = ea.onPasteHook({
ea,
payload: data,
event,
excalidrawFile: this.file,
view: this,
pointerPosition: this.currentPosition,
});
if(typeof res === "boolean" && res === false) return false;
} catch (e) {
errorlog({where: "ExcalidrawView.onPaste", fn: ea.onPasteHook, error: e});
}
}
// Disables Middle Mouse Button Paste Functionality on Linux
@@ -4087,6 +4141,12 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
this.addImageWithURL(data.text);
return false;
}
const obsidianURLFilePath = getFilePathFromObsidianURL(data?.text);
if(obsidianURLFilePath) {
this.addImageWithURL(obsidianURLFilePath);
return false;
}
if(data && data.text && !this.modifierKeyDown.shiftKey) {
const isCodeblock = Boolean(data.text.replaceAll("\r\n", "\n").replaceAll("\r", "\n").match(/^`{3}[^\n]*\n.+\n`{3}\s*$/ms));
if(isCodeblock) {
@@ -4609,7 +4669,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
const {folderpath, filename} = splitFolderAndFilename(path);
path = getNewUniqueFilepath(this.app.vault, filename, folderpath);
try {
const newFile = await this.app.vault.create(path, child.text);
const newFile = await createOrOverwriteFile(this.app, path, child.text);
if(!newFile) {
new Notice("Unexpected error");
return;
@@ -5948,6 +6008,9 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
//@ts-ignore
scene.forceFlushSync = true;
}
if(scene.elements) {
scene.elements = syncInvalidIndices(scene.elements);
}
try {
api.updateScene(scene);
} catch (e) {

View File

@@ -494,7 +494,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
display: this.state.minimized ? "none" : "block",
}}
>
<div className="panelColumn">
<div className="selected-shape-actions">
<fieldset>
<legend>Utility actions</legend>
<div className="buttonList buttonListIcon">

View File

@@ -50,17 +50,17 @@ export class CanvasNodeFactory {
}
public async initialize() {
//@ts-ignore
const app = this.view.app;
const canvasPlugin = app.internalPlugins.plugins["canvas"];
if(!canvasPlugin._loaded) {
await canvasPlugin.load();
}
const doc = this.view.ownerDocument;
const rootSplit:WorkspaceSplit = new (WorkspaceSplit as ConstructableWorkspaceSplit)(this.view.app.workspace, "vertical");
rootSplit.getRoot = () => this.view.app.workspace[doc === document ? 'rootSplit' : 'floatingSplit'];
const rootSplit:WorkspaceSplit = new (WorkspaceSplit as ConstructableWorkspaceSplit)(app.workspace, "vertical");
rootSplit.getRoot = () => app.workspace[doc === document ? 'rootSplit' : 'floatingSplit'];
rootSplit.getContainer = () => getContainerForDocument(doc);
this.leaf = this.view.app.workspace.createLeafInParent(rootSplit, 0);
this.leaf = app.workspace.createLeafInParent(rootSplit, 0);
this.canvas = canvasPlugin.views.canvas(this.leaf).canvas;
this.initialized = true;
}

View File

@@ -12,8 +12,7 @@ import { getEA } from "src/core";
import { insertEmbeddableToView, insertImageToView } from "src/utils/excalidrawViewUtils";
import { t } from "src/lang/helpers";
import ExcalidrawPlugin from "src/core/main";
import { getInternalLinkOrFileURLLink, getNewUniqueFilepath, getURLImageExtension, splitFolderAndFilename } from "src/utils/fileUtils";
import { getAttachmentsFolderAndFilePath } from "src/utils/obsidianUtils";
import { getInternalLinkOrFileURLLink, getURLImageExtension, importFileToVault } from "src/utils/fileUtils";
import { ScriptEngine } from "src/shared/Scripts";
import { UniversalInsertFileModal } from "src/shared/Dialogs/UniversalInsertFileModal";
import { Position } from "src/types/excalidrawViewTypes";
@@ -372,8 +371,7 @@ export class DropManager {
(async () => {
const droppedFilename = event.dataTransfer.files[i].name;
const fileToImport = await event.dataTransfer.files[i].arrayBuffer();
let {folder:_, filepath} = await getAttachmentsFolderAndFilePath(this.app, this.file.path, droppedFilename);
const maybeFile = this.app.vault.getAbstractFileByPath(filepath);
const maybeFile = this.app.metadataCache.getFirstLinkpathDest(droppedFilename, this.file.path);
if(maybeFile && maybeFile instanceof TFile) {
const action = await ScriptEngine.suggester(
this.app,[
@@ -385,12 +383,11 @@ export class DropManager {
"Overwrite",
"Import",
],
"A file with the same name/path already exists in the Vault",
"A file with the same name/path already exists in the Vault\n" +
maybeFile.path,
);
switch(action) {
case "Import":
const {folderpath,filename,basename:_,extension:__} = splitFolderAndFilename(filepath);
filepath = getNewUniqueFilepath(this.app.vault, filename, folderpath);
break;
case "Overwrite":
await this.app.vault.modifyBinary(maybeFile, fileToImport);
@@ -403,7 +400,13 @@ export class DropManager {
return false;
}
}
const file = await this.app.vault.createBinary(filepath, fileToImport)
const file = await importFileToVault(
this.app,
droppedFilename,
fileToImport,
this.view.file,
this.view
);
const ea = getEA(this.view) as ExcalidrawAutomate;
await insertImageToView(ea, pos, file);
ea.destroy();
@@ -412,8 +415,13 @@ export class DropManager {
return true; //excalidarw to continue processing
} else {
(async () => {
const {folder:_, filepath} = await getAttachmentsFolderAndFilePath(this.app, this.file.path,event.dataTransfer.files[i].name);
const file = await this.app.vault.createBinary(filepath, await event.dataTransfer.files[i].arrayBuffer());
const file = await importFileToVault(
this.app,
event.dataTransfer.files[i].name,
await event.dataTransfer.files[i].arrayBuffer(),
this.view.file,
this.view,
);
const modal = new UniversalInsertFileModal(this.plugin, this.view);
modal.open(file, pos);
})();

View File

@@ -349,6 +349,10 @@ label.color-input-container > input {
overflow-y: auto;
}
.excalidraw .App-mobile-menu {
width: 12.5rem !important;
}
.excalidraw .panelColumn .buttonList {
max-width: 13rem;
}