Compare commits

...

25 Commits

Author SHA1 Message Date
zsviczian
5a58d17d99 2.7.3 2024-12-27 21:04:33 +01:00
zsviczian
982958a4c6 Merge pull request #2179 from dmscode/master
Update zh-cn.ts to d3b61a0
2024-12-27 19:35:52 +01:00
dmscode
d425884bb8 Update zh-cn.ts to d3b61a0 2024-12-27 19:26:02 +08:00
zsviczian
d3b61a0df1 shade master icon corrected 2024-12-27 09:18:12 +01:00
zsviczian
4bab0162ba EA colorMap functions, new duplicate command, minor fileloader fixes, view.loadSceneFiles callback and filewhitelist, publish shade master 1.0 2024-12-27 08:05:41 +01:00
zsviczian
d3f4437478 shade master and new duplicate image command 2024-12-26 23:38:29 +01:00
zsviczian
a64586c3e6 shade master fully functional 2024-12-25 20:44:15 +01:00
zsviczian
7a92e78851 added queue for svg update 2024-12-25 10:10:44 +01:00
zsviczian
af0122b21a store original colors 2024-12-25 01:05:38 +01:00
zsviczian
1f95f57e97 queue and sliders 2024-12-25 00:31:24 +01:00
zsviczian
f384e95e44 modal is draggable 2024-12-24 22:51:52 +01:00
zsviczian
a40521f07b Shade Master v2 2024-12-24 22:36:08 +01:00
zsviczian
9649b36175 shade master v1 2024-12-24 20:05:16 +01:00
zsviczian
6cb1394793 Merge pull request #2176 from dmscode/master
Update zh-cn.ts to 22d3f25
2024-12-24 08:27:19 +01:00
dmscode
e5b2977c0c Update zh-cn.ts to 22d3f25
And remove spaces from line end
2024-12-24 08:18:04 +08:00
zsviczian
22d3f25dc4 renderingConcurrency; createSliderWithText 2024-12-23 21:15:06 +01:00
zsviczian
d9534fcc4f Fixed: toggleImageAnchoring 2024-12-23 20:04:33 +01:00
zsviczian
fd1604c3a4 slideshow script will now remember last slide on multiple presentations within the same session when starting slideshow holding down the SHIFT Modifier key 2024-12-23 08:56:06 +01:00
zsviczian
8f0f8d64df colors to lower case 2024-12-22 10:33:43 +01:00
zsviczian
5a413ab910 2.7.2 2024-12-21 10:15:33 +01:00
zsviczian
d3133f055c updated deconstruct selected elements script 2024-12-21 09:31:26 +01:00
zsviczian
fe05518e31 resolve minify iOS 15/16 compatibility issue 2024-12-20 22:36:37 +01:00
zsviczian
8adcb7d850 pdfjs rendering race condition 2024-12-20 20:08:55 +01:00
zsviczian
be383f2b48 moved Drop handlers to DropManager, added await to page.getViewport as there seems to be a race condition impacting page.render() 2024-12-20 19:32:37 +01:00
zsviczian
682307b51d Merge pull request #2169 from zsviczian/2.7.2-casing
2.7.2 file and folder name casing + empty line before ## Text Elements
2024-12-20 14:14:14 +01:00
30 changed files with 2387 additions and 1007 deletions

View File

@@ -9,7 +9,7 @@ Select some elements in the scene. The script will take these elements and move
```javascript
*/
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.0.25")) {
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.7.3")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
@@ -79,15 +79,19 @@ ea.copyViewElementsToEAforEditing(els);
ea.getElements().filter(el=>el.type==="image").forEach(el=>{
const img = ea.targetView.excalidrawData.getFile(el.fileId);
const path = (img?.linkParts?.original)??(img?.file?.path);
if(img && path) {
const hyperlink = img?.hyperlink;
if(img && (path || hyperlink)) {
const colorMap = ea.getColorMapForImageElement(el);
ea.imagesDict[el.fileId] = {
mimeType: img.mimeType,
id: el.fileId,
dataURL: img.img,
created: img.mtime,
file: path,
hyperlink,
hasSVGwithBitmap: img.isSVGwithBitmap,
latex: null,
colorMap,
};
return;
}

View File

@@ -23,13 +23,17 @@ const {angle, backgroundColor, fillStyle, fontFamily, fontSize, height, width, o
const fragWithHTML = (html) => createFragment((frag) => (frag.createDiv().innerHTML = html));
function lc(x) {
return x?.toLocaleLowerCase();
}
//--------------------------
// RUN
//--------------------------
const run = () => {
selectedElements = elements.filter(el=>
((typeof config.angle === "undefined") || (el.angle === config.angle)) &&
((typeof config.backgroundColor === "undefined") || (el.backgroundColor === config.backgroundColor)) &&
((typeof config.backgroundColor === "undefined") || (lc(el.backgroundColor) === lc(config.backgroundColor))) &&
((typeof config.fillStyle === "undefined") || (el.fillStyle === config.fillStyle)) &&
((typeof config.fontFamily === "undefined") || (el.fontFamily === config.fontFamily)) &&
((typeof config.fontSize === "undefined") || (el.fontSize === config.fontSize)) &&
@@ -38,7 +42,7 @@ const run = () => {
((typeof config.opacity === "undefined") || (el.opacity === config.opacity)) &&
((typeof config.roughness === "undefined") || (el.roughness === config.roughness)) &&
((typeof config.roundness === "undefined") || (el.roundness === config.roundness)) &&
((typeof config.strokeColor === "undefined") || (el.strokeColor === config.strokeColor)) &&
((typeof config.strokeColor === "undefined") || (lc(el.strokeColor) === lc(config.strokeColor))) &&
((typeof config.strokeStyle === "undefined") || (el.strokeStyle === config.strokeStyle)) &&
((typeof config.strokeWidth === "undefined") || (el.strokeWidth === config.strokeWidth)) &&
((typeof config.type === "undefined") || (el.type === config.type)) &&

724
ea-scripts/Shade Master.md Normal file
View File

@@ -0,0 +1,724 @@
/*
This is an experimental script. If you find bugs, please consider debugging yourself then submitting a PR on github with the fix, instead of raising an issue. Thank you!
This script modifies the color lightness/hue/saturation/transparency of selected Excalidraw elements and SVG and nested Excalidraw drawings. Select eligible elements in the scene, then run the script.
- The color of Excalidraw elements (lines, ellipses, rectangles, etc.) will be changed by the script.
- 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*/
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>
`;
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.7.3")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
/*
SVGColorInfo is returned by ea.getSVGColorInfoForImgElement. Color info will all the color strings in the SVG file plus "fill" which represents the default fill color for SVG icons set at the SVG root element level. Fill if not set defaults to black:
type SVGColorInfo = Map<string, {
mappedTo: string;
fill: boolean;
stroke: boolean;
}>;
In the Excalidraw file under `## Embedded Files` the color map is included after the file. That color map implements ColorMap. ea.updateViewSVGImageColorMap takes a ColorMap as input.
interface ColorMap {
[color: string]: string;
};
*/
// Main script execution
const allElements = ea.getViewSelectedElements();
const svgImageElements = allElements.filter(el => {
if(el.type !== "image") return false;
const file = ea.getViewFileForImageElement(el);
if(!file) return false;
return el.type === "image" && (
file.extension === "svg" ||
ea.isExcalidrawFile(file)
);
});
if(allElements.length === 0) {
new Notice("Select at least one rectangle, ellipse, diamond, line, arrow, freedraw, text or SVG image elment");
return;
}
const originalColors = new Map();
const currentColors = new Map();
const colorInputs = new Map();
const sliderResetters = [];
let terminate = false;
const FORMAT = "Color Format";
const STROKE = "Modify Stroke Color";
const BACKGROUND = "Modify Background Color"
const ACTIONS = ["Hue", "Lightness", "Saturation", "Transparency"];
const precision = [1,2,2,3];
const minLigtness = 1/Math.pow(10,precision[2]);
const maxLightness = 100 - minLigtness;
const minSaturation = 1/Math.pow(10,precision[2]);
let settings = ea.getScriptSettings();
//set default values on first run
if(!settings[STROKE]) {
settings = {};
settings[FORMAT] = {
value: "HEX",
valueset: ["HSL", "RGB", "HEX"],
description: "Output color format."
};
settings[STROKE] = { value: true }
settings[BACKGROUND] = {value: true }
ea.setScriptSettings(settings);
}
function getRegularElements() {
ea.clear();
//loading view elements again as element objects change when colors are updated
const allElements = ea.getViewSelectedElements();
return allElements.filter(el =>
["rectangle", "ellipse", "diamond", "line", "arrow", "freedraw", "text"].includes(el.type)
);
}
const updatedImageElementColorMaps = new Map();
let isWaitingForSVGUpdate = false;
function updateViewImageColors() {
if(terminate || isWaitingForSVGUpdate || updatedImageElementColorMaps.size === 0) {
return;
}
isWaitingForSVGUpdate = true;
elementArray = Array.from(updatedImageElementColorMaps.keys());
colorMapArray = Array.from(updatedImageElementColorMaps.values());
updatedImageElementColorMaps.clear();
ea.updateViewSVGImageColorMap(elementArray, colorMapArray).then(()=>{
isWaitingForSVGUpdate = false;
updateViewImageColors();
});
}
async function storeOriginalColors() {
// Store colors for regular elements
for (const el of getRegularElements()) {
const key = el.id;
const colorData = {
type: "regular",
strokeColor: el.strokeColor,
backgroundColor: el.backgroundColor
};
originalColors.set(key, colorData);
}
// Store colors for SVG elements
for (const el of svgImageElements) {
const colorInfo = await ea.getSVGColorInfoForImgElement(el);
const svgColors = new Map();
for (const [color, info] of colorInfo.entries()) {
svgColors.set(color, {...info});
}
originalColors.set(el.id, {type: "svg",colors: svgColors});
}
copyOriginalsToCurrent();
}
function copyOriginalsToCurrent() {
for (const [key, value] of originalColors.entries()) {
if(value.type === "regular") {
currentColors.set(key, {...value});
} else {
const newColorMap = new Map();
for (const [color, info] of value.colors.entries()) {
newColorMap.set(color, {...info});
}
currentColors.set(key, {type: "svg", colors: newColorMap});
}
}
}
function clearSVGMapping() {
for (const resetter of sliderResetters) {
resetter();
}
// Reset SVG elements
if (svgImageElements.length === 1) {
const el = svgImageElements[0];
const original = originalColors.get(el.id);
const current = currentColors.get(el.id);
if (original && original.type === "svg") {
for (const color of original.colors.keys()) {
current.colors.get(color).mappedTo = color;
}
}
} else {
for (const el of svgImageElements) {
const original = originalColors.get(el.id);
const current = currentColors.get(el.id);
if (original && original.type === "svg") {
for (const color of original.colors.keys()) {
current.colors.get(color).mappedTo = color;
}
}
}
}
run("clear");
}
// Set colors
async function setColors(colors) {
debounceColorPicker = true;
const regularElements = getRegularElements();
if (regularElements.length > 0) {
ea.copyViewElementsToEAforEditing(regularElements);
for (const el of ea.getElements()) {
const original = colors.get(el.id);
if (original && original.type === "regular") {
if (original.strokeColor) el.strokeColor = original.strokeColor;
if (original.backgroundColor) el.backgroundColor = original.backgroundColor;
}
}
await ea.addElementsToView(false, false);
}
// Reset SVG elements
if (svgImageElements.length === 1) {
const el = svgImageElements[0];
const original = colors.get(el.id);
if (original && original.type === "svg") {
const newColorMap = {};
for (const [color, info] of original.colors.entries()) {
newColorMap[color] = info.mappedTo;
// Update UI components
const inputs = colorInputs.get(color);
if (inputs) {
if(info.mappedTo === "fill") {
info.mappedTo = "black";
//"fill" is a special value in case the SVG has no fill color defined (i.e black)
inputs.textInput.setValue("black");
inputs.colorPicker.setValue("#000000");
} else {
const cm = ea.getCM(info.mappedTo);
inputs.textInput.setValue(info.mappedTo);
inputs.colorPicker.setValue(cm.stringHEX({alpha: false}).toLowerCase());
}
}
}
updatedImageElementColorMaps.set(el, newColorMap);
}
} else {
for (const el of svgImageElements) {
const original = colors.get(el.id);
if (original && original.type === "svg") {
const newColorMap = {};
for (const [color, info] of original.colors.entries()) {
newColorMap[color] = info.mappedTo;
}
updatedImageElementColorMaps.set(el, newColorMap);
}
}
}
updateViewImageColors();
}
function modifyColor(color, isDecrease, step, action) {
if (!color) return null;
const cm = ea.getCM(color);
if (!cm) return color;
let modified = cm;
if (modified.lightness === 0) modified = modified.lightnessTo(minLigtness);
if (modified.lightness === 100) modified = modified.lightnessTo(maxLightness);
if (modified.saturation === 0) modified = modified.saturationTo(minSaturation);
switch(action) {
case "Lightness":
// handles edge cases where lightness is 0 or 100 would convert saturation and hue to 0
let lightness = cm.lightness;
const shouldRoundLight = (lightness === minLigtness || lightness === maxLightness);
if (shouldRoundLight) lightness = Math.round(lightness);
lightness += isDecrease ? -step : step;
if (lightness <= 0) lightness = minLigtness;
if (lightness >= 100) lightness = maxLightness;
modified = modified.lightnessTo(lightness);
break;
case "Hue":
modified = isDecrease ? modified.hueBy(-step) : modified.hueBy(step);
break;
case "Transparency":
modified = isDecrease ? modified.alphaBy(-step) : modified.alphaBy(step);
break;
default:
let saturation = cm.saturation;
const shouldRoundSat = saturation === minSaturation;
if (shouldRoundSat) saturation = Math.round(saturation);
saturation += isDecrease ? -step : step;
if (saturation <= 0) saturation = minSaturation;
modified = modified.saturationTo(saturation);
}
const hasAlpha = modified.alpha < 1;
const opts = { alpha: hasAlpha, precision };
const format = settings[FORMAT].value;
switch(format) {
case "RGB": return modified.stringRGB(opts).toLowerCase();
case "HEX": return modified.stringHEX(opts).toLowerCase();
default: return modified.stringHSL(opts).toLowerCase();
}
}
function slider(contentEl, action, min, max, step, invert) {
let prevValue = (max-min)/2;
let debounce = false;
let sliderControl;
new ea.obsidian.Setting(contentEl)
.setName(action)
.addSlider(slider => {
sliderControl = slider;
slider
.setLimits(min, max, step)
.setValue(prevValue)
.onChange(async (value) => {
if (debounce) return;
const isDecrease = invert ? value > prevValue : value < prevValue;
const step = Math.abs(value-prevValue);
prevValue = value;
if(step>0) {
run(action, isDecrease, step);
}
});
}
);
return () => {
debounce = true;
prevValue = (max-min)/2;
sliderControl.setValue(prevValue);
debounce = false;
}
}
function showModal() {
let debounceColorPicker = true;
const modal = new ea.obsidian.Modal(app);
let dirty = false;
modal.onOpen = async () => {
const { contentEl } = modal;
modal.bgOpacity = 0;
contentEl.createEl('h2', { text: 'Shade Master' });
const helpDiv = contentEl.createEl("details", {
attr: { style: "margin-bottom: 1em;background: var(--background-secondary); padding: 1em; border-radius: 4px;" }});
helpDiv.createEl("summary", { text: "Help & Usage Guide", attr: { style: "cursor: pointer; color: var(--text-accent);" } });
const helpDetailsDiv = helpDiv.createEl("div", {
attr: { style: "margin-top: 0em; " }
});
helpDetailsDiv.innerHTML = HELP_TEXT;
const component = new ea.obsidian.Setting(contentEl)
.setName(FORMAT)
.setDesc("Output color format")
.addDropdown(dropdown => dropdown
.addOptions({
"HSL": "HSL",
"RGB": "RGB",
"HEX": "HEX"
})
.setValue(settings[FORMAT].value)
.onChange(value => {
settings[FORMAT].value = value;
run();
dirty = true;
})
);
new ea.obsidian.Setting(contentEl)
.setName(STROKE)
.addToggle(toggle => toggle
.setValue(settings[STROKE].value)
.onChange(value => {
settings[STROKE].value = value;
dirty = true;
})
);
new ea.obsidian.Setting(contentEl)
.setName(BACKGROUND)
.addToggle(toggle => toggle
.setValue(settings[BACKGROUND].value)
.onChange(value => {
settings[BACKGROUND].value = value;
dirty = true;
})
);
// lightness and saturation are on a scale of 0%-100%
// Hue is in degrees, 360 for the full circle
// transparency is on a range between 0 and 1 (equivalent to 0%-100%)
// The range for lightness, saturation and transparency are double since
// the input could be at either end of the scale
// The range for Hue is 360 since regarless of the position on the circle moving
// the slider to the two extremes will travel the entire circle
// To modify blacks and whites, lightness first needs to be changed to value between 1% and 99%
sliderResetters.push(slider(contentEl, "Hue", 0, 360, 1, false));
sliderResetters.push(slider(contentEl, "Saturation", 0, 200, 1, false));
sliderResetters.push(slider(contentEl, "Lightness", 0, 200, 1, false));
sliderResetters.push(slider(contentEl, "Transparency", 0, 2, 0.05, true));
// Add color pickers if a single SVG image is selected
if (svgImageElements.length === 1) {
const svgElement = svgImageElements[0];
const colorInfo = await ea.getSVGColorInfoForImgElement(svgElement);
const colorSection = contentEl.createDiv();
colorSection.createEl('h3', { text: 'SVG Colors' });
for (const [color, info] of colorInfo.entries()) {
const row = new ea.obsidian.Setting(colorSection)
.setName(color === "fill" ? "SVG default" : color)
.setDesc(`${info.fill ? "Fill" : ""}${info.fill && info.stroke ? " & " : ""}${info.stroke ? "Stroke" : ""}`);
row.descEl.style.width = "100px";
row.nameEl.style.width = "100px";
// Create color preview div
const previewDiv = row.controlEl.createDiv();
previewDiv.style.width = "50px";
previewDiv.style.height = "20px";
previewDiv.style.border = "1px solid var(--background-modifier-border)";
if (color === "transparent") {
previewDiv.style.backgroundImage = "linear-gradient(45deg, #808080 25%, transparent 25%), linear-gradient(-45deg, #808080 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #808080 75%), linear-gradient(-45deg, transparent 75%, #808080 75%)";
previewDiv.style.backgroundSize = "10px 10px";
previewDiv.style.backgroundPosition = "0 0, 0 5px, 5px -5px, -5px 0px";
} else {
previewDiv.style.backgroundColor = ea.getCM(color).stringHEX({alpha: false}).toLowerCase();
}
const resetButton = new ea.obsidian.Setting(row.controlEl)
.addButton(button => button
.setButtonText(">>>")
.setClass("reset-color-button")
.onClick(async () => {
const original = originalColors.get(svgElement.id);
const current = currentColors.get(svgElement.id);
if (original?.type === "svg") {
const originalInfo = original.colors.get(color);
const currentInfo = current.colors.get(color);
if (originalInfo) {
currentInfo.mappedTo = color;
run("reset single color");
}
}
}))
resetButton.settingEl.style.padding = "0";
resetButton.settingEl.style.border = "0";
// Add text input for color value
const textInput = new ea.obsidian.TextComponent(row.controlEl)
.setValue(info.mappedTo)
.setPlaceholder("Color value");
textInput.inputEl.style.width = "100%";
textInput.onChange(value => {
const lower = value.toLowerCase();
if (lower === color) return;
textInput.setValue(lower);
})
const applyButtonComponent = new ea.obsidian.Setting(row.controlEl)
.addButton(button => button
.setIcon("check")
.setTooltip("Apply")
.onClick(async () => {
const value = textInput.getValue();
try {
if(!CSS.supports("color",value)) {
new Notice (`${value} is not a valid color string`);
return;
}
const cm = ea.getCM(value);
if (cm) {
const format = settings[FORMAT].value;
const alpha = cm.alpha < 1 ? true : false;
const newColor = format === "RGB"
? cm.stringRGB({alpha , precision }).toLowerCase()
: format === "HEX"
? cm.stringHEX({alpha}).toLowerCase()
: cm.stringHSL({alpha, precision }).toLowerCase();
textInput.setValue(newColor);
const colorInfo = currentColors.get(svgElement.id).colors;
colorInfo.get(color).mappedTo = newColor;
run("no action");
debounceColorPicker = true;
colorPicker.setValue(cm.stringHEX({alpha: false}).toLowerCase());
}
} catch (e) {
console.error("Invalid color value:", e);
}
}));
applyButtonComponent.settingEl.style.padding = "0";
applyButtonComponent.settingEl.style.border = "0";
// Add color picker
const colorPicker = new ea.obsidian.ColorComponent(row.controlEl)
.setValue(ea.getCM(info.mappedTo).stringHEX({alpha: false}).toLowerCase());
// Store references to the components
colorInputs.set(color, {
textInput,
colorPicker,
previewDiv,
resetButton
});
colorPicker.colorPickerEl.addEventListener('click', () => {
debounceColorPicker = false;
});
colorPicker.onChange(async (value) => {
try {
if(!debounceColorPicker) {
// Preserve alpha from original color
const originalAlpha = ea.getCM(info.mappedTo).alpha;
const cm = ea.getCM(value);
cm.alphaTo(originalAlpha);
const alpha = originalAlpha < 1 ? true : false;
const format = settings[FORMAT].value;
const newColor = format === "RGB"
? cm.stringRGB({alpha, precision }).toLowerCase()
: format === "HEX"
? cm.stringHEX({alpha}).toLowerCase()
: cm.stringHSL({alpha, precision }).toLowerCase();
// Update text input
textInput.setValue(newColor);
// Update SVG
const newColorMap = await ea.getColorMapForImageElement(svgElement);
if(color === newColor) {
delete newColorMap[color];
} else {
newColorMap[color] = newColor;
}
updatedImageElementColorMaps.set(svgElement, newColorMap);
updateViewImageColors();
}
} catch (e) {
console.error("Invalid color value:", e);
} finally {
debounceColorPicker = true;
}
});
}
}
const buttons = new ea.obsidian.Setting(contentEl);
if(svgImageElements.length > 0) {
buttons.addButton(button => button
.setButtonText("Initialize SVG Colors")
.onClick(() => {
debounceColorPicker = true;
clearSVGMapping();
})
);
}
buttons
.addButton(button => button
.setButtonText("Reset")
.onClick(() => {
for (const resetter of sliderResetters) {
resetter();
}
copyOriginalsToCurrent();
setColors(originalColors);
}))
.addButton(button => button
.setButtonText("Close")
.setCta(true)
.onClick(() => modal.close()));
makeModalDraggable(modal.modalEl);
};
modal.onClose = () => {
terminate = true;
if (dirty) {
ea.setScriptSettings(settings);
}
if(ea.targetView.isDirty()) {
ea.targetView.save(false);
}
};
modal.open();
}
/**
* Add draggable functionality to the modal element.
* @param {HTMLElement} modalEl - The modal element to make draggable.
*/
function makeModalDraggable(modalEl) {
let isDragging = false;
let startX, startY, initialX, initialY;
const header = modalEl.querySelector('.modal-titlebar') || modalEl; // Default to modalEl if no titlebar
header.style.cursor = 'move';
const onPointerDown = (e) => {
// Ensure the event target isn't an interactive element like slider, button, or input
if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
const rect = modalEl.getBoundingClientRect();
initialX = rect.left;
initialY = rect.top;
modalEl.style.position = 'absolute';
modalEl.style.margin = '0';
modalEl.style.left = `${initialX}px`;
modalEl.style.top = `${initialY}px`;
};
const onPointerMove = (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
modalEl.style.left = `${initialX + dx}px`;
modalEl.style.top = `${initialY + dy}px`;
};
const onPointerUp = () => {
isDragging = false;
};
header.addEventListener('pointerdown', onPointerDown);
document.addEventListener('pointermove', onPointerMove);
document.addEventListener('pointerup', onPointerUp);
// Clean up event listeners on modal close
modalEl.addEventListener('remove', () => {
header.removeEventListener('pointerdown', onPointerDown);
document.removeEventListener('pointermove', onPointerMove);
document.removeEventListener('pointerup', onPointerUp);
});
}
function executeChange(isDecrease, step, action) {
const modifyStroke = settings[STROKE].value;
const modifyBackground = settings[BACKGROUND].value;
const regularElements = getRegularElements();
// Process regular elements
if (regularElements.length > 0) {
for (const el of regularElements) {
const currentColor = currentColors.get(el.id);
if (modifyStroke && currentColor.strokeColor) {
currentColor.strokeColor = modifyColor(el.strokeColor, isDecrease, step, action);
}
if (modifyBackground && currentColor.backgroundColor) {
currentColor.backgroundColor = modifyColor(el.backgroundColor, isDecrease, step, action);
}
}
}
// Process SVG image elements
if (svgImageElements.length === 1) { // Only update UI for single SVG
const el = svgImageElements[0];
colorInfo = currentColors.get(el.id).colors;
// Process each color in the SVG
for (const [color, info] of colorInfo.entries()) {
let shouldModify = (modifyBackground && info.fill) || (modifyStroke && info.stroke);
if (shouldModify) {
const modifiedColor = modifyColor(info.mappedTo, isDecrease, step, action);
colorInfo.get(color).mappedTo = modifiedColor;
// Update UI components if they exist
const inputs = colorInputs.get(color);
if (inputs) {
const cm = ea.getCM(modifiedColor);
inputs.textInput.setValue(modifiedColor);
inputs.colorPicker.setValue(cm.stringHEX({alpha: false}).toLowerCase());
}
}
}
} else {
if (svgImageElements.length > 0) {
for (const el of svgImageElements) {
const colorInfo = currentColors.get(el.id).colors;
// Process each color in the SVG
for (const [color, info] of colorInfo.entries()) {
let shouldModify = (modifyBackground && info.fill) || (modifyStroke && info.stroke);
if (shouldModify) {
const modifiedColor = modifyColor(info.mappedTo, isDecrease, step, action);
colorInfo.get(color).mappedTo = modifiedColor;
}
}
}
}
}
}
let isRunning = false;
let queue = false;
function processQueue() {
if (!terminate && !isRunning && queue) {
queue = false;
isRunning = true;
setColors(currentColors).then(() => {
isRunning = false;
if (queue) processQueue();
});
}
}
function run(action="Hue", isDecrease=true, step=0) {
// passing invalid action (such as "clear") will bypass rewriting of colors using CM
// this is useful when resetting colors to original values
if(ACTIONS.includes(action)) {
executeChange(isDecrease, step, action);
}
queue = true;
if (!isRunning) processQueue();
}
await storeOriginalColors();
showModal();
processQueue();

