mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
63 Commits
move-input
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b203a6757 | ||
|
|
6689d9497c | ||
|
|
3e86419421 | ||
|
|
d2d4f16ebd | ||
|
|
e574d84b84 | ||
|
|
8f3ace89ea | ||
|
|
e4e4a07aa8 | ||
|
|
7a90bf753f | ||
|
|
6221d985ac | ||
|
|
41065ce9f9 | ||
|
|
351977f0d3 | ||
|
|
01b0b64a08 | ||
|
|
938e93cc43 | ||
|
|
cebcd2b43c | ||
|
|
80b6e30040 | ||
|
|
6e92a6a399 | ||
|
|
992ae8b238 | ||
|
|
5cd07e7766 | ||
|
|
07817a4d2d | ||
|
|
a22bb4c58c | ||
|
|
43e00a51be | ||
|
|
049b5bfa85 | ||
|
|
743afa73b4 | ||
|
|
6469eec051 | ||
|
|
9f46821a41 | ||
|
|
1fb3a47bdc | ||
|
|
bcd0cdda65 | ||
|
|
207fea3f57 | ||
|
|
e55ba3cc21 | ||
|
|
71c87a7630 | ||
|
|
e598a91f43 | ||
|
|
972fe1baea | ||
|
|
d7e1268afa | ||
|
|
7a3b937ea7 | ||
|
|
c850cd15ae | ||
|
|
e9f70fd09e | ||
|
|
2083443dfe | ||
|
|
69d9b8c1c9 | ||
|
|
65f4c9f3b3 | ||
|
|
b89a106523 | ||
|
|
6434a6e58a | ||
|
|
68180db2aa | ||
|
|
63505d17e9 | ||
|
|
0851b45977 | ||
|
|
16332a3e83 | ||
|
|
8240d87fa8 | ||
|
|
a7db044715 | ||
|
|
339c274f1b | ||
|
|
d5a19cbc09 | ||
|
|
400cffcd01 | ||
|
|
e5438c1e56 | ||
|
|
7e2ef3f115 | ||
|
|
ad091df4d9 | ||
|
|
0837c017a1 | ||
|
|
32d0301366 | ||
|
|
5accd657d9 | ||
|
|
8da97a63e0 | ||
|
|
a5d7731533 | ||
|
|
acb83fd697 | ||
|
|
271f21f85a | ||
|
|
371fb54787 | ||
|
|
3f19f9771a | ||
|
|
5a8596d113 |
17
.devcontainer/devcontainer.json
Normal file
17
.devcontainer/devcontainer.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -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
7
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "master"
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
660
ea-scripts/Printable Layout Wizard.md
Normal file
660
ea-scripts/Printable Layout Wizard.md
Normal file
@@ -0,0 +1,660 @@
|
||||
/*
|
||||
|
||||
Export Excalidraw to PDF Pages: Define printable page areas using frames, then export each frame as a separate page in a multi-page PDF. Perfect for turning your Excalidraw drawings into printable notes, handouts, or booklets. Supports standard and custom page sizes, margins, and easy frame arrangement.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
```js
|
||||
*/
|
||||
|
||||
|
||||
async function run() {
|
||||
// Help text for the script
|
||||
const HELP_TEXT = `
|
||||
**Easily split your Excalidraw drawing into printable pages!**
|
||||
|
||||
If you find this script helpful, consider [buying me a coffee](https://ko-fi.com/zsolt). Thank you.
|
||||
|
||||
---
|
||||
|
||||
### How it works
|
||||
|
||||
- **Define Pages:** Use frames to mark out each page area in your drawing. You can create the first frame with this script (choose a standard size or orientation), or draw your own frame for a custom page size.
|
||||
- **Add More Pages:** Select a frame, then use the arrow buttons to add new frames next to it. All new frames will match the size of the selected one.
|
||||
- **Rename Frames:** You can rename frames as you like. When exporting to PDF, pages will be ordered alphabetically by frame name.
|
||||
|
||||
---
|
||||
|
||||
### Important Notes
|
||||
|
||||
- **Same Size & Orientation:** All frames must have the same size and orientation (e.g., all A4 Portrait) to export to PDF. Excalidraw currently does not support PDFs with different-sized pages.
|
||||
- **Custom Sizes:** If you draw your own frame, the PDF will use that exact size—great for custom page layouts!
|
||||
- **Margins:** If you set a margin, the page size stays the same, but your content will shrink to fit inside the printable area.
|
||||
- **No Frame Borders/Titles in Print:** Frame borders and frame titles will *not* appear in the PDF.
|
||||
- **No Frame Clipping:** The script disables frame clipping for this drawing.
|
||||
- **Templates:** You can save a template document with prearranged frames (even locked ones) for reuse.
|
||||
- **Lock Frames:** Frames only define print areas—they don't "contain" elements. Locking frames is recommended to prevent accidental movement.
|
||||
- **Outside Content:** Anything outside the frames will *not* appear in the PDF.
|
||||
|
||||
---
|
||||
|
||||
### Printing
|
||||
|
||||
- **Export to PDF:** Click the printer button to export each frame as a separate page in a PDF.
|
||||
- **Order:** Pages are exported in alphabetical order of frame names.
|
||||
|
||||
---
|
||||
|
||||
### Settings
|
||||
|
||||
You can also access script settings at the bottom of Excalidraw Plugin settings. The script stores your preferences for:
|
||||
- Locking new frames after creation
|
||||
- Zooming to new frames
|
||||
- Closing the dialog after adding a frame
|
||||
- Default page size and orientation
|
||||
- Print margin
|
||||
|
||||
---
|
||||
|
||||
**Tip:** For more on templates, see [Mastering Excalidraw Templates](https://youtu.be/jgUpYznHP9A). For referencing pages in markdown, see [Image Fragments](https://youtu.be/sjZfdqpxqsg) and [Image Block References](https://youtu.be/yZQoJg2RCKI).
|
||||
`;
|
||||
|
||||
// Enable frame rendering
|
||||
const st = ea.getExcalidrawAPI().getAppState();
|
||||
const {enabled, clip, name, outline} = st.frameRendering;
|
||||
if(!enabled || clip || !name || !outline) {
|
||||
ea.viewUpdateScene({
|
||||
appState: {
|
||||
frameRendering: {
|
||||
enabled: true,
|
||||
clip: false,
|
||||
name: true,
|
||||
outline: true
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Page size options (using standard sizes from ExcalidrawAutomate)
|
||||
const PAGE_SIZES = [
|
||||
"A0", "A1", "A2", "A3", "A4", "A5", "A6",
|
||||
"Letter", "Legal", "Tabloid", "Ledger"
|
||||
];
|
||||
|
||||
const PAGE_ORIENTATIONS = ["portrait", "landscape"];
|
||||
|
||||
// Margin sizes in points
|
||||
const MARGINS = {
|
||||
"none": 0,
|
||||
"tiny": 10,
|
||||
"normal": 60,
|
||||
};
|
||||
|
||||
// Initialize settings
|
||||
let settings = ea.getScriptSettings();
|
||||
let dirty = false;
|
||||
|
||||
// Define setting keys
|
||||
const PAGE_SIZE = "Page size";
|
||||
const ORIENTATION = "Page orientation";
|
||||
const MARGIN = "Print-margin";
|
||||
const LOCK_FRAME = "Lock frame after it is created";
|
||||
const SHOULD_ZOOM = "Should zoom after adding page";
|
||||
const SHOULD_CLOSE = "Should close after adding page";
|
||||
|
||||
// Set default values on first run
|
||||
if (!settings[PAGE_SIZE]) {
|
||||
settings = {};
|
||||
settings[PAGE_SIZE] = { value: "A4", valueSet: PAGE_SIZES };
|
||||
settings[ORIENTATION] = { value: "portrait", valueSet: PAGE_ORIENTATIONS };
|
||||
settings[MARGIN] = { value: "none", valueSet: Object.keys(MARGINS)};
|
||||
settings[SHOULD_ZOOM] = { value: false };
|
||||
settings[SHOULD_CLOSE] = { value: false };
|
||||
settings[LOCK_FRAME] = { value: true };
|
||||
await ea.setScriptSettings(settings);
|
||||
}
|
||||
|
||||
const getSortedFrames = () => ea.getViewElements()
|
||||
.filter(el => el.type === "frame")
|
||||
.sort((a, b) => {
|
||||
const nameA = a.name || "";
|
||||
const nameB = b.name || "";
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
|
||||
// Find existing page frames and determine next page number
|
||||
const findExistingPages = (selectLastFrame = false) => {
|
||||
const frameElements = getSortedFrames();
|
||||
|
||||
// Extract page numbers from frame names
|
||||
const pageNumbers = frameElements
|
||||
.map(frame => {
|
||||
const match = frame.name?.match(/Page\s+(\d+)/i);
|
||||
return match ? parseInt(match[1]) : 0;
|
||||
})
|
||||
.filter(num => !isNaN(num));
|
||||
|
||||
// Find the highest page number
|
||||
const nextPageNumber = pageNumbers.length > 0
|
||||
? Math.max(...pageNumbers) + 1
|
||||
: 1;
|
||||
|
||||
if(selectLastFrame && frameElements.length > 0) {
|
||||
ea.selectElementsInView([frameElements[frameElements.length-1]]);
|
||||
}
|
||||
|
||||
return {
|
||||
frames: frameElements,
|
||||
nextPageNumber: nextPageNumber
|
||||
};
|
||||
};
|
||||
|
||||
// Check if there are frames in the scene and if a frame is selected
|
||||
let existingFrames = ea.getViewElements().filter(el => el.type === "frame");
|
||||
let selectedFrame = ea.getViewSelectedElements().find(el => el.type === "frame");
|
||||
const hasFrames = existingFrames.length > 0;
|
||||
if(hasFrames && !selectedFrame) {
|
||||
if(st.activeLockedId && existingFrames.find(f=>f.id === st.activeLockedId)) {
|
||||
selectedFrame = existingFrames.find(f=>f.id === st.activeLockedId);
|
||||
ea.viewUpdateScene({ appState: { activeLockedId: null }});
|
||||
ea.selectElementsInView([selectedFrame]);
|
||||
} else {
|
||||
findExistingPages(true);
|
||||
selectedFrame = ea.getViewSelectedElements().find(el => el.type === "frame");
|
||||
}
|
||||
}
|
||||
const hasSelectedFrame = !!selectedFrame;
|
||||
const modal = new ea.FloatingModal(ea.plugin.app);
|
||||
let lockFrame = !!settings[LOCK_FRAME]?.value;
|
||||
let shouldClose = settings[SHOULD_CLOSE].value;
|
||||
let shouldZoom = settings[SHOULD_ZOOM].value;
|
||||
|
||||
// Show notice if there are frames but none selected
|
||||
if (hasFrames && !hasSelectedFrame) {
|
||||
new Notice("Select a frame before running the script", 7000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the first frame
|
||||
const createFirstFrame = async (pageSize, orientation) => {
|
||||
// Use ExcalidrawAutomate's built-in function to get page dimensions
|
||||
const dimensions = ea.getPagePDFDimensions(pageSize, orientation);
|
||||
|
||||
if (!dimensions) {
|
||||
new Notice("Invalid page size selected");
|
||||
return;
|
||||
}
|
||||
|
||||
// Save settings when creating first frame
|
||||
if (settings[PAGE_SIZE].value !== pageSize) {
|
||||
settings[PAGE_SIZE].value = pageSize;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
if (settings[ORIENTATION].value !== orientation) {
|
||||
settings[ORIENTATION].value = orientation;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
// Format page number with leading zero
|
||||
const pageName = "Page 01";
|
||||
|
||||
// Calculate position to center the frame
|
||||
const appState = ea.getExcalidrawAPI().getAppState();
|
||||
const x = (appState.width - dimensions.width) / 2;
|
||||
const y = (appState.height - dimensions.height) / 2;
|
||||
|
||||
return await addFrameElement(x, y, dimensions.width, dimensions.height, pageName, true);
|
||||
};
|
||||
|
||||
// Add new page frame
|
||||
const addPage = async (direction, pageSize, orientation) => {
|
||||
selectedFrame = ea.getViewSelectedElements().find(el => el.type === "frame");
|
||||
if (!selectedFrame) return;
|
||||
|
||||
const { frames, nextPageNumber } = findExistingPages();
|
||||
|
||||
// Get dimensions from selected frame
|
||||
const dimensions = {
|
||||
width: selectedFrame.width,
|
||||
height: selectedFrame.height
|
||||
};
|
||||
|
||||
// Format page number with leading zero
|
||||
const pageName = `Page ${nextPageNumber.toString().padStart(2, '0')}`;
|
||||
|
||||
// Calculate position based on direction and selected frame
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
|
||||
switch (direction) {
|
||||
case "right":
|
||||
x = selectedFrame.x + selectedFrame.width;
|
||||
y = selectedFrame.y;
|
||||
break;
|
||||
case "left":
|
||||
x = selectedFrame.x - dimensions.width;
|
||||
y = selectedFrame.y;
|
||||
break;
|
||||
case "down":
|
||||
x = selectedFrame.x;
|
||||
y = selectedFrame.y + selectedFrame.height;
|
||||
break;
|
||||
case "up":
|
||||
x = selectedFrame.x;
|
||||
y = selectedFrame.y - dimensions.height;
|
||||
break;
|
||||
}
|
||||
|
||||
return await addFrameElement(x, y, dimensions.width, dimensions.height, pageName);
|
||||
};
|
||||
|
||||
addFrameElement = async (x, y, width, height, pageName, repositionToCursor = false) => {
|
||||
const frameId = ea.addFrame(x, y, width, height, pageName);
|
||||
if(lockFrame) {
|
||||
ea.getElement(frameId).locked = true;
|
||||
}
|
||||
await ea.addElementsToView(repositionToCursor);
|
||||
const addedFrame = ea.getViewElements().find(el => el.id === frameId);
|
||||
if(shouldZoom) {
|
||||
ea.viewZoomToElements(true, [addedFrame]);
|
||||
} else {
|
||||
ea.selectElementsInView([addedFrame]);
|
||||
}
|
||||
|
||||
//ready for the next frame
|
||||
ea.clear();
|
||||
selectedFrame = addedFrame;
|
||||
if(shouldClose) {
|
||||
modal.close();
|
||||
}
|
||||
return addedFrame;
|
||||
}
|
||||
|
||||
const translateToZero = ({ top, left, bottom, right }, padding=0) => {
|
||||
const {topX, topY, width, height} = ea.getBoundingBox(ea.getViewElements());
|
||||
const newTop = top - (topY - padding);
|
||||
const newLeft = left - (topX - padding);
|
||||
const newBottom = bottom - (topY - padding);
|
||||
const newRight = right - (topX - padding);
|
||||
|
||||
return {
|
||||
top: newTop,
|
||||
left: newLeft,
|
||||
bottom: newBottom,
|
||||
right: newRight,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if all frames have the same size
|
||||
const checkFrameSizes = (frames) => {
|
||||
if (frames.length <= 1) return true;
|
||||
|
||||
const referenceWidth = frames[0].width;
|
||||
const referenceHeight = frames[0].height;
|
||||
|
||||
return frames.every(frame =>
|
||||
Math.abs(frame.width - referenceWidth) < 1 &&
|
||||
Math.abs(frame.height - referenceHeight) < 1
|
||||
);
|
||||
};
|
||||
|
||||
// Print frames to PDF
|
||||
const printToPDF = async (marginSize) => {
|
||||
const margin = MARGINS[marginSize] || 0;
|
||||
|
||||
// Save margin setting
|
||||
if (settings[MARGIN].value !== marginSize) {
|
||||
settings[MARGIN].value = marginSize;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
// Get all frame elements and sort by name
|
||||
const frames = getSortedFrames();
|
||||
|
||||
if (frames.length === 0) {
|
||||
new Notice("No frames found to print");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if all frames have the same size
|
||||
if (!checkFrameSizes(frames)) {
|
||||
new Notice("Only same sized pages are supported currently", 7000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a notice during processing
|
||||
const notice = new Notice("Preparing PDF, please wait...", 0);
|
||||
|
||||
// Create SVGs for each frame
|
||||
const svgPages = [];
|
||||
|
||||
const svgScene = await ea.createViewSVG({
|
||||
withBackground: true,
|
||||
theme: st.theme,
|
||||
frameRendering: { enabled: false, name: false, outline: false, clip: false },
|
||||
padding: 0,
|
||||
selectedOnly: false,
|
||||
skipInliningFonts: false,
|
||||
embedScene: false,
|
||||
});
|
||||
|
||||
for (const frame of frames) {
|
||||
const { top, left, bottom, right } = translateToZero({
|
||||
top: frame.y,
|
||||
left: frame.x,
|
||||
bottom: frame.y + frame.height,
|
||||
right: frame.x + frame.width,
|
||||
});
|
||||
|
||||
//always create the new SVG in the main Obsidian workspace (not the popout window, if present)
|
||||
const host = window.createDiv();
|
||||
host.innerHTML = svgScene.outerHTML;
|
||||
const clonedSVG = host.firstElementChild;
|
||||
const width = Math.abs(left-right);
|
||||
const height = Math.abs(top-bottom);
|
||||
clonedSVG.setAttribute("viewBox", `${left} ${top} ${width} ${height}`);
|
||||
clonedSVG.setAttribute("width", `${width}`);
|
||||
clonedSVG.setAttribute("height", `${height}`);
|
||||
svgPages.push(clonedSVG);
|
||||
}
|
||||
|
||||
// Use dimensions from the first frame
|
||||
const width = frames[0].width;
|
||||
const height = frames[0].height;
|
||||
|
||||
// Create PDF
|
||||
await ea.createPDF({
|
||||
SVG: svgPages,
|
||||
scale: { fitToPage: true },
|
||||
pageProps: {
|
||||
dimensions: { width, height },
|
||||
backgroundColor: "#ffffff",
|
||||
margin: {
|
||||
left: margin,
|
||||
right: margin,
|
||||
top: margin,
|
||||
bottom: margin
|
||||
},
|
||||
alignment: "center"
|
||||
},
|
||||
filename: ea.targetView.file.basename + "-pages.pdf"
|
||||
});
|
||||
notice.hide();
|
||||
};
|
||||
|
||||
// -----------------------
|
||||
// Create a floating modal
|
||||
// -----------------------
|
||||
|
||||
modal.titleEl.setText("Page Management");
|
||||
modal.titleEl.style.textAlign = "center";
|
||||
|
||||
// Handle save settings on modal close
|
||||
modal.onClose = () => {
|
||||
if (dirty) {
|
||||
ea.setScriptSettings(settings);
|
||||
}
|
||||
};
|
||||
|
||||
// Create modal content
|
||||
modal.contentEl.createDiv({ cls: "excalidraw-page-manager" }, div => {
|
||||
const container = div.createDiv({
|
||||
attr: {
|
||||
style: "display: flex; flex-direction: column; gap: 15px; padding: 10px;"
|
||||
}
|
||||
});
|
||||
|
||||
// Add help section at the top
|
||||
const helpDiv = container.createDiv({
|
||||
attr: {
|
||||
style: "margin-bottom: 10px;"
|
||||
}
|
||||
});
|
||||
|
||||
helpDiv.createEl("details", {}, (details) => {
|
||||
details.createEl("summary", {
|
||||
text: "Help & Information",
|
||||
attr: {
|
||||
style: "cursor: pointer; font-weight: bold; margin-bottom: 10px;"
|
||||
}
|
||||
});
|
||||
|
||||
details.createEl("div", {
|
||||
attr: {
|
||||
style: "padding: 10px; border: 1px solid var(--background-modifier-border); border-radius: 4px; margin-top: 8px; font-size: 0.9em; max-height: 300px; overflow-y: auto;"
|
||||
}
|
||||
}, div => {
|
||||
ea.obsidian.MarkdownRenderer.render(ea.plugin.app, HELP_TEXT, div, "", ea.plugin)
|
||||
});
|
||||
});
|
||||
|
||||
let pageSizeDropdown, orientationDropdown, marginDropdown;
|
||||
|
||||
// Settings section - only show for first frame creation
|
||||
if (!hasFrames) {
|
||||
const settingsContainer = container.createDiv({
|
||||
attr: {
|
||||
style: "display: grid; grid-template-columns: auto 1fr; gap: 10px; align-items: center;"
|
||||
}
|
||||
});
|
||||
|
||||
// Page size dropdown
|
||||
settingsContainer.createEl("label", { text: "Page Size:" });
|
||||
pageSizeDropdown = settingsContainer.createEl("select", {
|
||||
cls: "dropdown",
|
||||
attr: { style: "width: 100%;" }
|
||||
});
|
||||
|
||||
PAGE_SIZES.forEach(size => {
|
||||
pageSizeDropdown.createEl("option", { text: size, value: size });
|
||||
});
|
||||
pageSizeDropdown.value = settings[PAGE_SIZE].value;
|
||||
|
||||
// Orientation dropdown
|
||||
settingsContainer.createEl("label", { text: "Orientation:" });
|
||||
orientationDropdown = settingsContainer.createEl("select", {
|
||||
cls: "dropdown",
|
||||
attr: { style: "width: 100%;" }
|
||||
});
|
||||
|
||||
PAGE_ORIENTATIONS.forEach(orientation => {
|
||||
orientationDropdown.createEl("option", { text: orientation, value: orientation });
|
||||
});
|
||||
orientationDropdown.value = settings[ORIENTATION].value;
|
||||
}
|
||||
|
||||
// Show margin settings only if frames exist
|
||||
if (hasFrames) {
|
||||
const marginContainer = container.createDiv({
|
||||
attr: {
|
||||
style: "display: grid; grid-template-columns: auto 1fr; gap: 10px; align-items: center;"
|
||||
}
|
||||
});
|
||||
|
||||
// Margin dropdown (for printing)
|
||||
marginContainer.createEl("label", { text: "Print Margin:" });
|
||||
marginDropdown = marginContainer.createEl("select", {
|
||||
cls: "dropdown",
|
||||
attr: { style: "width: 100%;" }
|
||||
});
|
||||
|
||||
Object.keys(MARGINS).forEach(margin => {
|
||||
marginDropdown.createEl("option", { text: margin, value: margin });
|
||||
});
|
||||
marginDropdown.value = settings[MARGIN].value;
|
||||
}
|
||||
|
||||
// Add checkboxes for zoom and modal behavior only when frames exist
|
||||
const optionsContainer = container.createDiv({
|
||||
attr: {
|
||||
style: "margin-top: 10px;"
|
||||
}
|
||||
});
|
||||
|
||||
new ea.obsidian.Setting(optionsContainer)
|
||||
.setName("Lock")
|
||||
.setDesc("Lock the new frame element after it is created.")
|
||||
.addToggle(toggle => {
|
||||
toggle.setValue(lockFrame)
|
||||
.onChange(value => {
|
||||
lockFrame = value;
|
||||
if (settings[LOCK_FRAME].value !== value) {
|
||||
settings[LOCK_FRAME].value = value;
|
||||
dirty = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Zoom to added frame checkbox
|
||||
new ea.obsidian.Setting(optionsContainer)
|
||||
.setName("Zoom to new frame")
|
||||
.setDesc("Automatically zoom to the newly created frame")
|
||||
.addToggle(toggle => {
|
||||
toggle.setValue(shouldZoom)
|
||||
.onChange(value => {
|
||||
shouldZoom = value;
|
||||
if (settings[SHOULD_ZOOM].value !== value) {
|
||||
settings[SHOULD_ZOOM].value = value;
|
||||
dirty = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Close after adding checkbox
|
||||
new ea.obsidian.Setting(optionsContainer)
|
||||
.setName("Close after adding")
|
||||
.setDesc("Close this dialog after adding a new frame")
|
||||
.addToggle(toggle => {
|
||||
toggle.setValue(shouldClose)
|
||||
.onChange(value => {
|
||||
shouldClose = value;
|
||||
if (settings[SHOULD_CLOSE].value !== value) {
|
||||
settings[SHOULD_CLOSE].value = value;
|
||||
dirty = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Buttons section
|
||||
const buttonContainer = container.createDiv({
|
||||
attr: {
|
||||
style: "display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 10px;"
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasFrames) {
|
||||
// First frame creation button (centered)
|
||||
const createFirstBtn = buttonContainer.createEl("button", {
|
||||
cls: "page-btn",
|
||||
attr: {
|
||||
style: "grid-column: 1 / span 3; height: 40px; background-color: var(--interactive-accent); color: var(--text-on-accent);"
|
||||
}
|
||||
});
|
||||
createFirstBtn.textContent = "Create First Frame";
|
||||
createFirstBtn.addEventListener("click", async () => {
|
||||
const tmpShouldClose = shouldClose;
|
||||
shouldClose = true;
|
||||
await createFirstFrame(pageSizeDropdown.value, orientationDropdown.value);
|
||||
shouldClose = tmpShouldClose;
|
||||
if(!shouldClose) run();
|
||||
});
|
||||
} else if (hasSelectedFrame) {
|
||||
// Only show navigation buttons and print when a frame is selected
|
||||
|
||||
// Up button (in middle of top row)
|
||||
const upBtn = buttonContainer.createEl("button", {
|
||||
cls: "page-btn",
|
||||
attr: {
|
||||
style: "grid-column: 2; grid-row: 1; height: 40px;"
|
||||
}
|
||||
});
|
||||
upBtn.innerHTML = ea.obsidian.getIcon("arrow-big-up").outerHTML;
|
||||
upBtn.addEventListener("click", async () => {
|
||||
await addPage("up");
|
||||
});
|
||||
|
||||
// Add empty space
|
||||
buttonContainer.createDiv({
|
||||
attr: { style: "grid-column: 3; grid-row: 1;" }
|
||||
});
|
||||
|
||||
// Left button
|
||||
const leftBtn = buttonContainer.createEl("button", {
|
||||
cls: "page-btn",
|
||||
attr: { style: "grid-column: 1; grid-row: 2; height: 40px;" }
|
||||
});
|
||||
leftBtn.innerHTML = ea.obsidian.getIcon("arrow-big-left").outerHTML;
|
||||
leftBtn.addEventListener("click", async () => {
|
||||
await addPage("left");
|
||||
});
|
||||
|
||||
// Print button (center)
|
||||
const printBtn = buttonContainer.createEl("button", {
|
||||
cls: "page-btn",
|
||||
attr: {
|
||||
style: "grid-column: 2; grid-row: 2; height: 40px; background-color: var(--interactive-accent);"
|
||||
}
|
||||
});
|
||||
printBtn.innerHTML = ea.obsidian.getIcon("printer").outerHTML;
|
||||
printBtn.addEventListener("click", async () => {
|
||||
await printToPDF(marginDropdown.value);
|
||||
});
|
||||
|
||||
// Right button
|
||||
const rightBtn = buttonContainer.createEl("button", {
|
||||
cls: "page-btn",
|
||||
attr: { style: "grid-column: 3; grid-row: 2; height: 40px;" }
|
||||
});
|
||||
rightBtn.innerHTML = ea.obsidian.getIcon("arrow-big-right").outerHTML;
|
||||
rightBtn.addEventListener("click", async () => {
|
||||
await addPage("right");
|
||||
});
|
||||
|
||||
// Down button (in middle of bottom row)
|
||||
const downBtn = buttonContainer.createEl("button", {
|
||||
cls: "page-btn",
|
||||
attr: { style: "grid-column: 2; grid-row: 3; height: 40px;" }
|
||||
});
|
||||
downBtn.innerHTML = ea.obsidian.getIcon("arrow-big-down").outerHTML;
|
||||
downBtn.addEventListener("click", async () => {
|
||||
await addPage("down");
|
||||
});
|
||||
|
||||
// Add empty space
|
||||
buttonContainer.createDiv({
|
||||
attr: { style: "grid-column: 1; grid-row: 3;" }
|
||||
});
|
||||
}
|
||||
|
||||
// Add CSS
|
||||
div.createEl("style", {
|
||||
text: `
|
||||
.page-btn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.page-btn:hover {
|
||||
background-color: var(--interactive-hover);
|
||||
}
|
||||
.dropdown {
|
||||
height: 30px;
|
||||
background-color: var(--background-secondary);
|
||||
color: var(--text-normal);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
padding: 0 10px;
|
||||
}
|
||||
`
|
||||
});
|
||||
});
|
||||
|
||||
modal.open();
|
||||
}
|
||||
|
||||
run();
|
||||
1
ea-scripts/Printable Layout Wizard.svg
Normal file
1
ea-scripts/Printable Layout Wizard.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg class="skip" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><path d="M6 9V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6"/><rect x="6" y="14" width="12" height="8" rx="1"/></svg>
|
||||
|
After Width: | Height: | Size: 386 B |
@@ -7,28 +7,23 @@ This script modifies the color lightness/hue/saturation/transparency of selected
|
||||
- The color of SVG elements and nested Excalidraw drawings will only be mapped. When mapping colors, the original image remains unchanged, only a mapping table is created and the image is recolored during rendering of your Excalidraw screen. In case you want to make manual changes you can also edit the mapping in Markdown View Mode under `## Embedded Files`
|
||||
|
||||
If you select only a single SVG or nested Excalidraw element, then the script offers an additional feature. You can map colors one by one in the image.
|
||||
```js*/
|
||||
```js
|
||||
*/
|
||||
|
||||
const HELP_TEXT = `
|
||||
<ul>
|
||||
<li dir="auto">Select SVG images, nested Excalidraw drawings and/or regular Excalidraw elements</li>
|
||||
<li dir="auto">For a single selected image, you can map colors individually in the color mapping section</li>
|
||||
<li dir="auto">For Excalidraw elements: stroke and background colors are modified permanently</li>
|
||||
<li dir="auto">For SVG/nested drawings: original files stay unchanged, color mapping is stored under <code>## Embedded Files</code></li>
|
||||
<li dir="auto">Using color maps helps maintain links between drawings while allowing different color themes</li>
|
||||
<li dir="auto">Sliders work on relative scale - the amount of change is applied to current values</li>
|
||||
<li dir="auto">Unlike Excalidraw's opacity setting which affects the whole element:
|
||||
<ul>
|
||||
<li dir="auto">Shade Master can set different opacity for stroke vs background</li>
|
||||
<li dir="auto">Note: SVG/nested drawing colors are mapped at color name level, thus "black" is different from "#000000"</li>
|
||||
<li dir="auto">Additionally if the same color is used as fill and stroke the color can only be mapped once</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li dir="auto">This is an experimental script - contributions welcome on GitHub via PRs</li>
|
||||
</ul>
|
||||
<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>
|
||||
- Select SVG images, nested Excalidraw drawings and/or regular Excalidraw elements
|
||||
- For a single selected image, you can map colors individually in the color mapping section
|
||||
- For Excalidraw elements: stroke and background colors are modified permanently
|
||||
- For SVG/nested drawings: original files stay unchanged, color mapping is stored under \`## Embedded Files\`
|
||||
- Using color maps helps maintain links between drawings while allowing different color themes
|
||||
- Sliders work on relative scale - the amount of change is applied to current values
|
||||
- Unlike Excalidraw's opacity setting which affects the whole element:
|
||||
- Shade Master can set different opacity for stroke vs background
|
||||
- **Note:** SVG/nested drawing colors are mapped at color name level, thus "black" is different from "#000000"
|
||||
- Additionally if the same color is used as fill and stroke the color can only be mapped once
|
||||
- This is an experimental script - contributions welcome on GitHub via PRs
|
||||
|
||||

|
||||
`;
|
||||
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.7.2")) {
|
||||
@@ -343,7 +338,8 @@ function showModal() {
|
||||
const helpDetailsDiv = helpDiv.createEl("div", {
|
||||
attr: { style: "margin-top: 0em; " }
|
||||
});
|
||||
helpDetailsDiv.innerHTML = HELP_TEXT;
|
||||
//helpDetailsDiv.innerHTML = HELP_TEXT;
|
||||
await ea.obsidian.MarkdownRenderer.render(ea.plugin.app, HELP_TEXT, helpDetailsDiv, "", ea.plugin);
|
||||
|
||||
const component = new ea.obsidian.Setting(contentEl)
|
||||
.setName(FORMAT)
|
||||
|
||||
@@ -97,6 +97,7 @@ const text = (await utils.inputPrompt({
|
||||
: textEl?.text ?? "",
|
||||
lines: 3,
|
||||
customComponents: isCircle ? circleArchControl : offsetControl,
|
||||
draggable: true,
|
||||
}))?.replace(" \n"," ").replace("\n ", " ").replace("\n"," ");
|
||||
|
||||
if(!text) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -47,6 +47,7 @@ I would love to include your contribution in the script library. If you have a s
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Golden%20Ratio.svg"/></div>|[[#Golden Ratio]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Grid%20Selected%20Images.svg"/></div>|[[#Grid selected images]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Mindmap%20format.svg"/></div>|[[#Mindmap format]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Printable%20Layout%20Wizard.svg"/></div>|[[#Printable Layout Wizard]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Zoom%20to%20Fit%20Selected%20Elements.svg"/></div>|[[#Zoom to Fit Selected Elements]]|
|
||||
|
||||
## Connectors and Arrows
|
||||
@@ -632,6 +633,12 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Uniform%20size.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data"><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-uniform-size.jpg"><br>The script will standardize the sizes of rectangles, diamonds and ellipses adjusting all the elements to match the largest width and height within the group.</td></tr></table>
|
||||
|
||||
# Printable Layout Wizard
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Printable%20Layout%20Wizard.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/Printable%20Layout%20Wizard.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Export Excalidraw to PDF Pages: Define printable page areas using frames, then export each frame as a separate page in a multi-page PDF. Perfect for turning your Excalidraw drawings into printable notes, handouts, or booklets. Supports standard and custom page sizes, margins, and easy frame arrangement.<br><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-layout-wizard-01.png" style="max-width: 400px;"><br><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-layout-wizard-02.png" style="max-width: 400px;"></td></tr></table>
|
||||
|
||||
## Zoom to Fit Selected Elements
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Zoom%20to%20Fit%20Selected%20Elements.md
|
||||
|
||||
BIN
images/scripts-layout-wizard-01.png
Normal file
BIN
images/scripts-layout-wizard-01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
BIN
images/scripts-layout-wizard-02.png
Normal file
BIN
images/scripts-layout-wizard-02.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "2.12.0",
|
||||
"minAppVersion": "1.1.6",
|
||||
"version": "2.14.1",
|
||||
"minAppVersion": "1.5.7",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
"authorUrl": "https://excalidraw-obsidian.online",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "2.12.0",
|
||||
"minAppVersion": "1.1.6",
|
||||
"version": "2.14.1",
|
||||
"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
|
||||
}
|
||||
}
|
||||
19099
package-lock.json
generated
19099
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -21,10 +21,10 @@
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@zsviczian/excalidraw": "0.18.0-17",
|
||||
"chroma-js": "^2.4.2",
|
||||
"@zsviczian/excalidraw": "0.18.0-28",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ const jsxRuntimeShim = `
|
||||
|
||||
const mathjaxtosvg_pkg = isLib ? "" : fs.readFileSync("./MathjaxToSVG/dist/index.js", "utf8");
|
||||
|
||||
const LANGUAGES = ['ru', 'zh-cn']; //english is not compressed as it is always loaded by default
|
||||
const LANGUAGES = ['ru', 'zh-cn', 'zh-tw', 'es']; //english is not compressed as it is always loaded by default
|
||||
|
||||
function trimLastSemicolon(input) {
|
||||
if (input.endsWith(";")) {
|
||||
|
||||
@@ -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) => {};
|
||||
|
||||
@@ -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;
|
||||
@@ -79,7 +79,7 @@ export const obsidianToExcalidrawMap: { [key: string]: string } = {
|
||||
'ur': 'ur-PK', // Assuming Pakistan for Urdu
|
||||
'vi': 'vi-VN',
|
||||
'zh': 'zh-CN',
|
||||
'zh-TW': 'zh-TW',
|
||||
'zh-tw': 'zh-TW',
|
||||
};
|
||||
|
||||
|
||||
@@ -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";
|
||||
@@ -230,6 +262,7 @@ export const FRONTMATTER_KEYS:{[key:string]: {name: string, type: string, depric
|
||||
"iframe-theme": {name: "excalidraw-iframe-theme", type: "text", depricated: true},
|
||||
"embeddable-theme": {name: "excalidraw-embeddable-theme", type: "text"},
|
||||
"open-as-markdown": {name: "excalidraw-open-md", type: "checkbox"},
|
||||
"embed-as-markdown": {name: "excalidraw-embed-md", type: "checkbox"},
|
||||
};
|
||||
|
||||
export const CaptureUpdateAction = {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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,
|
||||
@@ -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)),
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
})();
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -710,7 +710,7 @@ const isTextOnlyEmbed = (internalEmbedEl: Element):boolean => {
|
||||
if(!src) return true; //technically this does not mean this is a text only embed, but still should abort further processing
|
||||
const fnameParts = getEmbeddedFilenameParts(src);
|
||||
return !(fnameParts.hasArearef || fnameParts.hasGroupref || fnameParts.hasFrameref || fnameParts.hasClippedFrameref) &&
|
||||
(fnameParts.hasBlockref || fnameParts.hasSectionref)
|
||||
(fnameParts.hasBlockref || fnameParts.hasSectionref) && fnameParts.blockref !== "as-image"
|
||||
}
|
||||
|
||||
const tmpObsidianWYSIWYG = async (
|
||||
@@ -724,6 +724,9 @@ const tmpObsidianWYSIWYG = async (
|
||||
const file = app.vault.getAbstractFileByPath(ctx.sourcePath);
|
||||
if(!(file instanceof TFile)) return;
|
||||
if(!plugin.isExcalidrawFile(file)) return;
|
||||
if(ctx.frontmatter?.["excalidraw-embed-md"]) {
|
||||
return;
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
if (ctx.remainingNestLevel < 4) {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,10 @@ import {
|
||||
ButtonComponent,
|
||||
DropdownComponent,
|
||||
getIcon,
|
||||
htmlToMarkdown,
|
||||
Modifier,
|
||||
normalizePath,
|
||||
Notice,
|
||||
PluginSettingTab,
|
||||
Setting,
|
||||
TextComponent,
|
||||
@@ -18,6 +20,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 +31,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 +46,7 @@ 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";
|
||||
import { ContentSearcher } from "src/shared/components/ContentSearcher";
|
||||
|
||||
export interface ExcalidrawSettings {
|
||||
disableDoubleClickTextEditing: boolean;
|
||||
@@ -69,7 +72,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 +257,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 +327,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
experimentalFileTag: "✏️",
|
||||
experimentalLivePreview: true,
|
||||
fadeOutExcalidrawMarkup: false,
|
||||
loadPropertySuggestions: true,
|
||||
loadPropertySuggestions: false,
|
||||
experimentalEnableFourthFont: false,
|
||||
experimantalFourthFont: "Virgil",
|
||||
addDummyTextElement: false,
|
||||
@@ -600,6 +607,31 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
containerEl.addClass("excalidraw-settings");
|
||||
this.containerEl.empty();
|
||||
|
||||
// ------------------------------------------------
|
||||
// Search and Settings to Clipboard
|
||||
// ------------------------------------------------
|
||||
|
||||
const notebookLMLinkContainer = createDiv("setting-item-description excalidraw-settings-links-container");
|
||||
new ContentSearcher(containerEl, notebookLMLinkContainer);
|
||||
notebookLMLinkContainer.createEl("a",{
|
||||
href: "https://notebooklm.google.com/notebook/42d76a2f-c11d-4002-9286-1683c43d0ab0",
|
||||
attr: {
|
||||
"aria-label": t("NOTEBOOKLM_LINK_ARIA"),
|
||||
"style": "margin: auto;"
|
||||
}},
|
||||
(a)=> {
|
||||
//Lucide: message-circle-question-mark
|
||||
a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle-question-mark-icon lucide-message-circle-question-mark"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>${
|
||||
t("NOTEBOOKLM_LINK_TEXT")
|
||||
}`;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
// ------------------------------------------------
|
||||
// Promo links
|
||||
// ------------------------------------------------
|
||||
|
||||
const coffeeDiv = containerEl.createDiv("coffee");
|
||||
coffeeDiv.addClass("ex-coffee-div");
|
||||
const coffeeLink = coffeeDiv.createEl("a", {
|
||||
@@ -614,41 +646,53 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
|
||||
const iconLinks = [
|
||||
{
|
||||
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"></path><path d="M9 18c-4.51 2-5-2-7-2"></path></svg>`,
|
||||
icon: getIcon("bug").outerHTML,
|
||||
href: "https://github.com/zsviczian/obsidian-excalidraw-plugin/issues",
|
||||
aria: "Report bugs and raise feature requsts on the plugin's GitHub page",
|
||||
text: "Bugs and Feature Requests",
|
||||
aria: t("LINKS_BUGS_ARIA"),
|
||||
text: t("LINKS_BUGS"),
|
||||
},
|
||||
{
|
||||
icon: getIcon("globe").outerHTML,
|
||||
href: "https://excalidraw-obsidian.online/",
|
||||
aria: t("LINKS_WIKI_ARIA"),
|
||||
text: t("LINKS_WIKI"),
|
||||
},
|
||||
{
|
||||
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19c-2.3 0-6.4-.2-8.1-.6-.7-.2-1.2-.7-1.4-1.4-.3-1.1-.5-3.4-.5-5s.2-3.9.5-5c.2-.7.7-1.2 1.4-1.4C5.6 5.2 9.7 5 12 5s6.4.2 8.1.6c.7.2 1.2.7 1.4 1.4.3 1.1.5 3.4.5 5s-.2 3.9-.5 5c-.2.7-.7 1.2-1.4 1.4-1.7.4-5.8.6-8.1.6 0 0 0 0 0 0z"></path><polygon points="10 15 15 12 10 9"></polygon></svg>`,
|
||||
icon: getIcon("youtube").outerHTML,
|
||||
href: "https://www.youtube.com/@VisualPKM",
|
||||
aria: "Check out my YouTube channel to learn about Visual Thinking and Excalidraw",
|
||||
text: "Visual PKM on YouTube",
|
||||
aria: t("LINKS_YT_ARIA"),
|
||||
text: t("LINKS_YT"),
|
||||
},
|
||||
{
|
||||
icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" stroke="none" strokeWidth="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 640 512"><path d="M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z"/></svg>`,
|
||||
href: "https://discord.gg/DyfAXFwUHc",
|
||||
aria: "Join the Visual Thinking Workshop Discord Server",
|
||||
text: "Community on Discord",
|
||||
aria: t("LINKS_DISCORD_ARIA"),
|
||||
text: t("LINKS_DISCORD"),
|
||||
},
|
||||
{
|
||||
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"></path></svg>`,
|
||||
icon: getIcon("twitter").outerHTML,
|
||||
href: "https://twitter.com/zsviczian",
|
||||
aria: "Follow me on Twitter",
|
||||
text: "Follow me on Twitter",
|
||||
aria: t("LINKS_TWITTER"),
|
||||
text: t("LINKS_TWITTER"),
|
||||
},
|
||||
{
|
||||
icon: getIcon("graduation-cap").outerHTML,
|
||||
href: "https://visual-thinking-workshop.com",
|
||||
aria: "Learn about Visual PKM, Excalidraw, Obsidian, ExcaliBrain and more",
|
||||
text: "Join the Visual Thinking Workshop",
|
||||
}
|
||||
aria: t("LINKS_VTW_ARIA"),
|
||||
text: t("LINKS_VTW"),
|
||||
},
|
||||
{
|
||||
icon: getIcon("book").outerHTML,
|
||||
href: "https://sketch-your-mind.com",
|
||||
aria: t("LINKS_BOOK_ARIA"),
|
||||
text: t("LINKS_BOOK"),
|
||||
},
|
||||
];
|
||||
|
||||
const linksEl = containerEl.createDiv("setting-item-description excalidraw-settings-links-container");
|
||||
iconLinks.forEach(({ icon, href, aria, text }) => {
|
||||
linksEl.createEl("a",{href, attr: { "aria-label": aria }}, (a)=> {
|
||||
a.innerHTML = icon + text;
|
||||
a.innerHTML = icon + text;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -956,7 +1000,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 +1012,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 +1045,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 +2126,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 +2954,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);
|
||||
|
||||
149
src/lang/langDiffWithEn.js
Normal file
149
src/lang/langDiffWithEn.js
Normal file
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Language Pack Diff Checker Script
|
||||
*
|
||||
* Purpose:
|
||||
* Compare src/lang/locale/en.ts and a target language file (e.g. zh-cn.ts), output missing and extra keys.
|
||||
* Output is color-coded:
|
||||
* - Green: Key exists in en.ts but missing in the target language file
|
||||
* - Red: Key exists in the target language file but not in en.ts
|
||||
* - Yellow: The same key exists in both files but the line numbers are different
|
||||
*
|
||||
* Dependencies:
|
||||
* Node.js environment, no extra dependencies required.
|
||||
*
|
||||
* Usage:
|
||||
* 1. Open a terminal in this file's directory (or project root).
|
||||
* 2. Run:
|
||||
* node src/lang/langDiffWithEn.js zh-cn
|
||||
* # or node src/lang/langDiffWithEn.js ja
|
||||
* # The argument is the language file name (without extension), default is zh-cn
|
||||
*
|
||||
* Output:
|
||||
* + (green): Key exists in en.ts but missing in the target language file
|
||||
* - (red): Key exists in the target language file but not in en.ts
|
||||
* ! (yellow): The same key exists in both files but the line numbers are different
|
||||
* If there is no output, the two files have identical keys.
|
||||
*/
|
||||
/**
|
||||
* 语言包差异检测脚本
|
||||
*
|
||||
* 用途:
|
||||
* 比较 src/lang/locale/en.ts 与指定语言文件(如 zh-cn.ts)中的 key,输出缺失和多余的 key。
|
||||
* 输出内容会用不同颜色区分:
|
||||
* - 绿色:en.ts 有但目标语言缺失的 key
|
||||
* - 红色:目标语言有但 en.ts 没有的 key
|
||||
* - 黄色:相同的 key 所在的行号不相同
|
||||
*
|
||||
* 依赖:
|
||||
* Node.js 环境,无需额外依赖。
|
||||
*
|
||||
* 用法:
|
||||
* 1. 在命令行进入本文件所在目录(或项目根目录)。
|
||||
* 2. 运行:
|
||||
* node src/lang/langDiffWithEn.js zh-cn
|
||||
* # 或 node src/lang/langDiffWithEn.js ja
|
||||
* # 参数为语言文件名(不带扩展名),默认为 zh-cn
|
||||
*
|
||||
* 输出说明:
|
||||
* + 开头(绿色):en.ts 有但目标语言缺失的 key
|
||||
* - 开头(红色):目标语言有但 en.ts 没有的 key
|
||||
* ! 开头(黄色):相同的 key 所在的行号不相同
|
||||
* 若无输出则表示两个文件 key 完全一致。
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
/**
|
||||
* 读取 locale 文件夹下指定语言和 en.ts 文件,逐行解析键值和行号
|
||||
* @param lang 语言文件名(不含扩展名),如 zh-cn
|
||||
* @returns { en: Record<string, {value: string, line: number}>, lang: Record<string, {value: string, line: number}> }
|
||||
*/
|
||||
function readLocaleFiles(lang = "zh-cn") {
|
||||
// 构造 locale 目录路径
|
||||
const localeDir = path.join(__dirname, "./locale");
|
||||
// 获取 en.ts 文件路径
|
||||
const enFile = path.join(localeDir, "en.ts");
|
||||
// 获取指定语言文件路径
|
||||
const langFile = path.join(localeDir, `${lang}.ts`);
|
||||
/**
|
||||
* 解析指定的语言文件,提取每个 key 的值和所在行号
|
||||
* @param file 文件路径
|
||||
* @returns {Record<string, {value: string, line: number}>}
|
||||
*/
|
||||
const parse = (file) => {
|
||||
// 读取文件内容并按行分割
|
||||
const lines = fs.readFileSync(file, "utf8").split(/\r?\n/);
|
||||
const result = {};
|
||||
let inExport = false;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
// 检查是否进入 export default 块
|
||||
if (!inExport && line.includes("export default")) {
|
||||
inExport = true;
|
||||
continue;
|
||||
}
|
||||
if (!inExport) continue;
|
||||
// 匹配 key: "value", 格式的行
|
||||
const m = line.match(/^\s*([A-Z0-9_]+)\s*:\s*.*?,?$/);
|
||||
if (m) {
|
||||
let key = m[1];
|
||||
// 保存 key 及其所在行号
|
||||
result[key] = i + 1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
// 返回 en 和指定语言的解析结果
|
||||
return {
|
||||
en: parse(enFile),
|
||||
lang: parse(langFile),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较 en 和 lang 的 key/value,输出差异
|
||||
*/
|
||||
|
||||
function diffLang(lang = "zh-cn") {
|
||||
const { en, lang: l } = readLocaleFiles(lang);
|
||||
const allKeys = new Set([...Object.keys(en), ...Object.keys(l)]);
|
||||
const diffs = [];
|
||||
for (const key of allKeys) {
|
||||
if (!(key in en)) {
|
||||
diffs.push(`- ${key}: ${l[key]}`);
|
||||
} else if (!(key in l)) {
|
||||
diffs.push(`+ ${key}: ${en[key]}`);
|
||||
} else {
|
||||
if (en[key] !== l[key]) {
|
||||
diffs.push(`! ${key}: en: ${en[key]} <--> lang: ${l[key]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return diffs;
|
||||
}
|
||||
|
||||
// CLI
|
||||
if (require.main === module) {
|
||||
const lang = process.argv[2] || "zh-cn";
|
||||
const diffs = diffLang(lang);
|
||||
if (diffs.length === 0) {
|
||||
console.log(`语言文件 ${lang} 与 en.ts 无差异`);
|
||||
} else {
|
||||
diffs.forEach((line) => {
|
||||
if (line.startsWith("+")) {
|
||||
// 绿色
|
||||
console.log("\x1b[32m%s\x1b[0m", line);
|
||||
} else if (line.startsWith("-")) {
|
||||
// 红色
|
||||
console.log("\x1b[31m%s\x1b[0m", line);
|
||||
} else if (line.startsWith("!")) {
|
||||
// 黄色
|
||||
console.log("\x1b[33m%s\x1b[0m", line);
|
||||
} else {
|
||||
// 默认
|
||||
console.log(line);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -158,6 +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",
|
||||
BACKUP_SAVE_AS_FILE: "This drawing is empty. A non-empty backup is available. Would you like to restore 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",
|
||||
@@ -188,7 +192,32 @@ export default {
|
||||
SAVE_IS_TAKING_LONG: "Saving your previous file is taking a long time. Please wait...",
|
||||
SAVE_IS_TAKING_VERY_LONG: "For better performance, consider splitting large drawings into several smaller files.",
|
||||
|
||||
//ContentSearcher.ts
|
||||
SEARCH_COPIED_TO_CLIPBOARD: "Markdown ready on clipboard",
|
||||
SEARCH_COPY_TO_CLIPBOARD_ARIA: "Copy the entire dialog to the clipboard as Markdown. Ideal for use with tools like ChatGPT to search and understand.",
|
||||
SEARCH_SHOWHIDE_ARIA: "Show/Hide search bar",
|
||||
SEARCH_NEXT: "Next",
|
||||
SEARCH_PREVIOUS: "Previous",
|
||||
|
||||
|
||||
|
||||
//settings.ts
|
||||
NOTEBOOKLM_LINK_ARIA: "Ask NotebookLM for help about the plugin. This model is pre-loaded with all my video transcripts, release notes and other helpful content. Chat with NotebookLM to explore my 250+ videos and the Excalidraw documentation.",
|
||||
NOTEBOOKLM_LINK_TEXT: "Learn the Plugin. Access the NotebookLM knowledgebase.",
|
||||
LINKS_BUGS_ARIA: "Report bugs and raise feature requsts on the plugin's GitHub page",
|
||||
LINKS_BUGS: "Report Bugs",
|
||||
LINKS_YT_ARIA: "Check out my YouTube channel to learn about Visual Thinking and Excalidraw",
|
||||
LINKS_YT: "Learn on YouTube",
|
||||
LINKS_DISCORD_ARIA: "Join the Visual Thinking Workshop Discord Server",
|
||||
LINKS_DISCORD: "Join the Community",
|
||||
LINKS_TWITTER: "Follow me",
|
||||
LINKS_VTW_ARIA: "Learn about Visual PKM, Excalidraw, Obsidian, ExcaliBrain and more",
|
||||
LINKS_VTW: "Join a Workshop",
|
||||
LINKS_BOOK_ARIA: "Read Sketch Your Mind, my book on Visual Thinking",
|
||||
LINKS_BOOK: "Read the Book",
|
||||
LINKS_WIKI: "Plugin Wiki",
|
||||
LINKS_WIKI_ARIA: "Explore the Excalidraw Plugin Wiki",
|
||||
|
||||
RELEASE_NOTES_NAME: "Display Release Notes after update",
|
||||
RELEASE_NOTES_DESC:
|
||||
"<b><u>Toggle ON:</u></b> Display release notes each time you update Excalidraw to a newer version.<br>" +
|
||||
@@ -203,17 +232,25 @@ 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 suffix.",
|
||||
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.",
|
||||
"When annotating an image in markdown the replacement image link will include the width of the original image.",
|
||||
CROP_FOLDER_NAME: "Crop file folder (CaSE senSItive!)",
|
||||
CROP_FOLDER_DESC:
|
||||
"Default location for new drawings created when cropping an image. If empty, drawings will be created following the Vault attachments settings.",
|
||||
@@ -233,7 +270,7 @@ export default {
|
||||
"Template.md, the setting would be: Excalidraw/Template.md (or just Excalidraw/Template - you may omit the .md file extension). " +
|
||||
"If you are using Excalidraw in compatibility mode, then your template must be a legacy Excalidraw file as well " +
|
||||
"such as Excalidraw/Template.excalidraw. <br><b>Template Folder:</b> You can also set a folder as your template. " +
|
||||
"In this case you will be prompted which tempalte to use when creating a new drawing.<br>" +
|
||||
"In this case you will be prompted which template to use when creating a new drawing.<br>" +
|
||||
"<b>Pro Tip:</b> If you are using the Obsidian Templater plugin, you can add Templater code to your different Excalidraw " +
|
||||
"templates to automate configuration of your drawings.",
|
||||
SCRIPT_FOLDER_NAME: "Excalidraw Automate script folder (CASE SeNSitiVE!)",
|
||||
@@ -345,7 +382,7 @@ FILENAME_HEAD: "Filename",
|
||||
LEFTHANDED_MODE_NAME: "Left-handed mode",
|
||||
LEFTHANDED_MODE_DESC:
|
||||
"Currently only has effect in tray-mode. If turned on, the tray will be on the right side." +
|
||||
"<br><b><u>Toggle ON:</u></b> Left-handed mode.<br><b><u>Toggle OFF:</u></b> Right-handed moded",
|
||||
"<br><b><u>Toggle ON:</u></b> Left-handed mode.<br><b><u>Toggle OFF:</u></b> Right-handed mode.",
|
||||
IFRAME_MATCH_THEME_NAME: "Markdown embeds to match Excalidraw theme",
|
||||
IFRAME_MATCH_THEME_DESC:
|
||||
"<b><u>Toggle ON:</u></b> Set this to true if for example you are using Obsidian in dark-mode but use excalidraw with a light background. " +
|
||||
@@ -382,7 +419,8 @@ FILENAME_HEAD: "Filename",
|
||||
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_DESC:
|
||||
"...even if the file has the <b>excalidraw-open-md: true</b> frontmatter key.<br>" +
|
||||
"When this setting is off and the file is set to open in md by default, the hover preview will show the " +
|
||||
"markdown side of the document.",
|
||||
"markdown side of the document.<br>" +
|
||||
"Note: <b>excalidraw-open-md</b> is different from <b>excalidraw-embed-md</b>. If <b>excalidraw-embed-md</b> is set to true, the hover preview will always show the markdown side, regardless of this setting. To force image rendering when embedding, use <code>![[drawing#^as-image]]</code> in your markdown file.",
|
||||
SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME: "Render as image when in markdown reading mode of an Excalidraw file",
|
||||
SHOW_DRAWING_OR_MD_IN_READING_MODE_DESC:
|
||||
"When you are in markdown reading mode (aka. reading the back side of the drawing) should the Excalidraw drawing be rendered as an image? " +
|
||||
@@ -399,7 +437,7 @@ FILENAME_HEAD: "Filename",
|
||||
"⚠️ You must close and reopen the Excalidraw/markdown file for changes to take effect. ⚠️",
|
||||
HOTKEY_OVERRIDE_HEAD: "Hotkey overrides",
|
||||
HOTKEY_OVERRIDE_DESC: `Some of the Excalidraw hotkeys such as <code>${labelCTRL()}+Enter</code> to edit text or <code>${labelCTRL()}+K</code> to create an element link ` +
|
||||
"conflict with Obsidian hotkey settings. The hotkey combinations you add below will override Obsidian's hotkey settings while useing Excalidraw, thus " +
|
||||
"conflict with Obsidian hotkey settings. The hotkey combinations you add below will override Obsidian's hotkey settings while using Excalidraw, thus " +
|
||||
`you can add <code>${labelCTRL()}+G</code> if you want to default to Group Object in Excalidraw instead of opening Graph View.`,
|
||||
THEME_HEAD: "Theme and styling",
|
||||
ZOOM_HEAD: "Zoom",
|
||||
@@ -534,7 +572,7 @@ FILENAME_HEAD: "Filename",
|
||||
"Use the <code>http://iframely.server.crestify.com/iframely?url=</code> to get title of page when dropping a link into Excalidraw",
|
||||
PDF_TO_IMAGE: "PDF to Image",
|
||||
PDF_TO_IMAGE_SCALE_NAME: "PDF to Image conversion scale",
|
||||
PDF_TO_IMAGE_SCALE_DESC: "Sets the resolution of the image that is generated from the PDF page. Higher resolution will result in bigger images in memory and consequently a higher load on your system (slower performance), but sharper imagee. " +
|
||||
PDF_TO_IMAGE_SCALE_DESC: "Sets the resolution of the image that is generated from the PDF page. Higher resolution will result in bigger images in memory and consequently a higher load on your system (slower performance), but sharper image. " +
|
||||
"Additionally, if you want to copy PDF pages (as images) to Excalidraw.com, the bigger image size may result in exceeding the 2MB limit on Excalidraw.com.",
|
||||
EMBED_TOEXCALIDRAW_HEAD: "Embed files into Excalidraw",
|
||||
EMBED_TOEXCALIDRAW_DESC: "In the Embed Files section of Excalidraw Settings, you can configure how various files are embedded into Excalidraw. This includes options for embedding interactive markdown files, PDFs, and markdown files as images.",
|
||||
@@ -694,7 +732,7 @@ FILENAME_HEAD: "Filename",
|
||||
DUMMY_TEXT_ELEMENT_LINT_SUPPORT_NAME: "Linter compatibility",
|
||||
DUMMY_TEXT_ELEMENT_LINT_SUPPORT_DESC: "Excalidraw is sensitive to the file structure below <code># Excalidraw Data</code>. Automatic linting of documents can create errors in Excalidraw Data. " +
|
||||
"While I've made some effort to make the data loading resilient to " +
|
||||
"lint changes, this solution is not foolproof.<br><mark>The best is to avoid liniting or otherwise automatically changing Excalidraw documents using different plugins.</mark><br>" +
|
||||
"lint changes, this solution is not foolproof.<br><mark>The best is to avoid linting or otherwise automatically changing Excalidraw documents using different plugins.</mark><br>" +
|
||||
"Use this setting if for good reasons you have decided to ignore my recommendation and configured linting of Excalidraw files.<br> " +
|
||||
"The <code>## Text Elements</code> section is sensitive to empty lines. A common linting approach is to add an empty line after section headings. In case of Excalidraw this will break/change the first text element in your drawing. " +
|
||||
"To overcome this, you can enable this setting. When enabled, Excalidraw will add a dummy element to the beginning of <code>## Text Elements</code> that the linter can safely modify." ,
|
||||
@@ -811,10 +849,10 @@ FILENAME_HEAD: "Filename",
|
||||
<strong>Note:</strong> If you're using Obsidian Sync and want to synchronize these font files across your devices, ensure that Obsidian Sync is set to synchronize "All other file types".`,
|
||||
LOAD_CHINESE_FONTS_NAME: "Load Chinese fonts from file at startup",
|
||||
LOAD_JAPANESE_FONTS_NAME: "Load Japanese fonts from file at startup",
|
||||
LOAD_KOREAN_FONTS_NAME: "Load Korean fonts frome file at startup",
|
||||
LOAD_KOREAN_FONTS_NAME: "Load Korean fonts from file at startup",
|
||||
SCRIPT_SETTINGS_HEAD: "Settings for installed Scripts",
|
||||
SCRIPT_SETTINGS_DESC: "Some of the Excalidraw Automate Scripts include settings. Settings are organized by script. Settings will only become visible in this list after you have executed the newly downloaded script once.",
|
||||
TASKBONE_HEAD: "Taskbone Optical Character Recogntion",
|
||||
TASKBONE_HEAD: "Taskbone Optical Character Recognition",
|
||||
TASKBONE_DESC: "This is an experimental integration of optical character recognition into Excalidraw. Please note, that taskbone is an independent external service not provided by Excalidraw, nor the Excalidraw-Obsidian plugin project. " +
|
||||
"The OCR service will grab legible text from freedraw lines and embedded pictures on your canvas and place the recognized text in the frontmatter of your drawing as well as onto clipboard. " +
|
||||
"Having the text in the frontmatter will enable you to search in Obsidian for the text contents of these. " +
|
||||
@@ -823,7 +861,7 @@ FILENAME_HEAD: "Filename",
|
||||
TASKBONE_ENABLE_DESC: "By enabling this service your agree to the Taskbone <a href='https://www.taskbone.com/legal/terms/' target='_blank'>Terms and Conditions</a> and the " +
|
||||
"<a href='https://www.taskbone.com/legal/privacy/' target='_blank'>Privacy Policy</a>.",
|
||||
TASKBONE_APIKEY_NAME: "Taskbone API Key",
|
||||
TASKBONE_APIKEY_DESC: "Taskbone offers a free service with a reasonable number of scans per month. If you want to use this feature more frequently, or you want to supoprt " +
|
||||
TASKBONE_APIKEY_DESC: "Taskbone offers a free service with a reasonable number of scans per month. If you want to use this feature more frequently, or you want to support " +
|
||||
"the developer of Taskbone (as you can imagine, there is no such thing as 'free', providing this awesome OCR service costs some money to the developer of Taskbone), you can " +
|
||||
"purchase a paid API key from <a href='https://www.taskbone.com/' target='_blank'>taskbone.com</a>. In case you have purchased a key, simply overwrite this auto generated free-tier API-key with your paid key.",
|
||||
|
||||
@@ -868,7 +906,7 @@ FILENAME_HEAD: "Filename",
|
||||
|
||||
//ExcalidrawData.ts
|
||||
LOAD_FROM_BACKUP: "Excalidraw file was corrupted. Loading from backup file.",
|
||||
FONT_LOAD_SLOW: "Loading Fonts...\n\n This is taking longer than expected. If this delay occurs regulary then you may download the fonts locally to your Vault. \n\n" +
|
||||
FONT_LOAD_SLOW: "Loading Fonts...\n\n This is taking longer than expected. If this delay occurs regularly then you may download the fonts locally to your Vault. \n\n" +
|
||||
"(click=dismiss, right-click=Info)",
|
||||
FONT_INFO_TITLE: "Starting v2.5.3 fonts load from the Internet",
|
||||
FONT_INFO_DETAILED: `
|
||||
@@ -910,8 +948,12 @@ FILENAME_HEAD: "Filename",
|
||||
|
||||
//IFrameActionsMenu.tsx
|
||||
NARROW_TO_HEADING: "Narrow to heading...",
|
||||
PIN_VIEW: "Pin view",
|
||||
DO_NOT_PIN_VIEW: "Do not pin view",
|
||||
NARROW_TO_BLOCK: "Narrow to block...",
|
||||
SHOW_ENTIRE_FILE: "Show entire file",
|
||||
SELECT_SECTION: "Select section from document",
|
||||
SELECT_VIEW: "Select view from base",
|
||||
ZOOM_TO_FIT: "Zoom to fit",
|
||||
RELOAD: "Reload original link",
|
||||
OPEN_IN_BROWSER: "Open current link in browser",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -158,7 +158,11 @@ export default {
|
||||
CONVERT_FILE: "转换为新格式",
|
||||
BACKUP_AVAILABLE: "加载绘图文件时出错,可能是由于 Obsidian 在上次保存时意外退出了(手机上更容易发生这种意外)。<br><br><b>好消息:</b>这台设备上存在备份。您是否想要恢复本设备上的备份?<br><br>(我建议您先尝试在最近使用过的其他设备上打开该绘图,以检查是否有更新的备份。)",
|
||||
BACKUP_RESTORED: "已恢复备份",
|
||||
CACHE_NOT_READY: "抱歉,加载绘图文件时出错。<br><br><mark>现在有耐心,将来更省心。</mark><br><br>该插件有备份机制,但您似乎刚刚打开 Obsidian,需要等待一分钟或更长的时间来读取缓存。缓存读取完毕时,您将会在右上角收到提示。<br><br>请点击 OK 并耐心等待缓存,或者选择点击取消后手动修复你的文件。<br>",
|
||||
BACKUP_SAVE_AS_FILE : "此绘图为空。存在一个非空的备份。您是否希望将其恢复为新文件并在新标签页中打开?" ,
|
||||
BACKUP_SAVE : "恢复" ,
|
||||
BACKUP_DELETE : "删除备份" ,
|
||||
BACKUP_CANCEL : "取消" ,
|
||||
CACHE_NOT_READY : "很抱歉给您带来不便,加载文件时发生了错误。<br><br><mark>稍作等待可能会节省您大量时间……</mark><br><br>插件有一个备份缓存,但似乎您刚刚启动了 Obsidian。初始化备份缓存可能需要一些时间,通常取决于设备性能,可能需要一分钟或更长时间。当缓存初始化完成时,您会在右上角收到通知。<br><br>请按“确定”尝试重新加载文件,并检查缓存是否已完成初始化。如果在此消息后看到一个完全空白的文件,我建议等待备份缓存准备就绪后再继续操作。或者,您也可以选择“取消”以手动修复您的文件。<br>" ,
|
||||
OBSIDIAN_TOOLS_PANEL: "Obsidian 工具面板",
|
||||
ERROR_SAVING_IMAGE: "获取图像时发生未知错误。可能是由于某种原因,图像不可用或拒绝了 Obsidian 的获取请求。",
|
||||
WARNING_PASTING_ELEMENT_AS_TEXT: "你不能将 Excalidraw 元素粘贴为文本元素!",
|
||||
@@ -188,7 +192,32 @@ export default {
|
||||
SAVE_IS_TAKING_LONG: "保存您之前的文件花费的时间较长,请稍候...",
|
||||
SAVE_IS_TAKING_VERY_LONG: "为了更好的性能,请考虑将大型绘图拆分成几个较小的文件。",
|
||||
|
||||
//ContentSearcher.ts
|
||||
SEARCH_COPIED_TO_CLIPBOARD: "Markdown 已复制到剪贴板",
|
||||
SEARCH_COPY_TO_CLIPBOARD_ARIA: "将整个对话框复制为 Markdown 到剪贴板。非常适合搭配 ChatGPT 等工具进行搜索和理解。",
|
||||
SEARCH_SHOWHIDE_ARIA : "显示/隐藏搜索栏" ,
|
||||
SEARCH_NEXT: "下一个",
|
||||
SEARCH_PREVIOUS: "上一个",
|
||||
|
||||
|
||||
|
||||
//settings.ts
|
||||
NOTEBOOKLM_LINK_ARIA : "向 NotebookLM 咨询有关插件的帮助。此模型已预加载我的所有视频转录、发布说明以及其他有用内容。与 NotebookLM 聊天,探索我的 250+ 视频以及 Excalidraw 文档。" ,
|
||||
NOTEBOOKLM_LINK_TEXT : "了解插件。访问 NotebookLM 知识库。" ,
|
||||
LINKS_BUGS_ARIA: "在插件的 GitHub 页面报告错误和提交功能请求",
|
||||
LINKS_BUGS: "报告错误",
|
||||
LINKS_YT_ARIA: "访问我的 YouTube 频道学习视觉思维和 Excalidraw",
|
||||
LINKS_YT: "在 YouTube 学习",
|
||||
LINKS_DISCORD_ARIA: "加入视觉思维研讨会 Discord 服务器",
|
||||
LINKS_DISCORD: "加入社区",
|
||||
LINKS_TWITTER: "关注我",
|
||||
LINKS_VTW_ARIA: "了解视觉知识管理、Excalidraw、Obsidian、ExcaliBrain 等内容",
|
||||
LINKS_VTW: "参加研讨会",
|
||||
LINKS_BOOK_ARIA: "阅读我的视觉思维著作《Sketch Your Mind》",
|
||||
LINKS_BOOK: "阅读书籍",
|
||||
LINKS_WIKI : "插件 Wiki" ,
|
||||
LINKS_WIKI_ARIA : "探索 Excalidraw 插件 Wiki" ,
|
||||
|
||||
RELEASE_NOTES_NAME: "显示更新说明",
|
||||
RELEASE_NOTES_DESC:
|
||||
"<b>开启:</b>每次更新本插件后,显示最新发行版本的说明。<br>" +
|
||||
@@ -203,14 +232,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 中标注图像时,替换后的图像链接将包含原始图像的宽度。",
|
||||
@@ -378,11 +415,12 @@ FILENAME_HEAD: "文件名",
|
||||
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_DESC:
|
||||
"在触控笔模式下使用涂鸦功能会显示十字准星 <b><u>打开:</u></b> 显示 <b><u>关闭:</u></b> 隐藏<br>"+
|
||||
"效果取决于设备。十字准星通常在绘图板、MS Surface 上可见。但在 iOS 上不可见。",
|
||||
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_NAME: "在鼠标悬停预览时将 Excalidraw 文件渲染文图片",
|
||||
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_NAME: "在鼠标悬停预览时将 Excalidraw 文件渲染为图片",
|
||||
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_DESC:
|
||||
"...即使文件具有 `<b>excalidraw-open-md: true</b>` frontmatter 属性。<br>" +
|
||||
"当此设置关闭且文件默认设置为以 md 格式打开时,悬停预览将显示文档的 Markdown 部分(背景笔记)。" +
|
||||
"",
|
||||
"<br>" +
|
||||
"注意:<b>excalidraw-open-md</b> 与 <b>excalidraw-embed-md</b> 是不同的。如果 <b>excalidraw-embed-md</b> 设置为 true,则悬停预览始终显示 markdown 部分,而不受此设置影响。要在嵌入时强制图像渲染,请在你的 markdown 文件中使用 <code>![[drawing#^as-image]]</code>。" ,
|
||||
SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME: "Excalidraw 文件在 Markdown 阅读模式下渲染为图片",
|
||||
SHOW_DRAWING_OR_MD_IN_READING_MODE_DESC:
|
||||
"当您处于 Markdown 阅读模式(即查看绘图的背景笔记)时,Excalidraw 绘图是否应该渲染为图像?" +
|
||||
@@ -910,8 +948,12 @@ FILENAME_HEAD: "文件名",
|
||||
|
||||
//IFrameActionsMenu.tsx
|
||||
NARROW_TO_HEADING: "缩放至标题",
|
||||
PIN_VIEW: "锁定视图",
|
||||
DO_NOT_PIN_VIEW: "取消锁定视图",
|
||||
NARROW_TO_BLOCK: "缩放至块",
|
||||
SHOW_ENTIRE_FILE: "显示全部",
|
||||
SELECT_SECTION: "从文档选择章节",
|
||||
SELECT_VIEW: "从 base 选择视图",
|
||||
ZOOM_TO_FIT: "缩放至合适大小",
|
||||
RELOAD: "重载链接",
|
||||
OPEN_IN_BROWSER: "在浏览器中打开",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
105
src/shared/Dialogs/FloatingModal.ts
Normal file
105
src/shared/Dialogs/FloatingModal.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { App, Modal } from "obsidian";
|
||||
|
||||
export class FloatingModal extends Modal {
|
||||
private dragging = false;
|
||||
private offsetX = 0;
|
||||
private offsetY = 0;
|
||||
private pointerDownHandler: (e: PointerEvent) => void;
|
||||
private pointerMoveHandler: (e: PointerEvent) => void;
|
||||
private pointerUpHandler: () => void;
|
||||
|
||||
constructor(app: App) {
|
||||
super(app);
|
||||
|
||||
// Initialize event handlers with proper binding
|
||||
this.pointerDownHandler = this.handlePointerDown.bind(this);
|
||||
this.pointerMoveHandler = this.handlePointerMove.bind(this);
|
||||
this.pointerUpHandler = this.handlePointerUp.bind(this);
|
||||
}
|
||||
|
||||
private handlePointerDown(e: PointerEvent): void {
|
||||
// Ignore if clicking on interactive elements
|
||||
if (
|
||||
e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
e.target instanceof HTMLSelectElement ||
|
||||
e.target instanceof HTMLButtonElement ||
|
||||
(e.target as HTMLElement).closest(".clickable-icon")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dragging = true;
|
||||
const { modalEl } = this;
|
||||
this.offsetX = e.clientX - modalEl.getBoundingClientRect().left;
|
||||
this.offsetY = e.clientY - modalEl.getBoundingClientRect().top;
|
||||
|
||||
// Add global event listeners for move and up events
|
||||
document.addEventListener("pointermove", this.pointerMoveHandler);
|
||||
document.addEventListener("pointerup", this.pointerUpHandler);
|
||||
// Capture the pointer to ensure we get events even when outside the target
|
||||
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
||||
}
|
||||
|
||||
private handlePointerMove(e: PointerEvent): void {
|
||||
if (!this.dragging) return;
|
||||
const { modalEl } = this;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const x = e.clientX - this.offsetX;
|
||||
const y = e.clientY - this.offsetY;
|
||||
|
||||
// Position the modal element
|
||||
modalEl.style.left = `${x}px`;
|
||||
modalEl.style.top = `${y}px`;
|
||||
modalEl.style.transform = "none"; // Remove centering transform
|
||||
}
|
||||
|
||||
private handlePointerUp(): void {
|
||||
this.dragging = false;
|
||||
document.removeEventListener("pointermove", this.pointerMoveHandler);
|
||||
document.removeEventListener("pointerup", this.pointerUpHandler);
|
||||
}
|
||||
|
||||
open(): void {
|
||||
super.open();
|
||||
setTimeout(() => {
|
||||
//@ts-ignore
|
||||
const { containerEl, modalEl, bgEl } = this;
|
||||
containerEl.style.pointerEvents = "none";
|
||||
if (bgEl) bgEl.style.display = "none";
|
||||
|
||||
// Set initial position and make modal draggable
|
||||
if (modalEl) {
|
||||
modalEl.style.pointerEvents = "auto";
|
||||
// Position absolute is needed for custom positioning
|
||||
modalEl.style.position = "absolute";
|
||||
|
||||
// Center the modal initially
|
||||
const rect = modalEl.getBoundingClientRect();
|
||||
const centerX = window.innerWidth / 2 - rect.width / 2;
|
||||
const centerY = window.innerHeight / 2 - rect.height / 2;
|
||||
|
||||
modalEl.style.left = `${centerX}px`;
|
||||
modalEl.style.top = `${centerY}px`;
|
||||
modalEl.style.transform = "none";
|
||||
|
||||
// Add pointer down listener to start dragging
|
||||
modalEl.addEventListener("pointerdown", this.pointerDownHandler);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
close(): void {
|
||||
const { modalEl } = this;
|
||||
// Clean up event listeners
|
||||
if (modalEl) {
|
||||
modalEl.removeEventListener("pointerdown", this.pointerDownHandler);
|
||||
}
|
||||
document.removeEventListener("pointermove", this.pointerMoveHandler);
|
||||
document.removeEventListener("pointerup", this.pointerUpHandler);
|
||||
|
||||
super.close();
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,111 @@ 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.14.1":`
|
||||
## Fixed
|
||||
- Excalidraw broke Obsidian's UI in Arabic, Persian, and Hebrew due to LTR, RTL conflict. [#2423](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2423)
|
||||
- Styling issues impacting native Obsidian search/replace dialogs. [#2420](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2420)
|
||||
- Now using native Obsidian attachment location function. 🙏 [mnaoumov](https://github.com/mnaoumov) [#2421](https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/2421), potentially fixes [#179](https://github.com/RainCat1998/obsidian-custom-attachment-location/issues/179) of the Obsidian Custom Attachment Location plugin issue.
|
||||
|
||||
## New
|
||||
- New frontmatter option \`excalidraw-embed-md\`. When set to \`true\`, embedding the drawing into a markdown note will render the "back of the note" (its markdown content) instead of rendering it as an image.
|
||||
- If you want to always display the drawing as an image, even when \`excalidraw-embed-md\` is enabled, use the special embed syntax: \`![[drawing#^as-image]]\`. Here, \`as-image\` is a phantom block reference that forces image rendering.
|
||||
- Added Spanish translation by [@Joakim31](https://github.com/Joakim31) [#2425](https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/2425)
|
||||
- Incremental minor updates from the main [Excalidraw project](https://github.com/excalidraw/excalidraw).
|
||||
`,
|
||||
"2.14.0":`
|
||||
## A Big "Small" Update
|
||||
- Added search to Excalidraw Settings, plus added a link to access the public NotebookLM workbook pre-loaded with everything about the plugin
|
||||
- New Taiwan-idiomatic Traditional Chinese translation by [@rlan](https://github.com/rlan) [#2413](https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/2413)
|
||||
`,
|
||||
"2.13.2":`
|
||||
## New
|
||||
- Excalidraw now properly supports drag and drop of obsidian links from Bases.
|
||||
- ExcalidrawAutomate exposes a new class: \`FloatingModal\`. This is a modified version of the Obsidian.Modal class that allows the modal to be dragged around the screen and that does not dim the background. You can use it to create custom dialogs that behave like Obsidian modals but with more flexibility.
|
||||
`,
|
||||
"2.13.1":`
|
||||
## New
|
||||
- Support for Obsidian bases as embeddables in Excalidraw.
|
||||
- **Note:** The feature is only available to Insiders who have Obsidian 1.9.4 or later installed.
|
||||
- If your base includes multiple views you can pin the desired view similar to filtering to a section (click top left # button; \`[[my.base|my view]]\`).
|
||||
|
||||
## Fixed
|
||||
- Cannot type in embedded web forms. In certain cases, typing within these embeds would trigger Excalidraw hotkeys instead of interacting with the embedded content. [#2403](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2403)
|
||||
`,
|
||||
"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
|
||||
- **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>
|
||||
@@ -227,160 +332,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**.
|
||||
|
||||
`,
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
@@ -792,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) {
|
||||
@@ -857,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;
|
||||
});
|
||||
@@ -887,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) {
|
||||
@@ -903,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();
|
||||
@@ -920,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,10 @@ export class ReleaseNotes extends Modal {
|
||||
async onClose() {
|
||||
this.contentEl.empty();
|
||||
await this.plugin.loadSettings();
|
||||
this.plugin.settings.previousRelease = PLUGIN_VERSION
|
||||
await this.plugin.saveSettings();
|
||||
if(this.plugin.settings.previousRelease !== PLUGIN_VERSION) {
|
||||
this.plugin.settings.previousRelease = PLUGIN_VERSION;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
async createForm() {
|
||||
@@ -39,7 +41,8 @@ export class ReleaseNotes extends Modal {
|
||||
.slice(0, 10)
|
||||
.join("\n\n---\n")
|
||||
: FIRST_RUN;
|
||||
await MarkdownRenderer.renderMarkdown(
|
||||
await MarkdownRenderer.render(
|
||||
this.app,
|
||||
message,
|
||||
this.contentEl,
|
||||
"",
|
||||
|
||||
@@ -1,80 +1,27 @@
|
||||
import { MarkdownRenderer, Modal, Notice, request } from "obsidian";
|
||||
import ExcalidrawPlugin from "../../core/main";
|
||||
import { errorlog, escapeRegExp } from "../../utils/utils";
|
||||
import { errorlog } from "../../utils/utils";
|
||||
import { log } from "src/utils/debugHelper";
|
||||
import { ContentSearcher } from "../components/ContentSearcher";
|
||||
|
||||
const URL =
|
||||
"https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/index-new.md";
|
||||
|
||||
export class ScriptInstallPrompt extends Modal {
|
||||
private contentDiv: HTMLDivElement;
|
||||
|
||||
constructor(private plugin: ExcalidrawPlugin) {
|
||||
super(plugin.app);
|
||||
// this.titleEl.setText(t("INSTAL_MODAL_TITLE"));
|
||||
}
|
||||
|
||||
async onOpen(): Promise<void> {
|
||||
const searchBarWrapper = document.createElement("div");
|
||||
searchBarWrapper.classList.add('search-bar-wrapper');
|
||||
|
||||
|
||||
const searchBar = document.createElement("input");
|
||||
searchBar.type = "text";
|
||||
searchBar.id = "search-bar";
|
||||
searchBar.placeholder = "Search...";
|
||||
//searchBar.style.width = "calc(100% - 120px)"; // space for the buttons and hit count
|
||||
|
||||
const nextButton = document.createElement("button");
|
||||
nextButton.textContent = "→";
|
||||
nextButton.onclick = () => this.navigateSearchResults("next");
|
||||
|
||||
const prevButton = document.createElement("button");
|
||||
prevButton.textContent = "←";
|
||||
prevButton.onclick = () => this.navigateSearchResults("previous");
|
||||
|
||||
const hitCount = document.createElement("span");
|
||||
hitCount.id = "hit-count";
|
||||
hitCount.classList.add('hit-count');
|
||||
|
||||
searchBarWrapper.appendChild(prevButton);
|
||||
searchBarWrapper.appendChild(nextButton);
|
||||
searchBarWrapper.appendChild(searchBar);
|
||||
searchBarWrapper.appendChild(hitCount);
|
||||
|
||||
this.contentEl.prepend(searchBarWrapper);
|
||||
|
||||
searchBar.addEventListener("input", (e) => {
|
||||
this.clearHighlights();
|
||||
const searchTerm = (e.target as HTMLInputElement).value;
|
||||
|
||||
if (searchTerm && searchTerm.length > 0) {
|
||||
this.highlightSearchTerm(searchTerm);
|
||||
const totalHits = this.contentDiv.querySelectorAll("mark.search-highlight").length;
|
||||
hitCount.textContent = totalHits > 0 ? `1/${totalHits}` : "";
|
||||
setTimeout(()=>this.navigateSearchResults("next"));
|
||||
} else {
|
||||
hitCount.textContent = "";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
searchBar.addEventListener("keydown", (e) => {
|
||||
// If Ctrl/Cmd + F is pressed, focus on search bar
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "f") {
|
||||
e.preventDefault();
|
||||
searchBar.focus();
|
||||
}
|
||||
// If Enter is pressed, navigate to next result
|
||||
else if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
this.navigateSearchResults(e.shiftKey ? "previous" : "next");
|
||||
}
|
||||
});
|
||||
|
||||
this.contentEl.classList.add("excalidraw-scriptengine-install");
|
||||
this.contentDiv = document.createElement("div");
|
||||
this.contentEl.appendChild(this.contentDiv);
|
||||
|
||||
new ContentSearcher(this.contentDiv);
|
||||
|
||||
this.containerEl.classList.add("excalidraw-scriptengine-install");
|
||||
try {
|
||||
const source = await request({ url: URL });
|
||||
@@ -111,99 +58,6 @@ export class ScriptInstallPrompt extends Modal {
|
||||
}
|
||||
}
|
||||
|
||||
highlightSearchTerm(searchTerm: string): void {
|
||||
// Create a walker to traverse text nodes
|
||||
const walker = document.createTreeWalker(
|
||||
this.contentDiv,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
{
|
||||
acceptNode: (node: Text) => {
|
||||
return node.nodeValue!.toLowerCase().includes(searchTerm.toLowerCase()) ?
|
||||
NodeFilter.FILTER_ACCEPT :
|
||||
NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const nodesToReplace: Text[] = [];
|
||||
while (walker.nextNode()) {
|
||||
nodesToReplace.push(walker.currentNode as Text);
|
||||
}
|
||||
|
||||
nodesToReplace.forEach(node => {
|
||||
const nodeContent = node.nodeValue!;
|
||||
const newNode = document.createDocumentFragment();
|
||||
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
const regex = new RegExp(escapeRegExp(searchTerm), 'gi');
|
||||
|
||||
// Iterate over all matches in the text node
|
||||
while ((match = regex.exec(nodeContent)) !== null) {
|
||||
const before = document.createTextNode(nodeContent.slice(lastIndex, match.index));
|
||||
const highlighted = document.createElement('mark');
|
||||
highlighted.className = 'search-highlight';
|
||||
highlighted.textContent = match[0];
|
||||
highlighted.classList.add('search-result');
|
||||
|
||||
newNode.appendChild(before);
|
||||
newNode.appendChild(highlighted);
|
||||
|
||||
lastIndex = regex.lastIndex;
|
||||
}
|
||||
newNode.appendChild(document.createTextNode(nodeContent.slice(lastIndex)));
|
||||
node.replaceWith(newNode);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
clearHighlights(): void {
|
||||
this.contentDiv.querySelectorAll("mark.search-highlight").forEach((el) => {
|
||||
el.outerHTML = el.innerHTML;
|
||||
});
|
||||
}
|
||||
|
||||
navigateSearchResults(direction: "next" | "previous"): void {
|
||||
const highlights: HTMLElement[] = Array.from(
|
||||
this.contentDiv.querySelectorAll("mark.search-highlight")
|
||||
);
|
||||
|
||||
if (highlights.length === 0) return;
|
||||
|
||||
const currentActiveIndex = highlights.findIndex((highlight) =>
|
||||
highlight.classList.contains("active-highlight")
|
||||
);
|
||||
|
||||
if (currentActiveIndex !== -1) {
|
||||
highlights[currentActiveIndex].classList.remove("active-highlight");
|
||||
highlights[currentActiveIndex].style.border = "none";
|
||||
}
|
||||
|
||||
let nextActiveIndex = 0;
|
||||
if (direction === "next") {
|
||||
nextActiveIndex =
|
||||
currentActiveIndex === highlights.length - 1
|
||||
? 0
|
||||
: currentActiveIndex + 1;
|
||||
} else if (direction === "previous") {
|
||||
nextActiveIndex =
|
||||
currentActiveIndex === 0
|
||||
? highlights.length - 1
|
||||
: currentActiveIndex - 1;
|
||||
}
|
||||
|
||||
const nextActiveHighlight = highlights[nextActiveIndex];
|
||||
nextActiveHighlight.classList.add("active-highlight");
|
||||
nextActiveHighlight.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
});
|
||||
|
||||
// Update the hit count
|
||||
const hitCount = document.getElementById("hit-count");
|
||||
hitCount.textContent = `${nextActiveIndex + 1}/${highlights.length}`;
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
|
||||
@@ -31,6 +31,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
desc: "The ExcalidrawPlugin object",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "FloatingModal",
|
||||
code: null,
|
||||
desc: "A modified version of the Obsidian.Modal class that allows the modal to be dragged around the screen and that does not dim the background.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "elementsDict",
|
||||
code: null,
|
||||
@@ -575,27 +581,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 +902,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",
|
||||
@@ -1076,6 +1067,13 @@ export const FRONTMATTER_KEYS_INFO: SuggesterInfo[] = [
|
||||
desc: "If this key is present the file will be opened as a markdown file in the editor",
|
||||
after: ": true",
|
||||
},
|
||||
{
|
||||
field: "embed-md",
|
||||
code: null,
|
||||
desc: "If this key is present, when embedding the ![[image]] into a markdown document, it will be embedded as markdown, not as an image.\n" +
|
||||
"If however you embed ![[image#^as-image]], i.e. you reference the 'as-image' block, then the image will be embedded as an image.",
|
||||
after: ": true",
|
||||
},
|
||||
{
|
||||
field: "autoexport",
|
||||
code: null,
|
||||
|
||||
@@ -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,8 @@ 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";
|
||||
import { FloatingModal } from "./Dialogs/FloatingModal";
|
||||
|
||||
extendPlugins([
|
||||
HarmonyPlugin,
|
||||
@@ -169,6 +171,15 @@ export class ExcalidrawAutomate {
|
||||
return obsidian_module;
|
||||
};
|
||||
|
||||
/**
|
||||
* This is a modified version of the Obsidian.Modal class
|
||||
* that allows the modal to be dragged around the screen
|
||||
* and that does not dim the background.
|
||||
*/
|
||||
get FloatingModal() {
|
||||
return FloatingModal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the laser pointer settings from the plugin.
|
||||
* @returns {Object} The laser pointer settings.
|
||||
@@ -317,6 +328,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 +1739,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 +1748,7 @@ export class ExcalidrawAutomate {
|
||||
focus: formatting?.startObjectId
|
||||
? determineFocusDistance(
|
||||
this.getElement(formatting?.startObjectId) as ExcalidrawBindableElement,
|
||||
elementsMap,
|
||||
endPoint,
|
||||
startPoint,
|
||||
)
|
||||
@@ -1734,6 +1760,7 @@ export class ExcalidrawAutomate {
|
||||
focus: formatting?.endObjectId
|
||||
? determineFocusDistance(
|
||||
this.getElement(formatting?.endObjectId) as ExcalidrawBindableElement,
|
||||
elementsMap,
|
||||
startPoint,
|
||||
endPoint,
|
||||
)
|
||||
@@ -2164,7 +2191,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 +2872,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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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":""}.`);
|
||||
|
||||
@@ -191,7 +191,7 @@ export class ScriptEngine {
|
||||
...this.scriptIconMap,
|
||||
};
|
||||
const splitname = splitFolderAndFilename(name)
|
||||
this.scriptIconMap[scriptPath] = { name:splitname.filename, group: splitname.folderpath === "/" ? "" : splitname.folderpath, svgString };
|
||||
this.scriptIconMap[scriptPath] = { name:splitname.filename, group: splitname.folderpath, svgString };
|
||||
this.updateToolPannels();
|
||||
}
|
||||
|
||||
|
||||
306
src/shared/components/ContentSearcher.ts
Normal file
306
src/shared/components/ContentSearcher.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
import { t } from "src/lang/helpers";
|
||||
import { escapeRegExp } from "../../utils/utils";
|
||||
// @ts-ignore
|
||||
import { getIcon, htmlToMarkdown, Notice } from "obsidian";
|
||||
|
||||
export class ContentSearcher {
|
||||
private contentDiv: HTMLElement;
|
||||
private searchBar: HTMLInputElement;
|
||||
private prevButton: HTMLButtonElement;
|
||||
private nextButton: HTMLButtonElement;
|
||||
private exportMarkdown: HTMLButtonElement;
|
||||
private showHideButton: HTMLButtonElement;
|
||||
private customElemenentContainer: HTMLDivElement;
|
||||
private inputContainer: HTMLDivElement;
|
||||
private customElement: HTMLElement;
|
||||
private hitCount: HTMLSpanElement;
|
||||
private searchBarWrapper: HTMLDivElement;
|
||||
|
||||
constructor(contentDiv: HTMLElement, customElement?: HTMLElement) {
|
||||
this.contentDiv = contentDiv;
|
||||
this.customElement = customElement;
|
||||
this.createSearchElements();
|
||||
this.setupEventListeners();
|
||||
contentDiv.prepend(this.getSearchBarWrapper());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates search UI elements styled like Obsidian's native search
|
||||
*/
|
||||
private createSearchElements(): void {
|
||||
this.searchBarWrapper = createDiv("excalidraw-search document-search-container");
|
||||
const documentSearch = createDiv("document-search");
|
||||
this.inputContainer = createDiv("search-input-container document-search-input");
|
||||
this.searchBar = createEl("input",{type: "text", placeholder: "Find..."});
|
||||
this.hitCount = createDiv("document-search-count");
|
||||
|
||||
this.inputContainer.appendChild(this.searchBar);
|
||||
this.inputContainer.appendChild(this.hitCount);
|
||||
const buttonContainer = createDiv("document-search-buttons");
|
||||
|
||||
this.prevButton = createEl("button", {
|
||||
cls: ["clickable-icon", "document-search-button"],
|
||||
attr: {
|
||||
"aria-label": t("SEARCH_PREVIOUS"),
|
||||
"data-tooltip-position": "top",
|
||||
},
|
||||
type: "button",
|
||||
});
|
||||
this.prevButton.innerHTML = getIcon("arrow-up").outerHTML;
|
||||
|
||||
this.nextButton = createEl("button", {
|
||||
cls: ["clickable-icon", "document-search-button"],
|
||||
attr: {
|
||||
"aria-label": t("SEARCH_NEXT"),
|
||||
"data-tooltip-position": "top",
|
||||
},
|
||||
type: "button",
|
||||
});
|
||||
this.nextButton.innerHTML = getIcon("arrow-down").outerHTML;
|
||||
|
||||
this.exportMarkdown = createEl("button", {
|
||||
cls: ["clickable-icon", "document-search-button"],
|
||||
attr: {
|
||||
"aria-label": t("SEARCH_COPY_TO_CLIPBOARD_ARIA"),
|
||||
"data-tooltip-position": "top",
|
||||
},
|
||||
type: "button",
|
||||
});
|
||||
this.exportMarkdown.innerHTML = getIcon("clipboard-copy").outerHTML;
|
||||
|
||||
this.showHideButton = createEl("button", {
|
||||
cls: ["clickable-icon", "document-search-button", "search-visible"],
|
||||
attr: {
|
||||
"aria-label": t("SEARCH_SHOWHIDE_ARIA"),
|
||||
"data-tooltip-position": "top",
|
||||
},
|
||||
type: "button",
|
||||
});
|
||||
this.showHideButton.innerHTML = getIcon("minimize-2").outerHTML;
|
||||
|
||||
buttonContainer.appendChild(this.prevButton);
|
||||
buttonContainer.appendChild(this.nextButton);
|
||||
buttonContainer.appendChild(this.exportMarkdown);
|
||||
buttonContainer.appendChild(this.showHideButton);
|
||||
|
||||
documentSearch.appendChild(this.inputContainer);
|
||||
documentSearch.appendChild(buttonContainer);
|
||||
|
||||
this.searchBarWrapper.appendChild(documentSearch);
|
||||
|
||||
this.customElemenentContainer = createDiv();
|
||||
if(this.customElement) {
|
||||
this.customElemenentContainer.appendChild(this.customElement);
|
||||
this.searchBarWrapper.appendChild(this.customElemenentContainer)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach event listeners to search elements
|
||||
*/
|
||||
private setupEventListeners(): void {
|
||||
this.nextButton.onclick = () => this.navigateSearchResults("next");
|
||||
this.prevButton.onclick = () => this.navigateSearchResults("previous");
|
||||
this.exportMarkdown.onclick = () => {
|
||||
// Get the full HTML content first
|
||||
const fullHtml = this.contentDiv.outerHTML;
|
||||
|
||||
// Find the index of the first <hr> element
|
||||
const startIndex = fullHtml.indexOf('<hr');
|
||||
|
||||
// Extract HTML from the first <hr> element onwards
|
||||
const html = startIndex > -1 ? fullHtml.substring(startIndex) : fullHtml;
|
||||
|
||||
function replaceHeading(html:string,level:number):string {
|
||||
const re = new RegExp(`<summary class="excalidraw-setting-h${level}">([^<]+)<\/summary>`,"g");
|
||||
return html.replaceAll(re,`<summary class="excalidraw-setting-h${level}"><h${level}>$1</h${level}></summary>`);
|
||||
}
|
||||
|
||||
let x = replaceHeading(html,1);
|
||||
x = replaceHeading(x,2);
|
||||
x = replaceHeading(x,3);
|
||||
x = replaceHeading(x,4);
|
||||
x = x.replaceAll(/<div class="setting-item-name">([^<]+)<\/div>/g,"<h5>$1</h5>");
|
||||
|
||||
const md = htmlToMarkdown(x);
|
||||
window.navigator.clipboard.writeText(md);
|
||||
new Notice(t("SEARCH_COPIED_TO_CLIPBOARD"));
|
||||
};
|
||||
this.showHideButton.onclick = () => {
|
||||
const setOpacity = (value:string|null) => {
|
||||
this.inputContainer.style.opacity = value;
|
||||
this.prevButton.style.opacity = value;
|
||||
this.nextButton.style.opacity = value;
|
||||
this.exportMarkdown.style.opacity = value;
|
||||
this.customElemenentContainer.style.opacity = value;
|
||||
}
|
||||
if(this.showHideButton.hasClass("search-visible")) {
|
||||
this.showHideButton.removeClass("search-visible");
|
||||
this.showHideButton.addClass("search-hidden");
|
||||
this.searchBarWrapper.style.backgroundColor = "transparent";
|
||||
setOpacity("0");
|
||||
this.showHideButton.innerHTML = getIcon("maximize-2").outerHTML;
|
||||
} else {
|
||||
this.showHideButton.removeClass("search-hidden");
|
||||
this.showHideButton.addClass("search-visible");
|
||||
this.searchBarWrapper.style.backgroundColor = null;
|
||||
setOpacity(null);
|
||||
this.showHideButton.innerHTML = getIcon("minimize-2").outerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
this.searchBar.addEventListener("input", (e) => {
|
||||
this.clearHighlights();
|
||||
const searchTerm = (e.target as HTMLInputElement).value;
|
||||
|
||||
if (searchTerm && searchTerm.length > 0) {
|
||||
this.highlightSearchTerm(searchTerm);
|
||||
const totalHits = this.contentDiv.querySelectorAll("mark.search-highlight").length;
|
||||
this.hitCount.textContent = totalHits > 0 ? `1 / ${totalHits}` : "";
|
||||
setTimeout(() => this.navigateSearchResults("next"));
|
||||
} else {
|
||||
this.hitCount.textContent = "";
|
||||
}
|
||||
});
|
||||
|
||||
this.searchBar.addEventListener("keydown", (e) => {
|
||||
// If Ctrl/Cmd + F is pressed, focus on search bar
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "f") {
|
||||
e.preventDefault();
|
||||
this.searchBar.focus();
|
||||
}
|
||||
// If Enter is pressed, navigate to next result
|
||||
else if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
this.navigateSearchResults(e.shiftKey ? "previous" : "next");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the search bar wrapper element to add to the DOM
|
||||
*/
|
||||
public getSearchBarWrapper(): HTMLElement {
|
||||
return this.searchBarWrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight all instances of the search term in the content
|
||||
*/
|
||||
public highlightSearchTerm(searchTerm: string): void {
|
||||
// Create a walker to traverse text nodes
|
||||
const walker = document.createTreeWalker(
|
||||
this.contentDiv,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
{
|
||||
acceptNode: (node: Text) => {
|
||||
return node.nodeValue!.toLowerCase().includes(searchTerm.toLowerCase()) ?
|
||||
NodeFilter.FILTER_ACCEPT :
|
||||
NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const nodesToReplace: Text[] = [];
|
||||
while (walker.nextNode()) {
|
||||
nodesToReplace.push(walker.currentNode as Text);
|
||||
}
|
||||
|
||||
nodesToReplace.forEach(node => {
|
||||
const nodeContent = node.nodeValue!;
|
||||
const newNode = document.createDocumentFragment();
|
||||
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
const regex = new RegExp(escapeRegExp(searchTerm), 'gi');
|
||||
|
||||
// Iterate over all matches in the text node
|
||||
while ((match = regex.exec(nodeContent)) !== null) {
|
||||
const before = document.createTextNode(nodeContent.slice(lastIndex, match.index));
|
||||
const highlighted = document.createElement('mark');
|
||||
highlighted.className = 'search-highlight';
|
||||
highlighted.textContent = match[0];
|
||||
highlighted.classList.add('search-result');
|
||||
|
||||
newNode.appendChild(before);
|
||||
newNode.appendChild(highlighted);
|
||||
|
||||
lastIndex = regex.lastIndex;
|
||||
}
|
||||
newNode.appendChild(document.createTextNode(nodeContent.slice(lastIndex)));
|
||||
node.replaceWith(newNode);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all search highlights
|
||||
*/
|
||||
public clearHighlights(): void {
|
||||
this.contentDiv.querySelectorAll("mark.search-highlight").forEach((el) => {
|
||||
el.outerHTML = el.innerHTML;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to next or previous search result
|
||||
*/
|
||||
public navigateSearchResults(direction: "next" | "previous"): void {
|
||||
const highlights: HTMLElement[] = Array.from(
|
||||
this.contentDiv.querySelectorAll("mark.search-highlight")
|
||||
);
|
||||
|
||||
if (highlights.length === 0) return;
|
||||
|
||||
const currentActiveIndex = highlights.findIndex((highlight) =>
|
||||
highlight.classList.contains("active-highlight")
|
||||
);
|
||||
|
||||
if (currentActiveIndex !== -1) {
|
||||
highlights[currentActiveIndex].classList.remove("active-highlight");
|
||||
highlights[currentActiveIndex].style.border = "none";
|
||||
}
|
||||
|
||||
let nextActiveIndex = 0;
|
||||
if (direction === "next") {
|
||||
nextActiveIndex =
|
||||
currentActiveIndex === highlights.length - 1
|
||||
? 0
|
||||
: currentActiveIndex + 1;
|
||||
} else if (direction === "previous") {
|
||||
nextActiveIndex =
|
||||
currentActiveIndex === 0
|
||||
? highlights.length - 1
|
||||
: currentActiveIndex - 1;
|
||||
}
|
||||
|
||||
const nextActiveHighlight = highlights[nextActiveIndex];
|
||||
nextActiveHighlight.classList.add("active-highlight");
|
||||
|
||||
// Expand all parent details elements
|
||||
this.expandParentDetails(nextActiveHighlight);
|
||||
|
||||
// Use setTimeout to ensure DOM has time to update after expanding details
|
||||
setTimeout(() => {
|
||||
nextActiveHighlight.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
});
|
||||
}, 100);
|
||||
|
||||
// Update the hit count
|
||||
this.hitCount.textContent = `${nextActiveIndex + 1} / ${highlights.length}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand all parent <details> elements to make the element visible
|
||||
*/
|
||||
private expandParentDetails(element: HTMLElement): void {
|
||||
let parent = element.parentElement;
|
||||
while (parent) {
|
||||
if (parent.tagName === "DETAILS") {
|
||||
parent.setAttribute("open", "");
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/types/excalidrawLib.d.ts
vendored
7
src/types/excalidrawLib.d.ts
vendored
@@ -1,7 +1,7 @@
|
||||
import { RestoredDataState } from "@zsviczian/excalidraw/types/excalidraw/data/restore";
|
||||
import { ImportedDataState } from "@zsviczian/excalidraw/types/excalidraw/data/types";
|
||||
import { BoundingBox } from "@zsviczian/excalidraw/types/element/src";
|
||||
import { ElementsMap, ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameElement, ExcalidrawFrameLikeElement, ExcalidrawTextContainer, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, NonDeletedExcalidrawElement, Theme } from "@zsviczian/excalidraw/types/element/src/types";
|
||||
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";
|
||||
@@ -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;
|
||||
@@ -230,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[];
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,13 @@ 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
|
||||
|
||||
137
src/utils/ErrorHandler.ts
Normal file
137
src/utils/ErrorHandler.ts
Normal 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();
|
||||
@@ -2,7 +2,7 @@ import { ExcalidrawEmbeddableElement, ExcalidrawFrameElement, ExcalidrawImageEle
|
||||
import { Mutable } from "@zsviczian/excalidraw/types/common/src/utility-types";
|
||||
import { getEA } from "src/core";
|
||||
import { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate";
|
||||
import { getCropFileNameAndFolder, getListOfTemplateFiles, splitFolderAndFilename } from "./fileUtils";
|
||||
import { getCropFileNameAndFolder, getListOfTemplateFiles } from "./fileUtils";
|
||||
import { Notice, TFile } from "obsidian";
|
||||
import { Radians } from "@zsviczian/excalidraw/types/math/src/types";
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
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
|
||||
* @param filepath
|
||||
* @returns folderpath will be normalized. This means "/" for root folder and no trailing "/" for other folders
|
||||
* @returns returns "" for root folder and normalized path for subfolders (no trailing "/", e.g. "folder/subfolder")
|
||||
*/
|
||||
type ImageExtension = keyof typeof IMAGE_MIME_TYPES;
|
||||
|
||||
@@ -23,11 +23,13 @@ export function splitFolderAndFilename(filepath: string): {
|
||||
} {
|
||||
const lastIndex = filepath.lastIndexOf("/");
|
||||
const filename = lastIndex == -1 ? filepath : filepath.substring(lastIndex + 1);
|
||||
const lastDotIndex = filename.lastIndexOf(".");
|
||||
const folderpath = filepath.substring(0, lastIndex);
|
||||
return {
|
||||
folderpath: normalizePath(filepath.substring(0, lastIndex)),
|
||||
folderpath: folderpath ? normalizePath(folderpath) : "",
|
||||
filename,
|
||||
basename: filename.replace(/\.[^/.]+$/, ""),
|
||||
extension: filename.substring(filename.lastIndexOf(".") + 1),
|
||||
extension: lastDotIndex > 0 ? filename.substring(lastDotIndex + 1) : "",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -405,8 +407,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 +420,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 +496,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) {
|
||||
await checkAndCreateFolder(folderpath);
|
||||
}
|
||||
const file = app.vault.getAbstractFileByPath(normalizePath(path));
|
||||
if (content instanceof Blob) {
|
||||
content = await content.arrayBuffer();
|
||||
@@ -514,4 +564,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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -3,10 +3,10 @@ import {
|
||||
Editor,
|
||||
FrontMatterCache,
|
||||
MarkdownView,
|
||||
normalizePath, OpenViewState, parseFrontMatterEntry, TFile, View, ViewState, Workspace, WorkspaceLeaf, WorkspaceSplit
|
||||
OpenViewState, parseFrontMatterEntry, TFile, View, ViewState, Workspace, WorkspaceLeaf, WorkspaceSplit
|
||||
} from "obsidian";
|
||||
import ExcalidrawPlugin from "../core/main";
|
||||
import { checkAndCreateFolder, splitFolderAndFilename } from "./fileUtils";
|
||||
import { splitFolderAndFilename } from "./fileUtils";
|
||||
import { linkClickModifierType, ModifierKeys } from "./modifierkeyHelper";
|
||||
import { DEVICE, EXCALIDRAW_PLUGIN, REG_BLOCK_REF_CLEAN, REG_SECTION_REF_CLEAN, VIEW_TYPE_EXCALIDRAW } from "src/constants/constants";
|
||||
import yaml from "js-yaml";
|
||||
@@ -174,25 +174,13 @@ export const getAttachmentsFolderAndFilePath = async (
|
||||
activeViewFilePath: string,
|
||||
newFileName: string
|
||||
): Promise<{ folder: string; filepath: string; }> => {
|
||||
let folder = app.vault.getConfig("attachmentFolderPath");
|
||||
// folder == null: save to vault root
|
||||
// folder == "./" save to same folder as current file
|
||||
// folder == "folder" save to specific folder in vault
|
||||
// folder == "./folder" save to specific subfolder of current active folder
|
||||
if (folder && folder.startsWith("./")) {
|
||||
// folder relative to current file
|
||||
const activeFileFolder = `${splitFolderAndFilename(activeViewFilePath).folderpath}/`;
|
||||
folder = normalizePath(activeFileFolder + folder.substring(2));
|
||||
}
|
||||
if (!folder || folder === "/") {
|
||||
folder = "";
|
||||
}
|
||||
await checkAndCreateFolder(folder);
|
||||
const { basename, extension } = splitFolderAndFilename(newFileName);
|
||||
const activeViewFile = app.vault.getFileByPath(activeViewFilePath);
|
||||
const attachmentFilePath = await app.vault.getAvailablePathForAttachments(basename, extension, activeViewFile);
|
||||
const { folderpath } = splitFolderAndFilename(attachmentFilePath);
|
||||
return {
|
||||
folder,
|
||||
filepath: normalizePath(
|
||||
folder === "" ? newFileName : `${folder}/${newFileName}`
|
||||
),
|
||||
folder: folderpath,
|
||||
filepath: attachmentFilePath
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ declare module "obsidian" {
|
||||
): WorkspaceLeaf;
|
||||
}
|
||||
interface Vault {
|
||||
getConfig(option: "attachmentFolderPath"): string;
|
||||
getAvailablePathForAttachments(filename: string, extension: string, file: TFile | null): Promise<string>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -966,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")) &&
|
||||
|
||||
@@ -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;
|
||||
@@ -429,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) {
|
||||
@@ -456,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;
|
||||
}
|
||||
@@ -549,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) {
|
||||
@@ -565,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -672,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) {
|
||||
@@ -686,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -843,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;
|
||||
@@ -860,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;
|
||||
@@ -878,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();
|
||||
}
|
||||
}
|
||||
@@ -1686,7 +1708,9 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
if(!this.excalidrawAPI || !this.excalidrawData.loaded || !this.isDirty()) {
|
||||
return;
|
||||
}
|
||||
if((this.excalidrawAPI as ExcalidrawImperativeAPI).getAppState().activeTool.type !== "image") {
|
||||
const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
const st = api.getAppState();
|
||||
if(st.activeTool.type !== "image" && st.activeEmbeddable?.state !== "active") {
|
||||
this.forceSave(true);
|
||||
}
|
||||
};
|
||||
@@ -2407,7 +2431,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>"
|
||||
@@ -2433,7 +2457,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);
|
||||
@@ -2448,6 +2472,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) {
|
||||
@@ -2518,6 +2569,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
if (!this.excalidrawAPI) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loader = new EmbeddedFilesLoader(this.plugin);
|
||||
|
||||
const runLoader = (l: EmbeddedFilesLoader) => {
|
||||
@@ -2705,7 +2757,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);
|
||||
}
|
||||
@@ -2996,10 +3048,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];
|
||||
@@ -3049,7 +3098,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"));
|
||||
@@ -3821,7 +3870,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,
|
||||
});
|
||||
@@ -3991,7 +4041,11 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
if(st.newElement?.type === "freedraw") {
|
||||
this.freedrawLastActiveTimestamp = Date.now();
|
||||
}
|
||||
if (st.newElement || st.editingTextElement || st.editingLinearElement) {
|
||||
if (
|
||||
st.newElement ||
|
||||
st.editingTextElement ||
|
||||
(st.selectedLinearElement && st.selectedLinearElement.isEditing)
|
||||
) {
|
||||
this.plugin.wasPenModeActivePreviously = st.penMode;
|
||||
}
|
||||
this.viewModeEnabled = st.viewModeEnabled;
|
||||
@@ -4033,7 +4087,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
/*st.resizingElement === null &&
|
||||
st.newElement === null &&
|
||||
st.editingGroupId === null &&*/
|
||||
st.editingLinearElement === null
|
||||
(st.selectedLinearElement === null || !st.selectedLinearElement.isEditing)
|
||||
) {
|
||||
this.checkSceneVersion(et);
|
||||
}
|
||||
@@ -4063,15 +4117,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
|
||||
@@ -4089,6 +4147,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) {
|
||||
@@ -4611,7 +4675,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;
|
||||
@@ -5950,6 +6014,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) {
|
||||
|
||||
@@ -9,6 +9,8 @@ import { ObsidianCanvasNode } from "src/view/managers/CanvasNodeFactory";
|
||||
import { processLinkText, patchMobileView, setFileToLocalGraph } from "src/utils/customEmbeddableUtils";
|
||||
import { EmbeddableMDCustomProps } from "src/shared/Dialogs/EmbeddableSettings";
|
||||
|
||||
const CANVAS_VIEWTYPES = new Set(["markdown", "bases"]);
|
||||
|
||||
declare module "obsidian" {
|
||||
interface Workspace {
|
||||
floatingSplit: any;
|
||||
@@ -223,7 +225,7 @@ function RenderObsidianView(
|
||||
if(viewType === "canvas") {
|
||||
leafRef.current.leaf.view.canvas?.setReadonly(true);
|
||||
}
|
||||
if ((viewType === "markdown") && view.canvasNodeFactory.isInitialized()) {
|
||||
if (CANVAS_VIEWTYPES.has(viewType) && view.canvasNodeFactory.isInitialized()) {
|
||||
setKeepOnTop();
|
||||
//I haven't found a better way of deciding if an .md file has its own view (e.g., kanban) or not
|
||||
//This runs only when the file is added, thus should not be a major performance issue
|
||||
@@ -295,6 +297,22 @@ function RenderObsidianView(
|
||||
canvasNode?.style.setProperty("--background-primary", color);
|
||||
canvasNodeContainer?.style.setProperty("background-color", color);
|
||||
}
|
||||
canvasNode?.style.setProperty("--bases-cards-container-background","var(--background-primary)");
|
||||
canvasNode?.style.setProperty("--bases-embed-border-color","var(--background-modifier-border)");
|
||||
canvasNode?.style.setProperty("--bases-table-header-color","var(--text-muted)");
|
||||
canvasNode?.style.setProperty("--bases-table-header-background","var(--background-primary)");
|
||||
canvasNode?.style.setProperty("--bases-table-header-background-hover","var(--background-modifier-hover)");
|
||||
canvasNode?.style.setProperty("--bases-table-header-sort-mask","linear-gradient(to left, transparent var(--size-4-6), black var(--size-4-6))");
|
||||
canvasNode?.style.setProperty("--bases-table-border-color","var(--table-border-color)");
|
||||
canvasNode?.style.setProperty("--bases-table-row-background-hover","var(--table-row-background-hover)");
|
||||
canvasNode?.style.setProperty("--bases-table-cell-shadow-active","0 0 0 2px var(--interactive-accent)");
|
||||
canvasNode?.style.setProperty("--bases-table-cell-background-active","var(--background-primary)");
|
||||
canvasNode?.style.setProperty("--bases-table-cell-background-disabled","var(--background-primary-alt)");
|
||||
canvasNode?.style.setProperty("--bases-cards-container-background","var(--background-primary)");
|
||||
canvasNode?.style.setProperty("--bases-cards-background","var(--background-primary)");
|
||||
canvasNode?.style.setProperty("--bases-cards-cover-background","var(--background-primary-alt)");
|
||||
canvasNode?.style.setProperty("--bases-cards-shadow","0 0 0 1px var(--background-modifier-border)");
|
||||
canvasNode?.style.setProperty("--bases-cards-shadow-hover","0 0 0 1px var(--background-modifier-border-hover)");
|
||||
|
||||
if(mdProps.borderMatchElement) {
|
||||
const opacity = (mdProps?.borderOpacity ?? 50)/100;
|
||||
|
||||
@@ -70,6 +70,28 @@ export class EmbeddableMenu {
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
private async actionBaseViewSelection (file: TFile, subpath: string, element: ExcalidrawEmbeddableElement) {
|
||||
this.view.updateScene({appState: {activeEmbeddable: null}, captureUpdate: CaptureUpdateAction.NEVER});
|
||||
const views = Array.from(
|
||||
(await this.view.app.vault.read(file)).matchAll(/\s*name\: (.*)$/gm)
|
||||
).map(x=>x?.[1]);
|
||||
let values, display;
|
||||
values = [""].concat(
|
||||
views.map((b: string) => `#${cleanSectionHeading(b)}`)
|
||||
);
|
||||
display = [t("DO_NOT_PIN_VIEW")].concat(
|
||||
views.map((b: string) => b)
|
||||
);
|
||||
|
||||
const newSubpath = await ScriptEngine.suggester(
|
||||
this.view.app, display, values, t("SELECT_VIEW")
|
||||
);
|
||||
if(!newSubpath && newSubpath!=="") return;
|
||||
if (newSubpath !== subpath) {
|
||||
this.updateElement(newSubpath, element, file);
|
||||
}
|
||||
}
|
||||
|
||||
private async actionMarkdownSelection (file: TFile, isExcalidrawFile: boolean, subpath: string, element: ExcalidrawEmbeddableElement) {
|
||||
this.view.updateScene({appState: {activeEmbeddable: null}, captureUpdate: CaptureUpdateAction.NEVER});
|
||||
const sections = (await this.view.app.metadataCache.blockCache
|
||||
@@ -89,7 +111,7 @@ export class EmbeddableMenu {
|
||||
);
|
||||
}
|
||||
const newSubpath = await ScriptEngine.suggester(
|
||||
this.view.app, display, values, "Select section from document"
|
||||
this.view.app, display, values, t("SELECT_SECTION")
|
||||
);
|
||||
if(!newSubpath && newSubpath!=="") return;
|
||||
if (newSubpath !== subpath) {
|
||||
@@ -110,7 +132,7 @@ export class EmbeddableMenu {
|
||||
paragraphs.map((b: any) => `${b.node?.id ? `#^${b.node.id}: ` : ``}${b.display.trim()}`));
|
||||
|
||||
const selectedBlock = await ScriptEngine.suggester(
|
||||
this.view.app, display, values, "Select section from document"
|
||||
this.view.app, display, values, t("SELECT_SECTION")
|
||||
);
|
||||
if(!selectedBlock) return;
|
||||
|
||||
@@ -212,6 +234,7 @@ export class EmbeddableMenu {
|
||||
const { subpath, file } = processLinkText(link, view);
|
||||
if(!file) return;
|
||||
const isMD = file.extension==="md";
|
||||
const isBase = file.extension==="base";
|
||||
const isExcalidrawFile = view.plugin.isExcalidrawFile(file);
|
||||
const isPDF = file.extension==="pdf";
|
||||
const { x, y } = sceneCoordsToViewportCoords( { sceneX: element.x, sceneY: element.y }, appState);
|
||||
@@ -238,6 +261,14 @@ export class EmbeddableMenu {
|
||||
display: "block",
|
||||
}}
|
||||
>
|
||||
{isBase && (
|
||||
<ActionButton
|
||||
key={"MarkdownSection"}
|
||||
title={t("PIN_VIEW")}
|
||||
action={async () => this.actionBaseViewSelection(file, subpath, element)}
|
||||
icon={ICONS.ZoomToSection}
|
||||
/>
|
||||
)}
|
||||
{isMD && (
|
||||
<ActionButton
|
||||
key={"MarkdownSection"}
|
||||
|
||||
@@ -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";
|
||||
@@ -135,6 +134,7 @@ export class DropManager {
|
||||
// Obsidian internal drag event
|
||||
//---------------------------------------------------------------------------------
|
||||
switch (draggable?.type) {
|
||||
case "link":
|
||||
case "file":
|
||||
if (!onDropHook("file", [draggable.file], null)) {
|
||||
const file:TFile = draggable.file;
|
||||
@@ -372,8 +372,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 +384,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 +401,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 +416,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);
|
||||
})();
|
||||
|
||||
131
styles.css
131
styles.css
@@ -431,25 +431,6 @@ div.excalidraw-draginfo {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.modal-content.excalidraw-scriptengine-install .search-bar-wrapper {
|
||||
position: sticky;
|
||||
top: 1rem;
|
||||
margin-right: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
flex-wrap: nowrap;
|
||||
z-index: 10;
|
||||
background: var(--background-secondary);
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
float: right;
|
||||
max-width: 28rem;
|
||||
}
|
||||
|
||||
div.search-bar-wrapper input {
|
||||
margin-right: -0.5rem;
|
||||
}
|
||||
|
||||
.modal-content.excalidraw-scriptengine-install .hit-count {
|
||||
margin-left: 0.5em;
|
||||
@@ -641,20 +622,27 @@ textarea.excalidraw-wysiwyg, .excalidraw input {
|
||||
}
|
||||
|
||||
.excalidraw-settings-links-container {
|
||||
display: flex; /* Align SVG and text horizontally */
|
||||
align-items: center; /* Center SVG and text vertically */
|
||||
text-decoration: none; /* Remove underline from links */
|
||||
color: inherit; /* Inherit text color */
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
color: inherit;
|
||||
display: flex;
|
||||
gap: .3em;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.excalidraw-settings-links-container a {
|
||||
display: flex; /* Align children horizontally */
|
||||
align-items: center; /* Center items vertically */
|
||||
text-align: left;
|
||||
margin-right: 4px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.excalidraw-settings-links-container svg {
|
||||
.excalidraw-settings-links-container svg,
|
||||
.ex-setting-actions-container svg {
|
||||
margin-right: 8px;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
@@ -776,3 +764,96 @@ textarea.excalidraw-wysiwyg, .excalidraw input {
|
||||
.excalidraw-prompt-buttonbar-bottom > div:last-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.excalidraw-search.document-search-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--background-secondary);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px 0 rgba(0,0,0,0.10);
|
||||
padding: 0.5em 0.8em;
|
||||
margin-bottom: 2em;
|
||||
min-width: 18rem;
|
||||
position: sticky;
|
||||
top: 1rem;
|
||||
margin-right: 1rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.excalidraw-search .document-search {
|
||||
align-items: center;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.excalidraw-search .search-input-container.document-search-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1 1 auto;
|
||||
background: var(--background-primary);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.excalidraw-search .search-input-container .clickable-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.excalidraw-search .search-input-container input[type="text"] {
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--text-normal);
|
||||
font-size: 1em;
|
||||
flex: 1 1 auto;
|
||||
padding: 0.1em 2em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.excalidraw-search .document-search-count {
|
||||
margin-left: 0.5em;
|
||||
color: var(--text-faint);
|
||||
font-size: 0.95em;
|
||||
white-space: nowrap;
|
||||
min-width: 3.5em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.excalidraw-search .document-search-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.excalidraw-search .document-search-button {
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
padding: 0.1em 0.2em;
|
||||
margin: 0 1px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: var(--text-faint);
|
||||
transition: background 0.15s;
|
||||
height: 2em;
|
||||
width: 2em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.excalidraw-search .document-search-button:hover, .excalidraw-search .document-search-button:focus {
|
||||
background: var(--background-modifier-hover);
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
||||
.excalidraw-search .document-search-button svg {
|
||||
width: 1.3em;
|
||||
height: 1.3em;
|
||||
stroke: currentColor;
|
||||
fill: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user