mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
25 Commits
2.6.2
...
2.6.8-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
382d4ca827 | ||
|
|
198e8f8cb7 | ||
|
|
d3baa74ce7 | ||
|
|
995bfe962e | ||
|
|
59255fd954 | ||
|
|
1e9bed9192 | ||
|
|
a747a6f698 | ||
|
|
b0d3976c27 | ||
|
|
7f77ab0743 | ||
|
|
79da8afa0b | ||
|
|
bb83523c0f | ||
|
|
f83c0a8458 | ||
|
|
7411d51477 | ||
|
|
55ce6456d8 | ||
|
|
da6619d55e | ||
|
|
6033c057c2 | ||
|
|
0efda1d6a6 | ||
|
|
59107f0c2a | ||
|
|
f7cd05f6c4 | ||
|
|
5cbd98e543 | ||
|
|
e2d5966ca3 | ||
|
|
dec2909db0 | ||
|
|
7233d1e037 | ||
|
|
5972f83369 | ||
|
|
0edfd7622c |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "2.6.2",
|
||||
"version": "2.6.8-beta-2",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "2.6.2",
|
||||
"version": "2.6.7",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@zsviczian/excalidraw": "0.17.6-8",
|
||||
"@zsviczian/excalidraw": "0.17.6-16",
|
||||
"chroma-js": "^2.4.2",
|
||||
"clsx": "^2.0.0",
|
||||
"@zsviczian/colormaster": "^1.2.2",
|
||||
@@ -38,6 +38,7 @@
|
||||
"es6-promise-pool": "2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jsesc": "^3.0.2",
|
||||
"@babel/core": "^7.22.9",
|
||||
"@babel/preset-env": "^7.22.10",
|
||||
"@babel/preset-react": "^7.22.5",
|
||||
|
||||
@@ -52,14 +52,15 @@ if (!isLib) console.log(manifest.version);
|
||||
|
||||
const packageString = isLib
|
||||
? ""
|
||||
: ';' + lzstring_pkg +
|
||||
: ';const INITIAL_TIMESTAMP=Date.now();' + lzstring_pkg +
|
||||
'\nlet REACT_PACKAGES = `' +
|
||||
jsesc(react_pkg + reactdom_pkg, { quotes: 'backtick' }) +
|
||||
'`;\n' +
|
||||
'let EXCALIDRAW_PACKAGE = ""; const unpackExcalidraw = () => {EXCALIDRAW_PACKAGE = LZString.decompressFromBase64("' + LZString.compressToBase64(excalidraw_pkg) + '");};\n' +
|
||||
'let {react, reactDOM } = window.eval.call(window, `(function() {' + '${REACT_PACKAGES};' + 'return {react: React, reactDOM: ReactDOM};})();`);\n' +
|
||||
`let excalidrawLib = {};\n` +
|
||||
'let PLUGIN_VERSION="' + manifest.version + '";';
|
||||
'const PLUGIN_VERSION="' + manifest.version + '";';
|
||||
|
||||
|
||||
const BASE_CONFIG = {
|
||||
input: 'src/main.ts',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Extension } from "@codemirror/state";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
import { HideTextBetweenCommentsExtension } from "./Fadeout";
|
||||
import { debug, DEBUGGING } from "src/utils/DebugHelper";
|
||||
export const EDITOR_FADEOUT = "fadeOutExcalidrawMarkup";
|
||||
|
||||
const editorExtensions: {[key:string]:Extension}= {
|
||||
@@ -10,13 +11,16 @@ const editorExtensions: {[key:string]:Extension}= {
|
||||
export class EditorHandler {
|
||||
private activeEditorExtensions: Extension[] = [];
|
||||
|
||||
constructor(private plugin: ExcalidrawPlugin) {}
|
||||
constructor(private plugin: ExcalidrawPlugin) {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(EditorHandler, `ExcalidrawPlugin.construct EditorHandler`);
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.plugin = null;
|
||||
}
|
||||
|
||||
setup(): void {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setup, `ExcalidrawPlugin.construct EditorHandler.setup`);
|
||||
this.plugin.registerEditorExtension(this.activeEditorExtensions);
|
||||
this.updateCMExtensionState(EDITOR_FADEOUT, this.plugin.settings.fadeOutExcalidrawMarkup);
|
||||
}
|
||||
|
||||
@@ -1107,66 +1107,9 @@ export class EmbeddedFilesLoader {
|
||||
|
||||
const getSVGData = async (app: App, file: TFile, colorMap: ColorMap | null): Promise<DataURL> => {
|
||||
const svgString = replaceSVGColors(await app.vault.read(file), colorMap) as string;
|
||||
|
||||
try {
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = svgString;
|
||||
|
||||
const svgElement = container.querySelector('svg');
|
||||
|
||||
if (!svgElement) {
|
||||
throw new Error('Invalid SVG content'); // Ensure there's an SVG element
|
||||
}
|
||||
|
||||
// Check for width and height attributes
|
||||
const hasWidth = svgElement.hasAttribute('width');
|
||||
const hasHeight = svgElement.hasAttribute('height');
|
||||
|
||||
// If width or height is missing, calculate based on viewBox
|
||||
if (!hasWidth || !hasHeight) {
|
||||
const viewBox = svgElement.getAttribute('viewBox');
|
||||
|
||||
if (viewBox) {
|
||||
const [ , , viewBoxWidth, viewBoxHeight] = viewBox.split(/\s+/).map(Number);
|
||||
|
||||
// Set width and height based on viewBox if they are missing
|
||||
if (!hasWidth) {
|
||||
svgElement.setAttribute('width', `${viewBoxWidth}px`);
|
||||
}
|
||||
if (!hasHeight) {
|
||||
svgElement.setAttribute('height', `${viewBoxHeight}px`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the updated SVG string from outerHTML
|
||||
const updatedSVGString = svgElement.outerHTML;
|
||||
|
||||
// Convert the updated SVG string to a base64 Data URL
|
||||
return svgToBase64(updatedSVGString) as DataURL;
|
||||
} catch (error) {
|
||||
errorlog({ where: "EmbeddedFileLoader.getSVGData", error });
|
||||
return svgToBase64(svgString) as DataURL;
|
||||
}
|
||||
return svgToBase64(svgString) as DataURL;
|
||||
};
|
||||
|
||||
/*export const generateIdFromFile = async (file: ArrayBuffer): Promise<FileId> => {
|
||||
let id: FileId;
|
||||
try {
|
||||
const hashBuffer = await window.crypto.subtle.digest("SHA-1", file);
|
||||
id =
|
||||
// convert buffer to byte array
|
||||
Array.from(new Uint8Array(hashBuffer))
|
||||
// convert to hex string
|
||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||
.join("") as FileId;
|
||||
} catch (error) {
|
||||
errorlog({ where: "EmbeddedFileLoader.generateIdFromFile", error });
|
||||
id = fileid() as FileId;
|
||||
}
|
||||
return id;
|
||||
};*/
|
||||
|
||||
export const generateIdFromFile = async (file: ArrayBuffer, key?: string): Promise<FileId> => {
|
||||
let id: FileId;
|
||||
try {
|
||||
|
||||
@@ -133,6 +133,10 @@ export class ExcalidrawAutomate {
|
||||
return DEVICE;
|
||||
}
|
||||
|
||||
public printStartupBreakdown() {
|
||||
this.plugin.printStarupBreakdown();
|
||||
}
|
||||
|
||||
public help(target: Function | string) {
|
||||
if (!target) {
|
||||
log("Usage: ea.help(ea.functionName) or ea.help('propertyName') or ea.help('utils.functionName') - notice property name and utils function name is in quotes");
|
||||
@@ -1547,6 +1551,10 @@ export class ExcalidrawAutomate {
|
||||
: imageFile.path + (scale || !anchor ? "":"|100%"),
|
||||
hasSVGwithBitmap: image.hasSVGwithBitmap,
|
||||
latex: null,
|
||||
size: { //must have the natural size here (e.g. for PDF cropping)
|
||||
height: image.size.height,
|
||||
width: image.size.width,
|
||||
},
|
||||
};
|
||||
if (scale && (Math.max(image.size.width, image.size.height) > MAX_IMAGE_SIZE)) {
|
||||
const scale =
|
||||
@@ -1816,7 +1824,7 @@ export class ExcalidrawAutomate {
|
||||
viewBackgroundColor: "#FFFFFF",
|
||||
gridSize: 0
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* returns true if MD file is an Excalidraw file
|
||||
@@ -2621,26 +2629,31 @@ export class ExcalidrawAutomate {
|
||||
return null;
|
||||
}
|
||||
|
||||
const size = await this.getOriginalImageSize(imgEl, true);
|
||||
if (size) {
|
||||
const originalArea = imgEl.width * imgEl.height;
|
||||
const originalAspectRatio = size.width / size.height;
|
||||
let newWidth = Math.sqrt(originalArea * originalAspectRatio);
|
||||
let newHeight = Math.sqrt(originalArea / originalAspectRatio);
|
||||
const centerX = imgEl.x + imgEl.width / 2;
|
||||
const centerY = imgEl.y + imgEl.height / 2;
|
||||
let originalArea, originalAspectRatio;
|
||||
if(imgEl.crop) {
|
||||
originalArea = imgEl.width * imgEl.height;
|
||||
originalAspectRatio = imgEl.crop.width / imgEl.crop.height;
|
||||
} else {
|
||||
const size = await this.getOriginalImageSize(imgEl, true);
|
||||
if (!size) { return false; }
|
||||
originalArea = imgEl.width * imgEl.height;
|
||||
originalAspectRatio = size.width / size.height;
|
||||
}
|
||||
let newWidth = Math.sqrt(originalArea * originalAspectRatio);
|
||||
let newHeight = Math.sqrt(originalArea / originalAspectRatio);
|
||||
const centerX = imgEl.x + imgEl.width / 2;
|
||||
const centerY = imgEl.y + imgEl.height / 2;
|
||||
|
||||
if (newWidth !== imgEl.width || newHeight !== imgEl.height) {
|
||||
if(!this.getElement(imgEl.id)) {
|
||||
this.copyViewElementsToEAforEditing([imgEl]);
|
||||
}
|
||||
const eaEl = this.getElement(imgEl.id);
|
||||
eaEl.width = newWidth;
|
||||
eaEl.height = newHeight;
|
||||
eaEl.x = centerX - newWidth / 2;
|
||||
eaEl.y = centerY - newHeight / 2;
|
||||
return true;
|
||||
if (newWidth !== imgEl.width || newHeight !== imgEl.height) {
|
||||
if(!this.getElement(imgEl.id)) {
|
||||
this.copyViewElementsToEAforEditing([imgEl]);
|
||||
}
|
||||
const eaEl = this.getElement(imgEl.id);
|
||||
eaEl.width = newWidth;
|
||||
eaEl.height = newHeight;
|
||||
eaEl.x = centerX - newWidth / 2;
|
||||
eaEl.y = centerY - newHeight / 2;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -2845,10 +2858,9 @@ export class ExcalidrawAutomate {
|
||||
}
|
||||
};
|
||||
|
||||
export async function initExcalidrawAutomate(
|
||||
export function initExcalidrawAutomate(
|
||||
plugin: ExcalidrawPlugin,
|
||||
): Promise<ExcalidrawAutomate> {
|
||||
await initFonts();
|
||||
): ExcalidrawAutomate {
|
||||
const ea = new ExcalidrawAutomate(plugin);
|
||||
//@ts-ignore
|
||||
window.ExcalidrawAutomate = ea;
|
||||
@@ -2883,14 +2895,6 @@ function getFontFamily(id: number):string {
|
||||
return getFontFamilyString({fontFamily:id})
|
||||
}
|
||||
|
||||
export async function initFonts():Promise<void> {
|
||||
/*await excalidrawLib.registerFontsInCSS();
|
||||
const fonts = excalidrawLib.getFontFamilies();
|
||||
for(let i=0;i<fonts.length;i++) {
|
||||
if(fonts[i] !== "Local Font") await (document as any).fonts.load(`16px ${fonts[i]}`);
|
||||
};*/
|
||||
}
|
||||
|
||||
export function _measureText(
|
||||
newText: string,
|
||||
fontSize: number,
|
||||
|
||||
@@ -55,6 +55,7 @@ import { updateElementIdsInScene } from "./utils/ExcalidrawSceneUtils";
|
||||
import { getNewUniqueFilepath } from "./utils/FileUtils";
|
||||
import { t } from "./lang/helpers";
|
||||
import { displayFontMessage } from "./utils/ExcalidrawViewUtils";
|
||||
import { getPDFRect } from "./utils/PDFUtils";
|
||||
|
||||
type SceneDataWithFiles = SceneData & { files: BinaryFiles };
|
||||
|
||||
@@ -755,7 +756,7 @@ export class ExcalidrawData {
|
||||
displayFontMessage(this.app);
|
||||
}
|
||||
},5000);
|
||||
await loadSceneFonts(this.scene.elements);
|
||||
const fontFaces = await loadSceneFonts(this.scene.elements);
|
||||
clearTimeout(timer);
|
||||
|
||||
if (!this.scene.files) {
|
||||
@@ -1579,6 +1580,26 @@ export class ExcalidrawData {
|
||||
return file;
|
||||
}
|
||||
|
||||
private syncCroppedPDFs() {
|
||||
let dirty = false;
|
||||
const scene = this.scene as SceneDataWithFiles;
|
||||
const pdfScale = this.plugin.settings.pdfScale;
|
||||
scene.elements
|
||||
.filter(el=>el.type === "image" && el.crop && !el.isDeleted)
|
||||
.forEach((el: Mutable<ExcalidrawImageElement>)=>{
|
||||
const ef = this.getFile(el.fileId);
|
||||
if(!ef.file) return;
|
||||
if(ef.file.extension !== "pdf") return;
|
||||
const pageRef = ef.linkParts.original.split("#")?.[1];
|
||||
if(!pageRef || !pageRef.startsWith("page=") || pageRef.includes("rect")) return;
|
||||
const restOfLink = el.link ? el.link.match(/&rect=\d*,\d*,\d*,\d*(.*)/)?.[1] : "";
|
||||
const link = ef.linkParts.original + getPDFRect(el.crop, pdfScale) + (restOfLink ? restOfLink : "]]");
|
||||
el.link = `[[${link}`;
|
||||
this.elementLinks.set(el.id, el.link);
|
||||
dirty = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* deletes fileIds from Excalidraw data for files no longer in the scene
|
||||
* @returns
|
||||
@@ -1699,6 +1720,7 @@ export class ExcalidrawData {
|
||||
this.updateElementLinksFromScene();
|
||||
result =
|
||||
result ||
|
||||
this.syncCroppedPDFs() ||
|
||||
this.setLinkPrefix() ||
|
||||
this.setUrlPrefix() ||
|
||||
this.setShowLinkBrackets() ||
|
||||
|
||||
@@ -148,6 +148,7 @@ import { Packages } from "./types/types";
|
||||
import React from "react";
|
||||
import { diagramToHTML } from "./utils/matic";
|
||||
import { IS_WORKER_SUPPORTED } from "./workers/compression-worker";
|
||||
import { getPDFCropRect } from "./utils/PDFUtils";
|
||||
|
||||
const EMBEDDABLE_SEMAPHORE_TIMEOUT = 2000;
|
||||
const PREVENT_RELOAD_TIMEOUT = 2000;
|
||||
@@ -218,6 +219,27 @@ export const addFiles = async (
|
||||
if (isDark === undefined) {
|
||||
isDark = s.scene.appState.theme;
|
||||
}
|
||||
// update element.crop naturalWidth and naturalHeight in case scale of PDF loading has changed
|
||||
// update crop.x crop.y, crop.width, crop.height according to the new scale
|
||||
files
|
||||
.filter((f:FileData) => view.excalidrawData.getFile(f.id)?.file?.extension === "pdf")
|
||||
.forEach((f:FileData) => {
|
||||
s.scene.elements
|
||||
.filter((el:ExcalidrawElement)=>el.type === "image" && el.fileId === f.id && el.crop && el.crop.naturalWidth !== f.size.width)
|
||||
.forEach((el:Mutable<ExcalidrawImageElement>) => {
|
||||
s.dirty = true;
|
||||
const scale = f.size.width / el.crop.naturalWidth;
|
||||
el.crop = {
|
||||
x: el.crop.x * scale,
|
||||
y: el.crop.y * scale,
|
||||
width: el.crop.width * scale,
|
||||
height: el.crop.height * scale,
|
||||
naturalWidth: f.size.width,
|
||||
naturalHeight: f.size.height,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
if (s.dirty) {
|
||||
//debug({where:"ExcalidrawView.addFiles",file:view.file.name,dataTheme:view.excalidrawData.scene.appState.theme,before:"updateScene",state:scene.appState})
|
||||
view.updateScene({
|
||||
@@ -273,7 +295,7 @@ 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 };
|
||||
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
|
||||
//Obsidian 0.15.0
|
||||
private draginfoDiv: HTMLDivElement;
|
||||
public canvasNodeFactory: CanvasNodeFactory;
|
||||
@@ -1505,6 +1527,10 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
|
||||
onload() {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onload, "ExcalidrawView.onload");
|
||||
if(this.plugin.settings.overrideObsidianFontSize) {
|
||||
document.documentElement.style.fontSize = "";
|
||||
}
|
||||
|
||||
const apiMissing = Boolean(typeof this.containerEl.onWindowMigrated === "undefined")
|
||||
this.packages = this.plugin.getPackage(this.ownerWindow);
|
||||
|
||||
@@ -3985,6 +4011,11 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onPaste, "ExcalidrawView.onPaste", data, event);
|
||||
const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
const ea = this.getHookServer();
|
||||
if(data?.elements) {
|
||||
data.elements
|
||||
.filter(el=>el.type==="text" && !el.hasOwnProperty("rawText"))
|
||||
.forEach(el=>(el as Mutable<ExcalidrawTextElement>).rawText = (el as ExcalidrawTextElement).originalText);
|
||||
};
|
||||
if(data && ea.onPasteHook) {
|
||||
const res = ea.onPasteHook({
|
||||
ea,
|
||||
@@ -4031,7 +4062,20 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
} else {
|
||||
if(link.match(/^[^#]*#page=\d*(&\w*=[^&]+){0,}&rect=\d*,\d*,\d*,\d*/g)) {
|
||||
const ea = getEA(this) as ExcalidrawAutomate;
|
||||
await ea.addImage(this.currentPosition.x, this.currentPosition.y,link);
|
||||
const imgID = await ea.addImage(this.currentPosition.x, this.currentPosition.y,link.split("&rect=")[0]);
|
||||
const el = ea.getElement(imgID) as Mutable<ExcalidrawImageElement>;
|
||||
const fd = ea.imagesDict[el.fileId] as FileData;
|
||||
el.crop = getPDFCropRect({
|
||||
scale: this.plugin.settings.pdfScale,
|
||||
link,
|
||||
naturalHeight: fd.size.height,
|
||||
naturalWidth: fd.size.width,
|
||||
});
|
||||
if(el.crop) {
|
||||
el.width = el.crop.width/this.plugin.settings.pdfScale;
|
||||
el.height = el.crop.height/this.plugin.settings.pdfScale;
|
||||
}
|
||||
el.link = `[[${link}]]`;
|
||||
ea.addElementsToView(false,false).then(()=>ea.destroy());
|
||||
} else {
|
||||
const modal = new UniversalInsertFileModal(this.plugin, this);
|
||||
@@ -4613,8 +4657,20 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
|
||||
//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) {
|
||||
private onBeforeTextEdit (textElement: ExcalidrawTextElement, isExistingElement: boolean): string {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onBeforeTextEdit, "ExcalidrawView.onBeforeTextEdit", textElement);
|
||||
const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
const st = api.getAppState();
|
||||
setDynamicStyle(
|
||||
this.plugin.ea,
|
||||
this,
|
||||
st.viewBackgroundColor === "transparent" ? "white" : st.viewBackgroundColor,
|
||||
this.plugin.settings.dynamicStyling,
|
||||
api.getColorAtScenePoint({sceneX: this.currentPosition.x, sceneY: this.currentPosition.y})
|
||||
);
|
||||
if(!isExistingElement) {
|
||||
return;
|
||||
}
|
||||
window.clearTimeout(this.isEditingTextResetTimer);
|
||||
this.isEditingTextResetTimer = null;
|
||||
this.semaphores.isEditingText = true; //to prevent autoresize on mobile when keyboard pops up
|
||||
|
||||
@@ -886,6 +886,7 @@ export const markdownPostProcessor = async (
|
||||
el: HTMLElement,
|
||||
ctx: MarkdownPostProcessorContext,
|
||||
) => {
|
||||
await plugin.awaitSettings();
|
||||
const isPrinting = Boolean(document.body.querySelectorAll("body > .print").length>0);
|
||||
//firstElementChild: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1956
|
||||
const isFrontmatter = el.hasClass("mod-frontmatter") ||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
App,
|
||||
Instruction,
|
||||
normalizePath,
|
||||
TAbstractFile,
|
||||
TFile,
|
||||
WorkspaceLeaf,
|
||||
@@ -22,6 +23,7 @@ export type ScriptIconMap = {
|
||||
|
||||
export class ScriptEngine {
|
||||
private plugin: ExcalidrawPlugin;
|
||||
private app: App;
|
||||
private scriptPath: string;
|
||||
//https://stackoverflow.com/questions/60218638/how-to-force-re-render-if-map-value-changes
|
||||
public scriptIconMap: ScriptIconMap;
|
||||
@@ -29,6 +31,7 @@ export class ScriptEngine {
|
||||
|
||||
constructor(plugin: ExcalidrawPlugin) {
|
||||
this.plugin = plugin;
|
||||
this.app = plugin.app;
|
||||
this.scriptIconMap = {};
|
||||
this.loadScripts();
|
||||
this.registerEventHandlers();
|
||||
@@ -58,7 +61,7 @@ export class ScriptEngine {
|
||||
if (!path.endsWith(".svg")) {
|
||||
return;
|
||||
}
|
||||
const scriptFile = app.vault.getAbstractFileByPath(
|
||||
const scriptFile = this.app.vault.getAbstractFileByPath(
|
||||
getIMGFilename(path, "md"),
|
||||
);
|
||||
if (scriptFile && scriptFile instanceof TFile) {
|
||||
@@ -107,19 +110,19 @@ export class ScriptEngine {
|
||||
|
||||
registerEventHandlers() {
|
||||
this.plugin.registerEvent(
|
||||
this.plugin.app.vault.on(
|
||||
this.app.vault.on(
|
||||
"delete",
|
||||
(file: TFile)=>this.deleteEventHandler(file)
|
||||
),
|
||||
);
|
||||
this.plugin.registerEvent(
|
||||
this.plugin.app.vault.on(
|
||||
this.app.vault.on(
|
||||
"create",
|
||||
(file: TFile)=>this.createEventHandler(file)
|
||||
),
|
||||
);
|
||||
this.plugin.registerEvent(
|
||||
this.plugin.app.vault.on(
|
||||
this.app.vault.on(
|
||||
"rename",
|
||||
(file: TAbstractFile, oldPath: string)=>this.renameEventHandler(file, oldPath)
|
||||
),
|
||||
@@ -138,15 +141,16 @@ export class ScriptEngine {
|
||||
|
||||
public getListofScripts(): TFile[] {
|
||||
this.scriptPath = this.plugin.settings.scriptFolderPath;
|
||||
if (!app.vault.getAbstractFileByPath(this.scriptPath)) {
|
||||
//this.scriptPath = null;
|
||||
if(!this.scriptPath) return;
|
||||
this.scriptPath = normalizePath(this.scriptPath);
|
||||
if (!this.app.vault.getAbstractFileByPath(this.scriptPath)) {
|
||||
return;
|
||||
}
|
||||
return app.vault
|
||||
return this.app.vault
|
||||
.getFiles()
|
||||
.filter(
|
||||
(f: TFile) =>
|
||||
f.path.startsWith(this.scriptPath) && f.extension === "md",
|
||||
f.path.startsWith(this.scriptPath+"/") && f.extension === "md",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -166,7 +170,10 @@ export class ScriptEngine {
|
||||
}
|
||||
|
||||
const subpath = path.split(`${this.scriptPath}/`)[1];
|
||||
const lastSlash = subpath.lastIndexOf("/");
|
||||
if(!subpath) {
|
||||
console.warn(`ScriptEngine.getScriptName unexpected basename: ${basename}; path: ${path}`)
|
||||
}
|
||||
const lastSlash = subpath?.lastIndexOf("/");
|
||||
if (lastSlash > -1) {
|
||||
return subpath.substring(0, lastSlash + 1) + basename;
|
||||
}
|
||||
@@ -175,10 +182,10 @@ export class ScriptEngine {
|
||||
|
||||
async addScriptIconToMap(scriptPath: string, name: string) {
|
||||
const svgFilePath = getIMGFilename(scriptPath, "svg");
|
||||
const file = app.vault.getAbstractFileByPath(svgFilePath);
|
||||
const file = this.app.vault.getAbstractFileByPath(svgFilePath);
|
||||
const svgString: string =
|
||||
file && file instanceof TFile
|
||||
? await app.vault.read(file)
|
||||
? await this.app.vault.read(file)
|
||||
: null;
|
||||
this.scriptIconMap = {
|
||||
...this.scriptIconMap,
|
||||
@@ -199,12 +206,12 @@ export class ScriptEngine {
|
||||
name: `(Script) ${scriptName}`,
|
||||
checkCallback: (checking: boolean) => {
|
||||
if (checking) {
|
||||
return Boolean(app.workspace.getActiveViewOfType(ExcalidrawView));
|
||||
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView));
|
||||
}
|
||||
const view = app.workspace.getActiveViewOfType(ExcalidrawView);
|
||||
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
||||
if (view) {
|
||||
(async()=>{
|
||||
const script = await app.vault.read(f);
|
||||
const script = await this.app.vault.read(f);
|
||||
if(script) {
|
||||
//remove YAML frontmatter if present
|
||||
this.executeScript(view, script, scriptName,f);
|
||||
@@ -218,7 +225,7 @@ export class ScriptEngine {
|
||||
}
|
||||
|
||||
unloadScripts() {
|
||||
const scripts = app.vault
|
||||
const scripts = this.app.vault
|
||||
.getFiles()
|
||||
.filter((f: TFile) => f.path.startsWith(this.scriptPath));
|
||||
scripts.forEach((f) => {
|
||||
@@ -236,11 +243,11 @@ export class ScriptEngine {
|
||||
|
||||
const commandId = `${PLUGIN_ID}:${basename}`;
|
||||
// @ts-ignore
|
||||
if (!this.plugin.app.commands.commands[commandId]) {
|
||||
if (!this.app.commands.commands[commandId]) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
delete this.plugin.app.commands.commands[commandId];
|
||||
delete this.app.commands.commands[commandId];
|
||||
}
|
||||
|
||||
async executeScript(view: ExcalidrawView, script: string, title: string, file: TFile) {
|
||||
@@ -271,7 +278,7 @@ export class ScriptEngine {
|
||||
ScriptEngine.inputPrompt(
|
||||
view,
|
||||
this.plugin,
|
||||
this.plugin.app,
|
||||
this.app,
|
||||
header,
|
||||
placeholder,
|
||||
value,
|
||||
@@ -288,7 +295,7 @@ export class ScriptEngine {
|
||||
instructions?: Instruction[],
|
||||
) =>
|
||||
ScriptEngine.suggester(
|
||||
this.plugin.app,
|
||||
this.app,
|
||||
displayItems,
|
||||
items,
|
||||
hint,
|
||||
@@ -304,7 +311,7 @@ export class ScriptEngine {
|
||||
}
|
||||
|
||||
private updateToolPannels() {
|
||||
const excalidrawViews = getExcalidrawViews(this.plugin.app);
|
||||
const excalidrawViews = getExcalidrawViews(this.app);
|
||||
excalidrawViews.forEach(excalidrawView => {
|
||||
excalidrawView.toolsPanelRef?.current?.updateScriptIconMap(
|
||||
this.scriptIconMap,
|
||||
|
||||
@@ -222,6 +222,7 @@ export const FRONTMATTER_KEYS:{[key:string]: {name: string, type: string, depric
|
||||
|
||||
export const EMBEDDABLE_THEME_FRONTMATTER_VALUES = ["light", "dark", "auto", "dafault"];
|
||||
export const VIEW_TYPE_EXCALIDRAW = "excalidraw";
|
||||
export const VIEW_TYPE_EXCALIDRAW_LOADING = "excalidraw-loading";
|
||||
export const ICON_NAME = "excalidraw-icon";
|
||||
export const MAX_COLORS = 5;
|
||||
export const COLOR_FREQ = 6;
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { FileView, TextFileView, View, WorkspaceLeaf } from "obsidian";
|
||||
import { App, FileView, WorkspaceLeaf } from "obsidian";
|
||||
import { VIEW_TYPE_EXCALIDRAW_LOADING } from "src/constants/constants";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
import { setExcalidrawView } from "src/utils/ObsidianUtils";
|
||||
|
||||
export default class ExcalidrawLoading extends FileView {
|
||||
export function switchToExcalidraw(app: App) {
|
||||
const leaves = app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW_LOADING).filter(l=>l.view instanceof ExcalidrawLoading);
|
||||
leaves.forEach(l=>(l.view as ExcalidrawLoading).switchToeExcalidraw());
|
||||
}
|
||||
|
||||
export class ExcalidrawLoading extends FileView {
|
||||
constructor(leaf: WorkspaceLeaf, private plugin: ExcalidrawPlugin) {
|
||||
super(leaf);
|
||||
this.switchToeExcalidraw();
|
||||
this.displayLoadingText();
|
||||
}
|
||||
|
||||
@@ -14,13 +19,12 @@ export default class ExcalidrawLoading extends FileView {
|
||||
this.displayLoadingText();
|
||||
}
|
||||
|
||||
private async switchToeExcalidraw() {
|
||||
await this.plugin.awaitInit();
|
||||
public switchToeExcalidraw() {
|
||||
setExcalidrawView(this.leaf);
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
return "excalidra-loading";
|
||||
return VIEW_TYPE_EXCALIDRAW_LOADING;
|
||||
}
|
||||
|
||||
getDisplayText() {
|
||||
|
||||
@@ -17,6 +17,47 @@ 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.6.7":`
|
||||
Hoping to finally move on to 2.7.0... but still have one last bug to fix in 2.6.x!
|
||||
|
||||
## Fixed
|
||||
I misread a line in the Excalidraw package code... ended up breaking image loading in 2.6.6. The icon library script didn't work right, and updating nested drawings caused all images in the scene to be dropped from memory. This led to image-placeholders in exports and broke copy-paste to Excalidraw.com and between drawings. I am surprised no one reported it! 😳
|
||||
`,
|
||||
"2.6.6":`
|
||||
## Fixed
|
||||
- Images and LaTeX formulas did not update in the scene when the source was changed until the Excalidraw drawing was closed and reopened. [#2105](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2105)
|
||||
`,
|
||||
"2.6.5":`
|
||||
## Fixed
|
||||
- Text sizing issue in the drawing that is first loaded after Obsidian restarts [#2086](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2086)
|
||||
- Excalidraw didn't load if there was a file in the Excalidraw folder with a name that starts the same way as the Scripts folder name. [#2095](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2095)
|
||||
- **OVERSIZED EXCALIDRAW TOOLBAR**: Added a new setting under "Excalidraw Appearance and Behavior > Theme and Styling" called "Limit Obsidian Font Size to Editor Text." This setting is off by default. When enabled, it restricts Obsidian's custom font size adjustments to editor text only, preventing unintended scaling of Excalidraw UI elements and other themes that rely on the default interface font size. Feel free to experiment with this setting to improve Excalidraw UI consistency. However, because this change affects the broader Obsidian UI, it's recommended to turn it off if any layout issues arise. [#2087](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2087)`,
|
||||
"2.6.4":`
|
||||
## 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)
|
||||
|
||||
@@ -713,27 +713,70 @@ export class ConfirmationPrompt extends Modal {
|
||||
}
|
||||
}
|
||||
|
||||
export async function linkPrompt (
|
||||
linkText:string,
|
||||
export async function linkPrompt(
|
||||
linkText: string,
|
||||
app: App,
|
||||
view?: ExcalidrawView,
|
||||
message: string = "Select link to open",
|
||||
):Promise<[file:TFile, linkText:string, subpath: string]> {
|
||||
const linksArray = REGEX_LINK.getResList(linkText);
|
||||
const tagsArray = REGEX_TAGS.getResList(linkText.replaceAll(/([^\s])#/g,"$1 "));
|
||||
message: string = t("SELECT_LINK_TO_OPEN"),
|
||||
): Promise<[file: TFile, linkText: string, subpath: string]> {
|
||||
const linksArray = REGEX_LINK.getResList(linkText).filter(x => Boolean(x.value));
|
||||
const links = linksArray.map(x => REGEX_LINK.getLink(x));
|
||||
|
||||
// Create a map to track duplicates by base link (without rect reference)
|
||||
const linkMap = new Map<string, number[]>();
|
||||
links.forEach((link, i) => {
|
||||
const linkBase = link.split("&rect=")[0];
|
||||
if (!linkMap.has(linkBase)) linkMap.set(linkBase, []);
|
||||
linkMap.get(linkBase).push(i);
|
||||
});
|
||||
|
||||
// Determine indices to keep
|
||||
const indicesToKeep = new Set<number>();
|
||||
linkMap.forEach(indices => {
|
||||
if (indices.length === 1) {
|
||||
// Only one link, keep it
|
||||
indicesToKeep.add(indices[0]);
|
||||
} else {
|
||||
// Multiple links: prefer the one with rect reference, if available
|
||||
const rectIndex = indices.find(i => links[i].includes("&rect="));
|
||||
if (rectIndex !== undefined) {
|
||||
indicesToKeep.add(rectIndex);
|
||||
} else {
|
||||
// No rect reference in duplicates, add the first one
|
||||
indicesToKeep.add(indices[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Final validation to ensure each duplicate group has at least one entry
|
||||
linkMap.forEach(indices => {
|
||||
const hasKeptEntry = indices.some(i => indicesToKeep.has(i));
|
||||
if (!hasKeptEntry) {
|
||||
// Add the first index if none were kept
|
||||
indicesToKeep.add(indices[0]);
|
||||
}
|
||||
});
|
||||
|
||||
// Filter linksArray, links, itemsDisplay, and items based on indicesToKeep
|
||||
const filteredLinksArray = linksArray.filter((_, i) => indicesToKeep.has(i));
|
||||
const tagsArray = REGEX_TAGS.getResList(linkText.replaceAll(/([^\s])#/g, "$1 ")).filter(x => Boolean(x.value));
|
||||
|
||||
let subpath: string = null;
|
||||
let file: TFile = null;
|
||||
let parts = linksArray[0] ?? tagsArray[0];
|
||||
let parts = filteredLinksArray[0] ?? tagsArray[0];
|
||||
|
||||
// Generate filtered itemsDisplay and items arrays
|
||||
const itemsDisplay = [
|
||||
...linksArray.filter(p=> Boolean(p.value)).map(p => {
|
||||
...filteredLinksArray.map(p => {
|
||||
const alias = REGEX_LINK.getAliasOrLink(p);
|
||||
return alias === "100%" ? REGEX_LINK.getLink(p) : alias;
|
||||
}),
|
||||
...tagsArray.filter(x=> Boolean(x.value)).map(x => REGEX_TAGS.getTag(x)),
|
||||
...tagsArray.map(x => REGEX_TAGS.getTag(x)),
|
||||
];
|
||||
|
||||
const items = [
|
||||
...linksArray.filter(p=>Boolean(p.value)),
|
||||
...tagsArray.filter(x=> Boolean(x.value)),
|
||||
...filteredLinksArray,
|
||||
...tagsArray,
|
||||
];
|
||||
|
||||
if (items.length>1) {
|
||||
|
||||
@@ -6,6 +6,8 @@ import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/constants/
|
||||
import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/utils/ModifierkeyHelper";
|
||||
|
||||
const CJK_FONTS = "CJK Fonts";
|
||||
declare const PLUGIN_VERSION:string;
|
||||
|
||||
// English
|
||||
export default {
|
||||
// main.ts
|
||||
@@ -76,6 +78,7 @@ export default {
|
||||
IMPORT_SVG_CONTEXTMENU: "Convert SVG to strokes - with limitations",
|
||||
INSERT_MD: "Insert markdown file from vault",
|
||||
INSERT_PDF: "Insert PDF file from vault",
|
||||
INSERT_LAST_ACTIVE_PDF_PAGE_AS_IMAGE: "Insert last active PDF page as image",
|
||||
UNIVERSAL_ADD_FILE: "Insert ANY file",
|
||||
INSERT_CARD: "Add back-of-note card",
|
||||
CONVERT_CARD_TO_FILE: "Move back-of-note card to File",
|
||||
@@ -101,6 +104,9 @@ export default {
|
||||
FONTS_LOADED: "Excalidraw: CJK Fonts loaded",
|
||||
FONTS_LOAD_ERROR: "Excalidraw: Could not find CJK Fonts in the assets folder\n",
|
||||
|
||||
//Prompt.ts
|
||||
SELECT_LINK_TO_OPEN: "Select a link to open",
|
||||
|
||||
//ExcalidrawView.ts
|
||||
NO_SEARCH_RESULT: "Didn't find a matching element in the drawing",
|
||||
FORCE_SAVE_ABORTED: "Force Save aborted because saving is in progress",
|
||||
@@ -322,6 +328,11 @@ FILENAME_HEAD: "Filename",
|
||||
"i.e. you are not using Excalidraw markdown files.<br><b><u>Toggle ON:</u></b> filename ends with .excalidraw.md<br><b><u>Toggle OFF:</u></b> filename ends with .md",
|
||||
DISPLAY_HEAD: "Excalidraw appearance and behavior",
|
||||
DISPLAY_DESC: "In the 'appearance and behavior' section of Excalidraw Settings, you can fine-tune how Excalidraw appears and behaves. This includes options for dynamic styling, left-handed mode, matching Excalidraw and Obsidian themes, default modes, and more.",
|
||||
OVERRIDE_OBSIDIAN_FONT_SIZE_NAME: "Limit Obsidian Font Size to Editor Text",
|
||||
OVERRIDE_OBSIDIAN_FONT_SIZE_DESC:
|
||||
"Obsidian's custom font size setting affects the entire interface, including Excalidraw and themes that depend on the default font size. " +
|
||||
"Enabling this option restricts font size changes to editor text, which will improve the look of Excalidraw. " +
|
||||
"If parts of the UI look incorrect after enabling, try turning this setting off.",
|
||||
DYNAMICSTYLE_NAME: "Dynamic styling",
|
||||
DYNAMICSTYLE_DESC:
|
||||
"Change Excalidraw UI colors to match the canvas color",
|
||||
@@ -955,4 +966,7 @@ FILENAME_HEAD: "Filename",
|
||||
IPM_GROUP_PAGES_DESC: "This will group all pages into a single group. This is recommended if you are locking the pages after import, because the group will be easier to unlock later rather than unlocking one by one.",
|
||||
IPM_SELECT_PDF: "Please select a PDF file",
|
||||
|
||||
//Utils.ts
|
||||
UPDATE_AVAILABLE: `A newer version of Excalidraw is available in Community Plugins.\n\nYou are using ${PLUGIN_VERSION}.\nThe latest is`,
|
||||
ERROR_PNG_TOO_LARGE: "Error exporting PNG - PNG too large, try a smaller resolution",
|
||||
};
|
||||
|
||||
@@ -6,6 +6,8 @@ import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/constants/
|
||||
import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/utils/ModifierkeyHelper";
|
||||
|
||||
const CJK_FONTS = "CJK Fonts";
|
||||
declare const PLUGIN_VERSION:string;
|
||||
|
||||
// 简体中文
|
||||
export default {
|
||||
// main.ts
|
||||
@@ -76,6 +78,7 @@ export default {
|
||||
IMPORT_SVG_CONTEXTMENU: "转换 SVG 到线条 - 有限制",
|
||||
INSERT_MD: "插入 Markdown 文档(以图像形式嵌入)到当前绘图中",
|
||||
INSERT_PDF: "插入 PDF 文档(以图像形式嵌入)到当前绘图中",
|
||||
INSERT_LAST_ACTIVE_PDF_PAGE_AS_IMAGE: "将最后激活的 PDF 页面插入为图片",
|
||||
UNIVERSAL_ADD_FILE: "插入任意文件(以交互形式嵌入,或者以图像形式嵌入)到当前绘图中",
|
||||
INSERT_CARD: "插入“背景笔记”卡片",
|
||||
CONVERT_CARD_TO_FILE: "将“背景笔记”卡片保存到文件",
|
||||
@@ -101,6 +104,9 @@ export default {
|
||||
FONTS_LOADED : "Excalidraw: CJK 字体已加载" ,
|
||||
FONTS_LOAD_ERROR : "Excalidraw: 在资源文件夹下找不到 CJK 字体\n" ,
|
||||
|
||||
//Prompt.ts
|
||||
SELECT_LINK_TO_OPEN: "选择要打开的链接",
|
||||
|
||||
//ExcalidrawView.ts
|
||||
NO_SEARCH_RESULT: "在绘图中未找到匹配的元素",
|
||||
FORCE_SAVE_ABORTED: "自动保存被中止,因为文件正在保存中",
|
||||
@@ -321,7 +327,12 @@ FILENAME_HEAD: "文件名",
|
||||
"该选项在兼容模式(即非 Excalidraw 专用 Markdown 文件)下不会生效。<br>" +
|
||||
"<b>开启:</b>使用 .excalidraw.md 作为扩展名。<br><b>关闭:</b>使用 .md 作为扩展名。",
|
||||
DISPLAY_HEAD: "界面 & 行为",
|
||||
DISPLAY_DESC: "包括:左手模式,主题匹配,缩放,激光笔工具,修饰键等的设置。",
|
||||
DISPLAY_DESC: "在 Excalidraw 设置的 '外观和行为' 部分,您可以微调 Excalidraw 的外观和行为。这包括动态样式、左手模式、匹配 Excalidraw 和 Obsidian 主题、默认模式等选项。",
|
||||
OVERRIDE_OBSIDIAN_FONT_SIZE_NAME : "限制 Obsidian 字体大小为编辑器文本" ,
|
||||
OVERRIDE_OBSIDIAN_FONT_SIZE_DESC :
|
||||
"Obsidian 的自定义字体大小设置会影响整个界面,包括 Excalidraw 和依赖默认字体大小的主题。" +
|
||||
"启用此选项将限制字体大小更改为编辑器文本,这将改善 Excalidraw 的外观。" +
|
||||
"如果启用后发现界面的某些部分看起来不正确,请尝试关闭此设置。" ,
|
||||
DYNAMICSTYLE_NAME: "动态样式",
|
||||
DYNAMICSTYLE_DESC:
|
||||
"根据画布颜色自动调节 Excalidraw 界面颜色",
|
||||
@@ -355,6 +366,7 @@ FILENAME_HEAD: "文件名",
|
||||
DEFAULT_PEN_MODE_DESC:
|
||||
"打开绘图时,是否自动开启触控笔模式?",
|
||||
DISABLE_DOUBLE_TAP_ERASER_NAME: "启用手写模式下的双击橡皮擦功能",
|
||||
DISABLE_SINGLE_FINGER_PANNING_NAME: "启用手写模式下的单指平移功能",
|
||||
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_NAME: "在触控笔模式下显示十字准星(+)",
|
||||
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_DESC:
|
||||
"在触控笔模式下使用涂鸦功能会显示十字准星 <b><u>打开:</u></b> 显示 <b><u>关闭:</u></b> 隐藏<br>"+
|
||||
@@ -865,6 +877,7 @@ FILENAME_HEAD: "文件名",
|
||||
对此带来的不便,我深表歉意。
|
||||
</p>
|
||||
`,
|
||||
|
||||
//ObsidianMenu.tsx
|
||||
GOTO_FULLSCREEN: "进入全屏模式",
|
||||
EXIT_FULLSCREEN: "退出全屏模式",
|
||||
@@ -953,4 +966,7 @@ FILENAME_HEAD: "文件名",
|
||||
IPM_GROUP_PAGES_DESC: "这将把所有页面建立为一个单独的组。如果您在导入后锁定页面,建议使用此方法,因为这样可以更方便地解锁整个组,而不是逐个解锁。",
|
||||
IPM_SELECT_PDF: "请选择一个 PDF 文件",
|
||||
|
||||
};
|
||||
//Utils.ts
|
||||
UPDATE_AVAILABLE: `Excalidraw 的新版本已在社区插件中可用。\n\n您正在使用 ${PLUGIN_VERSION}。\n最新版本是`,
|
||||
ERROR_PNG_TOO_LARGE: "导出 PNG 时出错 - PNG 文件过大,请尝试较小的分辨率",
|
||||
};
|
||||
|
||||
1008
src/main.ts
1008
src/main.ts
File diff suppressed because it is too large
Load Diff
@@ -41,6 +41,7 @@ import { Rank } from "./menu/ActionIcons";
|
||||
import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/constants/constSettingsTags";
|
||||
import { HotkeyEditor } from "./dialogs/HotkeyEditor";
|
||||
import { getExcalidrawViews } from "./utils/ObsidianUtils";
|
||||
import de from "./lang/locale/de";
|
||||
|
||||
export interface ExcalidrawSettings {
|
||||
folder: string;
|
||||
@@ -76,6 +77,7 @@ export interface ExcalidrawSettings {
|
||||
previewMatchObsidianTheme: boolean;
|
||||
width: string;
|
||||
height: string;
|
||||
overrideObsidianFontSize: boolean;
|
||||
dynamicStyling: DynamicStyle;
|
||||
isLeftHanded: boolean;
|
||||
iframeMatchExcalidrawTheme: boolean;
|
||||
@@ -253,6 +255,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
previewMatchObsidianTheme: false,
|
||||
width: "400",
|
||||
height: "",
|
||||
overrideObsidianFontSize: false,
|
||||
dynamicStyling: "colorful",
|
||||
isLeftHanded: false,
|
||||
iframeMatchExcalidrawTheme: true,
|
||||
@@ -516,6 +519,12 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
}
|
||||
|
||||
async hide() {
|
||||
if(this.plugin.settings.overrideObsidianFontSize) {
|
||||
document.documentElement.style.fontSize = "";
|
||||
} else if(!document.documentElement.style.fontSize) {
|
||||
document.documentElement.style.fontSize = getComputedStyle(document.body).getPropertyValue("--font-text-size");
|
||||
}
|
||||
|
||||
this.plugin.settings.scriptFolderPath = normalizePath(
|
||||
this.plugin.settings.scriptFolderPath,
|
||||
);
|
||||
@@ -1163,6 +1172,18 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
cls: "excalidraw-setting-h3",
|
||||
});
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("OVERRIDE_OBSIDIAN_FONT_SIZE_NAME"))
|
||||
.setDesc(fragWithHTML(t("OVERRIDE_OBSIDIAN_FONT_SIZE_DESC")))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.overrideObsidianFontSize)
|
||||
.onChange((value) => {
|
||||
this.plugin.settings.overrideObsidianFontSize = value;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("DYNAMICSTYLE_NAME"))
|
||||
.setDesc(fragWithHTML(t("DYNAMICSTYLE_DESC")))
|
||||
|
||||
@@ -13,6 +13,7 @@ export const setDynamicStyle = (
|
||||
view: ExcalidrawView, //the excalidraw view
|
||||
color: string,
|
||||
dynamicStyle: DynamicStyle,
|
||||
textBackgroundColor?: string,
|
||||
) => {
|
||||
if(dynamicStyle === "none") {
|
||||
view.excalidrawContainer?.removeAttribute("style");
|
||||
@@ -116,7 +117,9 @@ export const setDynamicStyle = (
|
||||
[`--h3-color`]: str(text),
|
||||
[`--h4-color`]: str(text),
|
||||
[`color`]: str(text),
|
||||
['--excalidraw-caret-color']: str(isLightTheme ? text : cmBG()),
|
||||
['--excalidraw-caret-color']: textBackgroundColor
|
||||
? str(isLightTheme ? invertColor(textBackgroundColor) : ea.getCM(textBackgroundColor))
|
||||
: str(isLightTheme ? text : cmBG()),
|
||||
[`--select-highlight-color`]: str(gray1()),
|
||||
[`--color-gray-90`]: str(isDark?text.darkerBy(5):text.lighterBy(5)), //search background
|
||||
[`--color-gray-80`]: str(isDark?text.darkerBy(10):text.lighterBy(10)), //frame
|
||||
|
||||
@@ -19,6 +19,7 @@ export async function insertImageToView(
|
||||
file: TFile | string,
|
||||
scale?: boolean,
|
||||
shouldInsertToView: boolean = true,
|
||||
repositionToCursor: boolean = false,
|
||||
):Promise<string> {
|
||||
if(shouldInsertToView) {ea.clear();}
|
||||
ea.style.strokeColor = "transparent";
|
||||
@@ -31,7 +32,7 @@ export async function insertImageToView(
|
||||
file,
|
||||
scale,
|
||||
);
|
||||
if(shouldInsertToView) {await ea.addElementsToView(false, true, true);}
|
||||
if(shouldInsertToView) {await ea.addElementsToView(repositionToCursor, true, true);}
|
||||
return id;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,37 @@
|
||||
//for future use, not used currently
|
||||
|
||||
import { ImageCrop } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { LinkParts } from "./Utils";
|
||||
|
||||
export function getPDFCropRect (props: {
|
||||
scale: number,
|
||||
linkParts: LinkParts,
|
||||
link: string,
|
||||
naturalHeight: number,
|
||||
naturalWidth: number,
|
||||
}) : ImageCrop | null {
|
||||
const cropRect = props.linkParts.ref.split("rect=")[1]?.split(",").map(x=>parseInt(x));
|
||||
const validRect = cropRect && cropRect.length === 4 && cropRect.every(x=>!isNaN(x));
|
||||
|
||||
if(!validRect) {
|
||||
const rectVal = props.link.match(/&rect=(\d*),(\d*),(\d*),(\d*)/);
|
||||
if (!rectVal || rectVal.length !== 5) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const R0 = parseInt(rectVal[1]);
|
||||
const R1 = parseInt(rectVal[2]);
|
||||
const R2 = parseInt(rectVal[3]);
|
||||
const R3 = parseInt(rectVal[4]);
|
||||
|
||||
return {
|
||||
x: cropRect[0] * props.scale,
|
||||
y: (props.naturalHeight/props.scale - cropRect[3]) * props.scale,
|
||||
width: (cropRect[2] - cropRect[0]) * props.scale,
|
||||
height: (cropRect[3] - cropRect[1]) * props.scale,
|
||||
x: R0 * props.scale,
|
||||
y: (props.naturalHeight/props.scale - R3) * props.scale,
|
||||
width: (R2 - R0) * props.scale,
|
||||
height: (R3 - R1) * props.scale,
|
||||
naturalWidth: props.naturalWidth,
|
||||
naturalHeight: props.naturalHeight,
|
||||
}
|
||||
}
|
||||
|
||||
export function getPDFRect(elCrop: ImageCrop, scale: number): string {
|
||||
const R0 = elCrop.x / scale;
|
||||
const R2 = elCrop.width / scale + R0;
|
||||
const R3 = (elCrop.naturalHeight - elCrop.y) / scale;
|
||||
const R1 = R3 - elCrop.height / scale;
|
||||
return `&rect=${Math.round(R0)},${Math.round(R1)},${Math.round(R2)},${Math.round(R3)}`;
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
getContainerElement,
|
||||
} from "../constants/constants";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { ExcalidrawElement, ExcalidrawTextElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { ExcalidrawElement, ExcalidrawTextElement, ImageCrop } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { ExportSettings } from "../ExcalidrawView";
|
||||
import { getDataURLFromURL, getIMGFilename, getMimeType, getURLImageExtension } from "./FileUtils";
|
||||
import { generateEmbeddableLink } from "./CustomEmbeddableUtils";
|
||||
@@ -30,6 +30,8 @@ import { CropImage } from "./CropImage";
|
||||
import opentype from 'opentype.js';
|
||||
import { runCompressionWorker } from "src/workers/compression-worker";
|
||||
import Pool from "es6-promise-pool";
|
||||
import { FileData } from "src/EmbeddedFileLoader";
|
||||
import { t } from "src/lang/helpers";
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
declare var LZString: any;
|
||||
@@ -76,7 +78,7 @@ export async function checkExcalidrawVersion() {
|
||||
|
||||
if (isVersionNewerThanOther(latestVersion,PLUGIN_VERSION)) {
|
||||
new Notice(
|
||||
`A newer version of Excalidraw is available in Community Plugins.\n\nYou are using ${PLUGIN_VERSION}.\nThe latest is ${latestVersion}`,
|
||||
t("UPDATE_AVAILABLE") + ` ${latestVersion}`,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -219,15 +221,6 @@ export async function getFontDataURL (
|
||||
const split = dataURL.split(";base64,", 2);
|
||||
dataURL = `${split[0]};charset=utf-8;base64,${split[1]}`;
|
||||
fontDef = ` @font-face {font-family: "${fontName}";src: url("${dataURL}") format("${format}")}`;
|
||||
/* const mimeType = f.extension.startsWith("woff")
|
||||
? "application/font-woff"
|
||||
: "font/truetype";
|
||||
fontName = name ?? f.basename;
|
||||
dataURL = await getDataURL(ab, mimeType);
|
||||
fontDef = ` @font-face {font-family: "${fontName}";src: url("${dataURL}")}`;
|
||||
//format("${f.extension === "ttf" ? "truetype" : f.extension}");}`;
|
||||
const split = fontDef.split(";base64,", 2);
|
||||
fontDef = `${split[0]};charset=utf-8;base64,${split[1]}`;*/
|
||||
}
|
||||
return { fontDef, fontName, dataURL };
|
||||
};
|
||||
@@ -374,7 +367,7 @@ export async function getPNG (
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
new Notice("Error exporting PNG - PNG too large, try a smaller resolution");
|
||||
new Notice(t("ERROR_PNG_TOO_LARGE"));
|
||||
errorlog({ where: "Utils.getPNG", error });
|
||||
return null;
|
||||
}
|
||||
@@ -428,14 +421,14 @@ export function addAppendUpdateCustomData (el: Mutable<ExcalidrawElement>, newDa
|
||||
|
||||
export function scaleLoadedImage (
|
||||
scene: any,
|
||||
files: any
|
||||
files: FileData[],
|
||||
): { dirty: boolean; scene: any } {
|
||||
let dirty = false;
|
||||
if (!files || !scene) {
|
||||
return { dirty, scene };
|
||||
}
|
||||
|
||||
for (const f of files.filter((f:any)=>{
|
||||
for (const img of files.filter((f:any)=>{
|
||||
if(!Boolean(EXCALIDRAW_PLUGIN)) return true; //this should never happen
|
||||
const ef = EXCALIDRAW_PLUGIN.filesMaster.get(f.id);
|
||||
if(!ef) return true; //mermaid SVG or equation
|
||||
@@ -443,34 +436,81 @@ export function scaleLoadedImage (
|
||||
if(!file || (file instanceof TFolder)) return false;
|
||||
return (file as TFile).extension==="md" || EXCALIDRAW_PLUGIN.isExcalidrawFile(file as TFile)
|
||||
})) {
|
||||
const [w_image, h_image] = [f.size.width, f.size.height];
|
||||
const imageAspectRatio = f.size.width / f.size.height;
|
||||
const [imgWidth, imgHeight] = [img.size.width, img.size.height];
|
||||
const imgAspectRatio = imgWidth / imgHeight;
|
||||
|
||||
scene.elements
|
||||
.filter((e: any) => e.type === "image" && e.fileId === f.id)
|
||||
.filter((e: any) => e.type === "image" && e.fileId === img.id)
|
||||
.forEach((el: any) => {
|
||||
const [w_old, h_old] = [el.width, el.height];
|
||||
if(el.customData?.isAnchored && f.shouldScale || !el.customData?.isAnchored && !f.shouldScale) {
|
||||
addAppendUpdateCustomData(el, f.shouldScale ? {isAnchored: false} : {isAnchored: true});
|
||||
const [elWidth, elHeight] = [el.width, el.height];
|
||||
const maintainArea = img.shouldScale; //true if image should maintain its area, false if image should display at 100% its size
|
||||
const elCrop: ImageCrop = el.crop;
|
||||
const isCropped = Boolean(elCrop);
|
||||
|
||||
|
||||
if(el.customData?.isAnchored && img.shouldScale || !el.customData?.isAnchored && !img.shouldScale) {
|
||||
//customData.isAnchored is used by the Excalidraw component to disable resizing of anchored images
|
||||
//customData.isAnchored has no direct role in the calculation in the scaleLoadedImage function
|
||||
addAppendUpdateCustomData(el, img.shouldScale ? {isAnchored: false} : {isAnchored: true});
|
||||
dirty = true;
|
||||
}
|
||||
if(f.shouldScale) {
|
||||
const elementAspectRatio = w_old / h_old;
|
||||
if (imageAspectRatio !== elementAspectRatio) {
|
||||
|
||||
if(isCropped) {
|
||||
if(elCrop.naturalWidth !== imgWidth || elCrop.naturalHeight !== imgHeight) {
|
||||
dirty = true;
|
||||
const h_new = Math.sqrt((w_old * h_old * h_image) / w_image);
|
||||
const w_new = Math.sqrt((w_old * h_old * w_image) / h_image);
|
||||
el.height = h_new;
|
||||
el.width = w_new;
|
||||
el.y += (h_old - h_new) / 2;
|
||||
el.x += (w_old - w_new) / 2;
|
||||
//the current crop area may be maintained, need to calculate the new crop.x, crop.y offsets
|
||||
el.crop.y += (imgHeight - elCrop.naturalHeight)/2;
|
||||
if(imgWidth < elCrop.width) {
|
||||
const scaleX = el.width / elCrop.width;
|
||||
el.crop.x = 0;
|
||||
el.crop.width = imgWidth;
|
||||
el.width = imgWidth * scaleX;
|
||||
} else {
|
||||
const ratioX = elCrop.x / (elCrop.naturalWidth - elCrop.x - elCrop.width);
|
||||
const gapX = imgWidth - elCrop.width;
|
||||
el.crop.x = ratioX * gapX / (1 + ratioX);
|
||||
if(el.crop.x + elCrop.width > imgWidth) {
|
||||
el.crop.x = (imgWidth - elCrop.width) / 2;
|
||||
}
|
||||
}
|
||||
if(imgHeight < elCrop.height) {
|
||||
const scaleY = el.height / elCrop.height;
|
||||
el.crop.y = 0;
|
||||
el.crop.height = imgHeight;
|
||||
el.height = imgHeight * scaleY;
|
||||
} else {
|
||||
const ratioY = elCrop.y / (elCrop.naturalHeight - elCrop.y - elCrop.height);
|
||||
const gapY = imgHeight - elCrop.height;
|
||||
el.crop.y = ratioY * gapY / (1 + ratioY);
|
||||
if(el.crop.y + elCrop.height > imgHeight) {
|
||||
el.crop.y = (imgHeight - elCrop.height)/2;
|
||||
}
|
||||
}
|
||||
el.crop.naturalWidth = imgWidth;
|
||||
el.crop.naturalHeight = imgHeight;
|
||||
const noCrop = el.crop.width === imgWidth && el.crop.height === imgHeight;
|
||||
if(noCrop) {
|
||||
el.crop = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if(w_old !== w_image || h_old !== h_image) {
|
||||
} else if(maintainArea) {
|
||||
const elAspectRatio = elWidth / elHeight;
|
||||
if (imgAspectRatio !== elAspectRatio) {
|
||||
dirty = true;
|
||||
el.height = h_image;
|
||||
el.width = w_image;
|
||||
el.y += (h_old - h_image) / 2;
|
||||
el.x += (w_old - w_image) / 2;
|
||||
const elNewHeight = Math.sqrt((elWidth * elHeight * imgHeight) / imgWidth);
|
||||
const elNewWidth = Math.sqrt((elWidth * elHeight * imgWidth) / imgHeight);
|
||||
el.height = elNewHeight;
|
||||
el.width = elNewWidth;
|
||||
el.y += (elHeight - elNewHeight) / 2;
|
||||
el.x += (elWidth - elNewWidth) / 2;
|
||||
}
|
||||
} else { //100% size
|
||||
if(elWidth !== imgWidth || elHeight !== imgHeight) {
|
||||
dirty = true;
|
||||
el.height = imgHeight;
|
||||
el.width = imgWidth;
|
||||
el.y += (elHeight - imgHeight) / 2;
|
||||
el.x += (elWidth - imgWidth) / 2;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -725,6 +765,8 @@ export function getPNGScale (plugin: ExcalidrawPlugin, file: TFile): number {
|
||||
};
|
||||
|
||||
export function isVersionNewerThanOther (version: string, otherVersion: string): boolean {
|
||||
if(!version || !otherVersion) return true;
|
||||
|
||||
const v = version.match(/(\d*)\.(\d*)\.(\d*)/);
|
||||
const o = otherVersion.match(/(\d*)\.(\d*)\.(\d*)/);
|
||||
|
||||
|
||||
@@ -346,7 +346,7 @@ label.color-input-container > input {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.excalidraw-settings input:not([type="color"]) {
|
||||
.excalidraw-settings input[type="text"] {
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user