View 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="M11 17a4 4 0 0 1-8 0V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2Z"/><path d="M16.7 13H19a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H7"/><path d="M 7 17h.01"/><path d="m11 8 2.3-2.3a2.4 2.4 0 0 1 3.404.004L18.6 7.6a2.4 2.4 0 0 1 .026 3.434L9.9 19.8"/></svg>

After

Width:  |  Height:  |  Size: 434 B

View File

@@ -26,6 +26,10 @@ if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.1.7")) {
return;
}
if(ea.targetView.isDirty()) {
ea.targetView.forceSave(true);
}
const hostLeaf = ea.targetView.leaf;
const hostView = hostLeaf.view;
const statusBarElement = document.querySelector("div.status-bar");
@@ -33,7 +37,7 @@ const ctrlKey = ea.targetView.modifierKeyDown.ctrlKey || ea.targetView.modifierK
const altKey = ea.targetView.modifierKeyDown.altKey || ctrlKey;
const shiftKey = ea.targetView.modifierKeyDown.shiftKey;
const shouldStartWithLastSlide = shiftKey && window.ExcalidrawSlideshow &&
(window.ExcalidrawSlideshow.script === utils.scriptFile.path) && (typeof window.ExcalidrawSlideshow.slide === "number")
(window.ExcalidrawSlideshow.script === utils.scriptFile.path) && (typeof window.ExcalidrawSlideshow.slide?.[ea.targetView.file.path] === "number")
//-------------------------------
//constants
//-------------------------------
@@ -57,8 +61,9 @@ const SVG_LASER_OFF = ea.obsidian.getIcon("lucide-wand").outerHTML;
//-------------------------------
//utility & convenience functions
//-------------------------------
let shouldSaveAfterThePresentation = false;
let isLaserOn = false;
let slide = shouldStartWithLastSlide ? window.ExcalidrawSlideshow.slide : 0;
let slide = shouldStartWithLastSlide ? window.ExcalidrawSlideshow.slide?.[ea.targetView.file.path] : 0;
let isFullscreen = false;
const ownerDocument = ea.targetView.ownerDocument;
const startFullscreen = !altKey;
@@ -350,8 +355,8 @@ const navigate = async (dir) => {
}
if(selectSlideDropdown) selectSlideDropdown.value = slide+1;
await scrollToNextRect(nextRect);
if(window.ExcalidrawSlideshow && (typeof window.ExcalidrawSlideshow.slide === "number")) {
window.ExcalidrawSlideshow.slide = slide;
if(window.ExcalidrawSlideshow && (typeof window.ExcalidrawSlideshow.slide?.[ea.targetView.file.path] === "number")) {
window.ExcalidrawSlideshow.slide[ea.targetView.file.path] = slide;
}
}
@@ -505,6 +510,7 @@ const createPresentationNavigationPanel = () => {
new ea.obsidian.ToggleComponent(el)
.setValue(isHidden)
.onChange(value => {
shouldSaveAfterThePresentation = true;
if(value) {
excalidrawAPI.setToast({
message:"The presentation path remain hidden after the presentation. No need to select the line again. Just click the slideshow button to start the next presentation.",
@@ -730,6 +736,9 @@ const exitPresentation = async (openForEdit = false) => {
hostView.refreshCanvasOffset();
excalidrawAPI.setActiveTool({type: "selection"});
})
if(!shouldSaveAfterThePresentation) {
ea.targetView.clearDirty();
}
}
//--------------------------
@@ -755,6 +764,7 @@ const start = async () => {
resetControlPanelElPosition();
}
if(presentationPathType === "line") await toggleArrowVisibility(isHidden);
ea.targetView.clearDirty();
}
const timestamp = Date.now();
@@ -769,10 +779,14 @@ if(window.ExcalidrawSlideshow && (window.ExcalidrawSlideshow.script === utils.sc
window.clearTimeout(window.ExcalidrawSlideshowStartTimer);
delete window.ExcalidrawSlideshowStartTimer;
}
window.ExcalidrawSlideshow = {
script: utils.scriptFile.path,
timestamp,
slide: 0
};
if(!window.ExcalidrawSlideshow) {
window.ExcalidrawSlideshow = {
script: utils.scriptFile.path,
slide: {},
};
}
window.ExcalidrawSlideshow.timestamp = timestamp;
window.ExcalidrawSlideshow.slide[ea.targetView.file.path] = 0;
window.ExcalidrawSlideshowStartTimer = window.setTimeout(start,500);
}

File diff suppressed because one or more lines are too long

View File

@@ -94,6 +94,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/Set%20Dimensions.svg"/></div>|[[#Set Dimensions]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Grid.svg"/></div>|[[#Set Grid]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Stroke%20Width%20of%20Selected%20Elements.svg"/></div>|[[#Set Stroke Width of Selected Elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Shade%20Master.svg"/></div>|[[#Shade Master]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Toggle%20Grid.svg"/></div>|[[#Toggle Grid]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Uniform%20size.svg"/></div>|[[#Uniform Size]]|
@@ -563,6 +564,13 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/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/Set%20Text%20Alignment.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Sets text alignment of text block (cetner, right, left). Useful if you want to set a keyboard shortcut for selecting text alignment.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-text-align.jpg'></td></tr></table>
## Shade Master
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Shade%20Master.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/Shade%20Master.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">You can modify the colors of SVG images, embedded files, and Excalidraw elements in a drawing by changing Hue, Saturation, Lightness and Transparency; and if only a single SVG or nested Excalidraw drawing is selected, then you can remap image colors.<br><iframe width="560" height="315" 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></td></tr></table>
## Slideshow
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Slideshow.md

View File

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

View File

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

View File

@@ -24,7 +24,7 @@
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@zsviczian/excalidraw": "0.17.6-22",
"@zsviczian/excalidraw": "0.17.6-24",
"chroma-js": "^2.4.2",
"clsx": "^2.0.0",
"@zsviczian/colormaster": "^1.2.2",

View File

@@ -5,6 +5,7 @@ import { terser } from "rollup-plugin-terser";
import copy from "rollup-plugin-copy";
import typescript2 from "rollup-plugin-typescript2";
import fs from 'fs';
import path from 'path';
import LZString from 'lz-string';
import postprocess from '@zsviczian/rollup-plugin-postprocess';
import cssnano from 'cssnano';
@@ -16,6 +17,8 @@ import dotenv from 'dotenv';
dotenv.config();
const DIST_FOLDER = 'dist';
const absolutePath = path.resolve(DIST_FOLDER);
fs.mkdirSync(absolutePath, { recursive: true });
const isProd = (process.env.NODE_ENV === "production");
const isLib = (process.env.NODE_ENV === "lib");
console.log(`Running: ${process.env.NODE_ENV}; isProd: ${isProd}; isLib: ${isLib}`);
@@ -32,13 +35,16 @@ function trimLastSemicolon(input) {
}
function minifyCode(code) {
const minified = minify(code,{
compress: true,
const minified = minify(code, {
compress: {
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2170
reduce_vars: false,
},
mangle: true,
output: {
comments: false,
beautify: false,
},
}
});
if (minified.error) {
@@ -55,7 +61,7 @@ function compressLanguageFile(lang) {
return LZString.compressToBase64(minifyCode(`x = ${content};`));
}
const excalidraw_pkg = isLib ? "" : minifyCode( isProd
const excalidraw_pkg = isLib ? "" : minifyCode(isProd
? fs.readFileSync("./node_modules/@zsviczian/excalidraw/dist/excalidraw.production.min.js", "utf8")
: fs.readFileSync("./node_modules/@zsviczian/excalidraw/dist/excalidraw.development.js", "utf8"));
const react_pkg = isLib ? "" : minifyCode(isProd
@@ -145,7 +151,7 @@ const BUILD_CONFIG = {
tsconfig: isProd ? "tsconfig.json" : "tsconfig.dev.json",
sourcemap: !isProd,
clean: true,
verbosity: isProd ? 1 : 2,
//verbosity: isProd ? 1 : 2,
},
...(isProd ? [
terser({

View File

@@ -35,7 +35,6 @@ import {
DEVICE,
FONTS_STYLE_ID,
CJK_STYLE_ID,
updateExcalidrawLib,
loadMermaid,
setRootElementSize,
} from "../constants/constants";

View File

@@ -16,6 +16,7 @@ import {
IMAGE_TYPES,
DEVICE,
sceneCoordsToViewportCoords,
fileid,
} from "../../constants/constants";
import ExcalidrawView, { TextMode } from "../../view/ExcalidrawView";
import {
@@ -53,7 +54,7 @@ import {
getImageSize,
} from "../../utils/utils";
import { extractSVGPNGFileName, getActivePDFPageNumberFromPDFView, getAttachmentsFolderAndFilePath, isObsidianThemeDark, mergeMarkdownFiles, setExcalidrawView } from "../../utils/obsidianUtils";
import { ExcalidrawElement, ExcalidrawEmbeddableElement, ExcalidrawImageElement, ExcalidrawTextElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ExcalidrawElement, ExcalidrawEmbeddableElement, ExcalidrawImageElement, ExcalidrawTextElement, FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ReleaseNotes } from "../../shared/Dialogs/ReleaseNotes";
import { ScriptInstallPrompt } from "../../shared/Dialogs/ScriptInstallPrompt";
import Taskbone from "../../shared/OCR/Taskbone";
@@ -71,6 +72,7 @@ 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;
@@ -1044,6 +1046,43 @@ export class CommandManager {
}
})
this.addCommand({
id: "duplicate-image",
name: t("DUPLICATE_IMAGE"),
checkCallback: (checking:boolean) => {
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if(!view) return false;
if(!view.excalidrawAPI) return false;
const els = view.getViewSelectedElements().filter(el=>el.type==="image");
if(els.length !== 1) {
if(checking) return false;
new Notice("Select a single image element and try again");
return false;
}
const el = els[0] as ExcalidrawImageElement;
const ef = view.excalidrawData.getFile(el.fileId);
if(!ef?.file) return false;
if(checking) return true;
(async()=>{
const ea = getEA(view) as ExcalidrawAutomate;
const isAnchored = Boolean(el.customData?.isAnchored);
const imgId = await ea.addImage(el.x+el.width/5, el.y+el.height/5, ef.file,!isAnchored, isAnchored);
const img = ea.getElement(imgId) as Mutable<ExcalidrawImageElement>;
img.width = el.width;
img.height = el.height;
if(el.crop) img.crop = {...el.crop};
const newFileId = fileid() as FileId;
ea.imagesDict[newFileId] = ea.imagesDict[img.fileId];
ea.imagesDict[newFileId].id = newFileId;
delete ea.imagesDict[img.fileId]
img.fileId = newFileId;
await ea.addElementsToView(false, false, true);
ea.selectElementsInView([imgId]);
ea.destroy();
})();
}
})
this.addCommand({
id: "reset-image-to-100",
name: t("RESET_IMG_TO_100"),

View File

@@ -1,7 +1,7 @@
import { WorkspaceLeaf, TFile, Editor, MarkdownView, MarkdownFileInfo, MetadataCache, App, EventRef, Menu, FileView } from "obsidian";
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { getLink } from "../../utils/fileUtils";
import { editorInsertText, getParentOfClass, setExcalidrawView } from "../../utils/obsidianUtils";
import { editorInsertText, getExcalidrawViews, getParentOfClass, setExcalidrawView } from "../../utils/obsidianUtils";
import ExcalidrawPlugin from "src/core/main";
import { DEBUGGING, debug } from "src/utils/debugHelper";
import { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate";
@@ -81,6 +81,8 @@ export class EventManager {
//save Excalidraw leaf and update embeds when switching to another leaf
this.registerEvent(this.plugin.app.workspace.on("active-leaf-change", this.onActiveLeafChangeHandler.bind(this)));
this.registerEvent(this.app.workspace.on("layout-change", this.onLayoutChangeHandler.bind(this)));
//File Save Trigger Handlers
//Save the drawing if the user clicks outside the Excalidraw Canvas
const onClickEventSaveActiveDrawing = this.onClickSaveActiveDrawing.bind(this);
@@ -101,6 +103,10 @@ export class EventManager {
this.plugin.registerEvent(this.plugin.app.workspace.on("editor-menu", this.onEditorMenuHandler.bind(this)));
}
private onLayoutChangeHandler() {
getExcalidrawViews(this.app).forEach(excalidrawView=>excalidrawView.refresh());
}
private onPasteHandler (evt: ClipboardEvent, editor: Editor, info: MarkdownView | MarkdownFileInfo ) {
if(evt.defaultPrevented) return
const data = evt.clipboardData.getData("text/plain");

View File

@@ -41,6 +41,7 @@ import { Rank } from "src/constants/actionIcons";
import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/constants/constSettingsTags";
import { HotkeyEditor } from "src/shared/Dialogs/HotkeyEditor";
import { getExcalidrawViews } from "src/utils/obsidianUtils";
import { createSliderWithText } from "src/utils/sliderUtils";
export interface ExcalidrawSettings {
folder: string;
@@ -70,6 +71,7 @@ export interface ExcalidrawSettings {
annotatePreserveSize: boolean;
displaySVGInPreview: boolean; //No longer used since 1.9.13
previewImageType: PreviewImageType; //Introduced with 1.9.13
renderingConcurrency: number;
allowImageCache: boolean;
allowImageCacheInScene: boolean;
displayExportedImageIfAvailable: boolean;
@@ -248,6 +250,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
annotatePreserveSize: false,
displaySVGInPreview: undefined,
previewImageType: undefined,
renderingConcurrency: 3,
allowImageCache: true,
allowImageCacheInScene: true,
displayExportedImageIfAvailable: false,
@@ -1053,55 +1056,6 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
cls: "excalidraw-setting-h1",
});
new Setting(detailsEl)
.setName(t("DEFAULT_PEN_MODE_NAME"))
.setDesc(fragWithHTML(t("DEFAULT_PEN_MODE_DESC")))
.addDropdown((dropdown) =>
dropdown
.addOption("never", "Never")
.addOption("mobile", "On Obsidian Mobile")
.addOption("always", "Always")
.setValue(this.plugin.settings.defaultPenMode)
.onChange(async (value: "never" | "always" | "mobile") => {
this.plugin.settings.defaultPenMode = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("DISABLE_DOUBLE_TAP_ERASER_NAME"))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.penModeDoubleTapEraser)
.onChange(async (value) => {
this.plugin.settings.penModeDoubleTapEraser = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("DISABLE_SINGLE_FINGER_PANNING_NAME"))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.penModeSingleFingerPanning)
.onChange(async (value) => {
this.plugin.settings.penModeSingleFingerPanning = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_NAME"))
.setDesc(fragWithHTML(t("SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.penModeCrosshairVisible)
.onChange(async (value) => {
this.plugin.settings.penModeCrosshairVisible = value;
this.applySettingsUpdate();
}),
);
const readingModeEl = new Setting(detailsEl)
.setName(t("SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME"))
.setDesc(fragWithHTML(t("SHOW_DRAWING_OR_MD_IN_READING_MODE_DESC")))
@@ -1328,27 +1282,77 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}),
);
let zoomText: HTMLDivElement;
new Setting(detailsEl)
.setName(t("ZOOM_TO_FIT_MAX_LEVEL_NAME"))
.setDesc(fragWithHTML(t("ZOOM_TO_FIT_MAX_LEVEL_DESC")))
.addSlider((slider) =>
slider
.setLimits(0.5, 10, 0.5)
.setValue(this.plugin.settings.zoomToFitMaxLevel)
.onChange(async (value) => {
zoomText.innerText = ` ${value.toString()}`;
this.plugin.settings.zoomToFitMaxLevel = value;
createSliderWithText(detailsEl, {
name: t("ZOOM_TO_FIT_MAX_LEVEL_NAME"),
desc: t("ZOOM_TO_FIT_MAX_LEVEL_DESC"),
value: this.plugin.settings.zoomToFitMaxLevel,
min: 0.5,
max: 10,
step: 0.5,
onChange: (value) => {
this.plugin.settings.zoomToFitMaxLevel = value;
this.applySettingsUpdate();
}
})
// ------------------------------------------------
// Pen
// ------------------------------------------------
detailsEl = displayDetailsEl.createEl("details");
detailsEl.createEl("summary", {
text: t("PEN_HEAD"),
cls: "excalidraw-setting-h3",
});
new Setting(detailsEl)
.setName(t("DEFAULT_PEN_MODE_NAME"))
.setDesc(fragWithHTML(t("DEFAULT_PEN_MODE_DESC")))
.addDropdown((dropdown) =>
dropdown
.addOption("never", "Never")
.addOption("mobile", "On Obsidian Mobile")
.addOption("always", "Always")
.setValue(this.plugin.settings.defaultPenMode)
.onChange(async (value: "never" | "always" | "mobile") => {
this.plugin.settings.defaultPenMode = value;
this.applySettingsUpdate();
}),
)
.settingEl.createDiv("", (el) => {
zoomText = el;
el.style.minWidth = "2.3em";
el.style.textAlign = "right";
el.innerText = ` ${this.plugin.settings.zoomToFitMaxLevel.toString()}`;
});
);
new Setting(detailsEl)
.setName(t("DISABLE_DOUBLE_TAP_ERASER_NAME"))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.penModeDoubleTapEraser)
.onChange(async (value) => {
this.plugin.settings.penModeDoubleTapEraser = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("DISABLE_SINGLE_FINGER_PANNING_NAME"))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.penModeSingleFingerPanning)
.onChange(async (value) => {
this.plugin.settings.penModeSingleFingerPanning = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_NAME"))
.setDesc(fragWithHTML(t("SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.penModeCrosshairVisible)
.onChange(async (value) => {
this.plugin.settings.penModeCrosshairVisible = value;
this.applySettingsUpdate();
}),
);
// ------------------------------------------------
// Grid
@@ -1397,28 +1401,20 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
);
// Grid opacity slider (hex value between 00 and FF)
let opacityValue: HTMLDivElement;
new Setting(detailsEl)
.setName(t("GRID_OPACITY_NAME"))
.setDesc(fragWithHTML(t("GRID_OPACITY_DESC")))
.addSlider((slider) =>
slider
.setLimits(0, 100, 1) // 0 to 100 in decimal
.setValue(this.plugin.settings.gridSettings.OPACITY)
.onChange(async (value) => {
opacityValue.innerText = ` ${value.toString()}`;
this.plugin.settings.gridSettings.OPACITY = value;
this.applySettingsUpdate();
updateGridColor();
}),
)
.settingEl.createDiv("", (el) => {
opacityValue = el;
el.style.minWidth = "3em";
el.style.textAlign = "right";
el.innerText = ` ${this.plugin.settings.gridSettings.OPACITY}`;
});
createSliderWithText(detailsEl, {
name: t("GRID_OPACITY_NAME"),
desc: t("GRID_OPACITY_DESC"),
value: this.plugin.settings.gridSettings.OPACITY,
min: 0,
max: 100,
step: 1,
onChange: (value) => {
this.plugin.settings.gridSettings.OPACITY = value;
this.applySettingsUpdate();
updateGridColor();
},
minWidth: "3em",
})
// ------------------------------------------------
// Laser Pointer
@@ -1439,47 +1435,33 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}),
);
let decayTime: HTMLDivElement;
new Setting(detailsEl)
.setName(t("LASER_DECAY_TIME_NAME"))
.setDesc(fragWithHTML(t("LASER_DECAY_TIME_DESC")))
.addSlider((slider) =>
slider
.setLimits(500, 20000, 500)
.setValue(this.plugin.settings.laserSettings.DECAY_TIME)
.onChange(async (value) => {
decayTime.innerText = ` ${value.toString()}`;
this.plugin.settings.laserSettings.DECAY_TIME = value;
this.applySettingsUpdate();
}),
)
.settingEl.createDiv("", (el) => {
decayTime = el;
el.style.minWidth = "3em";
el.style.textAlign = "right";
el.innerText = ` ${this.plugin.settings.laserSettings.DECAY_TIME.toString()}`;
});
createSliderWithText(detailsEl, {
name: t("LASER_DECAY_TIME_NAME"),
desc: t("LASER_DECAY_TIME_DESC"),
value: this.plugin.settings.laserSettings.DECAY_TIME,
min: 500,
max: 20000,
step: 500,
onChange: (value) => {
this.plugin.settings.laserSettings.DECAY_TIME = value;
this.applySettingsUpdate();
},
minWidth: "3em",
})
let decayLength: HTMLDivElement;
new Setting(detailsEl)
.setName(t("LASER_DECAY_LENGTH_NAME"))
.setDesc(fragWithHTML(t("LASER_DECAY_LENGTH_DESC")))
.addSlider((slider) =>
slider
.setLimits(25, 2000, 25)
.setValue(this.plugin.settings.laserSettings.DECAY_LENGTH)
.onChange(async (value) => {
decayLength.innerText = ` ${value.toString()}`;
this.plugin.settings.laserSettings.DECAY_LENGTH = value;
this.applySettingsUpdate();
}),
)
.settingEl.createDiv("", (el) => {
decayLength = el;
el.style.minWidth = "3em";
el.style.textAlign = "right";
el.innerText = ` ${this.plugin.settings.laserSettings.DECAY_LENGTH.toString()}`;
});
createSliderWithText(detailsEl, {
name: t("LASER_DECAY_LENGTH_NAME"),
desc: t("LASER_DECAY_LENGTH_DESC"),
value: this.plugin.settings.laserSettings.DECAY_LENGTH,
min: 25,
max: 2000,
step: 25,
onChange: (value) => {
this.plugin.settings.laserSettings.DECAY_LENGTH = value;
this.applySettingsUpdate();
},
minWidth: "3em",
})
detailsEl = displayDetailsEl.createEl("details");
detailsEl.createEl("summary", {
@@ -1488,47 +1470,31 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
});
detailsEl.createDiv({ text: t("DRAG_MODIFIER_DESC"), cls: "setting-item-description" });
let longPressDesktop: HTMLDivElement;
new Setting(detailsEl)
.setName(t("LONG_PRESS_DESKTOP_NAME"))
.setDesc(fragWithHTML(t("LONG_PRESS_DESKTOP_DESC")))
.addSlider((slider) =>
slider
.setLimits(300, 3000, 100)
.setValue(this.plugin.settings.longPressDesktop)
.onChange(async (value) => {
longPressDesktop.innerText = ` ${value.toString()}`;
this.plugin.settings.longPressDesktop = value;
this.applySettingsUpdate(true);
}),
)
.settingEl.createDiv("", (el) => {
longPressDesktop = el;
el.style.minWidth = "2.3em";
el.style.textAlign = "right";
el.innerText = ` ${this.plugin.settings.longPressDesktop.toString()}`;
});
createSliderWithText(detailsEl, {
name: t("LONG_PRESS_DESKTOP_NAME"),
desc: t("LONG_PRESS_DESKTOP_DESC"),
value: this.plugin.settings.longPressDesktop,
min: 300,
max: 3000,
step: 100,
onChange: (value) => {
this.plugin.settings.longPressDesktop = value;
this.applySettingsUpdate(true);
},
})
let longPressMobile: HTMLDivElement;
new Setting(detailsEl)
.setName(t("LONG_PRESS_MOBILE_NAME"))
.setDesc(fragWithHTML(t("LONG_PRESS_MOBILE_DESC")))
.addSlider((slider) =>
slider
.setLimits(300, 3000, 100)
.setValue(this.plugin.settings.longPressMobile)
.onChange(async (value) => {
longPressMobile.innerText = ` ${value.toString()}`;
this.plugin.settings.longPressMobile = value;
this.applySettingsUpdate(true);
}),
)
.settingEl.createDiv("", (el) => {
longPressMobile = el;
el.style.minWidth = "2.3em";
el.style.textAlign = "right";
el.innerText = ` ${this.plugin.settings.longPressMobile.toString()}`;
});
createSliderWithText(detailsEl, {
name: t("LONG_PRESS_MOBILE_NAME"),
desc: t("LONG_PRESS_MOBILE_DESC"),
value: this.plugin.settings.longPressMobile,
min: 300,
max: 3000,
step: 100,
onChange: (value) => {
this.plugin.settings.longPressMobile = value;
this.applySettingsUpdate(true);
},
})
new Setting(detailsEl)
.setName(t("DOUBLE_CLICK_LINK_OPEN_VIEW_MODE"))
@@ -1700,26 +1666,18 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
);
donePrefixSetting.setDisabled(!this.plugin.settings.parseTODO);
let opacityText: HTMLDivElement;
new Setting(detailsEl)
.setName(t("LINKOPACITY_NAME"))
.setDesc(fragWithHTML(t("LINKOPACITY_DESC")))
.addSlider((slider) =>
slider
.setLimits(0, 1, 0.05)
.setValue(this.plugin.settings.linkOpacity)
.onChange(async (value) => {
opacityText.innerText = ` ${value.toString()}`;
this.plugin.settings.linkOpacity = value;
this.applySettingsUpdate(true);
}),
)
.settingEl.createDiv("", (el) => {
opacityText = el;
el.style.minWidth = "2.3em";
el.style.textAlign = "right";
el.innerText = ` ${this.plugin.settings.linkOpacity.toString()}`;
});
createSliderWithText(detailsEl, {
name: t("LINKOPACITY_NAME"),
desc: t("LINKOPACITY_DESC"),
value: this.plugin.settings.linkOpacity,
min: 0,
max: 1,
step: 0.05,
onChange: (value) => {
this.plugin.settings.linkOpacity = value;
this.applySettingsUpdate(true);
},
});
new Setting(detailsEl)
.setName(t("HOVERPREVIEW_NAME"))
@@ -1953,6 +1911,19 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
cls: "excalidraw-setting-h3",
});
createSliderWithText(detailsEl, {
name: t("RENDERING_CONCURRENCY_NAME"),
desc: t("RENDERING_CONCURRENCY_DESC"),
min: 1,
max: 5,
step: 1,
value: this.plugin.settings.renderingConcurrency,
onChange: (value) => {
this.plugin.settings.renderingConcurrency = value;
this.applySettingsUpdate();
}
});
new Setting(detailsEl)
.setName(t("EMBED_IMAGE_CACHE_NAME"))
.setDesc(fragWithHTML(t("EMBED_IMAGE_CACHE_DESC")))
@@ -2077,49 +2048,31 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}),
);
let scaleText: HTMLDivElement;
createSliderWithText(detailsEl, {
name: t("EXPORT_PNG_SCALE_NAME"),
desc: t("EXPORT_PNG_SCALE_DESC"),
value: this.plugin.settings.pngExportScale,
min: 1,
max: 5,
step: 0.5,
onChange: (value) => {
this.plugin.settings.pngExportScale = value;
this.applySettingsUpdate();
}
});
new Setting(detailsEl)
.setName(t("EXPORT_PNG_SCALE_NAME"))
.setDesc(fragWithHTML(t("EXPORT_PNG_SCALE_DESC")))
.addSlider((slider) =>
slider
.setLimits(1, 5, 0.5)
.setValue(this.plugin.settings.pngExportScale)
.onChange(async (value) => {
scaleText.innerText = ` ${value.toString()}`;
this.plugin.settings.pngExportScale = value;
this.applySettingsUpdate();
}),
)
.settingEl.createDiv("", (el) => {
scaleText = el;
el.style.minWidth = "2.3em";
el.style.textAlign = "right";
el.innerText = ` ${this.plugin.settings.pngExportScale.toString()}`;
});
let exportPadding: HTMLDivElement;
new Setting(detailsEl)
.setName(t("EXPORT_PADDING_NAME"))
.setDesc(fragWithHTML(t("EXPORT_PADDING_DESC")))
.addSlider((slider) =>
slider
.setLimits(0, 50, 5)
.setValue(this.plugin.settings.exportPaddingSVG)
.onChange(async (value) => {
exportPadding.innerText = ` ${value.toString()}`;
this.plugin.settings.exportPaddingSVG = value;
this.applySettingsUpdate();
}),
)
.settingEl.createDiv("", (el) => {
exportPadding = el;
el.style.minWidth = "2.3em";
el.style.textAlign = "right";
el.innerText = ` ${this.plugin.settings.exportPaddingSVG.toString()}`;
});
createSliderWithText(detailsEl, {
name: t("EXPORT_PADDING_NAME"),
desc: fragWithHTML(t("EXPORT_PADDING_DESC")),
value: this.plugin.settings.exportPaddingSVG,
min: 0,
max: 50,
step: 5,
onChange: (value) => {
this.plugin.settings.exportPaddingSVG = value;
this.applySettingsUpdate();
}
});
detailsEl = exportDetailsEl.createEl("details");
detailsEl.createEl("summary", {
@@ -2460,27 +2413,19 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
cls: "excalidraw-setting-h3",
});
let areaZoomText: HTMLDivElement;
new Setting(detailsEl)
.setName(t("MAX_IMAGE_ZOOM_IN_NAME"))
.setDesc(fragWithHTML(t("MAX_IMAGE_ZOOM_IN_DESC")))
.addSlider((slider) =>
slider
.setLimits(1, 10, 0.5)
.setValue(this.plugin.settings.areaZoomLimit)
.onChange(async (value) => {
areaZoomText.innerText = ` ${value.toString()}`;
this.plugin.settings.areaZoomLimit = value;
this.applySettingsUpdate();
this.plugin.excalidrawConfig.updateValues(this.plugin);
}),
)
.settingEl.createDiv("", (el) => {
areaZoomText = el;
el.style.minWidth = "2.3em";
el.style.textAlign = "right";
el.innerText = ` ${this.plugin.settings.areaZoomLimit.toString()}`;
createSliderWithText(detailsEl, {
name: t("MAX_IMAGE_ZOOM_IN_NAME"),
desc: fragWithHTML(t("MAX_IMAGE_ZOOM_IN_DESC")),
value: this.plugin.settings.areaZoomLimit,
min: 1,
max: 10,
step: 0.5,
onChange: (value) => {
this.plugin.settings.areaZoomLimit = value;
this.applySettingsUpdate();
this.plugin.excalidrawConfig.updateValues(this.plugin);
},
});
detailsEl = nonstandardDetailsEl.createEl("details");

View File

@@ -30,6 +30,7 @@ export default {
"Script is up to date - Click to reinstall",
OPEN_AS_EXCALIDRAW: "Open as Excalidraw Drawing",
TOGGLE_MODE: "Toggle between Excalidraw and Markdown mode",
DUPLICATE_IMAGE: "Duplicate selected image with a different image ID",
CONVERT_NOTE_TO_EXCALIDRAW: "Convert markdown note to Excalidraw Drawing",
CONVERT_EXCALIDRAW: "Convert *.excalidraw to *.md files",
CREATE_NEW: "Create new drawing",
@@ -416,6 +417,7 @@ FILENAME_HEAD: "Filename",
ZOOM_TO_FIT_MAX_LEVEL_NAME: "Zoom to fit max ZOOM level",
ZOOM_TO_FIT_MAX_LEVEL_DESC:
"Set the maximum level to which zoom to fit will enlarge the drawing. Minimum is 0.5 (50%) and maximum is 10 (1000%).",
PEN_HEAD: "Pen",
GRID_HEAD: "Grid",
GRID_DYNAMIC_COLOR_NAME: "Dynamic grid color",
GRID_DYNAMIC_COLOR_DESC:
@@ -578,7 +580,11 @@ FILENAME_HEAD: "Filename",
EMBED_CANVAS_DESC:
"Hide canvas node border and background when embedding an Excalidraw drawing to Canvas. " +
"Note that for a full transparent background for your image, you will still need to configure Excalidraw to export images with transparent background.",
EMBED_CACHING: "Image caching",
EMBED_CACHING: "Image caching and rendering optimization",
RENDERING_CONCURRENCY_NAME: "Image rendering concurrency",
RENDERING_CONCURRENCY_DESC:
"Number of parallel workers to use for image rendering. Increasing this number will speed up the rendering process, but may slow down the rest of the system. " +
"The default value is 3. You can increase this number if you have a powerful system.",
EXPORT_SUBHEAD: "Export Settings",
EMBED_SIZING: "Image sizing",
EMBED_THEME_BACKGROUND: "Image theme and background color",

View File

@@ -30,6 +30,7 @@ export default {
"脚本已是最新 - 点击重新安装",
OPEN_AS_EXCALIDRAW: "打开为 Excalidraw 绘图",
TOGGLE_MODE: "在 Excalidraw 和 Markdown 模式之间切换",
DUPLICATE_IMAGE : "复制选定的图像,并分配一个不同的图像 ID",
CONVERT_NOTE_TO_EXCALIDRAW: "转换:空白 Markdown 文档 => Excalidraw 绘图文件",
CONVERT_EXCALIDRAW: "转换: *.excalidraw => *.md",
CREATE_NEW: "新建绘图文件",
@@ -196,7 +197,7 @@ export default {
NEWVERSION_NOTIFICATION_DESC:
"<b>开启:</b>当本插件存在可用更新时,显示通知。<br>" +
"<b>关闭:</b>您需要手动检查本插件的更新(设置 - 第三方插件 - 检查更新)。",
BASIC_HEAD: "基本",
BASIC_DESC: `包括:更新说明,更新提示,新绘图文件、模板文件、脚本文件的存储路径等的设置。`,
FOLDER_NAME: "Excalidraw 文件夹(區分大小寫!)",
@@ -406,7 +407,7 @@ FILENAME_HEAD: "文件名",
DEFAULT_WHEELZOOM_NAME: "鼠标滚轮缩放页面",
DEFAULT_WHEELZOOM_DESC:
`<b>开启:</b>鼠标滚轮为缩放页面,${labelCTRL()}+鼠标滚轮为滚动页面</br><b>关闭:</b>鼠标滚轮为滚动页面,${labelCTRL()}+鼠标滚轮为缩放页面`,
ZOOM_TO_FIT_NAME: "调节面板尺寸后自动缩放页面",
ZOOM_TO_FIT_DESC: "调节面板尺寸后,自适应地缩放页面" +
"<br><b>开启:</b>自动缩放。<br><b>关闭:</b>禁用自动缩放。",
@@ -416,6 +417,7 @@ FILENAME_HEAD: "文件名",
ZOOM_TO_FIT_MAX_LEVEL_NAME: "自动缩放的最高级别",
ZOOM_TO_FIT_MAX_LEVEL_DESC:
"自动缩放画布时,允许放大的最高级别。该值不能低于 0.550%)且不能超过 101000%)。",
PEN_HEAD: "手写笔",
GRID_HEAD: "网格",
GRID_DYNAMIC_COLOR_NAME: "动态网格颜色",
GRID_DYNAMIC_COLOR_DESC:
@@ -575,10 +577,14 @@ FILENAME_HEAD: "文件名",
此外,还有自动导出 SVG 或 PNG 文件并保持与绘图文件状态同步的设置。`,
EMBED_CANVAS: "Obsidian 白板支持",
EMBED_CANVAS_NAME: "沉浸式嵌入",
EMBED_CANVAS_DESC:
EMBED_CANVAS_DESC:
"当嵌入绘图到 Obsidian 白板中时,隐藏元素的边界和背景。" +
"注意:如果想要背景完全透明,您依然需要在 Excalidraw 中设置“导出的图像不包含背景”。",
EMBED_CACHING: "预览图缓存",
EMBED_CACHING : "图像缓存和渲染优化" ,
RENDERING_CONCURRENCY_NAME : "图像渲染并发性" ,
RENDERING_CONCURRENCY_DESC :
"用于图像渲染的并行工作线程数。增加此数值可以加快渲染速度,但可能会减慢系统的其他部分运行速度。" +
"默认值为 3。如果您的系统性能强大可以增加此数值。" ,
EXPORT_SUBHEAD: "导出",
EMBED_SIZING: "图像尺寸",
EMBED_THEME_BACKGROUND: "图像的主题和背景色",
@@ -586,7 +592,7 @@ FILENAME_HEAD: "文件名",
EMBED_IMAGE_CACHE_DESC: "可提高下次嵌入的速度。" +
"但如果绘图中又嵌入了子绘图,当子绘图改变时,您需要打开子绘图并手动保存,才能够更新父绘图的预览图。",
SCENE_IMAGE_CACHE_NAME: "缓存场景中嵌套的 Excalidraw",
SCENE_IMAGE_CACHE_DESC: "缓存场景中嵌套的 Excalidraw 以加快场景渲染速度。这将加快渲染过程,特别是在您的场景中有深度嵌套的 Excalidraw 时。" +
SCENE_IMAGE_CACHE_DESC: "缓存场景中嵌套的 Excalidraw 以加快场景渲染速度。这将加快渲染过程,特别是在您的场景中有深度嵌套的 Excalidraw 时。" +
"Excalidraw 将智能地尝试识别嵌套 Excalidraw 的子元素是否发生变化,并更新缓存。 " +
"如果您怀疑缓存未能正确更新,您可能需要关闭此功能。",
EMBED_IMAGE_CACHE_CLEAR: "清除缓存",
@@ -630,7 +636,7 @@ FILENAME_HEAD: "文件名",
"如果您选择了 PNG 或 SVG 副本,当副本不存在时,该命令将会插入一条损坏的链接,您需要打开绘图文件并手动导出副本才能修复 —— " +
"也就是说,该选项不会自动帮您生成 PNG/SVG 副本,而只会引用已有的 PNG/SVG 副本。",
EMBED_MARKDOWN_COMMENT_NAME: "将链接作为注释嵌入",
EMBED_MARKDOWN_COMMENT_DESC:
EMBED_MARKDOWN_COMMENT_DESC:
"在图像下方以 Markdown 链接的形式嵌入原始 Excalidraw 文件的链接,例如:<code>%%[[drawing.excalidraw]]%%</code>。<br>" +
"除了添加 Markdown 注释之外,您还可以选择嵌入的 SVG 或 PNG并使用命令面板" +
"'<code>Excalidraw: 打开 Excalidraw 绘图</code>'来打开该绘图",
@@ -709,7 +715,7 @@ FILENAME_HEAD: "文件名",
"文件浏览器等创建的绘图都将是旧格式(*.excalidraw。" +
"此外,您打开旧格式绘图文件时将不再收到警告消息。",
MATHJAX_NAME: "MathJax (LaTeX) 的 javascript 库服务器",
MATHJAX_DESC: "如果您在绘图中使用 LaTeX插件需要从服务器获取并加载一个 javascript 库。" +
MATHJAX_DESC: "如果您在绘图中使用 LaTeX插件需要从服务器获取并加载一个 javascript 库。" +
"如果您的网络无法访问某些库服务器,可以尝试通过此选项更换库服务器。"+
"更改此选项后,您可能需要重启 Obsidian 来使其生效。",
LATEX_DEFAULT_NAME: "插入 LaTeX 时的默认表达式",
@@ -728,7 +734,7 @@ FILENAME_HEAD: "文件名",
EXPERIMENTAL_HEAD: "杂项",
EXPERIMENTAL_DESC: `包括:默认的 LaTeX 公式字段建议绘图文件的类型标识符OCR 等设置。`,
EA_HEAD: "Excalidraw 自动化",
EA_DESC:
EA_DESC:
"ExcalidrawAutomate 是用于 Excalidraw 自动化脚本的 API但是目前说明文档还不够完善" +
"建议阅读 <a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/docs/API/ExcalidrawAutomate.d.ts'>ExcalidrawAutomate.d.ts</a> 文件源码," +
"参考 <a href='https://zsviczian.github.io/obsidian-excalidraw-plugin/'>ExcalidrawAutomate How-to</a> 网页(不过该网页" +
@@ -791,7 +797,7 @@ FILENAME_HEAD: "文件名",
CJK_ASSETS_FOLDER_NAME: "CJK 字体文件夹(區分大小寫!)",
CJK_ASSETS_FOLDER_DESC: `您可以在此设置 CJK 字体文件夹的位置。例如,您可以选择将其放置在 <code>Excalidraw/CJK Fonts</code> 下。<br><br>
<strong>重要:</strong> 请勿将此文件夹设置为 Vault 根目录!请勿在此文件夹中放置其他字体。<br><br>
<strong>注意:</strong> 如果您使用 Obsidian Sync 并希望在设备之间同步这些字体文件,请确保 Obsidian Sync 设置为同步“所有其他文件类型”。`,
<strong>注意:</strong> 如果您使用 Obsidian Sync 并希望在设备之间同步这些字体文件,请确保 Obsidian Sync 设置为同步“所有其他文件类型”。`,
LOAD_CHINESE_FONTS_NAME: "启动时从文件加载中文字体",
LOAD_JAPANESE_FONTS_NAME: "启动时从文件加载日文字体",
LOAD_KOREAN_FONTS_NAME: "启动时从文件加载韩文字体",
@@ -806,7 +812,7 @@ FILENAME_HEAD: "文件名",
TASKBONE_ENABLE_DESC: "启用意味着您同意 Taskbone <a href='https://www.taskbone.com/legal/terms/' target='_blank'>条款及细则</a> 以及 " +
"<a href='https://www.taskbone.com/legal/privacy/' target='_blank'>隐私政策</a>。",
TASKBONE_APIKEY_NAME: "Taskbone API Key",
TASKBONE_APIKEY_DESC: "Taskbone 的免费 API key 提供了一定数量的每月识别次数。如果您非常频繁地使用此功能,或者想要支持 " +
TASKBONE_APIKEY_DESC: "Taskbone 的免费 API key 提供了一定数量的每月识别次数。如果您非常频繁地使用此功能,或者想要支持 " +
"Taskbone 的开发者您懂的没有人能用爱发电Taskbone 开发者也需要投入资金来维持这项 OCR 服务)您可以" +
"到 <a href='https://www.taskbone.com/' target='_blank'>taskbone.com</a> 购买一个商用 API key。购买后请将它填写到旁边这个文本框里替换掉原本自动生成的免费 API key。",

View File

@@ -17,6 +17,88 @@ I develop this plugin as a hobby, spending my free time doing this. If you find
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://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.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)
@@ -78,56 +160,4 @@ I misread a line in the Excalidraw package code... ended up breaking image loadi
## Fixed
- Error saving when cropping images embedded from a URL (not from a file in the Vault) [#2096](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2096)
`,
"2.6.3":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/OfUWAvCgbXk" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## New
- **Cropping PDF Pages**
- Improved PDF++ cropping: You can now double-click cropped images in Excalidraw to adjust the crop area, which will also appear as a highlight in PDF++. This feature applies to PDF cut-outs created in version 2.6.3 and beyond.
- **Insert Last Active PDF Page as Image**
- New command palette action lets you insert the currently active PDF page into Excalidraw. Ideal for setups with PDF and Excalidraw side-by-side. You can assign a hotkey for quicker access. Cropped areas in Excalidraw will show as highlights in PDF++.
## Fixed
- Fixed **Close Settings** button toggle behavior [#2085](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2085)
- Resolved text wrapping issues causing layout shifts due to trailing whitespaces [#8714](https://github.com/excalidraw/excalidraw/pull/8714)
- **Aspect Ratio and Size Reset** commands now function correctly with cropped images.
- **Cropped Drawings**: Adjustments to cropped Excalidraw drawings are now supported. However, for nested Excalidraw drawings, it's recommended to use area, group, and frame references instead of cropping.
## Refactoring
- Further font loading optimizations on Excalidraw.com; no impact expected in Obsidian [#8693](https://github.com/excalidraw/excalidraw/pull/8693)
- Text wrapping improvements [#8715](https://github.com/excalidraw/excalidraw/pull/8715)
- Plugin initiation and error handling
`,
"2.6.2":`
## Fixed
- Image scaling issue with SVGs that miss the width and height property. [#8729](https://github.com/excalidraw/excalidraw/issues/8729)
`,
"2.6.1":`
## New
- Pen-mode single-finger panning enabled also for the "Selection" tool.
- You can disable pen-mode single-finger panning in Plugin Settings under Excalidraw Appearance and Behavior [#2080](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2080)
## Fixed
- Text tool did not work in pen-mode using finger [#2080](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2080)
- Pasting images to Excalidraw from the web resulted in filenames of "image_1.png", "image_2.png" instead of "Pasted Image TIMESTAMP" [#2081](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2081)
`,
"2.6.0":`
## Performance
- Much faster plugin initialization. Down from 1000-3000ms to 100-300ms. According to my testing speed varies on a wide spectrum depending on device, size of Vault and other plugins being loaded. I measured values ranging from 84ms up to 782ms [#2068](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2068)
- Faster loading of scenes with many embedded illustrations or PDF pages.
- SVG export results in even smaller files by further optimizing which characters are included in the embedded fonts. [#8641](https://github.com/excalidraw/excalidraw/pull/8641)
## New
- Image cropping tool. Double click the image to crop it. [#8613](https://github.com/excalidraw/excalidraw/pull/8613)
- Single finger panning in pen mode.
- Native handwritten CJK Font support [8530](https://github.com/excalidraw/excalidraw/pull/8530)
- Created a new **Fonts** section in settings. This includes configuration of the "Local Font" and downloading of the CJK fonts in case you need them offline.
- Option under **Appearance and Behavior / Link Click** to disable double-click link navigation in view mode. [#2075](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2075)
- New RU translation 🙏[@tovBender](https://github.com/tovBender)
## Updated
- CN translation 🙏[@dmscode](https://github.com/dmscode)
`,
};

View File

@@ -297,8 +297,13 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "addImage",
code: "async addImage(topX: number, topY: number, imageFile: TFile|string, scale?: boolean, anchor?: boolean): Promise<string>;",
desc: "imageFile may be a TFile or a string that contains a hyperlink. imageFile may also be an obsidian filepath including a reference eg.: 'path/my.pdf#page=3'\nSet scale to false if you want to embed the image at 100% of its original size. Default is true which will insert a scaled image.\nanchor will only be evaluated if scale is false. anchor true will add |100% to the end of the filename, resulting in an image that will always pop back to 100% when the source file is updated or when the Excalidraw file is reopened.",
code: "async addImage(opts: {topX: number, topY: number, imageFile: TFile|string, scale?: boolean, anchor?: boolean, colorMap?: ColorMap}): Promise<string>;",
desc: "imageFile may be a TFile or a string that contains a hyperlink.\n"+
"imageFile may also be an obsidian filepath including a reference eg.: 'path/my.pdf#page=3'\n"+
"Set scale to false if you want to embed the image at 100% of its original size. Default is true which will insert a scaled image.\n"+
"anchor will only be evaluated if scale is false. anchor true will add |100% to the end of the filename, resulting in an image that will always pop back to 100% when the source file is updated or when the Excalidraw file is reopened.\n"+
"colorMap is only used for SVG images and nested Excalidraw images. See the Shade Master script and the Deconstruct Selected Elements script for examples using colorMap.\n"+
"type ColorMap = { [color: string]: string; }",
after: "",
},
{
@@ -415,6 +420,47 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: "Returns the TFile file handle for the image element",
after: "",
},
{
field: "updateViewSVGImageColorMap",
code: "async updateViewSVGImageColorMap(elements: ExcalidrawImageElement | ExcalidrawImageElement[], colors: ColorMap | SVGColorInfo | ColorMap[] | SVGColorInfo[]): Promise<void>;",
desc: '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 (i.e. will be saved at next scheduled time) and the image will be reset using the color map.\n'+
'See "Shade Master" scritp in Script Library for an example of using this function.\n\n' +
'type SVGColorInfo = Map<string, { mappedTo: string; fill: boolean; stroke: boolean; }>\n' +
'type ColorMap = { [color: string]: string; }',
after: "",
},
{
field: "getColorMapForImageElement",
code: "getColorMapForImageElement(el: ExcalidrawElement): ColorMap",
desc: '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. The colorMap does not include all colors in the image, only those that have been mapped.\n' +
'See "Shade Master" scritp in Script Library for an example of using this function.\n\n' +
'type ColorMap = { [color: string]: string; }',
after: "",
},
{
field: "getSVGColorInfoForImgElement",
code: "async getColorMapForImgElement(el: ExcalidrawElement): Promise<SVGColorInfo>",
desc: 'This function must be awaited. 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.\n' +
'See "Shade Master" scritp in Script Library for an example of using this function.\n\n' +
'type SVGColorInfo = Map<string, { mappedTo: string; fill: boolean; stroke: boolean; }>',
after: "",
},
{
field: "getColosFromExcalidrawFile",
code: "async getColosFromExcalidrawFile(file:TFile, img: ExcalidrawImageElement): Promise<SVGColorInfo>",
desc: 'Must be awaited. Extracts the fill (background) and stroke colors from an excalidraw file and returns them as an SVGColorInfo. The SVGColorInfo is a map where the keys are the colors used in the SVG and the values contain information about whether the color is used for fill, stroke, or both.\n' +
'See "Shade Master" scritp in Script Library for an example of using this function.\n\n' +
'type SVGColorInfo = Map<string, { mappedTo: string; fill: boolean; stroke: boolean; }>',
after: "",
},
{
field: "getColorsFromSVGString",
code: "getColorsFromSVGString(svgString: string): SVGColorInfo",
desc: 'Extracts the fill and stroke colors from an SVG string and returns them as an SVGColorInfo. The SVGColorInfo is a map where the keys are the colors used in the SVG and the values contain information about whether the color is used for fill, stroke, or both.\n' +
'See "Shade Master" scritp in Script Library for an example of using this function.\n\n' +
'type SVGColorInfo = Map<string, { mappedTo: string; fill: boolean; stroke: boolean; }>',
after: "",
},
{
field: "copyViewElementsToEAforEditing",
code: "copyViewElementsToEAforEditing(elements: ExcalidrawElement[], copyImages: boolean = false): void;",

View File

@@ -107,6 +107,20 @@ const replaceSVGColors = (svg: SVGSVGElement | string, colorMap: ColorMap | null
if(typeof svg === 'string') {
// Replace colors in the SVG string
for (const [oldColor, newColor] of Object.entries(colorMap)) {
if(oldColor === "stroke" || oldColor === "fill") {
const [svgTag, prefix, suffix] = (svg.match(/(<svg[^>]*)(>)/i) || []) as string[];
if (!svgTag) continue;
svg = svg.replace(
svgTag,
svgTag.match(new RegExp(`${oldColor}=["'][^"']*["']`))
? prefix.replace(
new RegExp(`${oldColor}=["'][^"']*["']`,'i'),
`${oldColor}="${newColor}"`) + suffix
: `${prefix} ${oldColor}="${newColor}"${suffix}`
);
continue;
}
const fillRegex = new RegExp(`fill="${oldColor}"`, 'gi');
svg = svg.replaceAll(fillRegex, `fill="${newColor}"`);
const fillStyleRegex = new RegExp(`fill:${oldColor}`, 'gi');
@@ -137,6 +151,8 @@ const replaceSVGColors = (svg: SVGSVGElement | string, colorMap: ColorMap | null
}
}
if("fill" in colorMap) svg.setAttribute("fill", colorMap.fill);
if("stroke" in colorMap) svg.setAttribute("stroke", colorMap.stroke);
for (const child of svg.childNodes) {
childNodes(child);
}
@@ -515,7 +531,7 @@ export class EmbeddedFilesLoader {
) {
return null;
}
const ab = isHyperLink || isPDF
const ab = isHyperLink || isPDF || isExcalidrawFile
? null
: isLocalLink
? await readLocalFileBinary(this.getLocalPath((inFile as EmbeddedFile).hyperlink))
@@ -590,13 +606,19 @@ export class EmbeddedFilesLoader {
}
}
public async loadSceneFiles(
excalidrawData: ExcalidrawData,
addFiles: (files: FileData[], isDark: boolean, final?: boolean) => void,
depth:number,
isThemeChange:boolean = false,
fileIDWhiteList?: Set<FileId>,
) {
public async loadSceneFiles({
excalidrawData,
addFiles,
depth,
isThemeChange = false,
fileIDWhiteList,
}: {
excalidrawData: ExcalidrawData;
addFiles: (files: FileData[], isDark: boolean, final?: boolean) => void;
depth: number;
isThemeChange?: boolean;
fileIDWhiteList?: Set<FileId>;
}) {
if(depth > 7) {
new Notice(t("INFINITE_LOOP_WARNING")+depth.toString(), 6000);
@@ -759,13 +781,14 @@ export class EmbeddedFilesLoader {
}, 1200);
const iterator = loadIterator.bind(this)();
const concurency = 3;
const concurency = this.plugin.settings.renderingConcurrency;
await new PromisePool(iterator, concurency).all();
clearInterval(addFilesTimer);
this.emptyPDFDocsMap();
if (this.terminate) {
addFiles(undefined, this.isDark, true);
return;
}
//debug({where:"EmbeddedFileLoader.loadSceneFiles",uid:this.uid,status:"add Files"});
@@ -810,7 +833,19 @@ export class EmbeddedFilesLoader {
viewport
};
await page.render(renderCtx).promise;
//when obsidian loads there seems to be an occasional race condition where the rendering is cancelled
//this is a workaround for that
const maxRetries = 4;
for (let i = 0; i < maxRetries; i++) {
try {
await page.render(renderCtx).promise;
break;
} catch (e) {
if (i === maxRetries - 1) throw e; // Throw on last retry
await sleep(50*(i+1));
continue;
}
}
if(validRect) {
const [left, bottom, _, top] = page.view;

View File

@@ -13,7 +13,7 @@ import {
ExcalidrawFrameElement,
ExcalidrawTextContainer,
} from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { MimeType } from "./EmbeddedFileLoader";
import { ColorMap, MimeType } from "./EmbeddedFileLoader";
import { Editor, normalizePath, Notice, OpenViewState, RequestUrlResponse, TFile, TFolder, WorkspaceLeaf } from "obsidian";
import * as obsidian_module from "obsidian";
import ExcalidrawView, { ExportSettings, TextMode, getTextMode } from "src/view/ExcalidrawView";
@@ -95,6 +95,9 @@ import { addBackOfTheNoteCard, getFrameBasedOnFrameNameOrId } from "../utils/exc
import { log } from "../utils/debugHelper";
import { ExcalidrawLib } from "../types/excalidrawLib";
import { GlobalPoint } from "@zsviczian/excalidraw/types/math/types";
import { AddImageOptions, ImageInfo, SVGColorInfo } from "src/types/excalidrawAutomateTypes";
import { errorMessage, filterColorMap, getEmbeddedFileForImageElment, isColorStringTransparent, isSVGColorInfo, mergeColorMapIntoSVGColorInfo, svgColorInfoToColorMap, updateOrAddSVGColorInfo } from "src/utils/excalidrawAutomateUtils";
import { Color } from "chroma-js";
extendPlugins([
HarmonyPlugin,
@@ -390,7 +393,7 @@ export class ExcalidrawAutomate {
plugin: ExcalidrawPlugin;
elementsDict: {[key:string]:any}; //contains the ExcalidrawElements currently edited in Automate indexed by el.id
imagesDict: {[key: FileId]: any}; //the images files including DataURL, indexed by fileId
imagesDict: {[key: FileId]: ImageInfo}; //the images files including DataURL, indexed by fileId
mostRecentMarkdownSVG:SVGSVGElement = null; //Markdown renderer will drop a copy of the most recent SVG here for debugging purposes
style: {
strokeColor: string; //https://www.w3schools.com/colors/default.asp
@@ -772,6 +775,10 @@ export class ExcalidrawAutomate {
? `\n## Embedded Files\n`
: "\n";
const embeddedFile = (key: FileId, path: string, colorMap?:ColorMap): string => {
return `${key}: [[${path}]]${colorMap ? " " + JSON.stringify(colorMap): ""}\n\n`;
}
Object.keys(this.imagesDict).forEach((key: FileId)=> {
const item = this.imagesDict[key];
if(item.latex) {
@@ -779,9 +786,9 @@ export class ExcalidrawAutomate {
} else {
if(item.file) {
if(item.file instanceof TFile) {
outString += `${key}: [[${item.file.path}]]\n\n`;
outString += embeddedFile(key,item.file.path, item.colorMap);
} else {
outString += `${key}: [[${item.file}]]\n\n`;
outString += embeddedFile(key,item.file, item.colorMap);
}
} else {
const hyperlinkSplit = item.hyperlink.split("#");
@@ -789,8 +796,8 @@ export class ExcalidrawAutomate {
if(file && file instanceof TFile) {
const hasFileRef = hyperlinkSplit.length === 2
outString += hasFileRef
? `${key}: [[${file.path}#${hyperlinkSplit[1]}]]\n\n`
: `${key}: [[${file.path}]]\n\n`;
? embeddedFile(key,`${file.path}#${hyperlinkSplit[1]}`, item.colorMap)
: embeddedFile(key,file.path, item.colorMap);
} else {
outString += `${key}: ${item.hyperlink}\n\n`;
}
@@ -1535,12 +1542,26 @@ export class ExcalidrawAutomate {
* @returns
*/
async addImage(
topX: number,
topXOrOpts: number | AddImageOptions,
topY: number,
imageFile: TFile | string, //string may also be an Obsidian filepath with a reference such as folder/path/my.pdf#page=2
scale: boolean = true, //default is true which will scale the image to MAX_IMAGE_SIZE, false will insert image at 100% of its size
anchor: boolean = true, //only has effect if scale is false. If anchor is true the image path will include |100%, if false the image will be inserted at 100%, but if resized by the user it won't pop back to 100% the next time Excalidraw is opened.
): Promise<string> {
let colorMap: ColorMap;
let topX: number;
if(typeof topXOrOpts === "number") {
topX = topXOrOpts;
} else {
topY = topXOrOpts.topY;
topX = topXOrOpts.topX;
imageFile = topXOrOpts.imageFile;
scale = topXOrOpts.scale ?? true;
anchor = topXOrOpts.anchor ?? true;
colorMap = topXOrOpts.colorMap;
}
const id = nanoid();
const loader = new EmbeddedFilesLoader(
this.plugin,
@@ -1574,6 +1595,7 @@ export class ExcalidrawAutomate {
height: image.size.height,
width: image.size.width,
},
colorMap,
};
if (scale && (Math.max(image.size.width, image.size.height) > MAX_IMAGE_SIZE)) {
const scale =
@@ -1985,23 +2007,196 @@ export class ExcalidrawAutomate {
* @returns TFile file handle for the image element
*/
getViewFileForImageElement(el: ExcalidrawElement): TFile | null {
//@ts-ignore
return getEmbeddedFileForImageElment(this,el)?.file;
};
getColorMapForImageElement(el: ExcalidrawElement): ColorMap {
const cm = getEmbeddedFileForImageElment(this,el)?.colorMap
if(!cm) {
return {};
}
return cm;
}
async updateViewSVGImageColorMap(
elements: ExcalidrawImageElement | ExcalidrawImageElement[],
colors: ColorMap | SVGColorInfo | ColorMap[] | SVGColorInfo[]
): Promise<void> {
const elementArray = Array.isArray(elements) ? elements : [elements];
const colorArray = Array.isArray(colors) ? colors : [colors];
let colorMaps: ColorMap[];
if(colorArray.length !== elementArray.length) {
errorMessage("Elements and colors arrays must have same length", "updateViewSVGImageColorMap()");
return;
}
if (isSVGColorInfo(colorArray[0])) {
colorMaps = (colors as SVGColorInfo[]).map(svgColorInfoToColorMap);
} else {
colorMaps = colors as ColorMap[];
}
const fileIDWhiteList = new Set<FileId>();
for(let i = 0; i < elementArray.length; i++) {
const el = elementArray[i];
const colorMap = filterColorMap(colorMaps[i]);
const ef = getEmbeddedFileForImageElment(this, el);
if (!ef || !ef.file || !colorMap) {
errorMessage("Must provide an image element and a colorMap as input", "updateViewSVGImageColorMap()");
continue;
}
if (!colorMap || typeof colorMap !== 'object' || Object.keys(colorMap).length === 0) {
ef.colorMap = null;
} else {
ef.colorMap = colorMap;
//delete special mappings for default/SVG root color values
if (ef.colorMap["fill"] === "black") {
delete ef.colorMap["fill"];
}
if (ef.colorMap["stroke"] === "none") {
delete ef.colorMap["stroke"];
}
}
ef.resetImage(this.targetView.file.path, ef.linkParts.original);
fileIDWhiteList.add(el.fileId);
}
if(fileIDWhiteList.size > 0) {
this.targetView.setDirty();
await new Promise<void>((resolve) => {
this.targetView.loadSceneFiles(
false,
fileIDWhiteList,
resolve
);
});
}
return;
};
async getSVGColorInfoForImgElement(el: ExcalidrawElement): Promise<SVGColorInfo> {
if (!this.targetView || !this.targetView?._loaded) {
errorMessage("targetView not set", "getViewFileForImageElement()");
return null;
return;
}
if (!el || el.type !== "image") {
errorMessage(
"Must provide an image element as input",
"getViewFileForImageElement()",
);
return null;
return;
}
return (this.targetView as ExcalidrawView)?.excalidrawData?.getFile(
el.fileId,
)?.file;
const ef = getEmbeddedFileForImageElment(this, el);
const file = ef?.file;
if(!file || !(file.extension === "svg" || this.isExcalidrawFile(file))) {
errorMessage("Must provide an SVG or nested Excalidraw image element as input", "getColorMapForImgElement()");
return;
}
if (file.extension === "svg") {
const svgString = await this.plugin.app.vault.cachedRead(file);
const svgColors = this.getColorsFromSVGString(svgString);
return mergeColorMapIntoSVGColorInfo(ef.colorMap, svgColors);
}
const svgColors = await this.getColosFromExcalidrawFile(file, el);
return mergeColorMapIntoSVGColorInfo(ef.colorMap, svgColors);
};
async getColosFromExcalidrawFile(file:TFile, img: ExcalidrawImageElement): Promise<SVGColorInfo> {
if(!file || !this.isExcalidrawFile(file)) {
errorMessage("Must provide an Excalidraw file as input", "getColosFromExcalidrawFile()");
return;
}
const ef = getEmbeddedFileForImageElment(this, img);
const ed = new ExcalidrawData(this.plugin);
if(file.extension === "excalidraw") {
await ed.loadLegacyData(await this.plugin.app.vault.cachedRead(file), file);
} else {
await ed.loadData(await this.plugin.app.vault.cachedRead(file), file,TextMode.raw);
}
const svgColors: SVGColorInfo = new Map();
if (!ed.loaded) {
return svgColors;
}
ed.scene.elements.forEach((el:ExcalidrawElement) => {
if("strokeColor" in el) {
updateOrAddSVGColorInfo(svgColors, el.strokeColor, {stroke: true});
}
if("backgroundColor" in el) {
updateOrAddSVGColorInfo(svgColors, el.backgroundColor, {fill: true});
}
});
return svgColors;
}
/**
*
* @param svgString
* @returns
*/
getColorsFromSVGString(svgString: string): SVGColorInfo {
const colorMap = new Map<string, {mappedTo: string, fill: boolean, stroke: boolean}>();
if(!svgString) {
return colorMap;
}
const parser = new DOMParser();
const doc = parser.parseFromString(svgString, "image/svg+xml");
// Function to process an element and extract its colors
function processElement(element: Element, isRoot = false) {
// Check for fill attribute
const fillColor = element.getAttribute("fill");
if (fillColor !== "none") {
if (fillColor) {
updateOrAddSVGColorInfo(colorMap, fillColor, {fill: true});
} else if (isRoot) {
// If the root element has no fill, assume it is white
updateOrAddSVGColorInfo(colorMap, "fill", {fill: true, mappedTo: "black"});
}
}
// Check for stroke attribute
const strokeColor = element.getAttribute("stroke");
if (strokeColor && strokeColor !== "none") {
updateOrAddSVGColorInfo(colorMap, strokeColor, {stroke: true});
}
// Check for style attribute that might contain fill or stroke
const style = element.getAttribute("style");
if (style) {
// Extract fill from style
const fillMatch = style.match(/fill:\s*([^;}\s]+)/);
if (fillMatch && fillMatch[1] !== "none") {
updateOrAddSVGColorInfo(colorMap, fillMatch[1], {fill: true});
}
// Extract stroke from style
const strokeMatch = style.match(/stroke:\s*([^;}\s]+)/);
if (strokeMatch && strokeMatch[1] !== "none") {
updateOrAddSVGColorInfo(colorMap, strokeMatch[1], {stroke: true});
}
}
// Recursively process child elements
for (const child of Array.from(element.children)) {
processElement(child);
}
}
// Process the root SVG element
const svgElement = doc.documentElement;
processElement(svgElement, true);
return colorMap;
}
/**
* copies elements from view to elementsDict for editing
* @param elements
@@ -2026,21 +2221,20 @@ export class ExcalidrawAutomate {
id: el.fileId,
dataURL: sceneFile.dataURL,
created: sceneFile.created,
hasSVGwithBitmap: ef ? ef.isSVGwithBitmap : false,
...ef ? {
isHyperLink: ef.isHyperLink || imageWithRef,
isHyperLink: ef.isHyperLink || Boolean(imageWithRef),
hyperlink: imageWithRef ? `${ef.file.path}#${ef.linkParts.ref}` : ef.hyperlink,
file: imageWithRef ? null : ef.file,
hasSVGwithBitmap: ef.isSVGwithBitmap,
latex: null,
} : {},
...equation ? {
file: null,
isHyperLink: false,
hyperlink: null,
hasSVGwithBitmap: false,
latex: equation.latex,
} : {},
};
}
}
});
} else {
@@ -2839,7 +3033,13 @@ export class ExcalidrawAutomate {
color = this.colorNameToHex(color);
}
return CM(color);
const cm = CM(color);
//ColorMaster converts #FFFFFF00 to #FFFFFF, which is not what we want
//same is true for rgba and hsla transparent colors
if(isColorStringTransparent(color as string)) {
return cm.alphaTo(0);
}
return cm;
}
/**
@@ -3027,24 +3227,29 @@ async function getTemplate(
if (loadFiles) {
//debug({where:"getTemplate",template:file.name,loader:loader.uid});
await loader.loadSceneFiles(excalidrawData, (fileArray: FileData[]) => {
//, isDark: boolean) => {
if (!fileArray || fileArray.length === 0) {
return;
}
for (const f of fileArray) {
if (f.hasSVGwithBitmap) {
hasSVGwithBitmap = true;
await loader.loadSceneFiles({
excalidrawData,
addFiles: (fileArray: FileData[]) => {
//, isDark: boolean) => {
if (!fileArray || fileArray.length === 0) {
return;
}
excalidrawData.scene.files[f.id] = {
mimeType: f.mimeType,
id: f.id,
dataURL: f.dataURL,
created: f.created,
};
}
scene = scaleLoadedImage(excalidrawData.scene, fileArray).scene;
}, depth, false, fileIDWhiteList);
for (const f of fileArray) {
if (f.hasSVGwithBitmap) {
hasSVGwithBitmap = true;
}
excalidrawData.scene.files[f.id] = {
mimeType: f.mimeType,
id: f.id,
dataURL: f.dataURL,
created: f.created,
};
}
scene = scaleLoadedImage(excalidrawData.scene, fileArray).scene;
},
depth,
fileIDWhiteList
});
}
excalidrawData.destroy();
@@ -3338,32 +3543,6 @@ export function repositionElementsToCursor(
return restore({elements}, null, null).elements;
}
function errorMessage(message: string, source: string):void {
switch (message) {
case "targetView not set":
errorlog({
where: "ExcalidrawAutomate",
source,
message:
"targetView not set, or no longer active. Use setView before calling this function",
});
break;
case "mobile not supported":
errorlog({
where: "ExcalidrawAutomate",
source,
message: "this function is not available on Obsidian Mobile",
});
break;
default:
errorlog({
where: "ExcalidrawAutomate",
source,
message: message??"unknown error",
});
}
}
export const insertLaTeXToView = (view: ExcalidrawView) => {
const app = view.plugin.app;
const ea = view.plugin.ea;

View File

@@ -2028,7 +2028,8 @@ export class ExcalidrawData {
this.file.path,
masterFile.blockrefData
? masterFile.path + "#" + masterFile.blockrefData
: masterFile.path
: masterFile.path,
masterFile.colorMapJSON
);
this.files.set(fileId,embeddedFile);
return embeddedFile;

View File

@@ -253,6 +253,12 @@ export class ScriptEngine {
if (!view || !script || !title) {
return;
}
//addresses the situation when after paste text element IDs are not updated to 8 characters
//linked to onPaste save issue with the false parameter
if(view.getScene().elements.some(el=>!el.isDeleted && el.type === "text" && el.id.length > 8)) {
await view.save(false, true);
}
script = script.replace(/^---.*?---\n/gs, "");
const ea = getEA(view);
this.eaInstances.push(ea);

View File

@@ -0,0 +1,33 @@
import { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
import { TFile } from "obsidian";
import { FileId } from "src/core";
import { ColorMap, MimeType } from "src/shared/EmbeddedFileLoader";
export type SVGColorInfo = Map<string, {
mappedTo: string;
fill: boolean;
stroke: boolean;
}>;
export type ImageInfo = {
mimeType: MimeType,
id: FileId,
dataURL: DataURL,
created: number,
isHyperLink?: boolean,
hyperlink?: string,
file?:string | TFile,
hasSVGwithBitmap: boolean,
latex?: string,
size?: {height: number, width: number},
colorMap?: ColorMap,
}
export interface AddImageOptions {
topX: number;
topY: number;
imageFile: TFile | string;
scale?: boolean;
anchor?: boolean;
colorMap?: ColorMap;
}

View File

@@ -4,18 +4,7 @@ import { WorkspaceLeaf } from "obsidian";
import { FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ObsidianCanvasNode } from "../view/managers/CanvasNodeFactory";
export interface DropData {
files?: File[];
text?: string;
html?: string;
uri?: string;
}
export interface DropContext {
event: DragEvent;
position: {x: number; y: number};
modifierAction: string;
}
export type Position = { x: number; y: number };
export interface SelectedElementWithLink {
id: string | null;

View File

@@ -0,0 +1,114 @@
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate";
import { errorlog } from "./utils";
import { ColorMap } from "src/shared/EmbeddedFileLoader";
import { SVGColorInfo } from "src/types/excalidrawAutomateTypes";
export function isSVGColorInfo(obj: ColorMap | SVGColorInfo): boolean {
return (
typeof obj === 'object' &&
obj !== null &&
'stroke' in obj &&
'fill' in obj &&
'mappedTo' in obj
);
}
export function mergeColorMapIntoSVGColorInfo(
colorMap: ColorMap,
svgColorInfo: SVGColorInfo
): SVGColorInfo {
if(colorMap) {
for(const key of Object.keys(colorMap)) {
if(svgColorInfo.has(key)) {
svgColorInfo.get(key).mappedTo = colorMap[key];
}
}
}
return svgColorInfo;
}
export function svgColorInfoToColorMap(svgColorInfo: SVGColorInfo): ColorMap {
const colorMap: ColorMap = {};
svgColorInfo.forEach((info, color) => {
if (info.fill || info.stroke) {
colorMap[color] = info.mappedTo;
}
});
return colorMap;
}
//Remove identical key-value pairs from a ColorMap
export function filterColorMap(colorMap: ColorMap): ColorMap {
return Object.fromEntries(
Object.entries(colorMap).filter(([key, value]) => key.toLocaleLowerCase() !== value?.toLocaleLowerCase())
);
}
export function updateOrAddSVGColorInfo(
svgColorInfo: SVGColorInfo,
color: string,
info: {fill?: boolean, stroke?: boolean, mappedTo?: string}
): SVGColorInfo {
const {fill, stroke, mappedTo} = info;
color = color.toLocaleLowerCase();
const colorData = svgColorInfo.get(color) || {mappedTo: color, fill: false, stroke: false};
if(fill !== undefined) {
colorData.fill = fill;
}
if(stroke !== undefined) {
colorData.stroke = stroke;
}
if(mappedTo !== undefined) {
colorData.mappedTo = mappedTo;
}
return svgColorInfo.set(color, colorData);
}
export function getEmbeddedFileForImageElment(ea: ExcalidrawAutomate, el: ExcalidrawElement) {
if (!ea.targetView || !ea.targetView?._loaded) {
errorMessage("targetView not set", "getViewFileForImageElement()");
return null;
}
if (!el || el.type !== "image") {
errorMessage(
"Must provide an image element as input",
"getViewFileForImageElement()",
);
return null;
}
return ea.targetView?.excalidrawData?.getFile(el.fileId);
}
export function errorMessage(message: string, source: string):void {
switch (message) {
case "targetView not set":
errorlog({
where: "ExcalidrawAutomate",
source,
message:
"targetView not set, or no longer active. Use setView before calling this function",
});
break;
case "mobile not supported":
errorlog({
where: "ExcalidrawAutomate",
source,
message: "this function is not available on Obsidian Mobile",
});
break;
default:
errorlog({
where: "ExcalidrawAutomate",
source,
message: message??"unknown error",
});
}
}
export function isColorStringTransparent(color: string): boolean {
const rgbaHslaTransparentRegex = /^(rgba|hsla)\(.*?,.*?,.*?,\s*0(\.0+)?\)$/i;
const hexTransparentRegex = /^#[a-fA-F0-9]{8}00$/i;
return rgbaHslaTransparentRegex.test(color) || hexTransparentRegex.test(color);
}

View File

@@ -4,7 +4,7 @@ import { App, Modal, Notice, TFile, WorkspaceLeaf } from "obsidian";
import { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate";
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK, getExcalidrawMarkdownHeaderSection, REGEX_TAGS } from "../shared/ExcalidrawData";
import ExcalidrawView from "src/view/ExcalidrawView";
import { ExcalidrawElement, ExcalidrawFrameElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ExcalidrawElement, ExcalidrawFrameElement, ExcalidrawImageElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { getEmbeddedFilenameParts, getLinkParts, isImagePartRef } from "./utils";
import { cleanSectionHeading } from "./obsidianUtils";
import { getEA } from "src/core";
@@ -12,6 +12,8 @@ import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/
import { EmbeddableMDCustomProps } from "src/shared/Dialogs/EmbeddableSettings";
import { nanoid } from "nanoid";
import { t } from "src/lang/helpers";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import { EmbeddedFile } from "src/shared/EmbeddedFileLoader";
export async function insertImageToView(
ea: ExcalidrawAutomate,
@@ -44,8 +46,21 @@ export async function insertEmbeddableToView (
shouldInsertToView: boolean = true,
):Promise<string> {
if(shouldInsertToView) {ea.clear();}
ea.style.strokeColor = "transparent";
ea.style.backgroundColor = "transparent";
const api = ea.getExcalidrawAPI() as ExcalidrawImperativeAPI;
const st = api.getAppState();
if(ea.plugin.settings.embeddableMarkdownDefaults.backgroundMatchElement) {
ea.style.backgroundColor = st.currentItemBackgroundColor;
} else {
ea.style.backgroundColor = "transparent";
}
if(ea.plugin.settings.embeddableMarkdownDefaults.borderMatchElement) {
ea.style.strokeColor = st.currentItemStrokeColor;
} else {
ea.style.strokeColor = "transparent";
}
if(file && (IMAGE_TYPES.contains(file.extension) || ea.isExcalidrawFile(file)) && !ANIMATED_IMAGE_TYPES.contains(file.extension)) {
return await insertImageToView(ea, position, link??file, undefined, shouldInsertToView);
} else {
@@ -418,4 +433,35 @@ export function displayFontMessage(app: App) {
}
modal.open();
}
export async function toggleImageAnchoring(
el: ExcalidrawImageElement,
view: ExcalidrawView,
shouldAnchor: boolean,
ef: EmbeddedFile,
) {
const ea = getEA(view) as ExcalidrawAutomate;
let imgEl = view.getViewElements().find((x:ExcalidrawElement)=>x.id === el.id) as Mutable<ExcalidrawImageElement>;
if(!imgEl) {
ea.destroy();
return;
}
ea.copyViewElementsToEAforEditing([imgEl]);
imgEl = ea.getElements()[0] as Mutable<ExcalidrawImageElement>;
if(!imgEl.customData) {
imgEl.customData = {};
}
imgEl.customData.isAnchored = shouldAnchor;
if(shouldAnchor) {
const {height, width} = ef.size;
const dX = width - imgEl.width;
const dY = height - imgEl.height;
imgEl.height = height;
imgEl.width = width;
imgEl.x -= dX/2;
imgEl.y -= dY/2;
}
await ea.addElementsToView(false, false);
ea.destroy();
}

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

@@ -0,0 +1,38 @@
import { Setting } from "obsidian";
export type SliderSetting = {
name: string;
desc?: string | DocumentFragment;
min: number;
max: number;
step: number;
value: number;
minWidth?: string;
onChange: (value: number) => void;
}
export const createSliderWithText = (
container: HTMLElement,
settings: SliderSetting
): void => {
let valueText: HTMLDivElement;
new Setting(container)
.setName(settings.name)
.setDesc(settings.desc || '')
.addSlider((slider) =>
slider
.setLimits(settings.min, settings.max, settings.step)
.setValue(settings.value)
.onChange(async (value) => {
valueText.innerText = ` ${value.toString()}`;
settings.onChange(value);
}),
)
.settingEl.createDiv("", (el) => {
valueText = el;
el.style.minWidth = settings.minWidth || '2.3em';
el.style.textAlign = "right";
el.innerText = ` ${settings.value.toString()}`;
});
}

View File

@@ -40,7 +40,6 @@ import {
TEXT_DISPLAY_RAW_ICON_NAME,
TEXT_DISPLAY_PARSED_ICON_NAME,
IMAGE_TYPES,
REG_LINKINDEX_INVALIDCHARS,
KEYCODE,
FRONTMATTER_KEYS,
DEVICE,
@@ -81,7 +80,6 @@ import {
download,
getDataURLFromURL,
getIMGFilename,
getInternalLinkOrFileURLLink,
getMimeType,
getNewUniqueFilepath,
getURLImageExtension,
@@ -100,7 +98,6 @@ import {
scaleLoadedImage,
svgToBase64,
hyperlinkIsImage,
hyperlinkIsYouTubeLink,
getYouTubeThumbnailLink,
isContainer,
fragWithHTML,
@@ -128,11 +125,10 @@ import { getTextElementAtPointer, getImageElementAtPointer, getElementWithLinkAt
import { excalidrawSword, ICONS, LogoWrapper, Rank, saveIcon, SwordColors } from "../constants/actionIcons";
import { ExportDialog } from "../shared/Dialogs/ExportDialog";
import { getEA } from "src/core"
import { anyModifierKeysPressed, emulateKeysForLinkClick, webbrowserDragModifierType, internalDragModifierType, isWinALTorMacOPT, isWinCTRLorMacCMD, isWinMETAorMacCTRL, isSHIFT, linkClickModifierType, localFileDragModifierType, ModifierKeys, modifierKeyTooltipMessages } from "../utils/modifierkeyHelper";
import { anyModifierKeysPressed, emulateKeysForLinkClick, isWinALTorMacOPT, isWinCTRLorMacCMD, isWinMETAorMacCTRL, isSHIFT, linkClickModifierType, localFileDragModifierType, ModifierKeys, modifierKeyTooltipMessages } from "../utils/modifierkeyHelper";
import { setDynamicStyle } from "../utils/dynamicStyling";
import { InsertPDFModal } from "../shared/Dialogs/InsertPDFModal";
import { CustomEmbeddable, renderWebView } from "./components/CustomEmbeddable";
import { addBackOfTheNoteCard, getExcalidrawFileForwardLinks, getFrameBasedOnFrameNameOrId, getLinkTextFromLink, insertEmbeddableToView, insertImageToView, isTextImageTransclusion, openExternalLink, parseObsidianLink, renderContextMenuAction, tmpBruteForceCleanup } from "../utils/excalidrawViewUtils";
import { addBackOfTheNoteCard, getExcalidrawFileForwardLinks, getFrameBasedOnFrameNameOrId, getLinkTextFromLink, insertEmbeddableToView, insertImageToView, isTextImageTransclusion, openExternalLink, parseObsidianLink, renderContextMenuAction, tmpBruteForceCleanup, toggleImageAnchoring } from "../utils/excalidrawViewUtils";
import { imageCache } from "../shared/ImageCache";
import { CanvasNodeFactory, ObsidianCanvasNode } from "./managers/CanvasNodeFactory";
import { EmbeddableMenu } from "./components/menu/EmbeddableActionsMenu";
@@ -149,7 +145,8 @@ import React from "react";
import { diagramToHTML } from "../utils/matic";
import { IS_WORKER_SUPPORTED } from "../shared/Workers/compression-worker";
import { getPDFCropRect } from "../utils/PDFUtils";
import { ViewSemaphores } from "../types/excalidrawViewTypes";
import { Position, ViewSemaphores } from "../types/excalidrawViewTypes";
import { DropManager } from "./managers/DropManager";
const EMBEDDABLE_SEMAPHORE_TIMEOUT = 2000;
const PREVENT_RELOAD_TIMEOUT = 2000;
@@ -278,6 +275,7 @@ type ActionButtons = "save" | "isParsed" | "isRaw" | "link" | "scriptInstall";
let windowMigratedDisableZoomOnce = false;
export default class ExcalidrawView extends TextFileView implements HoverParent{
private dropManager: DropManager;
public hoverPopover: HoverPopover;
private freedrawLastActiveTimestamp: number = 0;
public exportDialog: ExportDialog;
@@ -296,9 +294,8 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
private lastLoadedFile: TFile = null;
//store key state for view mode link resolution
private modifierKeyDown: ModifierKeys = {shiftKey:false, metaKey: false, ctrlKey: false, altKey: false}
public currentPosition: {x:number,y:number} = { x: 0, y: 0 }; //these are scene coord thus would be more apt to call them sceneX and sceneY, however due to scrits already using x and y, I will keep it as is
public currentPosition: Position = { x: 0, y: 0 }; //these are scene coord thus would be more apt to call them sceneX and sceneY, however due to scrits already using x and y, I will keep it as is
//Obsidian 0.15.0
private draginfoDiv: HTMLDivElement;
public canvasNodeFactory: CanvasNodeFactory;
private embeddableRefs = new Map<ExcalidrawElement["id"], HTMLIFrameElement | HTMLWebViewElement>();
private embeddableLeafRefs = new Map<ExcalidrawElement["id"], any>();
@@ -363,6 +360,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
this.excalidrawData = new ExcalidrawData(plugin);
this.canvasNodeFactory = new CanvasNodeFactory(this);
this.setHookServer();
this.dropManager = new DropManager(this);
}
get hookServer (): ExcalidrawAutomate {
@@ -390,7 +388,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
}
}
private getHookServer () {
public getHookServer () {
return this.hookServer ?? this.plugin.ea;
}
@@ -975,9 +973,14 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
if (!link || ef.linkParts.original === link) {
return;
}
const originalAnchor = Boolean(ef.linkParts.original.endsWith("|100%"));
const nextAnchor = Boolean(link.endsWith("|100%"));
ef.resetImage(this.file.path, link);
this.excalidrawData.setFile(fileId, ef);
this.setDirty(2);
if(originalAnchor !== nextAnchor) {
await toggleImageAnchoring(el, this, nextAnchor, ef)
}
await this.save(false);
await sleep(100);
if(!this.plugin.isExcalidrawFile(ef.file) && !link.endsWith("|100%")) {
@@ -1889,6 +1892,12 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
//onClose happens after onunload
protected async onClose(): Promise<void> {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onClose,`ExcalidrawView.onClose, file:${this.file?.name}`);
// This happens when the user right clicks a tab and selects delete
// in this case the onDelete event handler tirggers, but then Obsidian's delete event handler reaches onclose first, and
// when the function is called a second time via on delete an error is thrown.)
if(!this.file) return;
this.exitFullscreen();
await this.forceSaveIfRequired();
if (this.excalidrawRoot) {
@@ -1900,10 +1909,10 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
this.clearEmbeddableNodeIsEditingTimer();
this.plugin.scriptEngine?.removeViewEAs(this);
this.excalidrawAPI = null;
if(this.draginfoDiv) {
this.ownerDocument.body.removeChild(this.draginfoDiv);
delete this.draginfoDiv;
}
this.dropManager.destroy();
this.dropManager = null;
if(this.canvasNodeFactory) {
this.canvasNodeFactory.destroy();
}
@@ -2414,7 +2423,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
public activeLoader: EmbeddedFilesLoader = null;
private nextLoader: EmbeddedFilesLoader = null;
public async loadSceneFiles(isThemeChange: boolean = false) {
public async loadSceneFiles(isThemeChange: boolean = false, fileIDWhiteList?: Set<FileId>, callback?: Function) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.loadSceneFiles, "ExcalidrawView.loadSceneFiles", isThemeChange);
if (!this.excalidrawAPI) {
return;
@@ -2424,13 +2433,18 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
const runLoader = (l: EmbeddedFilesLoader) => {
this.nextLoader = null;
this.activeLoader = l;
l.loadSceneFiles(
this.excalidrawData,
(files: FileData[], isDark: boolean, final:boolean = true) => {
if (!files) {
return;
}
addFiles(files, this, isDark);
l.loadSceneFiles({
excalidrawData: this.excalidrawData,
addFiles: (files: FileData[], isDark: boolean, final:boolean = true) => {
if(callback && final) {
callback();
}
if(!this.file || !this.excalidrawAPI) {
return; //The view was closed in the mean time
}
if (files && files.length > 0) {
addFiles(files, this, isDark);
}
if(!final) return;
this.activeLoader = null;
if (this.nextLoader) {
@@ -2450,8 +2464,11 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
return false;
})
}
},0,isThemeChange,
);
},
depth: 0,
isThemeChange,
fileIDWhiteList,
});
};
if (!this.activeLoader) {
runLoader(loader);
@@ -2480,7 +2497,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
return;
}
this.semaphores.saving = true;
let reloadFiles = false;
let reloadFiles = new Set<FileId>();
try {
const deletedIds = inData.deletedElements.map(el=>el.id);
@@ -2503,13 +2520,13 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
incomingElement.fileId,
inData.getFile(incomingElement.fileId)
);
reloadFiles = true;
reloadFiles.add(incomingElement.fileId);
} else if (inData.getEquation(incomingElement.fileId)) {
this.excalidrawData.setEquation(
incomingElement.fileId,
inData.getEquation(incomingElement.fileId)
)
reloadFiles = true;
reloadFiles.add(incomingElement.fileId);
}
break;
}
@@ -2588,7 +2605,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
incomingElement.fileId,
inData.getFile(incomingElement.fileId)
);
reloadFiles = true;
reloadFiles.add(incomingElement.fileId);
}
}
})
@@ -2599,7 +2616,9 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
this.setDirty(3);
}
this.updateScene({elements: sceneElements, storeAction: "capture"});
if(reloadFiles) this.loadSceneFiles();
if(reloadFiles.size>0) {
this.loadSceneFiles(false,reloadFiles);
}
} catch(e) {
errorlog({
where:"ExcalidrawView.synchronizeWithData",
@@ -3538,34 +3557,6 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
}
};
private dropAction(transfer: DataTransfer) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.dropAction, "ExcalidrawView.dropAction");
// Return a 'copy' or 'link' action according to the content types, or undefined if no recognized type
const files = (this.app as any).dragManager.draggable?.files;
if (files) {
if (files[0] == this.file) {
files.shift();
(
this.app as any
).dragManager.draggable.title = `${files.length} files`;
}
}
if (
["file", "files"].includes(
(this.app as any).dragManager.draggable?.type,
)
) {
return "link";
}
if (
transfer.types?.includes("text/html") ||
transfer.types?.includes("text/plain") ||
transfer.types?.includes("Files")
) {
return "copy";
}
};
/**
* identify which element to navigate to on click
* @returns
@@ -3782,50 +3773,6 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
this.clearHoverPreview();
}
private onDragOver(e: any) {
const action = this.dropAction(e.dataTransfer);
if (action) {
if(!this.draginfoDiv) {
this.draginfoDiv = createDiv({cls:"excalidraw-draginfo"});
this.ownerDocument.body.appendChild(this.draginfoDiv);
}
let msg: string = "";
if((this.app as any).dragManager.draggable) {
//drag from Obsidian file manager
msg = modifierKeyTooltipMessages().InternalDragAction[internalDragModifierType(e)];
} else if(e.dataTransfer.types.length === 1 && e.dataTransfer.types.includes("Files")) {
//drag from OS file manager
msg = modifierKeyTooltipMessages().LocalFileDragAction[localFileDragModifierType(e)];
if(DEVICE.isMacOS && isWinCTRLorMacCMD(e)) {
msg = "CMD is reserved by MacOS for file system drag actions.\nCan't use it in Obsidian.\nUse a combination of SHIFT, CTRL, OPT instead."
}
} else {
//drag from Internet
msg = modifierKeyTooltipMessages().WebBrowserDragAction[webbrowserDragModifierType(e)];
}
if(!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
msg += DEVICE.isMacOS || DEVICE.isIOS
? "\nTry SHIFT, OPT, CTRL combinations for other drop actions"
: "\nTry SHIFT, CTRL, ALT, Meta combinations for other drop actions";
}
if(this.draginfoDiv.innerText !== msg) this.draginfoDiv.innerText = msg;
const top = `${e.clientY-parseFloat(getComputedStyle(this.draginfoDiv).fontSize)*8}px`;
const left = `${e.clientX-this.draginfoDiv.clientWidth/2}px`;
if(this.draginfoDiv.style.top !== top) this.draginfoDiv.style.top = top;
if(this.draginfoDiv.style.left !== left) this.draginfoDiv.style.left = left;
e.dataTransfer.dropEffect = action;
e.preventDefault();
return false;
}
}
private onDragLeave() {
if(this.draginfoDiv) {
this.ownerDocument.body.removeChild(this.draginfoDiv);
delete this.draginfoDiv;
}
}
private onPointerUpdate(p: {
pointer: { x: number; y: number; tool: "pointer" | "laser" };
button: "down" | "up";
@@ -4177,480 +4124,6 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
window.setTimeout(()=>setDynamicStyle(this.plugin.ea,this,this.previousBackgroundColor,this.plugin.settings.dynamicStyling));
}
private onDrop (event: React.DragEvent<HTMLDivElement>): boolean {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onDrop, "ExcalidrawView.onDrop", event);
if(this.draginfoDiv) {
this.ownerDocument.body.removeChild(this.draginfoDiv);
delete this.draginfoDiv;
}
const api = this.excalidrawAPI;
if (!api) {
return false;
}
const st: AppState = api.getAppState();
this.currentPosition = viewportCoordsToSceneCoords(
{ clientX: event.clientX, clientY: event.clientY },
st,
);
const draggable = (this.app as any).dragManager.draggable;
const internalDragAction = internalDragModifierType(event);
const externalDragAction = webbrowserDragModifierType(event);
const localFileDragAction = localFileDragModifierType(event);
//Call Excalidraw Automate onDropHook
const onDropHook = (
type: "file" | "text" | "unknown",
files: TFile[],
text: string,
): boolean => {
if (this.getHookServer().onDropHook) {
try {
return this.getHookServer().onDropHook({
ea: this.getHookServer(), //the ExcalidrawAutomate object
event, //React.DragEvent<HTMLDivElement>
draggable, //Obsidian draggable object
type, //"file"|"text"
payload: {
files, //TFile[] array of dropped files
text, //string
},
excalidrawFile: this.file, //the file receiving the drop event
view: this, //the excalidraw view receiving the drop
pointerPosition: this.currentPosition, //the pointer position on canvas at the time of drop
});
} catch (e) {
new Notice("on drop hook error. See console log for details");
errorlog({ where: "ExcalidrawView.onDrop", error: e });
return false;
}
} else {
return false;
}
};
//---------------------------------------------------------------------------------
// Obsidian internal drag event
//---------------------------------------------------------------------------------
switch (draggable?.type) {
case "file":
if (!onDropHook("file", [draggable.file], null)) {
const file:TFile = draggable.file;
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/422
if (file.path.match(REG_LINKINDEX_INVALIDCHARS)) {
new Notice(t("FILENAME_INVALID_CHARS"), 4000);
return false;
}
if (
["image", "image-fullsize"].contains(internalDragAction) &&
(IMAGE_TYPES.contains(file.extension) ||
file.extension === "md" ||
file.extension.toLowerCase() === "pdf" )
) {
if(file.extension.toLowerCase() === "pdf") {
const insertPDFModal = new InsertPDFModal(this.plugin, this);
insertPDFModal.open(file);
} else {
(async () => {
const ea: ExcalidrawAutomate = getEA(this);
ea.selectElementsInView([
await insertImageToView(
ea,
this.currentPosition,
file,
!(internalDragAction==="image-fullsize")
)
]);
ea.destroy();
})();
}
return false;
}
if (internalDragAction === "embeddable") {
(async () => {
const ea: ExcalidrawAutomate = getEA(this);
ea.selectElementsInView([
await insertEmbeddableToView(
ea,
this.currentPosition,
file,
)
]);
ea.destroy();
})();
return false;
}
//internalDragAction === "link"
this.addText(
`[[${this.app.metadataCache.fileToLinktext(
draggable.file,
this.file.path,
true,
)}]]`,
);
}
return false;
case "files":
if (!onDropHook("file", draggable.files, null)) {
(async () => {
if (["image", "image-fullsize"].contains(internalDragAction)) {
const ea:ExcalidrawAutomate = getEA(this);
ea.canvas.theme = api.getAppState().theme;
let counter:number = 0;
const ids:string[] = [];
for (const f of draggable.files) {
if ((IMAGE_TYPES.contains(f.extension) || f.extension === "md")) {
ids.push(await ea.addImage(
this.currentPosition.x + counter*50,
this.currentPosition.y + counter*50,
f,
!(internalDragAction==="image-fullsize"),
));
counter++;
await ea.addElementsToView(false, false, true);
ea.selectElementsInView(ids);
}
if (f.extension.toLowerCase() === "pdf") {
const insertPDFModal = new InsertPDFModal(this.plugin, this);
insertPDFModal.open(f);
}
}
ea.destroy();
return;
}
if (internalDragAction === "embeddable") {
const ea:ExcalidrawAutomate = getEA(this);
let column:number = 0;
let row:number = 0;
const ids:string[] = [];
for (const f of draggable.files) {
ids.push(await insertEmbeddableToView(
ea,
{
x:this.currentPosition.x + column*500,
y:this.currentPosition.y + row*550
},
f,
));
column = (column + 1) % 3;
if(column === 0) {
row++;
}
}
ea.destroy();
return false;
}
//internalDragAction === "link"
for (const f of draggable.files) {
await this.addText(
`[[${this.app.metadataCache.fileToLinktext(
f,
this.file.path,
true,
)}]]`, undefined,false
);
this.currentPosition.y += st.currentItemFontSize * 2;
}
this.save(false);
})();
}
return false;
}
//---------------------------------------------------------------------------------
// externalDragAction
//---------------------------------------------------------------------------------
if (event.dataTransfer.types.includes("Files")) {
if (event.dataTransfer.types.includes("text/plain")) {
const text: string = event.dataTransfer.getData("text");
if (text && onDropHook("text", null, text)) {
return false;
}
if(text && (externalDragAction === "image-url") && hyperlinkIsImage(text)) {
this.addImageWithURL(text);
return false;
}
if(text && (externalDragAction === "link")) {
if (
this.plugin.settings.iframelyAllowed &&
text.match(/^https?:\/\/\S*$/)
) {
this.addTextWithIframely(text);
return false;
} else {
this.addText(text);
return false;
}
}
if(text && (externalDragAction === "embeddable")) {
const ea = getEA(this) as ExcalidrawAutomate;
insertEmbeddableToView(
ea,
this.currentPosition,
undefined,
text,
).then(()=>ea.destroy());
return false;
}
}
if(event.dataTransfer.types.includes("text/html")) {
const html = event.dataTransfer.getData("text/html");
const src = html.match(/src=["']([^"']*)["']/)
if(src && (externalDragAction === "image-url") && hyperlinkIsImage(src[1])) {
this.addImageWithURL(src[1]);
return false;
}
if(src && (externalDragAction === "link")) {
if (
this.plugin.settings.iframelyAllowed &&
src[1].match(/^https?:\/\/\S*$/)
) {
this.addTextWithIframely(src[1]);
return false;
} else {
this.addText(src[1]);
return false;
}
}
if(src && (externalDragAction === "embeddable")) {
const ea = getEA(this) as ExcalidrawAutomate;
insertEmbeddableToView(
ea,
this.currentPosition,
undefined,
src[1],
).then(ea.destroy);
return false;
}
}
if (event.dataTransfer.types.length >= 1 && ["image-url","image-import","embeddable"].contains(localFileDragAction)) {
const files = Array.from(event.dataTransfer.files || []);
for(let i = 0; i < files.length; i++) {
// Try multiple ways to get file path
const file = files[i];
let path = file?.path
if(!path && file && DEVICE.isDesktop) {
//https://www.electronjs.org/docs/latest/breaking-changes#removed-filepath
const { webUtils } = require('electron');
if(webUtils && webUtils.getPathForFile) {
path = webUtils.getPathForFile(file);
}
}
if(!path) {
new Notice(t("ERROR_CANT_READ_FILEPATH"),6000);
return true; //excalidarw to continue processing
}
const link = getInternalLinkOrFileURLLink(path, this.plugin, event.dataTransfer.files[i].name, this.file);
const {x,y} = this.currentPosition;
const pos = {x:x+i*300, y:y+i*300};
if(link.isInternal) {
if(localFileDragAction === "embeddable") {
const ea = getEA(this) as ExcalidrawAutomate;
insertEmbeddableToView(ea, pos, link.file).then(()=>ea.destroy());
} else {
if(link.file.extension === "pdf") {
const insertPDFModal = new InsertPDFModal(this.plugin, this);
insertPDFModal.open(link.file);
}
const ea = getEA(this) as ExcalidrawAutomate;
insertImageToView(ea, pos, link.file).then(()=>ea.destroy()) ;
}
} else {
const extension = getURLImageExtension(link.url);
if(localFileDragAction === "image-import") {
if (IMAGE_TYPES.contains(extension)) {
(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);
if(maybeFile && maybeFile instanceof TFile) {
const action = await ScriptEngine.suggester(
this.app,[
"Use the file already in the Vault instead of importing",
"Overwrite existing file in the Vault",
"Import the file with a new name",
],[
"Use",
"Overwrite",
"Import",
],
"A file with the same name/path already exists in the Vault",
);
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);
// there is deliberately no break here
case "Use":
default:
const ea = getEA(this) as ExcalidrawAutomate;
await insertImageToView(ea, pos, maybeFile);
ea.destroy();
return false;
}
}
const file = await this.app.vault.createBinary(filepath, fileToImport)
const ea = getEA(this) as ExcalidrawAutomate;
await insertImageToView(ea, pos, file);
ea.destroy();
})();
} else if(extension === "excalidraw") {
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 modal = new UniversalInsertFileModal(this.plugin, this);
modal.open(file, pos);
})();
}
}
else if(localFileDragAction === "embeddable" || !IMAGE_TYPES.contains(extension)) {
const ea = getEA(this) as ExcalidrawAutomate;
insertEmbeddableToView(ea, pos, null, link.url).then(()=>ea.destroy());
if(localFileDragAction !== "embeddable") {
new Notice("Not imported to Vault. Embedded with local URI");
}
} else {
const ea = getEA(this) as ExcalidrawAutomate;
insertImageToView(ea, pos, link.url).then(()=>ea.destroy());
}
}
};
return false;
}
if(event.dataTransfer.types.length >= 1 && localFileDragAction === "link") {
const ea = getEA(this) as ExcalidrawAutomate;
for(let i=0;i<event.dataTransfer.files.length;i++) {
const file = event.dataTransfer.files[i];
let path = file?.path;
const name = file?.name;
if(!path && file && DEVICE.isDesktop) {
//https://www.electronjs.org/docs/latest/breaking-changes#removed-filepath
const { webUtils } = require('electron');
if(webUtils && webUtils.getPathForFile) {
path = webUtils.getPathForFile(file);
}
}
if(!path || !name) {
new Notice(t("ERROR_CANT_READ_FILEPATH"),6000);
ea.destroy();
return true; //excalidarw to continue processing
}
const link = getInternalLinkOrFileURLLink(path, this.plugin, name, this.file);
const id = ea.addText(
this.currentPosition.x+i*40,
this.currentPosition.y+i*20,
link.isInternal ? link.link :`📂 ${name}`);
if(!link.isInternal) {
ea.getElement(id).link = link.link;
}
}
ea.addElementsToView().then(()=>ea.destroy());
return false;
}
return true;
}
if (event.dataTransfer.types.includes("text/plain") || event.dataTransfer.types.includes("text/uri-list") || event.dataTransfer.types.includes("text/html")) {
const html = event.dataTransfer.getData("text/html");
const src = html.match(/src=["']([^"']*)["']/);
const htmlText = src ? src[1] : "";
const textText = event.dataTransfer.getData("text");
const uriText = event.dataTransfer.getData("text/uri-list");
let text: string = src ? htmlText : textText;
if (!text || text === "") {
text = uriText
}
if (!text || text === "") {
return true;
}
if (!onDropHook("text", null, text)) {
if(text && (externalDragAction==="embeddable") && /^(blob:)?(http|https):\/\/[^\s/$.?#].[^\s]*$/.test(text)) {
return true;
}
if(text && (externalDragAction==="image-url") && hyperlinkIsYouTubeLink(text)) {
this.addYouTubeThumbnail(text);
return false;
}
if(uriText && (externalDragAction==="image-url") && hyperlinkIsYouTubeLink(uriText)) {
this.addYouTubeThumbnail(uriText);
return false;
}
if(text && (externalDragAction==="image-url") && hyperlinkIsImage(text)) {
this.addImageWithURL(text);
return false;
}
if(uriText && (externalDragAction==="image-url") && hyperlinkIsImage(uriText)) {
this.addImageWithURL(uriText);
return false;
}
if(text && (externalDragAction==="image-import") && hyperlinkIsImage(text)) {
this.addImageSaveToVault(text);
return false;
}
if(uriText && (externalDragAction==="image-import") && hyperlinkIsImage(uriText)) {
this.addImageSaveToVault(uriText);
return false;
}
if (
this.plugin.settings.iframelyAllowed &&
text.match(/^https?:\/\/\S*$/)
) {
this.addTextWithIframely(text);
return false;
}
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/599
if(text.startsWith("obsidian://open?vault=")) {
const html = event.dataTransfer.getData("text/html");
if(html) {
const path = html.match(/href="app:\/\/obsidian\.md\/(.*?)"/);
if(path.length === 2) {
const link = decodeURIComponent(path[1]).split("#");
const f = this.app.vault.getAbstractFileByPath(link[0]);
if(f && f instanceof TFile) {
const path = this.app.metadataCache.fileToLinktext(f,this.file.path);
this.addText(`[[${
path +
(link.length>1 ? "#" + link[1] + "|" + path : "")
}]]`);
return;
}
this.addText(`[[${decodeURIComponent(path[1])}]]`);
return false;
}
}
const path = text.split("file=");
if(path.length === 2) {
this.addText(`[[${decodeURIComponent(path[1])}]]`);
return false;
}
}
this.addText(text.replace(/(!\[\[.*#[^\]]*\]\])/g, "$1{40}"));
}
return false;
}
if (onDropHook("unknown", null, null)) {
return false;
}
return true;
}
//returns the raw text of the element which is the original text without parsing
//in compatibility mode, returns the original text, and for backward compatibility the text if originalText is not available
private onBeforeTextEdit (textElement: ExcalidrawTextElement, isExistingElement: boolean): string {
@@ -5869,8 +5342,8 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
onPointerDown: this.onPointerDown.bind(this),
onMouseMove: this.onMouseMove.bind(this),
onMouseOver: this.onMouseOver.bind(this),
onDragOver : this.onDragOver.bind(this),
onDragLeave: this.onDragLeave.bind(this),
onDragOver : this.dropManager.onDragOver.bind(this.dropManager),
onDragLeave: this.dropManager.onDragLeave.bind(this.dropManager),
},
React.createElement(
Excalidraw,
@@ -5904,7 +5377,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
renderEmbeddableMenu: this.renderEmbeddableMenu.bind(this),
onPaste: this.onPaste.bind(this),
onThemeChange: this.onThemeChange.bind(this),
onDrop: this.onDrop.bind(this),
onDrop: this.dropManager.onDrop.bind(this.dropManager),
onBeforeTextEdit: this.onBeforeTextEdit.bind(this),
onBeforeTextSubmit: this.onBeforeTextSubmit.bind(this),
onLinkOpen: this.onLinkOpen.bind(this),
@@ -6425,4 +5898,4 @@ export function getTextMode(data: string): TextMode {
data.search("excalidraw-plugin: parsed\n") > -1 ||
data.search("excalidraw-plugin: locked\n") > -1; //locked for backward compatibility
return parsed ? TextMode.parsed : TextMode.raw;
}
}

View File

@@ -0,0 +1,628 @@
import { DEBUGGING, debug } from "src/utils/debugHelper";
import ExcalidrawView from "../ExcalidrawView";
import { App, Notice, TFile } from "obsidian";
import { AppState, ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
import { DEVICE, IMAGE_TYPES, REG_LINKINDEX_INVALIDCHARS, viewportCoordsToSceneCoords } from "src/constants/constants";
import { internalDragModifierType, isWinCTRLorMacCMD, localFileDragModifierType, modifierKeyTooltipMessages, webbrowserDragModifierType } from "src/utils/modifierkeyHelper";
import { errorlog, hyperlinkIsImage, hyperlinkIsYouTubeLink } from "src/utils/utils";
import { InsertPDFModal } from "src/shared/Dialogs/InsertPDFModal";
import { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate";
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 { ScriptEngine } from "src/shared/Scripts";
import { UniversalInsertFileModal } from "src/shared/Dialogs/UniversalInsertFileModal";
import { Position } from "src/types/excalidrawViewTypes";
/*
static getDropAction(event: DragEvent): string {
// Get modifier action
}
static parseDropData(event: DragEvent): DropData {
// Parse drop data into clean format
}
static handleInternalDrop(data: DropData, context: DropContext): boolean {
// Handle Obsidian internal file drops
}
static handleExternalFileDrop(data: DropData, context: DropContext): boolean {
// Handle external file drops
}
static handleTextDrop(data: DropData, context: DropContext): boolean {
// Handle text/url drops
}*/
export class DropManager {
private view: ExcalidrawView;
private app: App;
private draginfoDiv: HTMLDivElement;
constructor(view: ExcalidrawView) {
this.view = view;
this.app = this.view.app;
}
public destroy() {
if(this.draginfoDiv) {
this.ownerDocument.body.removeChild(this.draginfoDiv);
delete this.draginfoDiv;
}
}
get ownerDocument(): Document {
return this.view.ownerDocument;
}
get currentPosition(): Position {
return this.view.currentPosition;
}
set currentPosition(pos: Position) {
this.view.currentPosition = pos;
}
get excalidrawAPI():ExcalidrawImperativeAPI {
return this.view.excalidrawAPI;
}
get plugin(): ExcalidrawPlugin {
return this.view.plugin;
}
get file(): TFile {
return this.view.file;
}
public onDrop (event: React.DragEvent<HTMLDivElement>): boolean {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onDrop, "ExcalidrawView.onDrop", event);
if(this.draginfoDiv) {
this.ownerDocument.body.removeChild(this.draginfoDiv);
delete this.draginfoDiv;
}
const api = this.excalidrawAPI;
if (!api) {
return false;
}
const st: AppState = api.getAppState();
this.currentPosition = viewportCoordsToSceneCoords(
{ clientX: event.clientX, clientY: event.clientY },
st,
);
const draggable = (this.app as any).dragManager.draggable;
const internalDragAction = internalDragModifierType(event);
const externalDragAction = webbrowserDragModifierType(event);
const localFileDragAction = localFileDragModifierType(event);
//Call Excalidraw Automate onDropHook
const onDropHook = (
type: "file" | "text" | "unknown",
files: TFile[],
text: string,
): boolean => {
if (this.view.getHookServer().onDropHook) {
try {
return this.view.getHookServer().onDropHook({
ea: this.view.getHookServer(), //the ExcalidrawAutomate object
event, //React.DragEvent<HTMLDivElement>
draggable, //Obsidian draggable object
type, //"file"|"text"
payload: {
files, //TFile[] array of dropped files
text, //string
},
excalidrawFile: this.file, //the file receiving the drop event
view: this.view, //the excalidraw view receiving the drop
pointerPosition: this.currentPosition, //the pointer position on canvas at the time of drop
});
} catch (e) {
new Notice("on drop hook error. See console log for details");
errorlog({ where: "ExcalidrawView.onDrop", error: e });
return false;
}
} else {
return false;
}
};
//---------------------------------------------------------------------------------
// Obsidian internal drag event
//---------------------------------------------------------------------------------
switch (draggable?.type) {
case "file":
if (!onDropHook("file", [draggable.file], null)) {
const file:TFile = draggable.file;
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/422
if (file.path.match(REG_LINKINDEX_INVALIDCHARS)) {
new Notice(t("FILENAME_INVALID_CHARS"), 4000);
return false;
}
if (
["image", "image-fullsize"].contains(internalDragAction) &&
(IMAGE_TYPES.contains(file.extension) ||
file.extension === "md" ||
file.extension.toLowerCase() === "pdf" )
) {
if(file.extension.toLowerCase() === "pdf") {
const insertPDFModal = new InsertPDFModal(this.plugin, this.view);
insertPDFModal.open(file);
} else {
(async () => {
const ea: ExcalidrawAutomate = getEA(this.view);
ea.selectElementsInView([
await insertImageToView(
ea,
this.currentPosition,
file,
!(internalDragAction==="image-fullsize")
)
]);
ea.destroy();
})();
}
return false;
}
if (internalDragAction === "embeddable") {
(async () => {
const ea: ExcalidrawAutomate = getEA(this.view);
ea.selectElementsInView([
await insertEmbeddableToView(
ea,
this.currentPosition,
file,
)
]);
ea.destroy();
})();
return false;
}
//internalDragAction === "link"
this.view.addText(
`[[${this.app.metadataCache.fileToLinktext(
draggable.file,
this.file.path,
true,
)}]]`,
);
}
return false;
case "files":
if (!onDropHook("file", draggable.files, null)) {
(async () => {
if (["image", "image-fullsize"].contains(internalDragAction)) {
const ea:ExcalidrawAutomate = getEA(this.view);
ea.canvas.theme = api.getAppState().theme;
let counter:number = 0;
const ids:string[] = [];
for (const f of draggable.files) {
if ((IMAGE_TYPES.contains(f.extension) || f.extension === "md")) {
ids.push(await ea.addImage(
this.currentPosition.x + counter*50,
this.currentPosition.y + counter*50,
f,
!(internalDragAction==="image-fullsize"),
));
counter++;
await ea.addElementsToView(false, false, true);
ea.selectElementsInView(ids);
}
if (f.extension.toLowerCase() === "pdf") {
const insertPDFModal = new InsertPDFModal(this.plugin, this.view);
insertPDFModal.open(f);
}
}
ea.destroy();
return;
}
if (internalDragAction === "embeddable") {
const ea:ExcalidrawAutomate = getEA(this.view);
let column:number = 0;
let row:number = 0;
const ids:string[] = [];
for (const f of draggable.files) {
ids.push(await insertEmbeddableToView(
ea,
{
x:this.currentPosition.x + column*500,
y:this.currentPosition.y + row*550
},
f,
));
column = (column + 1) % 3;
if(column === 0) {
row++;
}
}
ea.destroy();
return false;
}
//internalDragAction === "link"
for (const f of draggable.files) {
await this.view.addText(
`[[${this.app.metadataCache.fileToLinktext(
f,
this.file.path,
true,
)}]]`, undefined,false
);
this.currentPosition.y += st.currentItemFontSize * 2;
}
this.view.save(false);
})();
}
return false;
}
//---------------------------------------------------------------------------------
// externalDragAction
//---------------------------------------------------------------------------------
if (event.dataTransfer.types.includes("Files")) {
if (event.dataTransfer.types.includes("text/plain")) {
const text: string = event.dataTransfer.getData("text");
if (text && onDropHook("text", null, text)) {
return false;
}
if(text && (externalDragAction === "image-url") && hyperlinkIsImage(text)) {
this.view.addImageWithURL(text);
return false;
}
if(text && (externalDragAction === "link")) {
if (
this.plugin.settings.iframelyAllowed &&
text.match(/^https?:\/\/\S*$/)
) {
this.view.addTextWithIframely(text);
return false;
} else {
this.view.addText(text);
return false;
}
}
if(text && (externalDragAction === "embeddable")) {
const ea = getEA(this.view) as ExcalidrawAutomate;
insertEmbeddableToView(
ea,
this.currentPosition,
undefined,
text,
).then(()=>ea.destroy());
return false;
}
}
if(event.dataTransfer.types.includes("text/html")) {
const html = event.dataTransfer.getData("text/html");
const src = html.match(/src=["']([^"']*)["']/)
if(src && (externalDragAction === "image-url") && hyperlinkIsImage(src[1])) {
this.view.addImageWithURL(src[1]);
return false;
}
if(src && (externalDragAction === "link")) {
if (
this.plugin.settings.iframelyAllowed &&
src[1].match(/^https?:\/\/\S*$/)
) {
this.view.addTextWithIframely(src[1]);
return false;
} else {
this.view.addText(src[1]);
return false;
}
}
if(src && (externalDragAction === "embeddable")) {
const ea = getEA(this.view) as ExcalidrawAutomate;
insertEmbeddableToView(
ea,
this.currentPosition,
undefined,
src[1],
).then(ea.destroy);
return false;
}
}
if (event.dataTransfer.types.length >= 1 && ["image-url","image-import","embeddable"].contains(localFileDragAction)) {
const files = Array.from(event.dataTransfer.files || []);
for(let i = 0; i < files.length; i++) {
// Try multiple ways to get file path
const file = files[i];
let path = file?.path
if(!path && file && DEVICE.isDesktop) {
//https://www.electronjs.org/docs/latest/breaking-changes#removed-filepath
const { webUtils } = require('electron');
if(webUtils && webUtils.getPathForFile) {
path = webUtils.getPathForFile(file);
}
}
if(!path) {
new Notice(t("ERROR_CANT_READ_FILEPATH"),6000);
return true; //excalidarw to continue processing
}
const link = getInternalLinkOrFileURLLink(path, this.plugin, event.dataTransfer.files[i].name, this.file);
const {x,y} = this.currentPosition;
const pos = {x:x+i*300, y:y+i*300};
if(link.isInternal) {
if(localFileDragAction === "embeddable") {
const ea = getEA(this.view) as ExcalidrawAutomate;
insertEmbeddableToView(ea, pos, link.file).then(()=>ea.destroy());
} else {
if(link.file.extension === "pdf") {
const insertPDFModal = new InsertPDFModal(this.plugin, this.view);
insertPDFModal.open(link.file);
}
const ea = getEA(this.view) as ExcalidrawAutomate;
insertImageToView(ea, pos, link.file).then(()=>ea.destroy()) ;
}
} else {
const extension = getURLImageExtension(link.url);
if(localFileDragAction === "image-import") {
if (IMAGE_TYPES.contains(extension)) {
(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);
if(maybeFile && maybeFile instanceof TFile) {
const action = await ScriptEngine.suggester(
this.app,[
"Use the file already in the Vault instead of importing",
"Overwrite existing file in the Vault",
"Import the file with a new name",
],[
"Use",
"Overwrite",
"Import",
],
"A file with the same name/path already exists in the Vault",
);
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);
// there is deliberately no break here
case "Use":
default:
const ea = getEA(this.view) as ExcalidrawAutomate;
await insertImageToView(ea, pos, maybeFile);
ea.destroy();
return false;
}
}
const file = await this.app.vault.createBinary(filepath, fileToImport)
const ea = getEA(this.view) as ExcalidrawAutomate;
await insertImageToView(ea, pos, file);
ea.destroy();
})();
} else if(extension === "excalidraw") {
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 modal = new UniversalInsertFileModal(this.plugin, this.view);
modal.open(file, pos);
})();
}
}
else if(localFileDragAction === "embeddable" || !IMAGE_TYPES.contains(extension)) {
const ea = getEA(this.view) as ExcalidrawAutomate;
insertEmbeddableToView(ea, pos, null, link.url).then(()=>ea.destroy());
if(localFileDragAction !== "embeddable") {
new Notice("Not imported to Vault. Embedded with local URI");
}
} else {
const ea = getEA(this.view) as ExcalidrawAutomate;
insertImageToView(ea, pos, link.url).then(()=>ea.destroy());
}
}
};
return false;
}
if(event.dataTransfer.types.length >= 1 && localFileDragAction === "link") {
const ea = getEA(this.view) as ExcalidrawAutomate;
for(let i=0;i<event.dataTransfer.files.length;i++) {
const file = event.dataTransfer.files[i];
let path = file?.path;
const name = file?.name;
if(!path && file && DEVICE.isDesktop) {
//https://www.electronjs.org/docs/latest/breaking-changes#removed-filepath
const { webUtils } = require('electron');
if(webUtils && webUtils.getPathForFile) {
path = webUtils.getPathForFile(file);
}
}
if(!path || !name) {
new Notice(t("ERROR_CANT_READ_FILEPATH"),6000);
ea.destroy();
return true; //excalidarw to continue processing
}
const link = getInternalLinkOrFileURLLink(path, this.plugin, name, this.file);
const id = ea.addText(
this.currentPosition.x+i*40,
this.currentPosition.y+i*20,
link.isInternal ? link.link :`📂 ${name}`);
if(!link.isInternal) {
ea.getElement(id).link = link.link;
}
}
ea.addElementsToView().then(()=>ea.destroy());
return false;
}
return true;
}
if (event.dataTransfer.types.includes("text/plain") || event.dataTransfer.types.includes("text/uri-list") || event.dataTransfer.types.includes("text/html")) {
const html = event.dataTransfer.getData("text/html");
const src = html.match(/src=["']([^"']*)["']/);
const htmlText = src ? src[1] : "";
const textText = event.dataTransfer.getData("text");
const uriText = event.dataTransfer.getData("text/uri-list");
let text: string = src ? htmlText : textText;
if (!text || text === "") {
text = uriText
}
if (!text || text === "") {
return true;
}
if (!onDropHook("text", null, text)) {
if(text && (externalDragAction==="embeddable") && /^(blob:)?(http|https):\/\/[^\s/$.?#].[^\s]*$/.test(text)) {
return true;
}
if(text && (externalDragAction==="image-url") && hyperlinkIsYouTubeLink(text)) {
this.view.addYouTubeThumbnail(text);
return false;
}
if(uriText && (externalDragAction==="image-url") && hyperlinkIsYouTubeLink(uriText)) {
this.view.addYouTubeThumbnail(uriText);
return false;
}
if(text && (externalDragAction==="image-url") && hyperlinkIsImage(text)) {
this.view.addImageWithURL(text);
return false;
}
if(uriText && (externalDragAction==="image-url") && hyperlinkIsImage(uriText)) {
this.view.addImageWithURL(uriText);
return false;
}
if(text && (externalDragAction==="image-import") && hyperlinkIsImage(text)) {
this.view.addImageSaveToVault(text);
return false;
}
if(uriText && (externalDragAction==="image-import") && hyperlinkIsImage(uriText)) {
this.view.addImageSaveToVault(uriText);
return false;
}
if (
this.plugin.settings.iframelyAllowed &&
text.match(/^https?:\/\/\S*$/)
) {
this.view.addTextWithIframely(text);
return false;
}
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/599
if(text.startsWith("obsidian://open?vault=")) {
const html = event.dataTransfer.getData("text/html");
if(html) {
const path = html.match(/href="app:\/\/obsidian\.md\/(.*?)"/);
if(path.length === 2) {
const link = decodeURIComponent(path[1]).split("#");
const f = this.app.vault.getAbstractFileByPath(link[0]);
if(f && f instanceof TFile) {
const path = this.app.metadataCache.fileToLinktext(f,this.file.path);
this.view.addText(`[[${
path +
(link.length>1 ? "#" + link[1] + "|" + path : "")
}]]`);
return;
}
this.view.addText(`[[${decodeURIComponent(path[1])}]]`);
return false;
}
}
const path = text.split("file=");
if(path.length === 2) {
this.view.addText(`[[${decodeURIComponent(path[1])}]]`);
return false;
}
}
this.view.addText(text.replace(/(!\[\[.*#[^\]]*\]\])/g, "$1{40}"));
}
return false;
}
if (onDropHook("unknown", null, null)) {
return false;
}
return true;
}
public onDragOver(e: any) {
const action = this.dropAction(e.dataTransfer);
if (action) {
if(!this.draginfoDiv) {
this.draginfoDiv = createDiv({cls:"excalidraw-draginfo"});
this.ownerDocument.body.appendChild(this.draginfoDiv);
}
let msg: string = "";
if((this.app as any).dragManager.draggable) {
//drag from Obsidian file manager
msg = modifierKeyTooltipMessages().InternalDragAction[internalDragModifierType(e)];
} else if(e.dataTransfer.types.length === 1 && e.dataTransfer.types.includes("Files")) {
//drag from OS file manager
msg = modifierKeyTooltipMessages().LocalFileDragAction[localFileDragModifierType(e)];
if(DEVICE.isMacOS && isWinCTRLorMacCMD(e)) {
msg = "CMD is reserved by MacOS for file system drag actions.\nCan't use it in Obsidian.\nUse a combination of SHIFT, CTRL, OPT instead."
}
} else {
//drag from Internet
msg = modifierKeyTooltipMessages().WebBrowserDragAction[webbrowserDragModifierType(e)];
}
if(!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
msg += DEVICE.isMacOS || DEVICE.isIOS
? "\nTry SHIFT, OPT, CTRL combinations for other drop actions"
: "\nTry SHIFT, CTRL, ALT, Meta combinations for other drop actions";
}
if(this.draginfoDiv.innerText !== msg) this.draginfoDiv.innerText = msg;
const top = `${e.clientY-parseFloat(getComputedStyle(this.draginfoDiv).fontSize)*8}px`;
const left = `${e.clientX-this.draginfoDiv.clientWidth/2}px`;
if(this.draginfoDiv.style.top !== top) this.draginfoDiv.style.top = top;
if(this.draginfoDiv.style.left !== left) this.draginfoDiv.style.left = left;
e.dataTransfer.dropEffect = action;
e.preventDefault();
return false;
}
}
public onDragLeave() {
if(this.draginfoDiv) {
this.ownerDocument.body.removeChild(this.draginfoDiv);
delete this.draginfoDiv;
}
}
private dropAction(transfer: DataTransfer) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.dropAction, "ExcalidrawView.dropAction");
// Return a 'copy' or 'link' action according to the content types, or undefined if no recognized type
const files = (this.app as any).dragManager.draggable?.files;
if (files) {
if (files[0] == this.file) {
files.shift();
(
this.app as any
).dragManager.draggable.title = `${files.length} files`;
}
}
if (
["file", "files"].includes(
(this.app as any).dragManager.draggable?.type,
)
) {
return "link";
}
if (
transfer.types?.includes("text/html") ||
transfer.types?.includes("text/plain") ||
transfer.types?.includes("Files")
) {
return "copy";
}
};
}