mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1769a65a82 | ||
|
|
941eb56769 | ||
|
|
a317613ef4 | ||
|
|
173571846f | ||
|
|
ab1078d393 | ||
|
|
681321a595 | ||
|
|
fcd50d4bc2 | ||
|
|
7e214e5aaa | ||
|
|
de39053857 | ||
|
|
f543e3218e | ||
|
|
e668aea214 | ||
|
|
d810daa735 | ||
|
|
b94b3118eb | ||
|
|
06cbd0c92d | ||
|
|
96ebbbf11d | ||
|
|
fc1467b05b | ||
|
|
53c27f2a59 | ||
|
|
db80f5c715 | ||
|
|
405c98ca50 | ||
|
|
4892aed9e6 | ||
|
|
1b04b94db2 | ||
|
|
4a430f5fe7 | ||
|
|
325bfd825f | ||
|
|
471553f913 | ||
|
|
a43d0689d8 | ||
|
|
afacb8a94b | ||
|
|
afe5300e00 | ||
|
|
ef85d0e323 | ||
|
|
d3aeedb9f6 | ||
|
|
7718f69269 | ||
|
|
af50b0c5c6 | ||
|
|
b576faad82 | ||
|
|
4583e603e9 | ||
|
|
7ee316a605 | ||
|
|
7dca225691 | ||
|
|
8a27d0240a | ||
|
|
939bd6fd91 | ||
|
|
dfbd385de7 | ||
|
|
0d791070dd |
@@ -243,11 +243,11 @@ Drag the desired file from the Obsidian file explorer and hold down <kbd>SHIFT</
|
||||
- In plugin settings, you can add a custom fourth font. For more details, see this [video](https://youtu.be/eKFmrSQhFA4)
|
||||
- The plugin includes OCR support using Taskbone OCR. For more details, see this [video](https://youtu.be/7gu4ETx7zro)
|
||||
- You can convert SVG files into Excalidraw drawings (with some limitation). For more details, see this [video](https://youtu.be/vlC1-iBvIfo)
|
||||
- You can define custom freedraw pens. See documentation [here].(https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Alternative%20Pens.md), [video](https://youtu.be/uZz5MgzWXiM)
|
||||
- You can define custom pens and higlighters and pin them to the sidebar. For more details, see this [video](https://youtu.be/OjNhjaH2KjI). Using ExcalidrawAutomate, you can add support for [auto-toggling](<ea-scripts/Auto Draw for Pen.md>) pen & support for [hardware eraser buttons](<ea-scripts/Hardware Eraser Support.md>).
|
||||
|
||||
### Script Engine
|
||||
|
||||
- Since 1.5.0, you can easily execute ExcalidrawAutomate macros and assign command palette shortcuts to them, using the ScriptEngine. You will find an intro video and a growing library of ready to install scripts [here](https://github.com/zsviczian/obsidian-excalidraw-plugin/tree/master/ea-scripts).
|
||||
- Since 1.5.0, you can easily execute ExcalidrawAutomate macros and assign command palette shortcuts to them, using the ScriptEngine. You will find an intro video and a growing library of ready to install scripts [here](ea-scripts/README.md).
|
||||
- You can organize scripts into groups on the Obsidian Tools Panel in Excalidraw by moving scripts and accompanying SVG icon files to folders. See the demo [video](https://youtu.be/wTtaXmRJ7wg?t=16).
|
||||
|
||||
### Other
|
||||
|
||||
4
docs/API/ExcalidrawAutomate.d.ts
vendored
4
docs/API/ExcalidrawAutomate.d.ts
vendored
@@ -6,7 +6,7 @@ import * as obsidian_module from "obsidian";
|
||||
import ExcalidrawView, { ExportSettings } from "src/ExcalidrawView";
|
||||
import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI, Point } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { EmbeddedFilesLoader } from "src/EmbeddedFileLoader";
|
||||
import { ConnectionPoint, DeviceType } from "src/types";
|
||||
import { ConnectionPoint, DeviceType } from "src/types/types";
|
||||
import { ColorMaster } from "colormaster";
|
||||
import { TInput } from "colormaster/types";
|
||||
import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard";
|
||||
@@ -123,7 +123,7 @@ export declare class ExcalidrawAutomate {
|
||||
* @param val //1: Virgil, 2:Helvetica, 3:Cascadia
|
||||
* @returns
|
||||
*/
|
||||
setFontFamily(val: number): "Virgil, Segoe UI Emoji" | "Helvetica, Segoe UI Emoji" | "Cascadia, Segoe UI Emoji" | "LocalFont";
|
||||
setFontFamily(val: number): "Virgil, Segoe UI Emoji" | "Helvetica, Segoe UI Emoji" | "Cascadia, Segoe UI Emoji" | "Local Font";
|
||||
/**
|
||||
* @param val //0:"light", 1:"dark"
|
||||
* @returns
|
||||
|
||||
@@ -427,7 +427,7 @@ const run = async (text) => {
|
||||
|
||||
const requestObject = isImageEditRequest
|
||||
? {
|
||||
...imageDataURL ? {image: imageDataURL} : {},
|
||||
...imageDataURL ? {image: {url: imageDataURL}} : {},
|
||||
...(text && text.trim() !== "") ? {text} : {},
|
||||
imageGenerationProperties: {
|
||||
size: imageSize,
|
||||
@@ -437,7 +437,7 @@ const run = async (text) => {
|
||||
},
|
||||
}
|
||||
: {
|
||||
...imageDataURL ? {image: imageDataURL} : {},
|
||||
...imageDataURL ? {image: {url: imageDataURL}} : {},
|
||||
...(text && text.trim() !== "") ? {text} : {},
|
||||
systemPrompt: systemPrompt.prompt,
|
||||
instruction: outputType.instruction,
|
||||
|
||||
@@ -33,8 +33,8 @@ const run = () => {
|
||||
((typeof config.fillStyle === "undefined") || (el.fillStyle === config.fillStyle)) &&
|
||||
((typeof config.fontFamily === "undefined") || (el.fontFamily === config.fontFamily)) &&
|
||||
((typeof config.fontSize === "undefined") || (el.fontSize === config.fontSize)) &&
|
||||
((typeof config.height === "undefined") || Math.abs(el.height - config.height) < 0.01) &&
|
||||
((typeof config.width === "undefined") || Math.abs(el.width - config.width) < 0.01) &&
|
||||
((typeof config.height === "undefined") || Math.abs(el.height - config.height) < 0.01) &&
|
||||
((typeof config.width === "undefined") || Math.abs(el.width - config.width) < 0.01) &&
|
||||
((typeof config.opacity === "undefined") || (el.opacity === config.opacity)) &&
|
||||
((typeof config.roughness === "undefined") || (el.roughness === config.roughness)) &&
|
||||
((typeof config.roundness === "undefined") || (el.roundness === config.roundness)) &&
|
||||
@@ -56,7 +56,7 @@ const run = () => {
|
||||
const showInstructions = () => {
|
||||
const instructionsModal = new ea.obsidian.Modal(app);
|
||||
instructionsModal.onOpen = () => {
|
||||
instructionsModal.contentEl.createEl("h2", {text: "Instructions"});
|
||||
instructionsModal.contentEl.createEl("h2", {text: "Instructions"});
|
||||
instructionsModal.contentEl.createEl("p", {text: "Step 1: Choose the attributes that you want the selected elements to match."});
|
||||
instructionsModal.contentEl.createEl("p", {text: "Step 2: Select an action:"});
|
||||
instructionsModal.contentEl.createEl("ul", {}, el => {
|
||||
@@ -71,14 +71,14 @@ const showInstructions = () => {
|
||||
const selectAttributesToCopy = () => {
|
||||
const configModal = new ea.obsidian.Modal(app);
|
||||
configModal.onOpen = () => {
|
||||
config = {};
|
||||
config = {};
|
||||
configModal.contentEl.createEl("h1", {text: "Select Similar Elements"});
|
||||
new ea.obsidian.Setting(configModal.contentEl)
|
||||
.setDesc("Choose the attributes you want the selected elements to match, then select an action.")
|
||||
.addButton(button => button
|
||||
.setButtonText("Instructions")
|
||||
.onClick(showInstructions)
|
||||
);
|
||||
new ea.obsidian.Setting(configModal.contentEl)
|
||||
.setDesc("Choose the attributes you want the selected elements to match, then select an action.")
|
||||
.addButton(button => button
|
||||
.setButtonText("Instructions")
|
||||
.onClick(showInstructions)
|
||||
);
|
||||
|
||||
|
||||
// Add Toggles for the rest of the attributes
|
||||
@@ -103,7 +103,7 @@ const selectAttributesToCopy = () => {
|
||||
|
||||
attributes.forEach(attr => {
|
||||
const attrValue = elements[0][attr.key];
|
||||
if(attrValue || (attr.key === "startArrowhead" && elements[0].type === "arrow") || (attr.key === "endArrowhead" && elements[0].type === "arrow")) {
|
||||
if((typeof attrValue !== "undefined" && attrValue !== null) || (attr.key === "startArrowhead" && elements[0].type === "arrow") || (attr.key === "endArrowhead" && elements[0].type === "arrow")) {
|
||||
let description = '';
|
||||
|
||||
switch(attr.key) {
|
||||
@@ -144,8 +144,6 @@ const selectAttributesToCopy = () => {
|
||||
description = `${attrValue}`;
|
||||
break;
|
||||
default:
|
||||
console.log(attr.key);
|
||||
console.log(attrValue);
|
||||
description = `${attrValue.charAt(0).toUpperCase() + attrValue.slice(1)}`;
|
||||
break;
|
||||
}
|
||||
@@ -192,7 +190,9 @@ const selectAttributesToCopy = () => {
|
||||
|
||||
|
||||
configModal.onClose = () => {
|
||||
setTimeout(()=>delete configModal);
|
||||
setTimeout(()=>{
|
||||
delete configModal
|
||||
});
|
||||
}
|
||||
|
||||
configModal.open();
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -20,7 +20,7 @@ If you want to modify scripts, I recommend moving them to the `Excalidraw Automa
|
||||
I would love to include your contribution in the script library. If you have a script of your own that you would like to share with the community, please open a [PR](https://github.com/zsviczian/obsidian-excalidraw-plugin/pulls) on GitHub. Be sure to include the following in your pull request
|
||||
- The [script file](https://github.com/zsviczian/obsidian-excalidraw-plugin/tree/master/ea-scripts) with a self explanetory name. The name of the file will be the name of the script in the Command Palette.
|
||||
- An [image](https://github.com/zsviczian/obsidian-excalidraw-plugin/tree/master/images) explaining the scripts purpose. Remember a picture speaks thousand words!
|
||||
- An update to this file [ea-scripts/index.md](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/index.md)
|
||||
- An update to this file [ea-scripts/index-new.md](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/index-new.md)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "2.1.8.2-beta-1",
|
||||
"version": "2.2.10-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.2.4",
|
||||
"version": "2.2.11",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-excalidraw-plugin",
|
||||
"version": "2.0.14",
|
||||
"version": "2.2.5",
|
||||
"description": "This is an Obsidian.md plugin that lets you view and edit Excalidraw drawings",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
@@ -19,7 +19,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@zsviczian/excalidraw": "0.17.1-obsidian-26",
|
||||
"@zsviczian/excalidraw": "0.17.1-obsidian-34",
|
||||
"chroma-js": "^2.4.2",
|
||||
"clsx": "^2.0.0",
|
||||
"colormaster": "^1.2.1",
|
||||
@@ -32,7 +32,9 @@
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"roughjs": "^4.5.2",
|
||||
"js-yaml": "^4.1.0"
|
||||
"js-yaml": "^4.1.0",
|
||||
"opentype.js": "^1.3.4",
|
||||
"woff2sfnt-sfnt2woff": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dotenv": "^16.4.5",
|
||||
@@ -52,6 +54,7 @@
|
||||
"@types/react": "^18.2.45",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/opentype.js": "^1.3.8",
|
||||
"@zerollup/ts-transform-paths": "^1.7.18",
|
||||
"cross-env": "^7.0.3",
|
||||
"cssnano": "^6.0.2",
|
||||
|
||||
@@ -54,11 +54,11 @@ if (!isLib) console.log(manifest.version);
|
||||
const packageString = isLib
|
||||
? ""
|
||||
: ';' + lzstring_pkg +
|
||||
'\nconst EXCALIDRAW_PACKAGES = "' + LZString.compressToBase64(react_pkg + reactdom_pkg + excalidraw_pkg) + '";\n' +
|
||||
'const {react, reactDOM, excalidrawLib} = window.eval.call(window, `(function() {' +
|
||||
'${LZString.decompressFromBase64(EXCALIDRAW_PACKAGES)};' +
|
||||
'\nlet EXCALIDRAW_PACKAGES = LZString.decompressFromBase64("' + LZString.compressToBase64(react_pkg + reactdom_pkg + excalidraw_pkg) + '");\n' +
|
||||
'let {react, reactDOM, excalidrawLib} = window.eval.call(window, `(function() {' +
|
||||
'${EXCALIDRAW_PACKAGES};' +
|
||||
'return {react: React, reactDOM: ReactDOM, excalidrawLib: ExcalidrawLib};})();`);\n' +
|
||||
'const PLUGIN_VERSION="' + manifest.version + '";';
|
||||
'let PLUGIN_VERSION="' + manifest.version + '";';
|
||||
|
||||
const BASE_CONFIG = {
|
||||
input: 'src/main.ts',
|
||||
@@ -107,7 +107,7 @@ const BUILD_CONFIG = {
|
||||
babel({
|
||||
presets: [['@babel/preset-env', {
|
||||
targets: {
|
||||
esmodules: true,
|
||||
ios: "15", // ios Compatibility //esmodules: true,
|
||||
},
|
||||
}]],
|
||||
exclude: "node_modules/**",
|
||||
|
||||
@@ -12,6 +12,10 @@ export class EditorHandler {
|
||||
|
||||
constructor(private plugin: ExcalidrawPlugin) {}
|
||||
|
||||
destroy(): void {
|
||||
this.plugin = null;
|
||||
}
|
||||
|
||||
setup(): void {
|
||||
this.plugin.registerEditorExtension(this.activeEditorExtensions);
|
||||
this.updateCMExtensionState(EDITOR_FADEOUT, this.plugin.settings.fadeOutExcalidrawMarkup);
|
||||
|
||||
@@ -4,11 +4,6 @@
|
||||
import { ExcalidrawElement, FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { BinaryFileData, DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { App, MarkdownRenderer, Notice, TFile } from "obsidian";
|
||||
import {
|
||||
ASSISTANT_FONT,
|
||||
CASCADIA_FONT,
|
||||
VIRGIL_FONT,
|
||||
} from "./constants/constFonts";
|
||||
import {
|
||||
DEFAULT_MD_EMBED_CSS,
|
||||
fileid,
|
||||
@@ -16,6 +11,7 @@ import {
|
||||
nanoid,
|
||||
THEME_FILTER,
|
||||
FRONTMATTER_KEYS,
|
||||
getFontDefinition,
|
||||
} from "./constants/constants";
|
||||
import { createSVG } from "./ExcalidrawAutomate";
|
||||
import { ExcalidrawData, getTransclusion } from "./ExcalidrawData";
|
||||
@@ -38,12 +34,13 @@ import {
|
||||
LinkParts,
|
||||
svgToBase64,
|
||||
isMaskFile,
|
||||
getEmbeddedFilenameParts,
|
||||
} from "./utils/Utils";
|
||||
import { ValueOf } from "./types";
|
||||
import { ValueOf } from "./types/types";
|
||||
import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
|
||||
import { mermaidToExcalidraw } from "src/constants/constants";
|
||||
import { ImageKey, imageCache } from "./utils/ImageCache";
|
||||
import { PreviewImageType } from "./utils/UtilTypes";
|
||||
import { FILENAMEPARTS, PreviewImageType } from "./utils/UtilTypes";
|
||||
|
||||
//An ugly workaround for the following situation.
|
||||
//File A is a markdown file that has an embedded Excalidraw file B
|
||||
@@ -144,8 +141,6 @@ const replaceSVGColors = (svg: SVGSVGElement | string, colorMap: ColorMap | null
|
||||
return svg;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class EmbeddedFile {
|
||||
public file: TFile = null;
|
||||
public isSVGwithBitmap: boolean = false;
|
||||
@@ -156,6 +151,7 @@ export class EmbeddedFile {
|
||||
public mimeType: MimeType = "application/octet-stream";
|
||||
public size: Size = { height: 0, width: 0 };
|
||||
public linkParts: LinkParts;
|
||||
public filenameparts: FILENAMEPARTS
|
||||
private hostPath: string;
|
||||
public attemptCounter: number = 0;
|
||||
public isHyperLink: boolean = false;
|
||||
@@ -203,7 +199,7 @@ export class EmbeddedFile {
|
||||
if (!this.linkParts.height) {
|
||||
this.linkParts.height = this.plugin.settings.mdSVGmaxHeight;
|
||||
}
|
||||
this.file = app.metadataCache.getFirstLinkpathDest(
|
||||
this.file = this.plugin.app.metadataCache.getFirstLinkpathDest(
|
||||
this.linkParts.path,
|
||||
hostPath,
|
||||
);
|
||||
@@ -214,6 +210,9 @@ export class EmbeddedFile {
|
||||
5000,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.filenameparts = getEmbeddedFilenameParts(imgPath);
|
||||
this.filenameparts.filepath = this.file.path;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,26 +358,32 @@ export class EmbeddedFilesLoader {
|
||||
: false,
|
||||
withTheme: !!forceTheme,
|
||||
isMask,
|
||||
skipInliningFonts: false,
|
||||
};
|
||||
|
||||
const hasColorMap = Boolean(inFile instanceof EmbeddedFile ? inFile.colorMap : null);
|
||||
const shouldUseCache = !hasColorMap && this.plugin.settings.allowImageCacheInScene && file && imageCache.isReady();
|
||||
const hasFilenameParts = Boolean((inFile instanceof EmbeddedFile) && inFile.filenameparts);
|
||||
const filenameParts = hasFilenameParts ? (inFile as EmbeddedFile).filenameparts : null;
|
||||
const cacheKey:ImageKey = {
|
||||
filepath: file.path,
|
||||
blockref: null,
|
||||
sectionref: null,
|
||||
...hasFilenameParts? filenameParts : {
|
||||
filepath: file.path,
|
||||
hasBlockref: false,
|
||||
hasGroupref: false,
|
||||
hasTaskbone: false,
|
||||
hasArearef: false,
|
||||
hasFrameref: false,
|
||||
hasClippedFrameref: false,
|
||||
hasSectionref: false,
|
||||
blockref: null,
|
||||
sectionref: null,
|
||||
linkpartReference: null,
|
||||
linkpartAlias: null,
|
||||
},
|
||||
isDark,
|
||||
previewImageType: PreviewImageType.SVG,
|
||||
scale: 1,
|
||||
isTransparent: !exportSettings.withBackground,
|
||||
hasBlockref: false,
|
||||
hasGroupref: false,
|
||||
hasTaskbone: false,
|
||||
hasArearef: false,
|
||||
hasFrameref: false,
|
||||
hasSectionref: false,
|
||||
linkpartReference: null,
|
||||
linkpartAlias: null,
|
||||
}
|
||||
|
||||
const maybeSVG = shouldUseCache
|
||||
@@ -389,9 +394,17 @@ export class EmbeddedFilesLoader {
|
||||
? maybeSVG
|
||||
: replaceSVGColors(
|
||||
await createSVG(
|
||||
file?.path,
|
||||
true,
|
||||
exportSettings,
|
||||
hasFilenameParts
|
||||
? (filenameParts.hasGroupref || filenameParts.hasBlockref ||
|
||||
filenameParts.hasSectionref || filenameParts.hasFrameref ||
|
||||
filenameParts.hasClippedFrameref
|
||||
? filenameParts.filepath + filenameParts.linkpartReference
|
||||
: file.path)
|
||||
: file?.path,
|
||||
false, //false
|
||||
hasFilenameParts && filenameParts.hasClippedFrameref
|
||||
? {...exportSettings, frameRendering: {enabled: true, name: false, outline: false, clip: true}}
|
||||
: exportSettings,
|
||||
this,
|
||||
forceTheme,
|
||||
null,
|
||||
@@ -424,8 +437,19 @@ export class EmbeddedFilesLoader {
|
||||
hasSVGwithBitmap = true;
|
||||
}
|
||||
if(shouldUseCache && !Boolean(maybeSVG)) {
|
||||
//cache SVG should have the width and height parameters and not the embedded font
|
||||
//see svgWithFont below
|
||||
imageCache.addImageToCache(cacheKey,"", svg);
|
||||
}
|
||||
|
||||
if(!svg.hasAttribute("width") && svg.hasAttribute("viewBox")){
|
||||
//2024.06.09
|
||||
//this addresses backward compatibility issues where the cache does not have the width and height attributes
|
||||
//this should be removed in the future
|
||||
const vb = svg.getAttr("viewBox").split(" ");
|
||||
Boolean(vb[2]) && svg.setAttribute("width", vb[2]);
|
||||
Boolean(vb[3]) && svg.setAttribute("height", vb[3]);
|
||||
}
|
||||
const dURL = svgToBase64(svg.outerHTML) as DataURL;
|
||||
return {dataURL: dURL as DataURL, hasSVGwithBitmap};
|
||||
};
|
||||
@@ -544,7 +568,8 @@ export class EmbeddedFilesLoader {
|
||||
return {
|
||||
mimeType,
|
||||
fileId: await generateIdFromFile(
|
||||
isHyperLink || isPDF ? (new TextEncoder()).encode(dataURL as string) : ab
|
||||
isHyperLink || isPDF ? (new TextEncoder()).encode(dataURL as string) : ab,
|
||||
inFile instanceof EmbeddedFile ? inFile.filenameparts?.linkpartReference : undefined
|
||||
),
|
||||
dataURL,
|
||||
created: isHyperLink || isLocalLink ? 0 : file.stat.mtime,
|
||||
@@ -787,13 +812,29 @@ export class EmbeddedFilesLoader {
|
||||
}
|
||||
switch (fontName) {
|
||||
case "Virgil":
|
||||
fontDef = VIRGIL_FONT;
|
||||
fontDef = await getFontDefinition(1);
|
||||
break;
|
||||
case "Cascadia":
|
||||
fontDef = CASCADIA_FONT;
|
||||
fontDef = await getFontDefinition(3);
|
||||
break;
|
||||
case "Assistant":
|
||||
fontDef = ASSISTANT_FONT;
|
||||
case "Assistant":
|
||||
case "Helvetica":
|
||||
fontDef = await getFontDefinition(2);
|
||||
break;
|
||||
case "Excalifont":
|
||||
fontDef = await getFontDefinition(5);
|
||||
break;
|
||||
case "Nunito":
|
||||
fontDef = await getFontDefinition(6);
|
||||
break;
|
||||
case "Lilita One":
|
||||
fontDef = await getFontDefinition(7);
|
||||
break;
|
||||
case "Comic Shanns":
|
||||
fontDef = await getFontDefinition(8);
|
||||
break;
|
||||
case "Liberation Sans":
|
||||
fontDef = await getFontDefinition(9);
|
||||
break;
|
||||
case "":
|
||||
fontDef = "";
|
||||
@@ -993,7 +1034,7 @@ const getSVGData = async (app: App, file: TFile, colorMap: ColorMap | null): Pro
|
||||
return svgToBase64(svg) as DataURL;
|
||||
};
|
||||
|
||||
export const generateIdFromFile = async (file: ArrayBuffer): Promise<FileId> => {
|
||||
/*export const generateIdFromFile = async (file: ArrayBuffer): Promise<FileId> => {
|
||||
let id: FileId;
|
||||
try {
|
||||
const hashBuffer = await window.crypto.subtle.digest("SHA-1", file);
|
||||
@@ -1008,4 +1049,37 @@ export const generateIdFromFile = async (file: ArrayBuffer): Promise<FileId> =>
|
||||
id = fileid() as FileId;
|
||||
}
|
||||
return id;
|
||||
};*/
|
||||
|
||||
export const generateIdFromFile = async (file: ArrayBuffer, key?: string): Promise<FileId> => {
|
||||
let id: FileId;
|
||||
try {
|
||||
// Convert the file ArrayBuffer to a Uint8Array
|
||||
const fileArray = new Uint8Array(file);
|
||||
|
||||
// If a key is provided, concatenate it to the file data
|
||||
let dataToHash: Uint8Array;
|
||||
if (key) {
|
||||
const encoder = new TextEncoder();
|
||||
const keyArray = encoder.encode(key);
|
||||
dataToHash = new Uint8Array(fileArray.length + keyArray.length);
|
||||
dataToHash.set(fileArray);
|
||||
dataToHash.set(keyArray, fileArray.length);
|
||||
} else {
|
||||
dataToHash = fileArray;
|
||||
}
|
||||
|
||||
// Hash the combined data (file and key, if provided)
|
||||
const hashBuffer = await window.crypto.subtle.digest("SHA-1", dataToHash);
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
GITHUB_RELEASES,
|
||||
determineFocusDistance,
|
||||
getCommonBoundingBox,
|
||||
getDefaultLineHeight,
|
||||
getLineHeight,
|
||||
getMaximumGroups,
|
||||
intersectElementWithLine,
|
||||
measureText,
|
||||
@@ -37,11 +37,11 @@ import {
|
||||
THEME_FILTER,
|
||||
mermaidToExcalidraw,
|
||||
refreshTextDimensions,
|
||||
getFontFamilyString,
|
||||
} from "src/constants/constants";
|
||||
import { blobToBase64, checkAndCreateFolder, getDrawingFilename, getExcalidrawEmbeddedFilesFiletree, getListOfTemplateFiles, getNewUniqueFilepath, hasExcalidrawEmbeddedImagesTreeChanged, } from "src/utils/FileUtils";
|
||||
import {
|
||||
//debug,
|
||||
embedFontsInSVG,
|
||||
errorlog,
|
||||
getEmbeddedFilenameParts,
|
||||
getImageSize,
|
||||
@@ -61,7 +61,7 @@ import { tex2dataURL } from "src/LaTeX";
|
||||
import { GenericInputPrompt, NewFileActions } from "src/dialogs/Prompt";
|
||||
import { t } from "src/lang/helpers";
|
||||
import { ScriptEngine } from "src/Scripts";
|
||||
import { ConnectionPoint, DeviceType } from "src/types";
|
||||
import { ConnectionPoint, DeviceType } from "src/types/types";
|
||||
import CM, { ColorMaster, extendPlugins } from "colormaster";
|
||||
import HarmonyPlugin from "colormaster/plugins/harmony";
|
||||
import MixPlugin from "colormaster/plugins/mix"
|
||||
@@ -92,7 +92,7 @@ import {
|
||||
import { EXCALIDRAW_AUTOMATE_INFO, EXCALIDRAW_SCRIPTENGINE_INFO } from "./dialogs/SuggesterInfo";
|
||||
import { addBackOfTheNoteCard, getFrameBasedOnFrameNameOrId } from "./utils/ExcalidrawViewUtils";
|
||||
import { log } from "./utils/DebugHelper";
|
||||
import { auto } from "@popperjs/core";
|
||||
import { ExcalidrawLib } from "./ExcalidrawLib";
|
||||
|
||||
extendPlugins([
|
||||
HarmonyPlugin,
|
||||
@@ -112,6 +112,7 @@ extendPlugins([
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
declare var LZString: any;
|
||||
declare const excalidrawLib: typeof ExcalidrawLib;
|
||||
|
||||
const GAP = 4;
|
||||
|
||||
@@ -387,7 +388,7 @@ export class ExcalidrawAutomate {
|
||||
opacity: number;
|
||||
strokeSharpness?: StrokeRoundness; //defaults to undefined, use strokeRoundess and roundess instead. Only kept for legacy script compatibility type StrokeRoundness = "round" | "sharp"
|
||||
roundness: null | { type: RoundnessType; value?: number };
|
||||
fontFamily: number; //1: Virgil, 2:Helvetica, 3:Cascadia, 4:LocalFont
|
||||
fontFamily: number; //1: Virgil, 2:Helvetica, 3:Cascadia, 4:Local Font
|
||||
fontSize: number;
|
||||
textAlign: string; //"left"|"right"|"center"
|
||||
verticalAlign: string; //"top"|"bottom"|"middle" :for future use, has no effect currently
|
||||
@@ -1188,14 +1189,14 @@ export class ExcalidrawAutomate {
|
||||
if (element.type !== "text") {
|
||||
return;
|
||||
}
|
||||
const { w, h, baseline } = _measureText(
|
||||
const { w, h } = _measureText(
|
||||
element.text,
|
||||
element.fontSize,
|
||||
element.fontFamily,
|
||||
getDefaultLineHeight(element.fontFamily)
|
||||
getLineHeight(element.fontFamily)
|
||||
);
|
||||
// @ts-ignore
|
||||
element.width = w; element.height = h; element.baseline = baseline;
|
||||
element.width = w;
|
||||
element.height = h;
|
||||
}
|
||||
|
||||
|
||||
@@ -1233,11 +1234,11 @@ export class ExcalidrawAutomate {
|
||||
: (formatting?.autoResize ?? true)
|
||||
text = (formatting?.wrapAt && autoresize) ? this.wrapText(text, formatting.wrapAt) : text;
|
||||
|
||||
const { w, h, baseline } = _measureText(
|
||||
const { w, h } = _measureText(
|
||||
text,
|
||||
this.style.fontSize,
|
||||
this.style.fontFamily,
|
||||
getDefaultLineHeight(this.style.fontFamily)
|
||||
getLineHeight(this.style.fontFamily)
|
||||
);
|
||||
const width = formatting?.width ? formatting.width : w;
|
||||
const height = formatting?.height ? formatting.height : h;
|
||||
@@ -1295,7 +1296,7 @@ export class ExcalidrawAutomate {
|
||||
containerId: isContainerBound ? boxId : null,
|
||||
originalText: isContainerBound ? originalText : text,
|
||||
rawText: isContainerBound ? originalText : text,
|
||||
lineHeight: getDefaultLineHeight(this.style.fontFamily),
|
||||
lineHeight: getLineHeight(this.style.fontFamily),
|
||||
autoResize: formatting?.box ? true : (formatting?.autoResize ?? true),
|
||||
};
|
||||
if (boxId && formatting?.box === "blob") {
|
||||
@@ -2417,13 +2418,19 @@ export class ExcalidrawAutomate {
|
||||
|
||||
/**
|
||||
* Gets all the elements from elements[] that are contained in the frame.
|
||||
* @param element
|
||||
* @param elements - typically all the non-deleted elements in the scene
|
||||
* @param frameElement - the frame element for which to get the elements
|
||||
* @param elements - typically all the non-deleted elements in the scene
|
||||
* @param shouldIncludeFrame - if true, the frame element will be included in the returned array
|
||||
* this is useful when generating an image in which you want the frame to be clipped
|
||||
* @returns
|
||||
*/
|
||||
getElementsInFrame(frameElement: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[] {
|
||||
getElementsInFrame(
|
||||
frameElement: ExcalidrawElement,
|
||||
elements: ExcalidrawElement[],
|
||||
shouldIncludeFrame: boolean = false,
|
||||
): ExcalidrawElement[] {
|
||||
if(!frameElement || !elements || frameElement.type !== "frame") return [];
|
||||
return elements.filter(el=>el.frameId === frameElement.id);
|
||||
return elements.filter(el=>(el.frameId === frameElement.id) || (shouldIncludeFrame && el.id === frameElement.id));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2489,7 +2496,7 @@ export class ExcalidrawAutomate {
|
||||
text,
|
||||
this.style.fontSize,
|
||||
this.style.fontFamily,
|
||||
getDefaultLineHeight(this.style.fontFamily),
|
||||
getLineHeight(this.style.fontFamily),
|
||||
);
|
||||
return { width: size.w ?? 0, height: size.h ?? 0 };
|
||||
};
|
||||
@@ -2702,6 +2709,20 @@ export class ExcalidrawAutomate {
|
||||
this.copyViewElementsToEAforEditing(res.content);
|
||||
return true;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.targetView = null;
|
||||
this.plugin = null;
|
||||
this.elementsDict = {};
|
||||
this.imagesDict = {};
|
||||
this.mostRecentMarkdownSVG = null;
|
||||
this.activeScript = null;
|
||||
//@ts-ignore
|
||||
this.style = {};
|
||||
//@ts-ignore
|
||||
this.canvas = {};
|
||||
this.colorPalette = {};
|
||||
}
|
||||
};
|
||||
|
||||
export async function initExcalidrawAutomate(
|
||||
@@ -2714,10 +2735,6 @@ export async function initExcalidrawAutomate(
|
||||
return ea;
|
||||
}
|
||||
|
||||
export function destroyExcalidrawAutomate() {
|
||||
delete window.ExcalidrawAutomate;
|
||||
}
|
||||
|
||||
function normalizeLinePoints(
|
||||
points: [x: number, y: number][],
|
||||
//box: { x: number; y: number; w: number; h: number },
|
||||
@@ -2741,22 +2758,15 @@ function getLineBox(points: [x: number, y: number][]) {
|
||||
}
|
||||
|
||||
function getFontFamily(id: number) {
|
||||
switch (id) {
|
||||
case 1:
|
||||
return "Virgil, Segoe UI Emoji";
|
||||
case 2:
|
||||
return "Helvetica, Segoe UI Emoji";
|
||||
case 3:
|
||||
return "Cascadia, Segoe UI Emoji";
|
||||
case 4:
|
||||
return "LocalFont";
|
||||
}
|
||||
getFontFamilyString({fontFamily:id})
|
||||
}
|
||||
|
||||
async function initFonts() {
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
await (document as any).fonts.load(`20px ${getFontFamily(i)}`);
|
||||
}
|
||||
export async function initFonts() {
|
||||
await excalidrawLib.registerFontsInCSS();
|
||||
/*const fonts = excalidrawLib.getFontFamilies();
|
||||
for(let i=0;i<fonts.length;i++) {
|
||||
await (document as any).fonts.load(`20px ${fonts[i]}`);
|
||||
};*/
|
||||
}
|
||||
|
||||
export function _measureText(
|
||||
@@ -2771,14 +2781,14 @@ export function _measureText(
|
||||
}
|
||||
if (!fontFamily) {
|
||||
fontFamily = 1;
|
||||
lineHeight = getDefaultLineHeight(fontFamily);
|
||||
lineHeight = getLineHeight(fontFamily);
|
||||
}
|
||||
const metrics = measureText(
|
||||
newText,
|
||||
`${fontSize.toString()}px ${getFontFamily(fontFamily)}` as any,
|
||||
lineHeight
|
||||
);
|
||||
return { w: metrics.width, h: metrics.height, baseline: metrics.baseline };
|
||||
return { w: metrics.width, h: metrics.height };
|
||||
}
|
||||
|
||||
async function getTemplate(
|
||||
@@ -2866,15 +2876,14 @@ async function getTemplate(
|
||||
groupElements = plugin.ea.getElementsInTheSameGroupWithElement(el[0],scene.elements)
|
||||
}
|
||||
}
|
||||
if(filenameParts.hasFrameref) {
|
||||
if(filenameParts.hasFrameref || filenameParts.hasClippedFrameref) {
|
||||
const el = getFrameBasedOnFrameNameOrId(filenameParts.blockref,scene.elements);
|
||||
|
||||
if(el) {
|
||||
groupElements = plugin.ea.getElementsInFrame(el,scene.elements)
|
||||
groupElements = plugin.ea.getElementsInFrame(el,scene.elements, filenameParts.hasClippedFrameref);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(filenameParts.hasTaskbone) {
|
||||
groupElements = groupElements.filter( el =>
|
||||
el.type==="freedraw" ||
|
||||
@@ -2883,6 +2892,7 @@ async function getTemplate(
|
||||
));
|
||||
}
|
||||
|
||||
excalidrawData.destroy();
|
||||
return {
|
||||
elements: convertMarkdownLinksToObsidianURLs
|
||||
? updateElementLinksToObsidianLinks({
|
||||
@@ -2951,6 +2961,7 @@ export async function createPNG(
|
||||
theme: forceTheme ?? template?.appState?.theme ?? canvasTheme,
|
||||
viewBackgroundColor:
|
||||
template?.appState?.viewBackgroundColor ?? canvasBackgroundColor,
|
||||
...template?.appState?.frameRendering ? {frameRendering: template.appState.frameRendering} : {},
|
||||
},
|
||||
files,
|
||||
},
|
||||
@@ -3030,6 +3041,9 @@ export async function createSVG(
|
||||
if (!loader) {
|
||||
loader = new EmbeddedFilesLoader(plugin);
|
||||
}
|
||||
if(typeof exportSettings.skipInliningFonts === "undefined") {
|
||||
exportSettings.skipInliningFonts = !embedFont;
|
||||
}
|
||||
const template = templatePath
|
||||
? await getTemplate(plugin, templatePath, true, loader, depth, convertMarkdownLinksToObsidianURLs)
|
||||
: null;
|
||||
@@ -3046,6 +3060,7 @@ export async function createSVG(
|
||||
const theme = forceTheme ?? template?.appState?.theme ?? canvasTheme;
|
||||
const withTheme = exportSettings?.withTheme ?? plugin.settings.exportWithTheme;
|
||||
|
||||
const filenameParts = getEmbeddedFilenameParts(templatePath);
|
||||
const svg = await getSVG(
|
||||
{
|
||||
//createAndOpenDrawing
|
||||
@@ -3057,6 +3072,7 @@ export async function createSVG(
|
||||
theme,
|
||||
viewBackgroundColor:
|
||||
template?.appState?.viewBackgroundColor ?? canvasBackgroundColor,
|
||||
...template?.appState?.frameRendering ? {frameRendering: template.appState.frameRendering} : {},
|
||||
},
|
||||
files,
|
||||
},
|
||||
@@ -3065,6 +3081,9 @@ export async function createSVG(
|
||||
exportSettings?.withBackground ?? plugin.settings.exportWithBackground,
|
||||
withTheme,
|
||||
isMask: exportSettings?.isMask ?? false,
|
||||
...filenameParts?.hasClippedFrameref
|
||||
? {frameRendering: {enabled: true, name: false, outline: false, clip: true}}
|
||||
: {},
|
||||
},
|
||||
padding,
|
||||
null,
|
||||
@@ -3072,9 +3091,8 @@ export async function createSVG(
|
||||
|
||||
if (withTheme && theme === "dark") addFilterToForeignObjects(svg);
|
||||
|
||||
const filenameParts = getEmbeddedFilenameParts(templatePath);
|
||||
if(
|
||||
!(filenameParts.hasGroupref || filenameParts.hasFrameref) &&
|
||||
!(filenameParts.hasGroupref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref) &&
|
||||
(filenameParts.hasBlockref || filenameParts.hasSectionref)
|
||||
) {
|
||||
let el = filenameParts.hasSectionref
|
||||
@@ -3096,7 +3114,7 @@ export async function createSVG(
|
||||
if (template?.hasSVGwithBitmap) {
|
||||
svg.setAttribute("hasbitmap", "true");
|
||||
}
|
||||
return embedFont ? embedFontsInSVG(svg, plugin) : svg;
|
||||
return svg;
|
||||
}
|
||||
|
||||
function estimateLineBound(points: any): [number, number, number, number] {
|
||||
|
||||
@@ -11,14 +11,13 @@ import {
|
||||
fileid,
|
||||
DEVICE,
|
||||
EMBEDDABLE_THEME_FRONTMATTER_VALUES,
|
||||
getDefaultLineHeight,
|
||||
getLineHeight,
|
||||
ERROR_IFRAME_CONVERSION_CANCELED,
|
||||
JSON_parse,
|
||||
FRONTMATTER_KEYS,
|
||||
refreshTextDimensions,
|
||||
getContainerElement,
|
||||
} from "./constants/constants";
|
||||
import { _measureText } from "./ExcalidrawAutomate";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import { TextMode } from "./ExcalidrawView";
|
||||
import {
|
||||
@@ -417,7 +416,6 @@ export class ExcalidrawData {
|
||||
string,
|
||||
{ raw: string; parsed: string}
|
||||
> = null;
|
||||
public elementLinks: Map<string, string> = null;
|
||||
public scene: any = null;
|
||||
public deletedElements: ExcalidrawElement[] = [];
|
||||
public file: TFile = null;
|
||||
@@ -429,6 +427,7 @@ export class ExcalidrawData {
|
||||
public autoexportPreference: AutoexportPreference = AutoexportPreference.inherit;
|
||||
private textMode: TextMode = TextMode.raw;
|
||||
public loaded: boolean = false;
|
||||
public elementLinks: Map<string, string> = null;
|
||||
public files: Map<FileId, EmbeddedFile> = null; //fileId, path
|
||||
private equations: Map<FileId, { latex: string; isLoaded: boolean }> = null; //fileId, path
|
||||
private mermaids: Map<FileId, { mermaid: string; isLoaded: boolean }> = null; //fileId, path
|
||||
@@ -439,12 +438,34 @@ export class ExcalidrawData {
|
||||
constructor(
|
||||
private plugin: ExcalidrawPlugin,
|
||||
) {
|
||||
this.app = plugin.app;
|
||||
this.app = this.plugin.app;
|
||||
this.files = new Map<FileId, EmbeddedFile>();
|
||||
this.equations = new Map<FileId, { latex: string; isLoaded: boolean }>();
|
||||
this.mermaids = new Map<FileId, { mermaid: string; isLoaded: boolean }>();
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.textElements = null;
|
||||
this.scene = null;
|
||||
this.deletedElements = [];
|
||||
this.file = null;
|
||||
this.app = null;
|
||||
this.showLinkBrackets = null;
|
||||
this.linkPrefix = null;
|
||||
this.embeddableTheme = null;
|
||||
this.urlPrefix = null;
|
||||
this.autoexportPreference = null;
|
||||
this.textMode = null;
|
||||
this.loaded = false;
|
||||
this.elementLinks = null;
|
||||
this.files = null;
|
||||
this.equations = null;
|
||||
this.mermaids = null;
|
||||
this.compatibilityMode = null;
|
||||
this.textElementCommentedOut = null;
|
||||
this.selectedElementIds = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1.5.4: for backward compatibility following the release of container bound text elements and the depreciation boundElementIds field
|
||||
*/
|
||||
@@ -528,7 +549,7 @@ export class ExcalidrawData {
|
||||
}
|
||||
|
||||
if (el.type === "text" && !el.hasOwnProperty("lineHeight")) {
|
||||
el.lineHeight = getDefaultLineHeight(el.fontFamily);
|
||||
el.lineHeight = getLineHeight(el.fontFamily);
|
||||
}
|
||||
|
||||
if (el.type === "image" && !el.hasOwnProperty("roundness")) {
|
||||
@@ -866,7 +887,7 @@ export class ExcalidrawData {
|
||||
? data.substring(indexOfNewEmbeddedFiles + embeddedFilesNewLength)
|
||||
: data.substring(indexOfOldEmbeddedFiles + embeddedFilesOldLength);
|
||||
//Load Embedded files
|
||||
const REG_FILEID_FILEPATH = /([\w\d]*):\s*\[\[([^\]]*)]]\s*(\{[^}]*})?\n/gm;
|
||||
const REG_FILEID_FILEPATH = /([\w\d]*):\s*\!?\[\[([^\]]*)]]\s*(\{[^}]*})?\n/gm;
|
||||
res = data.matchAll(REG_FILEID_FILEPATH);
|
||||
while (!(parts = res.next()).done) {
|
||||
const embeddedFile = new EmbeddedFile(
|
||||
|
||||
32
src/ExcalidrawLib.d.ts
vendored
32
src/ExcalidrawLib.d.ts
vendored
@@ -2,7 +2,8 @@ import { RestoredDataState } from "@zsviczian/excalidraw/types/excalidraw/data/r
|
||||
import { ImportedDataState } from "@zsviczian/excalidraw/types/excalidraw/data/types";
|
||||
import { BoundingBox } from "@zsviczian/excalidraw/types/excalidraw/element/bounds";
|
||||
import { ElementsMap, ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameElement, ExcalidrawTextContainer, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, NonDeletedExcalidrawElement, Theme } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { AppState, BinaryFiles, ExportOpts, Point, Zoom } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { FontMetadata } from "@zsviczian/excalidraw/types/excalidraw/fonts/metadata";
|
||||
import { AppState, BinaryFiles, Point, Zoom } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
|
||||
|
||||
type EmbeddedLink =
|
||||
@@ -46,6 +47,7 @@ declare namespace ExcalidrawLib {
|
||||
exportPadding?: number;
|
||||
exportingFrame: ExcalidrawFrameElement | null | undefined;
|
||||
renderEmbeddables?: boolean;
|
||||
skipInliningFonts?: boolean;
|
||||
}): Promise<SVGSVGElement>;
|
||||
|
||||
function sceneCoordsToViewportCoords(
|
||||
@@ -114,10 +116,9 @@ declare namespace ExcalidrawLib {
|
||||
text: string,
|
||||
font: FontString,
|
||||
lineHeight: number,
|
||||
): { width: number; height: number; baseline: number };
|
||||
|
||||
function getDefaultLineHeight(fontFamily: FontFamilyValues): number;
|
||||
): { width: number; height: number; };
|
||||
|
||||
function getLineHeight (fontFamily: FontFamilyValues):number;
|
||||
function wrapText(text: string, font: FontString, maxWidth: number): string;
|
||||
|
||||
function getFontString({
|
||||
@@ -128,6 +129,13 @@ declare namespace ExcalidrawLib {
|
||||
fontFamily: FontFamilyValues;
|
||||
}): FontString;
|
||||
|
||||
|
||||
function getFontFamilyString ({
|
||||
fontFamily,
|
||||
}: {
|
||||
fontFamily: number;
|
||||
}): string;
|
||||
|
||||
function getBoundTextMaxWidth(container: ExcalidrawElement): number;
|
||||
|
||||
function exportToBlob(
|
||||
@@ -155,4 +163,18 @@ declare namespace ExcalidrawLib {
|
||||
files?: any;
|
||||
error?: string;
|
||||
} | undefined>;
|
||||
}
|
||||
|
||||
var getSceneVersion: any;
|
||||
var Excalidraw: any;
|
||||
var MainMenu: any;
|
||||
var WelcomeScreen: any;
|
||||
var TTDDialogTrigger: any;
|
||||
var TTDDialog: any;
|
||||
|
||||
function destroyObsidianUtils(): void;
|
||||
function registerLocalFont(fontMetrics: FontMetadata, uri: string): void;
|
||||
function getFontFamilies(): string[];
|
||||
function registerFontsInCSS(): Promise<void>;
|
||||
function getFontDefinition(fontFamily: number): Promise<string>;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
14
src/LaTeX.ts
14
src/LaTeX.ts
@@ -7,21 +7,18 @@ import {RegisterHTMLHandler} from 'mathjax-full/js/handlers/html.js';
|
||||
import {AllPackages} from 'mathjax-full/js/input/tex/AllPackages.js';
|
||||
|
||||
import ExcalidrawView from "./ExcalidrawView";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import { FileData, MimeType } from "./EmbeddedFileLoader";
|
||||
import { FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { getImageSize, svgToBase64 } from "./utils/Utils";
|
||||
import { fileid } from "./constants/constants";
|
||||
import { TFile } from "obsidian";
|
||||
import { MathDocument } from "mathjax-full/js/core/MathDocument";
|
||||
import { stripVTControlCharacters } from "util";
|
||||
|
||||
export const updateEquation = async (
|
||||
equation: string,
|
||||
fileId: string,
|
||||
view: ExcalidrawView,
|
||||
addFiles: Function,
|
||||
plugin: ExcalidrawPlugin,
|
||||
) => {
|
||||
const data = await tex2dataURL(equation);
|
||||
if (data) {
|
||||
@@ -40,11 +37,15 @@ export const updateEquation = async (
|
||||
};
|
||||
|
||||
let adaptor: LiteAdaptor;
|
||||
let input: TeX<unknown, unknown, unknown>;
|
||||
let output: SVG<unknown, unknown, unknown>;
|
||||
let html: MathDocument<any, any, any>;
|
||||
let preamble: string;
|
||||
|
||||
export const clearMathJaxVariables = () => {
|
||||
adaptor = null;
|
||||
html = null;
|
||||
preamble = null;
|
||||
};
|
||||
|
||||
//https://github.com/xldenis/obsidian-latex/blob/master/main.ts
|
||||
const loadPreamble = async () => {
|
||||
const file = app.vault.getAbstractFileByPath("preamble.sty");
|
||||
@@ -63,6 +64,9 @@ export async function tex2dataURL(
|
||||
created: number;
|
||||
size: { height: number; width: number };
|
||||
}> {
|
||||
let input: TeX<unknown, unknown, unknown>;
|
||||
let output: SVG<unknown, unknown, unknown>;
|
||||
|
||||
if(!adaptor) {
|
||||
await loadPreamble();
|
||||
adaptor = liteAdaptor();
|
||||
|
||||
@@ -12,7 +12,6 @@ import { ExportSettings } from "./ExcalidrawView";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import {getIMGFilename,} from "./utils/FileUtils";
|
||||
import {
|
||||
embedFontsInSVG,
|
||||
getEmbeddedFilenameParts,
|
||||
getExportTheme,
|
||||
getQuickImagePreview,
|
||||
@@ -105,11 +104,13 @@ const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,ex
|
||||
const png =
|
||||
quickPNG ??
|
||||
(await createPNG(
|
||||
(filenameParts.hasGroupref || filenameParts.hasFrameref)
|
||||
(filenameParts.hasGroupref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref)
|
||||
? filenameParts.filepath + filenameParts.linkpartReference
|
||||
: file.path,
|
||||
scale,
|
||||
exportSettings,
|
||||
filenameParts.hasClippedFrameref
|
||||
? { ...exportSettings, frameRendering: { enabled: true, name: false, outline: false, clip: true}}
|
||||
: exportSettings,
|
||||
loader,
|
||||
theme,
|
||||
null,
|
||||
@@ -182,13 +183,16 @@ const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSetting
|
||||
}
|
||||
}
|
||||
|
||||
let svg = convertSVGStringToElement((
|
||||
exportSettings.skipInliningFonts = false;
|
||||
const svg = convertSVGStringToElement((
|
||||
await createSVG(
|
||||
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref
|
||||
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref
|
||||
? filenameParts.filepath + filenameParts.linkpartReference
|
||||
: file.path,
|
||||
true,
|
||||
exportSettings,
|
||||
filenameParts?.hasClippedFrameref
|
||||
? { ...exportSettings, frameRendering: { enabled: true, name: false, outline: false, clip: true}}
|
||||
: exportSettings,
|
||||
loader,
|
||||
theme,
|
||||
null,
|
||||
@@ -204,7 +208,6 @@ const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSetting
|
||||
return null;
|
||||
}
|
||||
|
||||
svg = embedFontsInSVG(svg, plugin, false);
|
||||
//need to remove width and height attributes to support area= embeds
|
||||
svg.removeAttribute("width");
|
||||
svg.removeAttribute("height");
|
||||
@@ -226,14 +229,17 @@ const _getSVGNative = async ({filenameParts,theme,cacheReady,containerElement,fi
|
||||
maybeSVG = await imageCache.getImageFromCache(cacheKey);
|
||||
}
|
||||
|
||||
let svg = (maybeSVG && (maybeSVG instanceof SVGSVGElement))
|
||||
exportSettings.skipInliningFonts = false;
|
||||
const svg = (maybeSVG && (maybeSVG instanceof SVGSVGElement))
|
||||
? maybeSVG
|
||||
: convertSVGStringToElement((await createSVG(
|
||||
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref
|
||||
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref
|
||||
? filenameParts.filepath + filenameParts.linkpartReference
|
||||
: file.path,
|
||||
false,
|
||||
exportSettings,
|
||||
filenameParts.hasClippedFrameref
|
||||
? { ...exportSettings, frameRendering: { enabled: true, name: false, outline: false, clip: true}}
|
||||
: exportSettings,
|
||||
loader,
|
||||
theme,
|
||||
null,
|
||||
@@ -250,11 +256,14 @@ const _getSVGNative = async ({filenameParts,theme,cacheReady,containerElement,fi
|
||||
return null;
|
||||
}
|
||||
|
||||
svg = embedFontsInSVG(svg, plugin, true);
|
||||
//cache SVG should have the width and height parameters and not the embedded font
|
||||
if(!Boolean(maybeSVG)) {
|
||||
cacheReady && imageCache.addImageToCache(cacheKey,"", svg);
|
||||
}
|
||||
|
||||
svg.removeAttribute("width");
|
||||
svg.removeAttribute("height");
|
||||
containerElement.append(svg);
|
||||
cacheReady && imageCache.addImageToCache(cacheKey,"", svg);
|
||||
return containerElement;
|
||||
}
|
||||
|
||||
@@ -358,7 +367,7 @@ const createImgElement = async (
|
||||
imgOrDiv.setAttribute("draggable","false");
|
||||
imgOrDiv.setAttribute("onCanvas",onCanvas?"true":"false");
|
||||
|
||||
let timer:NodeJS.Timeout;
|
||||
let timer:number;
|
||||
const clickEvent = (ev:PointerEvent) => {
|
||||
if(!(ev.target instanceof Element)) {
|
||||
return;
|
||||
@@ -411,7 +420,7 @@ const createImgElement = async (
|
||||
eventElement.addEventListener("pointermove",(ev)=>{
|
||||
if(!timer) return;
|
||||
if(Math.abs(ev.screenX-pointerDownEvent.screenX)>10 || Math.abs(ev.screenY-pointerDownEvent.screenY)>10) {
|
||||
clearTimeout(timer);
|
||||
window.clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
});
|
||||
@@ -420,11 +429,11 @@ const createImgElement = async (
|
||||
//@ts-ignore
|
||||
const PLUGIN = app.plugins.plugins["obsidian-excalidraw-plugin"] as ExcalidrawPlugin;
|
||||
const timeoutValue = DEVICE.isDesktop ? PLUGIN.settings.longPressDesktop : PLUGIN.settings.longPressMobile;
|
||||
timer = setTimeout(()=>clickEvent(ev),timeoutValue);
|
||||
timer = window.setTimeout(()=>clickEvent(ev),timeoutValue);
|
||||
pointerDownEvent = ev;
|
||||
});
|
||||
eventElement.addEventListener("pointerup",()=>{
|
||||
if(timer) clearTimeout(timer);
|
||||
if(timer) window.clearTimeout(timer);
|
||||
timer = null;
|
||||
})
|
||||
eventElement.addEventListener("dblclick",clickEvent);
|
||||
@@ -568,7 +577,7 @@ const isTextOnlyEmbed = (internalEmbedEl: Element):boolean => {
|
||||
const src = internalEmbedEl.getAttribute("src");
|
||||
if(!src) return true; //technically this does not mean this is a text only embed, but still should abort further processing
|
||||
const fnameParts = getEmbeddedFilenameParts(src);
|
||||
return !(fnameParts.hasArearef || fnameParts.hasGroupref || fnameParts.hasFrameref) &&
|
||||
return !(fnameParts.hasArearef || fnameParts.hasGroupref || fnameParts.hasFrameref || fnameParts.hasClippedFrameref) &&
|
||||
(fnameParts.hasBlockref || fnameParts.hasSectionref)
|
||||
}
|
||||
|
||||
@@ -658,7 +667,7 @@ const tmpObsidianWYSIWYG = async (
|
||||
} else {
|
||||
const warningEl = el.querySelector("div>h3[data-heading^='Unable to find section #^");
|
||||
if(warningEl) {
|
||||
const ref = warningEl.getAttr("data-heading").match(/Unable to find section (#\^(?:group=|area=|frame=)[^ ]*)/)?.[1];
|
||||
const ref = warningEl.getAttr("data-heading").match(/Unable to find section (#\^(?:group=|area=|frame=|clippedframe=)[^ ]*)/)?.[1];
|
||||
if(ref) {
|
||||
attr.fname = file.path + ref;
|
||||
areaPreview = true;
|
||||
@@ -708,15 +717,15 @@ const tmpObsidianWYSIWYG = async (
|
||||
internalEmbedDiv.appendChild(imgDiv);
|
||||
|
||||
//timer to avoid the image flickering when the user is typing
|
||||
let timer: NodeJS.Timeout = null;
|
||||
let timer: number = null;
|
||||
const markdownObserverFn: MutationCallback = (m) => {
|
||||
if (!["alt", "width", "height"].contains(m[0]?.attributeName)) {
|
||||
return;
|
||||
}
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
window.clearTimeout(timer);
|
||||
}
|
||||
timer = setTimeout(async () => {
|
||||
timer = window.setTimeout(async () => {
|
||||
timer = null;
|
||||
internalEmbedDiv.empty();
|
||||
const imgDiv = await processInternalEmbed(internalEmbedDiv,file);
|
||||
|
||||
145
src/Scripts.ts
145
src/Scripts.ts
@@ -12,6 +12,8 @@ import { ButtonDefinition, GenericInputPrompt, GenericSuggester } from "./dialog
|
||||
import { getIMGFilename } from "./utils/FileUtils";
|
||||
import { splitFolderAndFilename } from "./utils/FileUtils";
|
||||
import { getEA } from "src";
|
||||
import { ExcalidrawAutomate } from "./ExcalidrawAutomate";
|
||||
import { WeakArray } from "./utils/WeakArray";
|
||||
|
||||
export type ScriptIconMap = {
|
||||
[key: string]: { name: string; group: string; svgString: string };
|
||||
@@ -22,6 +24,7 @@ export class ScriptEngine {
|
||||
private scriptPath: string;
|
||||
//https://stackoverflow.com/questions/60218638/how-to-force-re-render-if-map-value-changes
|
||||
public scriptIconMap: ScriptIconMap;
|
||||
eaInstances = new WeakArray<ExcalidrawAutomate>();
|
||||
|
||||
constructor(plugin: ExcalidrawPlugin) {
|
||||
this.plugin = plugin;
|
||||
@@ -30,65 +33,95 @@ export class ScriptEngine {
|
||||
this.registerEventHandlers();
|
||||
}
|
||||
|
||||
registerEventHandlers() {
|
||||
const handleSvgFileChange = (path: string) => {
|
||||
if (!path.endsWith(".svg")) {
|
||||
return;
|
||||
public removeViewEAs(view: ExcalidrawView) {
|
||||
const eas = new Set<ExcalidrawAutomate>();
|
||||
this.eaInstances.forEach((ea) => {
|
||||
if (ea.targetView === view) {
|
||||
eas.add(ea);
|
||||
ea.destroy();
|
||||
}
|
||||
const scriptFile = app.vault.getAbstractFileByPath(
|
||||
getIMGFilename(path, "md"),
|
||||
);
|
||||
if (scriptFile && scriptFile instanceof TFile) {
|
||||
this.unloadScript(this.getScriptName(scriptFile), scriptFile.path);
|
||||
this.loadScript(scriptFile);
|
||||
}
|
||||
};
|
||||
});
|
||||
this.eaInstances.removeObjects(eas);
|
||||
}
|
||||
|
||||
const deleteEventHandler = async (file: TFile) => {
|
||||
if (!(file instanceof TFile)) {
|
||||
return;
|
||||
}
|
||||
if (!file.path.startsWith(this.scriptPath)) {
|
||||
return;
|
||||
}
|
||||
this.unloadScript(this.getScriptName(file), file.path);
|
||||
handleSvgFileChange(file.path);
|
||||
};
|
||||
this.plugin.registerEvent(
|
||||
app.vault.on("delete", deleteEventHandler),
|
||||
public destroy() {
|
||||
this.eaInstances.forEach((ea) => ea.destroy());
|
||||
this.eaInstances.clear();
|
||||
this.eaInstances = null;
|
||||
this.scriptIconMap = null;
|
||||
this.plugin = null;
|
||||
this.scriptPath = null;
|
||||
}
|
||||
|
||||
private handleSvgFileChange (path: string) {
|
||||
if (!path.endsWith(".svg")) {
|
||||
return;
|
||||
}
|
||||
const scriptFile = app.vault.getAbstractFileByPath(
|
||||
getIMGFilename(path, "md"),
|
||||
);
|
||||
if (scriptFile && scriptFile instanceof TFile) {
|
||||
this.unloadScript(this.getScriptName(scriptFile), scriptFile.path);
|
||||
this.loadScript(scriptFile);
|
||||
}
|
||||
}
|
||||
|
||||
const createEventHandler = async (file: TFile) => {
|
||||
if (!(file instanceof TFile)) {
|
||||
return;
|
||||
}
|
||||
if (!file.path.startsWith(this.scriptPath)) {
|
||||
return;
|
||||
}
|
||||
private async deleteEventHandler (file: TFile) {
|
||||
if (!(file instanceof TFile)) {
|
||||
return;
|
||||
}
|
||||
if (!file.path.startsWith(this.scriptPath)) {
|
||||
return;
|
||||
}
|
||||
this.unloadScript(this.getScriptName(file), file.path);
|
||||
this.handleSvgFileChange(file.path);
|
||||
};
|
||||
|
||||
private async createEventHandler (file: TFile) {
|
||||
if (!(file instanceof TFile)) {
|
||||
return;
|
||||
}
|
||||
if (!file.path.startsWith(this.scriptPath)) {
|
||||
return;
|
||||
}
|
||||
this.loadScript(file);
|
||||
this.handleSvgFileChange(file.path);
|
||||
};
|
||||
|
||||
private async renameEventHandler (file: TAbstractFile, oldPath: string) {
|
||||
if (!(file instanceof TFile)) {
|
||||
return;
|
||||
}
|
||||
const oldFileIsScript = oldPath.startsWith(this.scriptPath);
|
||||
const newFileIsScript = file.path.startsWith(this.scriptPath);
|
||||
if (oldFileIsScript) {
|
||||
this.unloadScript(this.getScriptName(oldPath), oldPath);
|
||||
this.handleSvgFileChange(oldPath);
|
||||
}
|
||||
if (newFileIsScript) {
|
||||
this.loadScript(file);
|
||||
handleSvgFileChange(file.path);
|
||||
};
|
||||
this.plugin.registerEvent(
|
||||
app.vault.on("create", createEventHandler),
|
||||
);
|
||||
this.handleSvgFileChange(file.path);
|
||||
}
|
||||
}
|
||||
|
||||
const renameEventHandler = async (file: TAbstractFile, oldPath: string) => {
|
||||
if (!(file instanceof TFile)) {
|
||||
return;
|
||||
}
|
||||
const oldFileIsScript = oldPath.startsWith(this.scriptPath);
|
||||
const newFileIsScript = file.path.startsWith(this.scriptPath);
|
||||
if (oldFileIsScript) {
|
||||
this.unloadScript(this.getScriptName(oldPath), oldPath);
|
||||
handleSvgFileChange(oldPath);
|
||||
}
|
||||
if (newFileIsScript) {
|
||||
this.loadScript(file);
|
||||
handleSvgFileChange(file.path);
|
||||
}
|
||||
};
|
||||
registerEventHandlers() {
|
||||
this.plugin.registerEvent(
|
||||
app.vault.on("rename", renameEventHandler),
|
||||
this.plugin.app.vault.on(
|
||||
"delete",
|
||||
(file: TFile)=>this.deleteEventHandler(file)
|
||||
),
|
||||
);
|
||||
this.plugin.registerEvent(
|
||||
this.plugin.app.vault.on(
|
||||
"create",
|
||||
(file: TFile)=>this.createEventHandler(file)
|
||||
),
|
||||
);
|
||||
this.plugin.registerEvent(
|
||||
this.plugin.app.vault.on(
|
||||
"rename",
|
||||
(file: TAbstractFile, oldPath: string)=>this.renameEventHandler(file, oldPath)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -215,6 +248,7 @@ export class ScriptEngine {
|
||||
}
|
||||
script = script.replace(/^---.*?---\n/gs, "");
|
||||
const ea = getEA(view);
|
||||
this.eaInstances.push(ea);
|
||||
ea.activeScript = title;
|
||||
|
||||
//https://stackoverflow.com/questions/45381204/get-asyncfunction-constructor-in-typescript changed tsconfig to es2017
|
||||
@@ -253,7 +287,7 @@ export class ScriptEngine {
|
||||
instructions?: Instruction[],
|
||||
) =>
|
||||
ScriptEngine.suggester(
|
||||
app,
|
||||
this.plugin.app,
|
||||
displayItems,
|
||||
items,
|
||||
hint,
|
||||
@@ -265,13 +299,12 @@ export class ScriptEngine {
|
||||
new Notice(t("SCRIPT_EXECUTION_ERROR"), 4000);
|
||||
errorlog({ script: this.plugin.ea.activeScript, error: e });
|
||||
}*/
|
||||
//ea.activeScript = null;
|
||||
return result;
|
||||
return result;
|
||||
}
|
||||
|
||||
private updateToolPannels() {
|
||||
const leaves =
|
||||
app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||
this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||
leaves.forEach((leaf: WorkspaceLeaf) => {
|
||||
const excalidrawView = leaf.view as ExcalidrawView;
|
||||
excalidrawView.toolsPanelRef?.current?.updateScriptIconMap(
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { DeviceType } from "../types";
|
||||
import { DeviceType } from "../types/types";
|
||||
import { ExcalidrawLib } from "../ExcalidrawLib";
|
||||
import { moment } from "obsidian";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
@@ -85,7 +85,7 @@ export const {
|
||||
getCommonBoundingBox,
|
||||
getMaximumGroups,
|
||||
measureText,
|
||||
getDefaultLineHeight,
|
||||
getLineHeight,
|
||||
wrapText,
|
||||
getFontString,
|
||||
getBoundTextMaxWidth,
|
||||
@@ -94,10 +94,14 @@ export const {
|
||||
mutateElement,
|
||||
restore,
|
||||
mermaidToExcalidraw,
|
||||
getFontFamilyString,
|
||||
getContainerElement,
|
||||
refreshTextDimensions,
|
||||
getFontDefinition,
|
||||
} = excalidrawLib;
|
||||
|
||||
export const FONTS_STYLE_ID = "excalidraw-custom-fonts";
|
||||
|
||||
export function JSON_parse(x: string): any {
|
||||
return JSON.parse(x.replaceAll("[", "["));
|
||||
}
|
||||
|
||||
@@ -19,20 +19,22 @@ declare module "obsidian" {
|
||||
}
|
||||
}
|
||||
|
||||
const getTheme = (view: ExcalidrawView, theme:string): string => view.excalidrawData.embeddableTheme === "dark"
|
||||
function getTheme (view: ExcalidrawView, theme:string): string {
|
||||
return view.excalidrawData.embeddableTheme === "dark"
|
||||
? "theme-dark"
|
||||
: view.excalidrawData.embeddableTheme === "light"
|
||||
? "theme-light"
|
||||
: view.excalidrawData.embeddableTheme === "auto"
|
||||
? theme === "dark" ? "theme-dark" : "theme-light"
|
||||
: isObsidianThemeDark() ? "theme-dark" : "theme-light";
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
//Render webview for anything other than Vimeo and Youtube
|
||||
//Vimeo and Youtube are rendered by Excalidraw because of the window messaging
|
||||
//required to control the video
|
||||
//--------------------------------------------------------------------------------
|
||||
export const renderWebView = (src: string, view: ExcalidrawView, id: string, appState: UIAppState):JSX.Element =>{
|
||||
export function renderWebView (src: string, view: ExcalidrawView, id: string, _: UIAppState):JSX.Element {
|
||||
const isDataURL = src.startsWith("data:");
|
||||
if(DEVICE.isDesktop && !isDataURL) {
|
||||
return (
|
||||
@@ -86,36 +88,36 @@ function RenderObsidianView(
|
||||
if (!file) {
|
||||
return null;
|
||||
}
|
||||
const react = view.plugin.getPackage(view.ownerWindow).react;
|
||||
const React = view.packages.react;
|
||||
|
||||
//@ts-ignore
|
||||
const leafRef = react.useRef<{leaf: WorkspaceLeaf; node?: ObsidianCanvasNode, editNode?: Function} | null>(null);
|
||||
const isEditingRef = react.useRef(false);
|
||||
const isActiveRef = react.useRef(false);
|
||||
const themeRef = react.useRef(theme);
|
||||
const elementRef = react.useRef(element);
|
||||
const leafRef = React.useRef<{leaf: WorkspaceLeaf; node?: ObsidianCanvasNode, editNode?: Function} | null>(null);
|
||||
const isEditingRef = React.useRef(false);
|
||||
const isActiveRef = React.useRef(false);
|
||||
const themeRef = React.useRef(theme);
|
||||
const elementRef = React.useRef(element);
|
||||
|
||||
// Update themeRef when theme changes
|
||||
react.useEffect(() => {
|
||||
React.useEffect(() => {
|
||||
themeRef.current = theme;
|
||||
}, [theme]);
|
||||
|
||||
// Update elementRef when element changes
|
||||
react.useEffect(() => {
|
||||
React.useEffect(() => {
|
||||
elementRef.current = element;
|
||||
}, [element]);
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
//block propagation of events to the parent if the iframe element is active
|
||||
//--------------------------------------------------------------------------------
|
||||
const stopPropagation = react.useCallback((event:React.PointerEvent<HTMLElement>) => {
|
||||
const stopPropagation = React.useCallback((event:React.PointerEvent<HTMLElement>) => {
|
||||
if(isActiveRef.current) {
|
||||
event.stopPropagation(); // Stop the event from propagating up the DOM tree
|
||||
}
|
||||
}, [isActiveRef.current]);
|
||||
|
||||
//runs once after mounting of the component and when the component is unmounted
|
||||
react.useEffect(() => {
|
||||
React.useEffect(() => {
|
||||
if(!containerRef?.current) {
|
||||
return;
|
||||
}
|
||||
@@ -134,7 +136,7 @@ function RenderObsidianView(
|
||||
}, []);
|
||||
|
||||
//blocking or not the propagation of events to the parent if the iframe is active
|
||||
react.useEffect(() => {
|
||||
React.useEffect(() => {
|
||||
EXTENDED_EVENT_TYPES.forEach((type) => containerRef.current.removeEventListener(type, stopPropagation));
|
||||
if(!containerRef?.current) {
|
||||
return;
|
||||
@@ -156,7 +158,7 @@ function RenderObsidianView(
|
||||
//--------------------------------------------------------------------------------
|
||||
//Mount the workspace leaf or the canvas node depending on subpath
|
||||
//--------------------------------------------------------------------------------
|
||||
react.useEffect(() => {
|
||||
React.useEffect(() => {
|
||||
if(!containerRef?.current) {
|
||||
return;
|
||||
}
|
||||
@@ -175,7 +177,7 @@ function RenderObsidianView(
|
||||
rootSplit.containerEl.style.height = '100%';
|
||||
rootSplit.containerEl.style.borderRadius = "var(--embeddable-radius)";
|
||||
leafRef.current = {
|
||||
leaf: app.workspace.createLeafInParent(rootSplit, 0),
|
||||
leaf: view.app.workspace.createLeafInParent(rootSplit, 0),
|
||||
node: null,
|
||||
editNode: null,
|
||||
};
|
||||
@@ -221,20 +223,29 @@ function RenderObsidianView(
|
||||
} else {
|
||||
const workspaceLeaf:HTMLDivElement = rootSplit.containerEl.querySelector("div.workspace-leaf");
|
||||
if(workspaceLeaf) workspaceLeaf.style.borderRadius = "var(--embeddable-radius)";
|
||||
rootSplit.containerEl.addClass("mod-visible");
|
||||
containerRef.current.appendChild(rootSplit.containerEl);
|
||||
setColors(containerRef.current, element, mdProps, canvasColor);
|
||||
}
|
||||
patchMobileView(view);
|
||||
view.updateEmbeddableLeafRef(element.id, leafRef.current);
|
||||
})();
|
||||
}
|
||||
|
||||
return () => {}; //cleanup on unmount
|
||||
return () => {
|
||||
if(!leafRef.current) {
|
||||
return;
|
||||
}
|
||||
view.canvasNodeFactory.removeNode(leafRef.current.node);
|
||||
leafRef.current.leaf?.detach();
|
||||
leafRef.current = null;
|
||||
}; //cleanup on unmount
|
||||
}, [linkText, subpath, containerRef]);
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
//Set colors of the canvas node
|
||||
//--------------------------------------------------------------------------------
|
||||
const setColors = (canvasNode: HTMLDivElement, element: NonDeletedExcalidrawElement, mdProps: EmbeddableMDCustomProps, canvasColor: string) => {
|
||||
function setColors (canvasNode: HTMLDivElement, element: NonDeletedExcalidrawElement, mdProps: EmbeddableMDCustomProps, canvasColor: string) {
|
||||
if(!mdProps) return;
|
||||
if (!leafRef.current?.hasOwnProperty("node")) return;
|
||||
|
||||
@@ -296,7 +307,7 @@ function RenderObsidianView(
|
||||
//--------------------------------------------------------------------------------
|
||||
//Set colors of the canvas node
|
||||
//--------------------------------------------------------------------------------
|
||||
react.useEffect(() => {
|
||||
React.useEffect(() => {
|
||||
if(!containerRef.current) {
|
||||
return;
|
||||
}
|
||||
@@ -314,7 +325,7 @@ function RenderObsidianView(
|
||||
//--------------------------------------------------------------------------------
|
||||
//Switch to preview mode when the iframe is not active
|
||||
//--------------------------------------------------------------------------------
|
||||
react.useEffect(() => {
|
||||
React.useEffect(() => {
|
||||
if(isEditingRef.current) {
|
||||
if(leafRef.current?.node) {
|
||||
containerRef.current?.addClasses(["is-editing", "is-focused"]);
|
||||
@@ -324,11 +335,10 @@ function RenderObsidianView(
|
||||
}
|
||||
}, [isEditingRef.current, leafRef]);
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
//Switch to edit mode when markdown view is clicked
|
||||
//--------------------------------------------------------------------------------
|
||||
const handleClick = react.useCallback((event?: React.PointerEvent<HTMLElement>) => {
|
||||
const handleClick = React.useCallback((event?: React.PointerEvent<HTMLElement>) => {
|
||||
if(isActiveRef.current) {
|
||||
event?.stopPropagation();
|
||||
}
|
||||
@@ -361,7 +371,7 @@ function RenderObsidianView(
|
||||
|
||||
if(leafRef.current) leafRef.current.editNode = handleClick;
|
||||
// Event listener for key press
|
||||
react.useEffect(() => {
|
||||
React.useEffect(() => {
|
||||
const handleKeyPress = (event: KeyboardEvent) => {
|
||||
if (event.key === "Enter") {
|
||||
handleClick(event); // Call handleClick function when Enter key is pressed
|
||||
@@ -378,7 +388,7 @@ function RenderObsidianView(
|
||||
//--------------------------------------------------------------------------------
|
||||
// Set isActiveRef and switch to preview mode when the iframe is not active
|
||||
//--------------------------------------------------------------------------------
|
||||
react.useEffect(() => {
|
||||
React.useEffect(() => {
|
||||
if(!containerRef?.current || !leafRef?.current) {
|
||||
return;
|
||||
}
|
||||
@@ -426,8 +436,8 @@ function RenderObsidianView(
|
||||
|
||||
|
||||
export const CustomEmbeddable: React.FC<{element: NonDeletedExcalidrawElement; view: ExcalidrawView; appState: UIAppState; linkText: string}> = ({ element, view, appState, linkText }) => {
|
||||
const react = view.plugin.getPackage(view.ownerWindow).react;
|
||||
const containerRef: React.RefObject<HTMLDivElement> = react.useRef(null);
|
||||
const React = view.packages.react;
|
||||
const containerRef: React.RefObject<HTMLDivElement> = React.useRef(null);
|
||||
const theme = getTheme(view, appState.theme);
|
||||
const mdProps: EmbeddableMDCustomProps = element.customData?.mdProps || null;
|
||||
return (
|
||||
|
||||
@@ -62,7 +62,6 @@ export class EmbeddableSettings extends Modal {
|
||||
this.mdCustomData.borderColor = borderCM.stringHEX({alpha: false});
|
||||
this.mdCustomData.borderOpacity = element.opacity;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
@@ -73,8 +72,16 @@ export class EmbeddableSettings extends Modal {
|
||||
|
||||
onClose() {
|
||||
this.containerEl.removeEventListener("keydown",this.onKeyDown);
|
||||
this.plugin = null;
|
||||
this.view = null;
|
||||
this.file = null;
|
||||
this.element = null;
|
||||
this.ea.destroy();
|
||||
this.ea = null;
|
||||
this.mdCustomData = null;
|
||||
}
|
||||
|
||||
|
||||
async createForm() {
|
||||
|
||||
this.contentEl.createEl("h1",{text: t("ES_TITLE")});
|
||||
@@ -140,16 +147,14 @@ export class EmbeddableSettings extends Modal {
|
||||
button
|
||||
.setButtonText(t("PROMPT_BUTTON_CANCEL"))
|
||||
.setTooltip("ESC")
|
||||
.onClick(() => {
|
||||
this.close();
|
||||
})
|
||||
.onClick(this.close.bind(this))
|
||||
)
|
||||
.addButton(button =>
|
||||
button
|
||||
.setButtonText(t("PROMPT_BUTTON_OK"))
|
||||
.setTooltip("CTRL/Opt+Enter")
|
||||
.setCta()
|
||||
.onClick(()=>this.applySettings())
|
||||
.onClick(this.applySettings.bind(this))
|
||||
)
|
||||
|
||||
|
||||
@@ -163,8 +168,6 @@ export class EmbeddableSettings extends Modal {
|
||||
this.containerEl.ownerDocument.addEventListener("keydown",onKeyDown);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private async applySettings() {
|
||||
let dirty = false;
|
||||
const el = this.ea.getElement(this.element.id) as Mutable<ExcalidrawEmbeddableElement>;
|
||||
@@ -224,10 +227,11 @@ export class EmbeddableSettings extends Modal {
|
||||
await this.ea.addElementsToView();
|
||||
//@ts-ignore
|
||||
this.ea.viewUpdateScene({appState: {}});
|
||||
this.close(); //close should only run once update scene is done
|
||||
})();
|
||||
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
this.close();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ export class ExportDialog extends Modal {
|
||||
private view: ExcalidrawView,
|
||||
private file: TFile,
|
||||
) {
|
||||
super(app);
|
||||
super(plugin.app);
|
||||
this.ea = getEA(this.view);
|
||||
this.api = this.ea.getExcalidrawAPI() as ExcalidrawImperativeAPI;
|
||||
this.padding = getExportPadding(this.plugin,this.file);
|
||||
@@ -47,6 +47,19 @@ export class ExportDialog extends Modal {
|
||||
this.saveSettings = false;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.app = null;
|
||||
this.plugin = null;
|
||||
this.ea.destroy();
|
||||
this.ea = null;
|
||||
this.view = null;
|
||||
this.file = null;
|
||||
this.api = null;
|
||||
this.theme = null;
|
||||
this.selectedOnlySetting = null;
|
||||
this.containerEl.remove();
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.containerEl.classList.add("excalidraw-release");
|
||||
this.titleEl.setText(`Export Image`);
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
SuggestModal,
|
||||
Scope,
|
||||
} from "obsidian";
|
||||
import { t } from "../lang/helpers";
|
||||
import { createPopper, Instance as PopperInstance } from "@popperjs/core";
|
||||
|
||||
class Suggester<T> {
|
||||
@@ -132,7 +131,7 @@ class Suggester<T> {
|
||||
export abstract class SuggestionModal<T> extends FuzzySuggestModal<T> {
|
||||
items: T[] = [];
|
||||
suggestions: HTMLDivElement[];
|
||||
popper: PopperInstance;
|
||||
popper: WeakRef<PopperInstance>;
|
||||
//@ts-ignore
|
||||
scope: Scope = new Scope(this.app.scope);
|
||||
suggester: Suggester<FuzzyMatch<T>>;
|
||||
@@ -197,7 +196,7 @@ export abstract class SuggestionModal<T> extends FuzzySuggestModal<T> {
|
||||
this.app.keymap.pushScope(this.scope);
|
||||
|
||||
this.inputEl.ownerDocument.body.appendChild(this.suggestEl);
|
||||
this.popper = createPopper(this.inputEl, this.suggestEl, {
|
||||
this.popper = new WeakRef(createPopper(this.inputEl, this.suggestEl, {
|
||||
placement: "bottom-start",
|
||||
modifiers: [
|
||||
{
|
||||
@@ -213,7 +212,7 @@ export abstract class SuggestionModal<T> extends FuzzySuggestModal<T> {
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
onEscape(): void {
|
||||
@@ -225,11 +224,15 @@ export abstract class SuggestionModal<T> extends FuzzySuggestModal<T> {
|
||||
this.app.keymap.popScope(this.scope);
|
||||
|
||||
this.suggester.setSuggestions([]);
|
||||
if (this.popper) {
|
||||
this.popper.destroy();
|
||||
if (this.popper?.deref()) {
|
||||
this.popper.deref().destroy();
|
||||
}
|
||||
|
||||
this.suggestEl.detach();
|
||||
this.inputEl.removeEventListener("input", this.onInputChanged.bind(this));
|
||||
this.inputEl.removeEventListener("focus", this.onFocus.bind(this));
|
||||
this.inputEl.removeEventListener("blur", this.close.bind(this));
|
||||
|
||||
this.suggestEl.detach();
|
||||
}
|
||||
createPrompt(prompts: HTMLSpanElement[]) {
|
||||
if (!this.promptEl) {
|
||||
|
||||
82
src/dialogs/FrameSettings.ts
Normal file
82
src/dialogs/FrameSettings.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
import { t } from "src/lang/helpers";
|
||||
|
||||
export const showFrameSettings = (ea: ExcalidrawAutomate) => {
|
||||
const {enabled, clip, name, outline} = ea.getExcalidrawAPI().getAppState().frameRendering;
|
||||
|
||||
// Create modal dialog
|
||||
const frameSettingsModal = new ea.obsidian.Modal(app);
|
||||
|
||||
frameSettingsModal.onOpen = () => {
|
||||
const {contentEl} = frameSettingsModal;
|
||||
|
||||
contentEl.createEl("h1", {text: t("FRAME_SETTINGS_TITLE")});
|
||||
|
||||
const settings = { enabled, clip, name, outline };
|
||||
|
||||
// Add toggles
|
||||
const enableFramesSetting = new ea.obsidian.Setting(contentEl)
|
||||
.setName(t("FRAME_SETTINGS_ENABLE"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(settings.enabled)
|
||||
.onChange(value => {
|
||||
settings.enabled = value;
|
||||
hideComponent(displayFrameNameSetting, !value);
|
||||
hideComponent(displayFrameOutlineSetting, !value);
|
||||
hideComponent(enableFrameClippingSetting, !value);
|
||||
})
|
||||
);
|
||||
|
||||
const displayFrameNameSetting = new ea.obsidian.Setting(contentEl)
|
||||
.setName(t("FRAME_SETTIGNS_NAME"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(settings.name)
|
||||
.onChange(value => settings.name = value)
|
||||
);
|
||||
|
||||
const displayFrameOutlineSetting = new ea.obsidian.Setting(contentEl)
|
||||
.setName(t("FRAME_SETTINGS_OUTLINE"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(settings.outline)
|
||||
.onChange(value => settings.outline = value)
|
||||
);
|
||||
|
||||
const enableFrameClippingSetting = new ea.obsidian.Setting(contentEl)
|
||||
.setName(t("FRAME_SETTINGS_CLIP"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(settings.clip)
|
||||
.onChange(value => settings.clip = value)
|
||||
);
|
||||
|
||||
// Hide or show components based on initial state
|
||||
hideComponent(displayFrameNameSetting, !settings.enabled);
|
||||
hideComponent(displayFrameOutlineSetting, !settings.enabled);
|
||||
hideComponent(enableFrameClippingSetting, !settings.enabled);
|
||||
|
||||
// Add OK button
|
||||
new ea.obsidian.Setting(contentEl)
|
||||
.addButton(button => button
|
||||
.setButtonText("OK")
|
||||
.onClick(() => {
|
||||
// Update appState with new settings
|
||||
ea.viewUpdateScene({
|
||||
// @ts-ignore
|
||||
appState: {
|
||||
frameRendering: settings
|
||||
}
|
||||
});
|
||||
frameSettingsModal.close();
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
frameSettingsModal.onClose = () => {
|
||||
ea.destroy();
|
||||
}
|
||||
frameSettingsModal.open();
|
||||
};
|
||||
|
||||
// Function to hide or show a component
|
||||
function hideComponent(comp:any, value:any) {
|
||||
comp.settingEl.style.display = value ? "none" : "";
|
||||
}
|
||||
@@ -11,6 +11,12 @@ export class ImportSVGDialog extends FuzzySuggestModal<TFile> {
|
||||
public plugin: ExcalidrawPlugin;
|
||||
private view: ExcalidrawView;
|
||||
|
||||
destroy() {
|
||||
this.app = null;
|
||||
this.plugin = null;
|
||||
this.view = null;
|
||||
}
|
||||
|
||||
constructor(plugin: ExcalidrawPlugin) {
|
||||
super(plugin.app);
|
||||
this.plugin = plugin;
|
||||
@@ -38,14 +44,23 @@ export class ImportSVGDialog extends FuzzySuggestModal<TFile> {
|
||||
return item.path;
|
||||
}
|
||||
|
||||
async onChooseItem(item: TFile, event: KeyboardEvent): Promise<void> {
|
||||
async onChooseItem(item: TFile, _: KeyboardEvent): Promise<void> {
|
||||
if(!item) return;
|
||||
const ea = getEA(this.view) as ExcalidrawAutomate;
|
||||
const svg = await app.vault.read(item);
|
||||
const svg = await this.app.vault.read(item);
|
||||
if(!svg || svg === "") return;
|
||||
ea.importSVG(svg);
|
||||
ea.addToGroup(ea.getElements().map(el=>el.id));
|
||||
ea.addElementsToView(true, true, true,true);
|
||||
await ea.addElementsToView(true, true, true,true);
|
||||
ea.destroy();
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
//deley this.view destruction until onChooseItem is called
|
||||
window.setTimeout(() => {
|
||||
this.view = null;
|
||||
});
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
public start(view: ExcalidrawView) {
|
||||
|
||||
@@ -6,6 +6,11 @@ export class InsertCommandDialog extends FuzzySuggestModal<TFile> {
|
||||
public app: App;
|
||||
private addText: Function;
|
||||
|
||||
destroy() {
|
||||
this.app = null;
|
||||
this.addText = null;
|
||||
}
|
||||
|
||||
constructor(app: App) {
|
||||
super(app);
|
||||
this.app = app;
|
||||
@@ -32,10 +37,18 @@ export class InsertCommandDialog extends FuzzySuggestModal<TFile> {
|
||||
onChooseItem(item: any): void {
|
||||
const cmdId = item?.id;
|
||||
this.addText(`⚙️[${item.name}](cmd://${item.id})`);
|
||||
this.addText = null;
|
||||
}
|
||||
|
||||
public start(addText: Function) {
|
||||
this.addText = addText;
|
||||
this.open();
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
window.setTimeout(()=>{
|
||||
this.addText = null;
|
||||
}) //onChooseItem must run first
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,28 @@ import { DEVICE, IMAGE_TYPES, REG_LINKINDEX_INVALIDCHARS } from "../constants/co
|
||||
import ExcalidrawView from "../ExcalidrawView";
|
||||
import { t } from "../lang/helpers";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { getEA } from "src";
|
||||
|
||||
export class InsertImageDialog extends FuzzySuggestModal<TFile> {
|
||||
public app: App;
|
||||
public plugin: ExcalidrawPlugin;
|
||||
private view: ExcalidrawView;
|
||||
|
||||
destroy() {
|
||||
this.app = null;
|
||||
this.plugin = null;
|
||||
this.view = null;
|
||||
this.inputEl.onkeyup = null;
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
//deley this.view destruction until onChooseItem is called
|
||||
window.setTimeout(() => {
|
||||
this.view = null;
|
||||
});
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
constructor(plugin: ExcalidrawPlugin) {
|
||||
super(plugin.app);
|
||||
this.plugin = plugin;
|
||||
@@ -55,13 +71,14 @@ export class InsertImageDialog extends FuzzySuggestModal<TFile> {
|
||||
}
|
||||
|
||||
onChooseItem(item: TFile, event: KeyboardEvent): void {
|
||||
const ea = this.plugin.ea.getAPI(this.view);
|
||||
const ea = getEA(this.view);
|
||||
ea.canvas.theme = this.view.excalidrawAPI.getAppState().theme;
|
||||
const scaleToFullsize = scaleToFullsizeModifier(event);
|
||||
(async () => {
|
||||
//this.view.currentPosition = this.position;
|
||||
await ea.addImage(0, 0, item, !scaleToFullsize);
|
||||
ea.addElementsToView(true, true, true);
|
||||
await ea.addElementsToView(true, true, true);
|
||||
ea.destroy();
|
||||
})();
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,12 @@ export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
|
||||
private addText: Function;
|
||||
private drawingPath: string;
|
||||
|
||||
destroy() {
|
||||
this.app = null;
|
||||
this.addText = null;
|
||||
this.drawingPath = null;
|
||||
}
|
||||
|
||||
constructor(private plugin: ExcalidrawPlugin) {
|
||||
super(plugin.app);
|
||||
this.app = plugin.app;
|
||||
@@ -51,6 +57,13 @@ export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
|
||||
this.addText(getLink(this.plugin,{embed: false, path: filepath, alias: item.alias}), filepath, item.alias);
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
window.setTimeout(()=>{
|
||||
this.addText = null
|
||||
}); //make sure this happens after onChooseItem runs
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
public start(drawingPath: string, addText: Function) {
|
||||
this.addText = addText;
|
||||
this.drawingPath = drawingPath;
|
||||
|
||||
@@ -2,12 +2,19 @@ import { App, FuzzySuggestModal, TFile } from "obsidian";
|
||||
import ExcalidrawView from "../ExcalidrawView";
|
||||
import { t } from "../lang/helpers";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { getEA } from "src";
|
||||
|
||||
export class InsertMDDialog extends FuzzySuggestModal<TFile> {
|
||||
public app: App;
|
||||
public plugin: ExcalidrawPlugin;
|
||||
private view: ExcalidrawView;
|
||||
|
||||
destroy() {
|
||||
this.app = null;
|
||||
this.plugin = null;
|
||||
this.view = null;
|
||||
}
|
||||
|
||||
constructor(plugin: ExcalidrawPlugin) {
|
||||
super(plugin.app);
|
||||
this.plugin = plugin;
|
||||
@@ -34,12 +41,11 @@ export class InsertMDDialog extends FuzzySuggestModal<TFile> {
|
||||
}
|
||||
|
||||
onChooseItem(item: TFile): void {
|
||||
const ea = this.plugin.ea;
|
||||
ea.reset();
|
||||
ea.setView(this.view);
|
||||
const ea = getEA(this.view);
|
||||
(async () => {
|
||||
await ea.addImage(0, 0, item);
|
||||
ea.addElementsToView(true, false, true);
|
||||
await ea.addElementsToView(true, false, true);
|
||||
ea.destroy();
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -47,4 +53,12 @@ export class InsertMDDialog extends FuzzySuggestModal<TFile> {
|
||||
this.view = view;
|
||||
this.open();
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
//deley this.view destruction until onChooseItem is called
|
||||
window.setTimeout(() => {
|
||||
this.view = null;
|
||||
});
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export class InsertPDFModal extends Modal {
|
||||
private plugin: ExcalidrawPlugin,
|
||||
private view: ExcalidrawView,
|
||||
) {
|
||||
super(app);
|
||||
super(plugin.app);
|
||||
}
|
||||
|
||||
open (file?: TFile) {
|
||||
@@ -54,12 +54,17 @@ export class InsertPDFModal extends Modal {
|
||||
this.plugin.settings.pdfNumRows = this.numRows;
|
||||
this.plugin.settings.pdfDirection = this.direction;
|
||||
this.plugin.settings.pdfLockAfterImport = this.lockAfterImport;
|
||||
this.plugin.saveSettings();
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
if(this.pdfDoc) {
|
||||
this.pdfDoc.destroy();
|
||||
this.pdfDoc = null;
|
||||
}
|
||||
this.plugin = null;
|
||||
this.view = null;
|
||||
this.app = null;
|
||||
this.imageSizeMessage.remove();
|
||||
this.setImageSizeMessage = null;
|
||||
}
|
||||
|
||||
private async getPageDimensions (pdfDoc: any) {
|
||||
@@ -408,6 +413,7 @@ export class InsertPDFModal extends Modal {
|
||||
const viewElements = ea.getViewElements().filter(el => ids.includes(el.id));
|
||||
api.selectElements(viewElements);
|
||||
api.zoomToFit(viewElements);
|
||||
ea.destroy();
|
||||
this.close();
|
||||
})
|
||||
importButton = button;
|
||||
|
||||
@@ -17,6 +17,85 @@ I develop this plugin as a hobby, spending my free time doing this. If you find
|
||||
|
||||
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://cdn.ko-fi.com/cdn/kofi3.png?v=3" height=45></a></div>
|
||||
`,
|
||||
"2.2.11": `
|
||||
<img alt="badges" src="https://github.com/user-attachments/assets/7591b523-6bc6-46ff-b552-5c3492139e4c" referrerpolicy="no-referrer" style="width: 100%;">
|
||||
|
||||
## New
|
||||
- Font picker with additional fonts (not yet fully configurable, but that will come in due time) [#8012](https://github.com/excalidraw/excalidraw/pull/8012)
|
||||
- Introducing Visual Thinking Badges. The more you use Excalidraw the higher your rank will be. Levels are: Bronze, Silver, Gold and Platinum.
|
||||
|
||||
## Fixed
|
||||
- Embedded PDF was not visible on phones [#1904](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1904)
|
||||
- F2 does not rename files in Excalidraw View [#1900](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1900)
|
||||
- Wireframe to Code now honors the GPT model settings in plugin settings. [#1901](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1901)
|
||||
- Updated ExcaliAI to support gpt-4o for vision. [#1859](https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/1859) 🙏@Saik0s
|
||||
- Minor fixes from excalidraw.com [#8287](https://github.com/excalidraw/excalidraw/pull/8287), [#8285](https://github.com/excalidraw/excalidraw/pull/8285)
|
||||
`,
|
||||
"2.2.10": `
|
||||
<div class="excalidraw-videoWrapper"><div>
|
||||
<iframe src="https://www.youtube.com/embed/sjZfdqpxqsg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div></div>
|
||||
|
||||
## Fixed
|
||||
- Drastically degraded rendering performance when zoomed in and when arrows with labels are used. [#8267](https://github.com/excalidraw/excalidraw/pull/8267), [#8266](https://github.com/excalidraw/excalidraw/pull/8266), [#1893](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1893)
|
||||
- Frame title font in exports.
|
||||
|
||||
## New
|
||||
- Area, Group, Frame, and Clipped-Frame references to images now also work when pasting images to Excalidraw.
|
||||
- The new reference type ${String.fromCharCode(96)}clippedframe=${String.fromCharCode(96)} works in the same way as ${String.fromCharCode(96)}frame=${String.fromCharCode(96)} but will display the elements clipped by the frame. ${String.fromCharCode(96)}clippedframe=${String.fromCharCode(96)} will always display the image with zero padding.
|
||||
- New command palette action: ${String.fromCharCode(96)}Frame Settings${String.fromCharCode(96)} gives you fine-grained control over how frames are rendered. Frame settings will also be reflected in image exports. For example, if you hide the frame name or outline, then in exports they will not be visible.
|
||||
`,
|
||||
"2.2.9": `
|
||||
## New
|
||||
- Improved the "Open the back-of-the-note of the selected Excalidraw image" action. It now works with grouped elements and keeps the popout window within the visible screen area when elements are close to the top of the canvas. Note: Due to an Obsidian bug, I do not recommend using this feature with versions 1.6.0 - 1.6.6, if you have Obsidian Sync enabled, because Obsidian may freeze when closing the popout window. It functions properly in Obsidian versions before 1.6.0 and from 1.6.7 onwards.
|
||||
|
||||
## Fixed
|
||||
- Drag and drop from a local folder (outside Obsidian) resulted in duplicate images.
|
||||
- Insert Link Action did not work [#1873](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1873)
|
||||
- Insert Obsidian Command Action did not work
|
||||
- Element link for text element got deleted when editing the text. [#1878](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1878)
|
||||
- When back-of-the-drawing Section Headings have spaces in them, clicking the link opens the drawing side not the markdown side. [#1877](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1877)
|
||||
- obsidian:// links did not work as expected. [#1872](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1872)
|
||||
- copying and moving a rectangle with text, moves the text unexpectedly. The issue should now be resolved (at least much less likely to occur) [#1867](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1867)
|
||||
`,
|
||||
"2.2.8": `
|
||||
While this release may appear modest with no new features, it represents nearly 50 hours of dedicated work. Here's what's been improved:
|
||||
|
||||
- **Enhanced Memory Management**: Significant improvements to optimize memory usage.
|
||||
- Bug Fixes:
|
||||
- Support for multi-file drag and drop from the operating system.
|
||||
- Correct theming of animated GIFs as Embeddables.
|
||||
- Several other minor bug fixes.
|
||||
|
||||
Please note that due to extensive refactoring of the codebase, there may be some unexpected issues. Thanks for your understanding and patience.
|
||||
`,
|
||||
"2.2.7": `
|
||||
## New
|
||||
- In Miscellaneous Settings: added **Load Excalidraw Properties into Obsidian Suggester**. This setting toggles the automatic loading of Excalidraw properties at startup. Enabled by default for easy use of front matter properties. Disabling it prevents auto-loading, but you'll need to manually remove unwanted properties using Obsidian properties view. A plugin restart is required after enabling auto-loading.
|
||||
|
||||
## Fixed
|
||||
- Zotero support [1835](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1835)
|
||||
- Lines binding to elements and selections [#8146](https://github.com/excalidraw/excalidraw/issues/8146), and plugin getting stuck with dragging an element [#8131](https://github.com/excalidraw/excalidraw/issues/8131)`
|
||||
,
|
||||
"2.2.6": `
|
||||
## Fixed
|
||||
- CTRL+F search for text elements in drawing, the result did not get selected. This was a regression in 2.2.5 [#1822](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1822)
|
||||
|
||||
## New
|
||||
- Zotero compatibility support for back-of-the-side markdown notes. This needs to be enabled in plugin settings under Compatibility [#1820](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1820)
|
||||
|
||||
## New from Excalidraw.com
|
||||
- ${String.fromCharCode(96)}Stats & Element Properties${String.fromCharCode(96)}, accessible via the context menu, is now editable, e.g. you can type in the exact position and size of objects, change font size and set element angle.
|
||||
- Pasting mermaid diagrams from chatGPT will embed a diagram instead of the text
|
||||
`,
|
||||
"2.2.5": `
|
||||
## Fixed
|
||||
- Cursor visibility in dark mode [#1812](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1812)
|
||||
- SVG to Excalidraw now...
|
||||
- converts elements inside the ${String.fromCharCode(96)}<switch>${String.fromCharCode(96)} tag, improving compatibility with SVGs from [The Noun Project](https://thenounproject.com/)
|
||||
- sets visibility for all elements, preventing invisible converted images.
|
||||
- Cached images sometimes lost their font face and natural size when nested in an Excalidraw scene. This issue occurred when drawings were embedded in a markdown note (native SVG) and nested in a drawing simultaneously. Depending on the update and render sequence, these drawings sometimes appeared incorrectly in the Excalidraw scene.
|
||||
`,
|
||||
"2.2.4":`
|
||||
<div style="text-align: center;">
|
||||
<a data-tooltip-position="top" aria-label="https://youtube.com/shorts/zF1p2yfk4f4" rel="noopener" class="external-link" href="https://youtube.com/shorts/zF1p2yfk4f4" target="_blank">
|
||||
|
||||
@@ -14,6 +14,13 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
|
||||
private action: openDialogAction;
|
||||
private onNewPane: boolean;
|
||||
|
||||
destroy() {
|
||||
this.app = null;
|
||||
this.plugin = null;
|
||||
this.action = null;
|
||||
this.inputEl.onkeyup = null;
|
||||
}
|
||||
|
||||
constructor(app: App, plugin: ExcalidrawPlugin) {
|
||||
super(app);
|
||||
this.app = app;
|
||||
|
||||
@@ -22,7 +22,7 @@ import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
import { MAX_IMAGE_SIZE, REG_LINKINDEX_INVALIDCHARS } from "src/constants/constants";
|
||||
import { REGEX_LINK } from "src/ExcalidrawData";
|
||||
import { ScriptEngine } from "src/Scripts";
|
||||
import { openExternalLink, openTagSearch } from "src/utils/ExcalidrawViewUtils";
|
||||
import { openExternalLink, openTagSearch, parseObsidianLink } from "src/utils/ExcalidrawViewUtils";
|
||||
|
||||
export type ButtonDefinition = { caption: string; tooltip?:string; action: Function };
|
||||
|
||||
@@ -527,6 +527,11 @@ export class NewFileActions extends Modal {
|
||||
onClose() {
|
||||
super.onClose();
|
||||
this.resolvePromise(this.newFile);
|
||||
this.app = null;
|
||||
this.plugin = null;
|
||||
this.view = null;
|
||||
this.parentFile = null;
|
||||
this.sourceElement = null;
|
||||
}
|
||||
|
||||
createForm(): void {
|
||||
@@ -588,7 +593,8 @@ export class NewFileActions extends Modal {
|
||||
ea.copyViewElementsToEAforEditing([this.sourceElement]);
|
||||
ea.getElement(this.sourceElement.id).isDeleted = true;
|
||||
ea.addEmbeddable(this.sourceElement.x, this.sourceElement.y,MAX_IMAGE_SIZE, MAX_IMAGE_SIZE, undefined,f);
|
||||
ea.addElementsToView();
|
||||
await ea.addElementsToView();
|
||||
ea.destroy();
|
||||
}
|
||||
this.close();
|
||||
};
|
||||
@@ -702,7 +708,12 @@ export class ConfirmationPrompt extends Modal {
|
||||
}
|
||||
}
|
||||
|
||||
export const linkPrompt = async (linkText:string, app: App, view?: ExcalidrawView, message: string = "Select link to open"):Promise<[file:TFile, linkText:string, subpath: 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 partsArray = REGEX_LINK.getResList(linkText);
|
||||
let subpath: string = null;
|
||||
let file: TFile = null;
|
||||
@@ -731,6 +742,9 @@ export const linkPrompt = async (linkText:string, app: App, view?: ExcalidrawVie
|
||||
|
||||
linkText = REGEX_LINK.getLink(parts);
|
||||
if(openExternalLink(linkText, app)) return;
|
||||
const maybeObsidianLink = parseObsidianLink(linkText, app, false);
|
||||
if (typeof maybeObsidianLink === "boolean" && maybeObsidianLink) return;
|
||||
if (typeof maybeObsidianLink === "string") linkText = maybeObsidianLink;
|
||||
|
||||
if (linkText.search("#") > -1) {
|
||||
const linkParts = getLinkParts(linkText, view ? view.file : undefined);
|
||||
|
||||
44
src/dialogs/RankMessage.ts
Normal file
44
src/dialogs/RankMessage.ts
Normal file
File diff suppressed because one or more lines are too long
@@ -55,6 +55,7 @@ export class SelectCard extends FuzzySuggestModal<string> {
|
||||
(async () => {
|
||||
await ea.addElementsToView(true, false, true);
|
||||
ea.selectElementsInView([id]);
|
||||
ea.destroy();
|
||||
})();
|
||||
}
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
{
|
||||
field: "style.fontFamily",
|
||||
code: "[number]",
|
||||
desc: "1: Virgil, 2:Helvetica, 3:Cascadia, 4:LocalFont",
|
||||
desc: "1: Virgil, 2:Helvetica, 3:Cascadia, 4:Local Font, 5: Excalifont, 6: Nunito, 7: Lilita One, 8: Comic Shanns, 9: Liberation Sans",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
@@ -481,6 +481,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
desc: "Gets all the elements from elements[] that share one or more groupIds with element.",
|
||||
after: ""
|
||||
},
|
||||
{
|
||||
field: "getElementsInFrame",
|
||||
code: " getElementsInFrame(frameElement: ExcalidrawElement,elements: ExcalidrawElement[],shouldIncludeFrame: boolean = false,): ExcalidrawElement[];",
|
||||
desc: "Gets all the elements from elements[] that are inside the frameElement. If shouldIncludeFrame is true, the frameElement will also be included in the result.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "activeScript",
|
||||
code: "activeScript: string;",
|
||||
|
||||
@@ -189,6 +189,7 @@ export class UniversalInsertFileModal extends Modal {
|
||||
`[[${path}${sectionPicker.selectEl.value}]]`,
|
||||
)]
|
||||
);
|
||||
ea.destroy();
|
||||
this.close();
|
||||
})
|
||||
actionIFrame = button;
|
||||
@@ -220,6 +221,7 @@ export class UniversalInsertFileModal extends Modal {
|
||||
ea.isExcalidrawFile(file) ? !anchorTo100 : undefined,
|
||||
)]
|
||||
);
|
||||
ea.destroy();
|
||||
this.close();
|
||||
})
|
||||
actionImage = button;
|
||||
@@ -271,5 +273,8 @@ export class UniversalInsertFileModal extends Modal {
|
||||
|
||||
onClose(): void {
|
||||
this.view.ownerWindow.removeEventListener("keydown", this.onKeyDown);
|
||||
this.view = null;
|
||||
this.file = null;
|
||||
this.plugin = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,17 +50,20 @@ export default {
|
||||
NEW_IN_POPOUT_WINDOW_EMBED: "Create new drawing - IN A POPOUT WINDOW - and embed into active document",
|
||||
TOGGLE_LOCK: "Toggle Text Element between edit RAW and PREVIEW",
|
||||
DELETE_FILE: "Delete selected image or Markdown file from Obsidian Vault",
|
||||
COPY_ELEMENT_LINK: "Copy markdown link for selected element(s)",
|
||||
COPY_ELEMENT_LINK: "Copy [[link]] for selected element(s)",
|
||||
COPY_DRAWING_LINK: "Copy ![[embed link]] for this drawing",
|
||||
INSERT_LINK_TO_ELEMENT:
|
||||
`Copy markdown link for selected element to clipboard. ${labelCTRL()}+CLICK to copy 'group=' link. ${labelSHIFT()}+CLICK to copy an 'area=' link. ${labelALT()}+CLICK to watch a help video.`,
|
||||
`Copy [[link]] for selected element to clipboard. ${labelCTRL()}+CLICK to copy 'group=' link. ${labelSHIFT()}+CLICK to copy an 'area=' link. ${labelALT()}+CLICK to watch a help video.`,
|
||||
INSERT_LINK_TO_ELEMENT_GROUP:
|
||||
"Copy 'group=' markdown link for selected element to clipboard.",
|
||||
"Copy 'group=' ![[link]] for selected element to clipboard.",
|
||||
INSERT_LINK_TO_ELEMENT_AREA:
|
||||
"Copy 'area=' markdown link for selected element to clipboard.",
|
||||
"Copy 'area=' ![[link]] for selected element to clipboard.",
|
||||
INSERT_LINK_TO_ELEMENT_FRAME:
|
||||
"Copy 'frame=' markdown link for selected element to clipboard.",
|
||||
"Copy 'frame=' ![[link]] for selected element to clipboard.",
|
||||
INSERT_LINK_TO_ELEMENT_FRAME_CLIPPED:
|
||||
"Copy 'clippedframe=' ![[link]] for selected element to clipboard.",
|
||||
INSERT_LINK_TO_ELEMENT_NORMAL:
|
||||
"Copy markdown link for selected element to clipboard.",
|
||||
"Copy [[link]] for selected element to clipboard.",
|
||||
INSERT_LINK_TO_ELEMENT_ERROR: "Select a single element in the scene",
|
||||
INSERT_LINK_TO_ELEMENT_READY: "Link is READY and available on the clipboard",
|
||||
INSERT_LINK: "Insert link to file",
|
||||
@@ -93,6 +96,23 @@ export default {
|
||||
TEMPORARY_ENABLE_AUTOSAVE: "Enable autosave",
|
||||
|
||||
//ExcalidrawView.ts
|
||||
FORCE_SAVE_ABORTED: "Force Save aborted because saving is in progress",
|
||||
LINKLIST_SECOND_ORDER_LINK: "Second Order Link",
|
||||
MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT_TITLE: "Customize the link",
|
||||
MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT: "Do not add [[square brackets]] around the filename!<br>Follow this format when editing your link:<br><mark>filename#^blockref|WIDTHxMAXHEIGHT</mark>",
|
||||
FRAME_CLIPPING_ENABLED: "Frame Rendering: Enabled",
|
||||
FRAME_CLIPPING_DISABLED: "Frame Rendering: Disabled",
|
||||
ARROW_BINDING_INVERSE_MODE: "Inverted Mode: Default arrow binding is now disabled. Use CTRL/CMD to temporarily enable binding when needed.",
|
||||
ARROW_BINDING_NORMAL_MODE: "Normal Mode: Arrow binding is now enabled. Use CTRL/CMD to temporarily disable binding when needed.",
|
||||
EXPORT_FILENAME_PROMPT: "Please provide filename",
|
||||
EXPORT_FILENAME_PROMPT_PLACEHOLDER: "filename, leave blank to cancel action",
|
||||
WARNING_SERIOUS_ERROR: "WARNING: Excalidraw ran into an unknown problem!\n\n" +
|
||||
"There is a risk that your most recent changes cannot be saved.\n\n" +
|
||||
"To be on the safe side...\n" +
|
||||
"1) Please select your drawing using CTRL/CMD+A and make a copy with CTRL/CMD+C.\n" +
|
||||
"2) Then create an empty drawing in a new pane by CTRL/CMD+clicking the Excalidraw ribbon button,\n" +
|
||||
"3) and paste your work to the new document with CTRL/CMD+V.",
|
||||
ARIA_LABEL_TRAY_MODE: "Tray-mode offers an alternative, more spacious canvas",
|
||||
MASK_FILE_NOTICE: "This is a mask file. It is used to crop images and mask out parts of the image. Press and hold notice to open the help video.",
|
||||
INSTALL_SCRIPT_BUTTON: "Install or update Excalidraw Scripts",
|
||||
OPEN_AS_MD: "Open as Markdown",
|
||||
@@ -119,6 +139,7 @@ export default {
|
||||
ERROR_SAVING_IMAGE: "Unknown error occurred while fetching the image. It could be that for some reason the image is not available or rejected the fetch request from Obsidian",
|
||||
WARNING_PASTING_ELEMENT_AS_TEXT: "PASTING EXCALIDRAW ELEMENTS AS A TEXT ELEMENT IS NOT ALLOWED",
|
||||
USE_INSERT_FILE_MODAL: "Use 'Insert Any File' to embed a markdown note",
|
||||
RECURSIVE_INSERT_ERROR: "You may not recursively insert part of an image into the same image as it would create an infinite loop",
|
||||
CONVERT_TO_MARKDOWN: "Convert to file...",
|
||||
SELECT_TEXTELEMENT_ONLY: "Select text element only (not container)",
|
||||
REMOVE_LINK: "Remove text element link",
|
||||
@@ -209,7 +230,7 @@ export default {
|
||||
"The default OpenAI API URL. This is a freetext field, so you can enter any valid OpenAI API compatible URL. " +
|
||||
"Excalidraw will use this URL when posting API requests to OpenAI. I am not doing any error handling on this field, so make sure you enter a valid URL and only change this if you know what you are doing. ",
|
||||
AI_OPENAI_DEFAULT_IMAGE_API_URL_NAME: "OpenAI Image Generation API URL",
|
||||
AI_OPENAI_DEFAULT_VISION_MODEL_PLACEHOLDER: "Enter your default AI vision model here. e.g.: gpt-4-vision-preview",
|
||||
AI_OPENAI_DEFAULT_VISION_MODEL_PLACEHOLDER: "Enter your default AI vision model here. e.g.: gpt-4o",
|
||||
SAVING_HEAD: "Saving",
|
||||
SAVING_DESC: "In the 'Saving' section of Excalidraw Settings, you can configure how your drawings are saved. This includes options for compressing Excalidraw JSON in Markdown, setting autosave intervals for both desktop and mobile, defining filename formats, and choosing whether to use the .excalidraw.md or .md file extension. ",
|
||||
COMPRESS_NAME: "Compress Excalidraw JSON in Markdown",
|
||||
@@ -554,7 +575,7 @@ FILENAME_HEAD: "Filename",
|
||||
"If turned off, the exported image will be transparent.",
|
||||
EXPORT_PADDING_NAME: "Image Padding",
|
||||
EXPORT_PADDING_DESC:
|
||||
"The padding (in pixels) around the exported SVG or PNG image. " +
|
||||
"The padding (in pixels) around the exported SVG or PNG image. Padding is set to 0 for clippedFrame references." +
|
||||
"If you have curved lines close to the edge of the image they might get cropped during image export. You can increase this value to avoid cropping. " +
|
||||
"You can also override this setting at a file level by adding the <code>excalidraw-export-padding: 5<code> frontmatter key.",
|
||||
EXPORT_THEME_NAME: "Export image with theme",
|
||||
@@ -586,13 +607,15 @@ FILENAME_HEAD: "Filename",
|
||||
"Double files will be exported both if auto-export SVG or PNG (or both) are enabled, as well as when clicking export on a single image.",
|
||||
COMPATIBILITY_HEAD: "Compatibility features",
|
||||
COMPATIBILITY_DESC: "You should only enable these features if you have a strong reason for wanting to work with excalidraw.com files instead of markdown files. Many of the plugin features are not supported on legacy files. Typical usecase would be if you use set your vault up on top of a Visual Studio Code project folder and you have .excalidraw drawings you want to access from Visual Studio Code as well. Another usecase might be using Excalidraw in Logseq and Obsidian in parallel.",
|
||||
DUMMY_TEXT_ELEMENT_LINT_SUPPORT_NAME: "Insert dummy first text element to support linting",
|
||||
DUMMY_TEXT_ELEMENT_LINT_SUPPORT_NAME: "Linter compatibility",
|
||||
DUMMY_TEXT_ELEMENT_LINT_SUPPORT_DESC: "Excalidraw is sensitive to the file structure below <code># Excalidraw Data</code>. Automatic linting of documents can create errors in Excalidraw Data. " +
|
||||
"While I've made some effort to make the data loading resilient to " +
|
||||
"lint changes, this solution is not foolproof.<br><mark>The best is to avoid liniting or otherwise automatically changing Excalidraw documents using different plugins.</mark><br>" +
|
||||
"Use this setting if for good reasons you have decided to ignore my recommendation and configured linting of Excalidraw files.<br> " +
|
||||
"The <code>## Text Elements</code> section is sensitive to empty lines. A common linting approach is to add an empty line after section headings. In case of Excalidraw this will break/change the first text element in your drawing. " +
|
||||
"To overcome this, you can enable this setting. When enabled, Excalidraw will add a dummy element to the beginning of <code>## Text Elements</code> that the linter can safely modify." ,
|
||||
PRESERVE_TEXT_AFTER_DRAWING_NAME: "Zotero compatibility",
|
||||
PRESERVE_TEXT_AFTER_DRAWING_DESC: "Preserve text after the ## Drawing section of the markdown file. This may have a very slight performance impact when saving very large drawings.",
|
||||
DEBUGMODE_NAME: "Enable debug messages",
|
||||
DEBUGMODE_DESC: "I recommend restarting Obsidian after enabling/disabling this setting. This enable debug messages in the console. This is useful for troubleshooting issues. " +
|
||||
"If you are experiencing problems with the plugin, please enable this setting, reproduce the issue, and include the console log in the issue you raise on <a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/issues'>GitHub</a>",
|
||||
@@ -624,7 +647,7 @@ FILENAME_HEAD: "Filename",
|
||||
LATEX_DEFAULT_DESC: "Leave empty if you don't want a default formula. You can add default formatting here such as <code>\\color{white}</code>.",
|
||||
NONSTANDARD_HEAD: "Non-Excalidraw.com supported features",
|
||||
NONSTANDARD_DESC: `These settings in the "Non-Excalidraw.com Supported Features" section provide customization options beyond the default Excalidraw.com features. These features are not available on excalidraw.com. When exporting the drawing to Excalidraw.com these features will appear different.
|
||||
You can configure the number of custom pens displayed next to the Obsidian Menu on the canvas, allowing you to choose from a range of options. Additionally, you can enable a fourth font option, which adds a fourth font button to the properties panel for text elements. `,
|
||||
You can configure the number of custom pens displayed next to the Obsidian Menu on the canvas, allowing you to choose from a range of options. Additionally, you can enable a local font option, which adds a local font to the list of fonts on the element properties panel for text elements. `,
|
||||
RENDER_TWEAK_HEAD: "Rendering tweaks",
|
||||
MAX_IMAGE_ZOOM_IN_NAME: "Maximum image zoom in resolution",
|
||||
MAX_IMAGE_ZOOM_IN_DESC: "To save on memory and because Apple Safari (Obsidian on iOS) has some hard-coded limitations, Excalidraw.com limits the max resolution of images and large objects when zooming in. You can override this limitation using a multiplicator. " +
|
||||
@@ -669,16 +692,21 @@ FILENAME_HEAD: "Filename",
|
||||
"fades out. The text is still there, but the visual clutter is reduced. Note, you can place the %% in the line right above # Text Elements, " +
|
||||
"in this case the entire drawing markdown will fade out including # Text Elements. The side effect is you won't be able to block reference text in other markdown notes, that is after the %% comment section. This is seldom an issue. " +
|
||||
"Should you want to edit the Excalidraw markdown script, simply switch to markdown view mode and temporarily remove the %% comment.",
|
||||
CUSTOM_FONT_HEAD: "Fourth font",
|
||||
ENABLE_FOURTH_FONT_NAME: "Enable fourth font option",
|
||||
EXCALIDRAW_PROPERTIES_NAME: "Load Excalidraw Properties into Obsidian Suggester",
|
||||
EXCALIDRAW_PROPERTIES_DESC: "Toggle this setting to load Excalidraw document properties into Obsidian's property suggester at plugin startup. "+
|
||||
"Enabling this feature simplifies the use of Excalidraw front matter properties, allowing you to leverage many powerful settings. If you prefer not to load these properties automatically, " +
|
||||
"you can disable this feature, but you will need to manually remove any unwanted properties from the suggester. " +
|
||||
"Note that turning on this setting requires restarting the plugin as properties are loaded at startup.",
|
||||
CUSTOM_FONT_HEAD: "Local font",
|
||||
ENABLE_FOURTH_FONT_NAME: "Enable local font option",
|
||||
ENABLE_FOURTH_FONT_DESC:
|
||||
"By turning this on, you will see a fourth font button on the properties panel for text elements. " +
|
||||
"Files that use this fourth font will (partly) lose their platform independence. " +
|
||||
"By turning this on, you will see a local font in the font list on the properties panel for text elements. " +
|
||||
"Files that use this local font will (partly) lose their platform independence. " +
|
||||
"Depending on the custom font set in settings, they will look differently when loaded in another vault, or at a later time. " +
|
||||
"Also the 4th font will display as system default font on excalidraw.com, or other Excalidraw versions.",
|
||||
FOURTH_FONT_NAME: "Fourth font file",
|
||||
FOURTH_FONT_NAME: "Local font file",
|
||||
FOURTH_FONT_DESC:
|
||||
"Select a .ttf, .woff or .woff2 font file from your vault to use as the fourth font. " +
|
||||
"Select a .ttf, .woff or .woff2 font file from your vault to use as the local font. " +
|
||||
"If no file is selected, Excalidraw will use the Virgil font by default.",
|
||||
SCRIPT_SETTINGS_HEAD: "Settings for installed Scripts",
|
||||
SCRIPT_SETTINGS_DESC: "Some of the Excalidraw Automate Scripts include settings. Settings are organized by script. Settings will only become visible in this list after you have executed the newly downloaded script once.",
|
||||
@@ -800,4 +828,11 @@ FILENAME_HEAD: "Filename",
|
||||
INTERNAL_DRAG_ACTION: "Obsidian Internal Drag Action",
|
||||
PANE_TARGET: "Link click behavior",
|
||||
DEFAULT_ACTION_DESC: "In case none of the combinations apply the default action for this group is: ",
|
||||
|
||||
//FrameSettings.ts
|
||||
FRAME_SETTINGS_TITLE: "Frame Settings",
|
||||
FRAME_SETTINGS_ENABLE: "Enable Frames",
|
||||
FRAME_SETTIGNS_NAME: "Display Frame Name",
|
||||
FRAME_SETTINGS_OUTLINE: "Display Frame Outline",
|
||||
FRAME_SETTINGS_CLIP: "Enable Frame Clipping",
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ export default {
|
||||
CONVERT_URL_TO_FILE: "从 URL 下载图像到本地",
|
||||
UNZIP_CURRENT_FILE: "解压当前 Excalidraw 文件",
|
||||
ZIP_CURRENT_FILE: "压缩当前 Excalidraw 文件",
|
||||
PUBLISH_SVG_CHECK: "Obsidian Publish: 搜索过期的 SVG 和 PNG 导出文件",
|
||||
PUBLISH_SVG_CHECK: "Obsidian Publish:搜索过期的 SVG 和 PNG 导出文件",
|
||||
EMBEDDABLE_PROPERTIES: "Embeddable 元素设置",
|
||||
EMBEDDABLE_RELATIVE_ZOOM: "使元素的缩放等级等于当前画布的缩放等级",
|
||||
OPEN_IMAGE_SOURCE: "打开 Excalidraw 绘图文件",
|
||||
@@ -50,15 +50,18 @@ export default {
|
||||
NEW_IN_POPOUT_WINDOW_EMBED: "新建绘图 - 于新窗口 - 并将其嵌入(形如 ![[drawing]])到当前 Markdown 文档中",
|
||||
TOGGLE_LOCK: "文本元素:原文模式(RAW)⟺ 预览模式(PREVIEW)",
|
||||
DELETE_FILE: "从库中删除所选图像(或以图像形式嵌入绘图中的 Markdown)的源文件",
|
||||
COPY_ELEMENT_LINK: "复制所选元素的 Markdown 链接",
|
||||
COPY_ELEMENT_LINK: "复制所选元素的链接(形如 [[file#^id]]])",
|
||||
COPY_DRAWING_LINK: "复制绘图的嵌入链接(形如 ![[darwing]])",
|
||||
INSERT_LINK_TO_ELEMENT:
|
||||
`复制所选元素为内部链接(形如 [[file#^id]] )。\n按住 ${labelCTRL()} 可复制元素所在分组为内部链接(形如 [[file#^group=id]] )。\n按住 ${labelSHIFT()} 可复制所选元素所在区域为内部链接(形如 [[file#^area=id]] )。\n按住 ${labelALT()} 可观看视频演示。`,
|
||||
INSERT_LINK_TO_ELEMENT_GROUP:
|
||||
"复制所选元素所在分组为内部链接(形如 [[file#^group=id]] )",
|
||||
"复制所选元素所在分组为嵌入链接(形如 ![[file#^group=id]] )",
|
||||
INSERT_LINK_TO_ELEMENT_AREA:
|
||||
"复制所选元素所在区域为内部链接(形如 [[file#^area=id]] )",
|
||||
"复制所选元素所在区域为嵌入链接(形如 ![[file#^area=id]] )",
|
||||
INSERT_LINK_TO_ELEMENT_FRAME:
|
||||
"复制所选框架为内部链接(形如 [[file#^frame=id]] )",
|
||||
"复制所选框架为嵌入链接(形如 ![[file#^frame=id]] )",
|
||||
INSERT_LINK_TO_ELEMENT_FRAME_CLIPPED:
|
||||
"复制所选框架(内容)为嵌入链接(形如 ![[file#^clippedframe=id]] )",
|
||||
INSERT_LINK_TO_ELEMENT_NORMAL:
|
||||
"复制所选元素为内部链接(形如 [[file#^id]] )",
|
||||
INSERT_LINK_TO_ELEMENT_ERROR: "未选择画布里的单个元素",
|
||||
@@ -72,14 +75,16 @@ export default {
|
||||
INSERT_PDF: "插入 PDF 文档(以图像形式嵌入)到当前绘图中",
|
||||
UNIVERSAL_ADD_FILE: "插入任意文件(以交互形式嵌入,或者以图像形式嵌入)到当前绘图中",
|
||||
INSERT_CARD: "插入“背景笔记”卡片",
|
||||
CONVERT_CARD_TO_FILE: "将“背景笔记”卡片保存到文件",
|
||||
ERROR_TRY_AGAIN: "请重试。",
|
||||
PASTE_CODEBLOCK: "粘贴代码块",
|
||||
INSERT_LATEX:
|
||||
`插入 LaTeX 公式到当前绘图`,
|
||||
ENTER_LATEX: "输入 LaTeX 表达式",
|
||||
READ_RELEASE_NOTES: "阅读本插件的更新说明",
|
||||
RUN_OCR: "OCR 完整画布: 识别涂鸦和图片里的文本并复制到剪贴板和文档属性中",
|
||||
RERUN_OCR: "重新运行 OCR 完整画笔: 识别涂鸦和图片里的文本并复制到剪贴板和文档属性中",
|
||||
RUN_OCR_ELEMENTS: "OCR 选中的元素: 识别涂鸦和图片里的文本并复制到剪贴板",
|
||||
RUN_OCR: "OCR 完整画布:识别涂鸦和图片里的文本并复制到剪贴板和文档属性中",
|
||||
RERUN_OCR: "重新运行 OCR 完整画笔:识别涂鸦和图片里的文本并复制到剪贴板和文档属性中",
|
||||
RUN_OCR_ELEMENTS: "OCR 选中的元素:识别涂鸦和图片里的文本并复制到剪贴板",
|
||||
TRAY_MODE: "绘图工具属性页:面板模式 ⟺ 托盘模式",
|
||||
SEARCH: "搜索文本",
|
||||
CROP_PAGE: "对所选页面裁剪并添加蒙版",
|
||||
@@ -91,11 +96,28 @@ export default {
|
||||
TEMPORARY_ENABLE_AUTOSAVE: "启用自动保存功能",
|
||||
|
||||
//ExcalidrawView.ts
|
||||
FORCE_SAVE_ABORTED: "自动保存被中止,因为文件正在保存中",
|
||||
LINKLIST_SECOND_ORDER_LINK: "二级链接",
|
||||
MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT_TITLE: "自定义链接",
|
||||
MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT: "不要在文件名周围添加[[方括号(Wiki 格式链接)]]!<br>编辑链接时请遵循以下格式:<br><mark>文件名#^块引用|宽度x最大高度</mark>",
|
||||
FRAME_CLIPPING_ENABLED: "渲染框架:已启用",
|
||||
FRAME_CLIPPING_DISABLED: "渲染框架:已禁用",
|
||||
ARROW_BINDING_INVERSE_MODE: "反转模式:默认方向按键已禁用。需要时请使用 Ctrl/CMD 临时启用。",
|
||||
ARROW_BINDING_NORMAL_MODE: "正常模式:方向键已启用。需要时请使用 Ctrl/CMD 临时禁用。",
|
||||
EXPORT_FILENAME_PROMPT: "请提供文件名",
|
||||
EXPORT_FILENAME_PROMPT_PLACEHOLDER: "请输入文件名,留空以取消操作",
|
||||
WARNING_SERIOUS_ERROR: "警告:Excalidraw 遇到了未知的问题!\n\n" +
|
||||
"您最近的更改可能无法保存。\n\n" +
|
||||
"为了安全起见,请按以下步骤操作:\n" +
|
||||
"1) 使用 Ctrl/CMD+A 选择您的绘图,然后使用 Ctrl/CMD+C 进行复制。\n" +
|
||||
"2) 然后在新窗格中,通过 Ctrl/CMD 点击 Excalidraw 功能区按钮创建一个空白绘图。\n" +
|
||||
"3) 最后,使用 Ctrl/CMD+V 将您的作品粘贴到新文档中。",
|
||||
ARIA_LABEL_TRAY_MODE: "托盘模式提供了一个更宽敞的画布空间",
|
||||
MASK_FILE_NOTICE: "这是一个蒙版图像。长按本提示来观看视频讲解。",
|
||||
INSTALL_SCRIPT_BUTTON: "安装或更新 Excalidraw 脚本",
|
||||
OPEN_AS_MD: "打开为 Markdown 文档",
|
||||
EXPORT_IMAGE: `导出为图像`,
|
||||
OPEN_LINK: "打开所选元素里的链接 \n(按住 SHIFT 在新面板打开)",
|
||||
OPEN_LINK: "打开所选元素里的链接 \n(按住 Shift 在新面板打开)",
|
||||
EXPORT_EXCALIDRAW: "导出为 .excalidraw 文件(旧版绘图文件格式)",
|
||||
LINK_BUTTON_CLICK_NO_TEXT:
|
||||
"请选择一个含有链接的图形或文本元素。",
|
||||
@@ -117,6 +139,7 @@ export default {
|
||||
ERROR_SAVING_IMAGE: "获取图像时发生未知错误。可能是由于某种原因,图像不可用或拒绝了 Obsidian 的获取请求。",
|
||||
WARNING_PASTING_ELEMENT_AS_TEXT: "你不能将 Excalidraw 元素粘贴为文本元素!",
|
||||
USE_INSERT_FILE_MODAL: "使用“插入任意文件”功能来嵌入 Markdown 文档",
|
||||
RECURSIVE_INSERT_ERROR: "你不能将图像的一部分嵌入到此图像中,因为这可能导致无限循环。",
|
||||
CONVERT_TO_MARKDOWN: "转存为 Markdown 文档(并嵌入为 MD-Embeddable)",
|
||||
SELECT_TEXTELEMENT_ONLY: "只选择文本元素(非容器)",
|
||||
REMOVE_LINK: "移除文字元素链接",
|
||||
@@ -154,7 +177,7 @@ export default {
|
||||
"剪贴图像时创建新绘图的默认存储路径。如果留空,将按照 Vault 附件设置创建。",
|
||||
ANNOTATE_FOLDER_NAME: "图片标注文件文件夹",
|
||||
ANNOTATE_FOLDER_DESC:
|
||||
"创建图片标注是的默认存储路径. 如果留空, 将按照 Vault 附件设置创建。",
|
||||
"创建图片标注是的默认存储路径。如果留空,将按照 Vault 附件设置创建。",
|
||||
FOLDER_EMBED_NAME:
|
||||
"将 Excalidraw 文件夹用于“新建绘图”系列命令",
|
||||
FOLDER_EMBED_DESC:
|
||||
@@ -164,12 +187,12 @@ export default {
|
||||
TEMPLATE_NAME: "Excalidraw 模板文件",
|
||||
TEMPLATE_DESC:
|
||||
"Excalidraw 模板文件(文件夹)的存储路径。<br>" +
|
||||
"<b>模板文件:</b>比如.: 如果您的模板在默认的 Excalidraw 文件夹中且文件名是 " +
|
||||
"Template.md, 则此项应设为 Excalidraw/Template.md(也可省略 .md 扩展名,即 Excalidraw/Template)。" +
|
||||
"<b>模板文件:</b>比如:如果您的模板在默认的 Excalidraw 文件夹中且文件名是 " +
|
||||
"Template.md,则此项应设为 Excalidraw/Template.md(也可省略 .md 扩展名,即 Excalidraw/Template)。" +
|
||||
"如果您在兼容模式下使用 Excalidraw,那么您的模板文件也必须是旧的 *.excalidraw 格式," +
|
||||
"例如 Excalidraw/Template.excalidraw. <br><b>模板文件夹:</b> 你还可以将文件夹设置为模板。 " +
|
||||
"例如 Excalidraw/Template.excalidraw。<br><b>模板文件夹:</b> 你还可以将文件夹设置为模板。 " +
|
||||
"在这种情况下,创建新绘图时将提示您选择使用哪个模板。<br>" +
|
||||
"<b>专业提示:</b> 如果您正在使用 Obsidian Templater 插件,您可以 将Templater 代码添加到不同的" +
|
||||
"<b>专业提示:</b> 如果您正在使用 Obsidian Templater 插件,您可以将 Templater 代码添加到不同的" +
|
||||
"Excalidraw 模板中,以自动配置您的绘图",
|
||||
SCRIPT_FOLDER_NAME: "Excalidraw 自动化脚本的文件夹(大小写敏感!)",
|
||||
SCRIPT_FOLDER_DESC:
|
||||
@@ -207,7 +230,7 @@ export default {
|
||||
"默认的 OpenAI API URL。请填写有效的 OpenAI API URL。" +
|
||||
"Excalidraw 会通过该 URL 发送 API 请求给 OpenAI。我没有对此选项做任何错误处理,请谨慎修改。",
|
||||
AI_OPENAI_DEFAULT_IMAGE_API_URL_NAME: "OpenAI 图像生成 API URL",
|
||||
AI_OPENAI_DEFAULT_VISION_MODEL_PLACEHOLDER: "gpt-4-vision-preview",
|
||||
AI_OPENAI_DEFAULT_VISION_MODEL_PLACEHOLDER: "gpt-4o",
|
||||
SAVING_HEAD: "保存",
|
||||
SAVING_DESC: "包括:压缩,自动保存的时间间隔,文件的命名格式和扩展名等的设置。",
|
||||
COMPRESS_NAME: "压缩 Excalidraw JSON",
|
||||
@@ -225,7 +248,7 @@ export default {
|
||||
DECOMPRESS_FOR_MD_DESC:
|
||||
"通过启用此功能,Excalidraw 将在切换到 Markdown 视图时自动解压缩绘图 JSON。" +
|
||||
"这将使您能够轻松阅读和编辑 JSON 字符串。" +
|
||||
"一旦您切换回Excalidraw视图并保存绘图(CTRL+S),绘图将再次被压缩。<br>" +
|
||||
"一旦您切换回 Excalidraw 视图并保存绘图(Ctrl+S),绘图将再次被压缩。<br>" +
|
||||
"我建议关闭此功能,因为这可以获得更小的文件尺寸,并避免在 Obsidian 搜索中出现不必要的结果。 " +
|
||||
"您始终可以使用命令面板中的“Excalidraw: 解压缩当前 Excalidraw 文件”命令"+
|
||||
"在需要阅读或编辑时手动解压缩绘图 JSON。",
|
||||
@@ -278,7 +301,7 @@ FILENAME_HEAD: "文件名",
|
||||
IFRAME_MATCH_THEME_DESC:
|
||||
"<b>开启:</b>当 Obsidian 和 Excalidraw 一个使用黑暗主题、一个使用明亮主题时," +
|
||||
"开启此项后,以交互形式嵌入到绘图中的元素(Embeddable) 将会匹配 Excalidraw 主题。<br>" +
|
||||
"<b>关闭:</b>如果您想要 Embeddable 匹配 Obsidian 主题,请关闭此项。",
|
||||
"<b>关闭:</b>如果您想要 Embeddable 匹配 Obsidian 主题,请关闭此项。",
|
||||
MATCH_THEME_NAME: "使新建的绘图匹配 Obsidian 主题",
|
||||
MATCH_THEME_DESC:
|
||||
"如果 Obsidian 使用黑暗主题,新建的绘图文件也将使用黑暗主题。<br>" +
|
||||
@@ -302,19 +325,19 @@ FILENAME_HEAD: "文件名",
|
||||
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_NAME: "在触控笔模式下显示十字准星(+)",
|
||||
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_DESC:
|
||||
"在触控笔模式下使用涂鸦功能会显示十字准星 <b><u>打开:</u></b> 显示 <b><u>关闭:</u></b> 隐藏<br>"+
|
||||
"效果取决于设备。十字准星通常在绘图板、MS Surface 上可见,但在iOS上不可见。",
|
||||
"效果取决于设备。十字准星通常在绘图板、MS Surface 上可见。但在 iOS 上不可见。",
|
||||
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_NAME: "在 Markdown 文件的悬停预览中渲染为图片",
|
||||
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_DESC:
|
||||
"这个设置影响 frontmatter 中具有 <b>excalidraw-open-md: true</b> 的文件.",
|
||||
"这个设置影响 frontmatter 中具有 <b>excalidraw-open-md: true</b> 的文件。",
|
||||
SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME: "在 Markdown 文件阅读模式下渲染为图片",
|
||||
SHOW_DRAWING_OR_MD_IN_READING_MODE_DESC:
|
||||
"必须关闭活动的 Excalidraw/Markdown 文件,然后重新打开才能使此更改生效。<br>当您处于 Markdown 阅读模式(即阅读 Excalidraw 的背景笔记)时 Excalidraw 绘图是否应该呈现为图像? " +
|
||||
"此设置不会影响您处于 Excalidraw 模式时的绘图显示,也不会影响将绘图嵌入到 Markdown 文档中或在渲染悬停预览时的显示。<br><ul>" +
|
||||
"<li>看下面的“嵌入和导出”中的<b>PDF导出</b>的其他相关设置。</li>" +
|
||||
"<li>看下面的“嵌入和导出”中的 <b>PDF 导出</b>的其他相关设置。</li>" +
|
||||
"<li>请务必查看“其他功能”部分中的<b>淡化设置</b>。</li></ul>",
|
||||
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME: "在 Markdown 模式下导出为 PDF 时渲染为图片",
|
||||
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_DESC:
|
||||
"必须关闭活动的 Excalidraw/Markdown 文件,然后重新打开才能使此更改生效。<br>当您处于 Markdown 阅读模式(即阅读 Excalidraw 的背景笔记)时将笔记导出为 PDF, Excalidraw 绘图是否应该呈现为图像? <br><ul>" +
|
||||
"必须关闭活动的 Excalidraw/Markdown 文件,然后重新打开才能使此更改生效。<br>当您处于 Markdown 阅读模式(即阅读 Excalidraw 的背景笔记)时将笔记导出为 PDF,Excalidraw 绘图是否应该呈现为图像? <br><ul>" +
|
||||
"<li>查看上面“外观和行为”下的 <b>Markdown 阅读模式</b>的其他相关设置。</li>" +
|
||||
"<li>请务必查看“其他功能”部分中的<b>淡化设置</b>。</li></ul>",
|
||||
THEME_HEAD: "主题和样式",
|
||||
@@ -322,7 +345,7 @@ FILENAME_HEAD: "文件名",
|
||||
DEFAULT_PINCHZOOM_NAME: "允许在触控笔模式下进行双指缩放",
|
||||
DEFAULT_PINCHZOOM_DESC:
|
||||
"在触控笔模式下使用自由画笔工具时,双指缩放可能造成干扰。<br>" +
|
||||
"<b>开启: </b>允许在触控笔模式下进行双指缩放<br><b>关闭: </b>禁止在触控笔模式下进行双指缩放",
|
||||
"<b>开启::</b>允许在触控笔模式下进行双指缩放<br><b>关闭: </b>禁止在触控笔模式下进行双指缩放",
|
||||
|
||||
DEFAULT_WHEELZOOM_NAME: "鼠标滚轮缩放页面",
|
||||
DEFAULT_WHEELZOOM_DESC:
|
||||
@@ -348,7 +371,7 @@ FILENAME_HEAD: "文件名",
|
||||
LINKS_DESC:
|
||||
`按住 ${labelCTRL()} 并点击包含 <code>[[链接]]</code> 的文本元素可以打开其中的链接。` +
|
||||
"如果所选文本元素包含多个 <code>[[有效的内部链接]]</code> ,只会打开第一个链接;" +
|
||||
"如果所选文本元素包含有效的 URL 链接 (如 <code>https://</code> 或 <code>http://</code>)," +
|
||||
"如果所选文本元素包含有效的 URL 链接(如 <code>https://</code> 或 <code>http://</code>)," +
|
||||
"插件会在浏览器中打开链接。<br>" +
|
||||
"链接的源文件被重命名时,绘图中相应的 <code>[[内部链接]]</code> 也会同步更新。" +
|
||||
"若您不愿绘图中的链接外观因此而变化,可使用 <code>[[内部链接|别名]]</code>。",
|
||||
@@ -357,7 +380,7 @@ FILENAME_HEAD: "文件名",
|
||||
"Excalidraw 不会检查您的设置是否合理,因此请谨慎设置,避免冲突。" +
|
||||
"以下选项在苹果和非苹果设备上区别很大,如果您在多个硬件平台上使用 Obsidian,需要分别进行设置。"+
|
||||
"选项里的 4 个开关依次代表 " +
|
||||
(DEVICE.isIOS || DEVICE.isMacOS ? "SHIFT, CMD, OPT, CONTROL." : "SHIFT, CTRL, ALT, META (Win 键)。"),
|
||||
(DEVICE.isIOS || DEVICE.isMacOS ? "Shift, CMD, OPT, CONTROL." : "Shift, Ctrl, Alt, META (Win 键)。"),
|
||||
LONG_PRESS_DESKTOP_NAME: "长按打开(电脑端)",
|
||||
LONG_PRESS_DESKTOP_DESC: "长按(以毫秒为单位)打开在 Markdown 文件中嵌入的 Excalidraw 绘图。",
|
||||
LONG_PRESS_MOBILE_NAME: "长按打开(移动端)",
|
||||
@@ -379,7 +402,7 @@ FILENAME_HEAD: "文件名",
|
||||
MAINWORKSPACE_PANE_NAME: "在主工作区中打开",
|
||||
MAINWORKSPACE_PANE_DESC:
|
||||
`按住 ${labelCTRL()}+${labelSHIFT()} 并点击绘图里的内部链接时,插件默认会在当前窗口的新面板中打开该链接。<br>` +
|
||||
"若开启此项,Excalidraw 会在主工作区的面板中打开该链接。",
|
||||
"若开启此项,Excalidraw 会在主工作区的面板中打开该链接。",
|
||||
LINK_BRACKETS_NAME: "在链接的两侧显示 <code>[[中括号]]</code>",
|
||||
LINK_BRACKETS_DESC: `${
|
||||
"文本元素处于预览(PREVIEW)模式时,在内部链接的两侧显示中括号。<br>" +
|
||||
@@ -404,7 +427,7 @@ FILENAME_HEAD: "文件名",
|
||||
HOVERPREVIEW_NAME: "鼠标悬停预览内部链接",
|
||||
HOVERPREVIEW_DESC:
|
||||
`<b>开启:</b>在 Excalidraw <u>阅读模式(View)</u>下,鼠标悬停在 <code>[[内部链接]]</code> 上即可预览;` +
|
||||
"而在<u>普通模式(Normal)</u>下, 鼠标悬停在内部链接右上角的蓝色标识上即可预览。<br> " +
|
||||
"而在<u>普通模式(Normal)</u>下,鼠标悬停在内部链接右上角的蓝色标识上即可预览。<br> " +
|
||||
`<b>关闭:</b>鼠标悬停在 <code>[[内部链接]]</code> 上,并且按住 ${labelCTRL()} 才能预览。`,
|
||||
LINKOPACITY_NAME: "链接标识的透明度",
|
||||
LINKOPACITY_DESC:
|
||||
@@ -447,12 +470,12 @@ FILENAME_HEAD: "文件名",
|
||||
MD_TRANSCLUDE_WIDTH_DESC:
|
||||
"MD-Embed 的宽度。该选项会影响到折行,以及图像元素的宽度。<br>" +
|
||||
"您可为绘图中的某个 MD-Embed 单独设置此项,方法是将绘图切换至 Markdown 模式," +
|
||||
"并修改相应的 <code>[[Embed文件名#标题|宽度x最大高度]]</code>。",
|
||||
"并修改相应的 <code>[[Embed 文件名#标题|宽度x最大高度]]</code>。",
|
||||
MD_TRANSCLUDE_HEIGHT_NAME:
|
||||
"MD-Embed 的默认最大高度",
|
||||
MD_TRANSCLUDE_HEIGHT_DESC:
|
||||
"MD-Embed 的高度取决于 Markdown 文档内容的多少,但最大不会超过该值。<br>" +
|
||||
"您可为绘图中的某个 MD-Embed 单独设置此项,方法是将绘图切换至 Markdown 模式,并修改相应的 <code>[[Embed文件名#^块引ID|宽度x最大高度]]</code>。",
|
||||
"您可为绘图中的某个 MD-Embed 单独设置此项,方法是将绘图切换至 Markdown 模式,并修改相应的 <code>[[Embed 文件名#^块引ID|宽度x最大高度]]</code>。",
|
||||
MD_DEFAULT_FONT_NAME:
|
||||
"MD-Embed 的默认字体",
|
||||
MD_DEFAULT_FONT_DESC:
|
||||
@@ -473,11 +496,11 @@ FILENAME_HEAD: "文件名",
|
||||
MD_CSS_DESC:
|
||||
"MD-Embed 图像所采用的 CSS 样式表文件名。需包含扩展名,例如 md-embed.css。" +
|
||||
"允许使用 Markdown 文件(如 md-embed-css.md),但其内容应符合 CSS 语法。<br>" +
|
||||
"如果您要查询 CSS 所作用的 HTML 节点,请在 Obsidian 开发者控制台(CTRL+SHIFT+i)中键入命令:" +
|
||||
"如果您要查询 CSS 所作用的 HTML 节点,请在 Obsidian 开发者控制台(Ctrl+Shift+I)中键入命令:" +
|
||||
"<code>ExcalidrawAutomate.mostRecentMarkdownSVG</code> —— 这将显示 Excalidraw 最近生成的 SVG。<br>" +
|
||||
"此外,在 CSS 中不能任意地设置字体,您一般只能使用系统默认的标准字体(详见 README)," +
|
||||
"但可以通过上面的设置来额外添加一个自定义字体。<br>" +
|
||||
"您可为某个 MD-Embed 单独设置此项,方法是在其源文件的 frontmatter 中添加形如 <code>excalidraw-css: 库中的CSS文件或CSS片段</code> 的键值对。",
|
||||
"您可为某个 MD-Embed 单独设置此项,方法是在其源文件的 frontmatter 中添加形如 <code>excalidraw-css: 库中的 CSS 文件或 CSS 片段</code> 的键值对。",
|
||||
EMBED_HEAD: "嵌入到 Markdown 文档中的绘图",
|
||||
EMBED_DESC: `包括:嵌入到 Markdown 文档中的绘图的预览图类型(SVG、PNG)、源文件类型(Excalidraw 绘图文件、SVG、PNG)、缓存、图像大小、图像主题,以及嵌入的语法等。
|
||||
此外,还有自动导出 SVG 或 PNG 文件并保持与绘图文件状态同步的设置。`,
|
||||
@@ -516,7 +539,7 @@ FILENAME_HEAD: "文件名",
|
||||
EMBED_PREVIEW_IMAGETYPE_DESC:
|
||||
"<b>Native SVG:</b>高品质、可交互。<br>" +
|
||||
"<b>SVG:</b>高品质、不可交互。<br>" +
|
||||
"<b>PNG:</b>高性能、<a href='https://www.youtube.com/watch?v=yZQoJg2RCKI&t=633s' target='_blank'>不可交互</a>。",
|
||||
"<b>PNG:</b>高性能、<a href='https://www.youtube.com/watch?v=yZQoJg2RCKI&t=633s' target='_blank'>不可交互</a>。",
|
||||
PREVIEW_MATCH_OBSIDIAN_NAME: "预览图匹配 Obsidian 主题",
|
||||
PREVIEW_MATCH_OBSIDIAN_DESC:
|
||||
"开启此项,则当 Obsidian 处于黑暗模式时,嵌入到 Markdown 文档中的绘图的预览图也会以黑暗模式渲染;当 Obsidian 处于明亮模式时,预览图也会以明亮模式渲染。<br>" +
|
||||
@@ -528,7 +551,7 @@ FILENAME_HEAD: "文件名",
|
||||
"方法是修改相应的内部链接格式为形如 <code>![[drawing.excalidraw|100]]</code> 或 <code>[[drawing.excalidraw|100x100]]</code>。",
|
||||
EMBED_HEIGHT_NAME: "预览图的默认高度",
|
||||
EMBED_HEIGHT_DESC:
|
||||
"嵌入到 Markdown 文档中的绘图的预览图得默认高度。该选项也适用于实时预览编辑和阅读模式,以及悬停预览。" +
|
||||
"嵌入到 Markdown 文档中的绘图的预览图得默认高度。该选项也适用于实时预览编辑和阅读模式,以及悬停预览。" +
|
||||
"您可以在使用 <code>![[drawing.excalidraw|100]]</code> 或者 <code>[[drawing.excalidraw|100x100]]</code>" +
|
||||
"格式在嵌入图像时指定自定义高度。",
|
||||
EMBED_TYPE_NAME: "“嵌入绘图到当前 Markdown 文档中”系列命令的源文件类型",
|
||||
@@ -540,7 +563,7 @@ FILENAME_HEAD: "文件名",
|
||||
EMBED_MARKDOWN_COMMENT_NAME: "将链接作为注释嵌入",
|
||||
EMBED_MARKDOWN_COMMENT_DESC:
|
||||
"在图像下方以 Markdown 链接的形式嵌入原始 Excalidraw 文件的链接,例如:<code>%%[[drawing.excalidraw]]%%</code>。<br>" +
|
||||
"除了添加 Markdown 注释之外,您还可以选择嵌入的 SVG 或 PNG,并使用命令面板: " +
|
||||
"除了添加 Markdown 注释之外,您还可以选择嵌入的 SVG 或 PNG,并使用命令面板:" +
|
||||
"'<code>Excalidraw: 打开 Excalidraw 绘图</code>'来打开该绘图",
|
||||
EMBED_WIKILINK_NAME: "“嵌入绘图到当前 Markdown 文档中”系列命令产生的内部链接类型",
|
||||
EMBED_WIKILINK_DESC:
|
||||
@@ -552,7 +575,7 @@ FILENAME_HEAD: "文件名",
|
||||
"如果关闭,将导出透明背景的图像。",
|
||||
EXPORT_PADDING_NAME: "导出的图像的空白边距",
|
||||
EXPORT_PADDING_DESC:
|
||||
"导出的 SVG/PNG 图像四周的空白边距(单位:像素)。<br>" +
|
||||
"导出的 SVG/PNG 图像四周的空白边距(单位:像素)。对于裁剪框架引用,间距被设置为 0。<br>" +
|
||||
"增加该值,可以避免在导出图像时,靠近图像边缘的图形被裁掉。<br>" +
|
||||
"您可为某个绘图单独设置此项,方法是在其 frontmatter 中添加形如 <code>excalidraw-export-padding: 5<code> 的键值对。",
|
||||
EXPORT_THEME_NAME: "导出的图像匹配主题",
|
||||
@@ -584,13 +607,15 @@ FILENAME_HEAD: "文件名",
|
||||
"该选项可作用于“自动导出 SVG 副本”、“自动导出 PNG 副本”,以及其他的手动的导出命令。",
|
||||
COMPATIBILITY_HEAD: "兼容性设置",
|
||||
COMPATIBILITY_DESC: "如果没有特殊原因(例如您想同时在 VSCode / Logseq 和 Obsidian 中使用 Excalidraw),建议您使用 markdown 格式的绘图文件,而不是旧的 excalidraw.com 格式,因为本插件的很多功能在旧格式中无法使用。",
|
||||
DUMMY_TEXT_ELEMENT_LINT_SUPPORT_NAME: "插入虚拟的第一个文本元素以支持代码格式化工具(Linting)",
|
||||
DUMMY_TEXT_ELEMENT_LINT_SUPPORT_NAME: "代码格式化(Linting)兼容性",
|
||||
DUMMY_TEXT_ELEMENT_LINT_SUPPORT_DESC: "Excalidraw 对 <code># Excalidraw Data</code> 下的文件结构非常敏感。文档的自动代码格式化(linting)可能会在 Excalidraw 数据中造成错误。" +
|
||||
"虽然我已经努力使数据加载对自动代码格式化(linting)变更具有一定的抗性,但这种解决方案并非万无一失。<br>"+
|
||||
"<mark>最好的方法是避免使用不同的插件对 Excalidraw 文档进行自动更改。</mark><br>" +
|
||||
"如果出于某些合理的原因,您决定忽略我的建议并配置了 Excalidraw 文件的自动代码格式化,那么可以使用这个设置<br> " +
|
||||
"<code>## Text Elements</code> 部分对空行很敏感。一种常见的代码格式化是在章节标题后添加一个空行。但对于 Excalidraw 来说,这将破坏/改变您绘图中的第一个文本元素。" +
|
||||
"为了解决这个问题,您可以启用这个设置。启用后 Excalidraw 将在 <code>## Text Elements</code> 的开头添加一个虚拟元素,供自动代码格式化工具修改。" ,
|
||||
PRESERVE_TEXT_AFTER_DRAWING_NAME: "Zotero 兼容性",
|
||||
PRESERVE_TEXT_AFTER_DRAWING_DESC: "保留 Markdown 文件中 <code>## Drawing</code> 部分之后的文本内容。保存非常大的绘图时,这可能会造成微小的性能影响。",
|
||||
DEBUGMODE_NAME: "开启 debug 信息",
|
||||
DEBUGMODE_DESC: "我建议在启用/禁用此设置后重新启动 Obsidian。这将在控制台中启用调试消息。这对于排查问题很有帮助。" +
|
||||
"如果您在使用插件时遇到问题,请启用此设置,重现问题,并在 <a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/issues'>GitHub</a> 上提出的问题中包含控制台日志。",
|
||||
@@ -627,7 +652,7 @@ FILENAME_HEAD: "文件名",
|
||||
MAX_IMAGE_ZOOM_IN_NAME: "最大图像放大倍数",
|
||||
MAX_IMAGE_ZOOM_IN_DESC: "为了节省内存,并且因为 Apple Safari (Obsidian on iOS) 有一些硬编码的限制,Excalidraw.com 在放大时会限制图像和大型对象的最大分辨率。您可以使用乘数来覆盖这个限制。" +
|
||||
"这意味着将乘以 Excalidraw 默认设置的限制,乘数越大,图像放大分辨率就越高,但也会消耗更多内存。" +
|
||||
"我建议尝试多个值来设置这个参数。当您放大一个较大的 PNG 图像时,如果图像突然从视图中消失,那就说明您已经达到了极限。默认值为 1。此设置对 iOS 无效。",
|
||||
"我建议尝试多个值来设置这个参数。当您放大一个较大的 PNG 图像时,如果图像突然从视图中消失,那就说明您已经达到了极限。默认值为 1。此设置对 iOS 无效。",
|
||||
CUSTOM_PEN_HEAD: "自定义画笔",
|
||||
CUSTOM_PEN_NAME: "自定义画笔工具的数量",
|
||||
CUSTOM_PEN_DESC: "在画布上的 Obsidian 菜单按钮旁边切换自定义画笔。长按画笔按钮可以修改其样式。",
|
||||
@@ -665,8 +690,13 @@ FILENAME_HEAD: "文件名",
|
||||
FADE_OUT_EXCALIDRAW_MARKUP_NAME: "淡化 Excalidraw 标记",
|
||||
FADE_OUT_EXCALIDRAW_MARKUP_DESC: "在 Markdown 视图模式下,在 Markdown 注释 %% " +
|
||||
"之后的部分会淡化。文本仍然存在,但视觉杂乱感会减少。请注意,您可以将 %% 放在 # Text Elements 行的上一行," +
|
||||
"这样,整个 Excalidraw Markdown 都会淡化,包括 # Text Elements。 副作用是您将无法在其他 Markdown 笔记中引用文本块,即 %% 注释部分之后的内容。这应该不是大问题。" +
|
||||
"这样,整个 Excalidraw Markdown 都会淡化,包括 # Text Elements。 副作用是您将无法在其他 Markdown 笔记中引用文本块,即 %% 注释部分之后的内容。这应该不是大问题。" +
|
||||
"如果您想编辑 Excalidraw Markdown 脚本,只需切换到 Markdown 视图模式并暂时删除 %% 注释。",
|
||||
EXCALIDRAW_PROPERTIES_NAME: "将 Excalidraw 属性加载到 Obsidian 的自动提示中",
|
||||
EXCALIDRAW_PROPERTIES_DESC: "切换此设置以在插件启动时将 Excalidraw 文档属性加载到 Obsidian 的属性自动提示中。"+
|
||||
"启用此功能简化了 Excalidraw 前置属性的使用,使您能够利用许多强大的设置。如果您不希望自动加载这些属性," +
|
||||
"您可以禁用此功能,但您将需要手动从自动提示中移除任何不需要的属性。" +
|
||||
"请注意,启用此设置需要重启插件,因为属性是在启动时加载的。",
|
||||
CUSTOM_FONT_HEAD: "自定义字体",
|
||||
ENABLE_FOURTH_FONT_NAME: "为文本元素启用本地字体",
|
||||
ENABLE_FOURTH_FONT_DESC:
|
||||
@@ -676,7 +706,7 @@ FILENAME_HEAD: "文件名",
|
||||
"若在 excalidraw.com 或者其他版本的 Excalidraw 中打开,使用本地字体的文本会变回系统默认字体。",
|
||||
FOURTH_FONT_NAME: "本地字体文件",
|
||||
FOURTH_FONT_DESC:
|
||||
"选择库文件夹中的一个 .ttf, .woff 或 .woff2 字体文件作为本地字体文件。" +
|
||||
"选择库文件夹中的一个 .ttf,.woff 或 .woff2 字体文件作为本地字体文件。" +
|
||||
"若未选择文件,则使用默认的 Virgil 字体。",
|
||||
SCRIPT_SETTINGS_HEAD: "已安装脚本的设置",
|
||||
SCRIPT_SETTINGS_DESC: "有些 Excalidraw 自动化脚本包含设置项,当执行这些脚本时,它们会在该列表下添加设置项。",
|
||||
@@ -687,7 +717,7 @@ FILENAME_HEAD: "文件名",
|
||||
"注意,识别的过程不是在本地进行的,而是通过在线 API,图像会被上传到 taskbone 的服务器(仅用于识别目的)。如果您介意,请不要使用这个功能。",
|
||||
TASKBONE_ENABLE_NAME: "启用 Taskbone",
|
||||
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>.",
|
||||
"<a href='https://www.taskbone.com/legal/privacy/' target='_blank'>隐私政策</a>。",
|
||||
TASKBONE_APIKEY_NAME: "Taskbone API Key",
|
||||
TASKBONE_APIKEY_DESC: "Taskbone 的免费 API key 提供了一定数量的每月识别次数。如果您非常频繁地使用此功能,或者想要支持 " +
|
||||
"Taskbone 的开发者(您懂的,没有人能用爱发电,Taskbone 开发者也需要投入资金来维持这项 OCR 服务)您可以" +
|
||||
@@ -696,7 +726,7 @@ FILENAME_HEAD: "文件名",
|
||||
//openDrawings.ts
|
||||
SELECT_FILE: "选择一个文件后按回车。",
|
||||
SELECT_COMMAND: "选择一个命令后按回车。",
|
||||
SELECT_FILE_WITH_OPTION_TO_SCALE: `选择一个文件后按回车,或者 ${labelSHIFT()}+${labelMETA()}+ENTER 以 100% 尺寸插入。`,
|
||||
SELECT_FILE_WITH_OPTION_TO_SCALE: `选择一个文件后按回车,或者 ${labelSHIFT()}+${labelMETA()}+Enter 以 100% 尺寸插入。`,
|
||||
NO_MATCH: "查询不到匹配的文件。",
|
||||
NO_MATCHING_COMMAND: "查询不到匹配的命令。",
|
||||
SELECT_FILE_TO_LINK: "选择要插入(以内部链接形式嵌入)到当前绘图中的文件。",
|
||||
@@ -714,7 +744,7 @@ FILENAME_HEAD: "文件名",
|
||||
//SelectCard.ts
|
||||
TYPE_SECTION: "输入章节名称(标题)进行选择",
|
||||
SELECT_SECTION_OR_TYPE_NEW:
|
||||
"选择现有章节(标题)或输入新章节(标题)的名称,然后按 Enter。",
|
||||
"选择现有章节(标题)或输入新章节(标题)的名称,然后按 Enter。",
|
||||
INVALID_SECTION_NAME: "无效的章节名称(标题)",
|
||||
EMPTY_SECTION_MESSAGE: "输入章节(标题)名称以创建",
|
||||
|
||||
@@ -798,4 +828,11 @@ FILENAME_HEAD: "文件名",
|
||||
INTERNAL_DRAG_ACTION: "在 Obsidian 内部拖放时",
|
||||
PANE_TARGET: "点击链接时",
|
||||
DEFAULT_ACTION_DESC: "无修饰键时的行为:",
|
||||
|
||||
//FrameSettings.ts
|
||||
FRAME_SETTINGS_TITLE: "框架设置",
|
||||
FRAME_SETTINGS_ENABLE: "启用框架",
|
||||
FRAME_SETTIGNS_NAME: "显示框架名称",
|
||||
FRAME_SETTINGS_OUTLINE: "显示框架外边框",
|
||||
FRAME_SETTINGS_CLIP: "启用框架裁剪",
|
||||
};
|
||||
|
||||
781
src/main.ts
781
src/main.ts
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,11 @@
|
||||
import * as React from "react";
|
||||
import ExcalidrawView from "../ExcalidrawView";
|
||||
import { Notice } from "obsidian";
|
||||
|
||||
type ButtonProps = {
|
||||
title: string;
|
||||
action: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
longpress?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
icon: JSX.Element;
|
||||
view: ExcalidrawView;
|
||||
};
|
||||
|
||||
type ButtonState = {
|
||||
@@ -24,6 +23,10 @@ export class ActionButton extends React.Component<ButtonProps, ButtonState> {
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.render = () => null;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<button
|
||||
@@ -48,7 +51,8 @@ export class ActionButton extends React.Component<ButtonProps, ButtonState> {
|
||||
onPointerDown={(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
this.toastMessageTimeout = window.setTimeout(
|
||||
() => {
|
||||
this.props.view.excalidrawAPI?.setToast({message:this.props.title, duration: 3000, closable: true});
|
||||
new Notice(this.props.title, 3000);
|
||||
//this.props.view.excalidrawAPI?.setToast({message:this.props.title, duration: 3000, closable: true});
|
||||
this.toastMessageTimeout = 0;
|
||||
},
|
||||
400,
|
||||
@@ -58,7 +62,8 @@ export class ActionButton extends React.Component<ButtonProps, ButtonState> {
|
||||
if(this.props.longpress) {
|
||||
this.props.longpress(event);
|
||||
} else {
|
||||
this.props.view.excalidrawAPI?.setToast({message:"Cannot pin this action", duration: 3000, closable: true});
|
||||
new Notice("Cannot pin this action", 3000);
|
||||
//this.props.view.excalidrawAPI?.setToast({message:"Cannot pin this action", duration: 3000, closable: true});
|
||||
}
|
||||
this.longpressTimeout = 0;
|
||||
},
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -17,6 +17,8 @@ import { getEA } from "src";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
|
||||
export class EmbeddableMenu {
|
||||
private menuFadeTimeout: number = 0;
|
||||
private menuElementId: string = null;
|
||||
|
||||
constructor(
|
||||
private view:ExcalidrawView,
|
||||
@@ -24,6 +26,19 @@ export class EmbeddableMenu {
|
||||
) {
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
if(this.menuFadeTimeout) {
|
||||
clearTimeout(this.menuFadeTimeout);
|
||||
this.menuFadeTimeout = null;
|
||||
}
|
||||
this.view = null;
|
||||
this.containerRef = null;
|
||||
this.updateElement = null;
|
||||
this.handleMouseEnter = null;
|
||||
this.handleMouseLeave = null;
|
||||
this.renderButtons = null;
|
||||
}
|
||||
|
||||
private updateElement = (subpath: string, element: ExcalidrawEmbeddableElement, file: TFile) => {
|
||||
if(!element) return;
|
||||
const view = this.view;
|
||||
@@ -40,23 +55,118 @@ export class EmbeddableMenu {
|
||||
ea.copyViewElementsToEAforEditing([element]);
|
||||
ea.getElement(element.id).link = link;
|
||||
view.excalidrawData.elementLinks.set(element.id, link);
|
||||
ea.addElementsToView(false, true, true);
|
||||
ea.addElementsToView(false, true, true).then(() => ea.destroy());
|
||||
}
|
||||
|
||||
private menuFadeTimeout: number = 0;
|
||||
private menuElementId: string = null;
|
||||
private handleMouseEnter () {
|
||||
clearTimeout(this.menuFadeTimeout);
|
||||
this.containerRef.current?.style.setProperty("opacity", "1");
|
||||
};
|
||||
|
||||
private handleMouseLeave () {
|
||||
const self = this;
|
||||
this.menuFadeTimeout = window.setTimeout(() => {
|
||||
self.containerRef.current?.style.setProperty("opacity", "0.2");
|
||||
this.containerRef.current?.style.setProperty("opacity", "0.2");
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
private async actionMarkdownSelection (file: TFile, isExcalidrawFile: boolean, subpath: string, element: ExcalidrawEmbeddableElement) {
|
||||
this.view.updateScene({appState: {activeEmbeddable: null}});
|
||||
const sections = (await app.metadataCache.blockCache
|
||||
.getForFile({ isCancelled: () => false },file))
|
||||
.blocks.filter((b: any) => b.display && b.node?.type === "heading")
|
||||
.filter((b: any) => !isExcalidrawFile || !MD_EX_SECTIONS.includes(b.display));
|
||||
let values, display;
|
||||
if(isExcalidrawFile) {
|
||||
values = sections.map((b: any) => `#${cleanSectionHeading(b.display)}`);
|
||||
display = sections.map((b: any) => b.display);
|
||||
} else {
|
||||
values = [""].concat(
|
||||
sections.map((b: any) => `#${cleanSectionHeading(b.display)}`)
|
||||
);
|
||||
display = [t("SHOW_ENTIRE_FILE")].concat(
|
||||
sections.map((b: any) => b.display)
|
||||
);
|
||||
}
|
||||
const newSubpath = await ScriptEngine.suggester(
|
||||
app, display, values, "Select section from document"
|
||||
);
|
||||
if(!newSubpath && newSubpath!=="") return;
|
||||
if (newSubpath !== subpath) {
|
||||
this.updateElement(newSubpath, element, file);
|
||||
}
|
||||
}
|
||||
|
||||
private async actionMarkdownBlock (file: TFile, subpath: string, element: ExcalidrawEmbeddableElement) {
|
||||
if(!file) return;
|
||||
this.view.updateScene({appState: {activeEmbeddable: null}});
|
||||
const paragraphs = (await app.metadataCache.blockCache
|
||||
.getForFile({ isCancelled: () => false },file))
|
||||
.blocks.filter((b: any) => b.display && b.node &&
|
||||
(b.node.type === "paragraph" || b.node.type === "blockquote" || b.node.type === "listItem" || b.node.type === "table" || b.node.type === "callout")
|
||||
);
|
||||
const values = ["entire-file"].concat(paragraphs);
|
||||
const display = [t("SHOW_ENTIRE_FILE")].concat(
|
||||
paragraphs.map((b: any) => `${b.node?.id ? `#^${b.node.id}: ` : ``}${b.display.trim()}`));
|
||||
|
||||
const selectedBlock = await ScriptEngine.suggester(
|
||||
app, display, values, "Select section from document"
|
||||
);
|
||||
if(!selectedBlock) return;
|
||||
|
||||
if(selectedBlock==="entire-file") {
|
||||
if(subpath==="") return;
|
||||
this.updateElement("", element, file);
|
||||
return;
|
||||
}
|
||||
|
||||
let blockID = selectedBlock.node.id;
|
||||
if(blockID && (`#^${blockID}` === subpath)) return;
|
||||
if (!blockID) {
|
||||
const offset = selectedBlock.node?.position?.end?.offset;
|
||||
if(!offset) return;
|
||||
blockID = nanoid();
|
||||
const fileContents = await app.vault.cachedRead(file);
|
||||
if(!fileContents) return;
|
||||
await app.vault.modify(file, fileContents.slice(0, offset) + ` ^${blockID}` + fileContents.slice(offset));
|
||||
await sleep(200); //wait for cache to update
|
||||
}
|
||||
this.updateElement(`#^${blockID}`, element, file);
|
||||
}
|
||||
|
||||
private actionZoomToElement (element: ExcalidrawEmbeddableElement, maxLevel?: number) {
|
||||
if(!element) return;
|
||||
const api = this.view.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
api.zoomToFit([element], maxLevel ?? this.view.plugin.settings.zoomToFitMaxLevel, 0.1);
|
||||
}
|
||||
|
||||
private actionProperties (element: ExcalidrawEmbeddableElement, file: TFile) {
|
||||
if(!element) return;
|
||||
new EmbeddableSettings(this.view.plugin,this.view,file,element).open();
|
||||
}
|
||||
|
||||
private actionCrop (element: ExcalidrawEmbeddableElement) {
|
||||
if(!element) return;
|
||||
//@ts-ignore
|
||||
this.view.app.commands.executeCommandById("obsidian-excalidraw-plugin:crop-image");
|
||||
}
|
||||
|
||||
private actionReload (iframe: HTMLIFrameElement, link: string) {
|
||||
iframe.src = link;
|
||||
}
|
||||
|
||||
private actionOpen (iframe: HTMLIFrameElement, element: ExcalidrawEmbeddableElement) {
|
||||
openExternalLink(
|
||||
!iframe.src.startsWith("https://www.youtube.com") && !iframe.src.startsWith("https://player.vimeo.com")
|
||||
? iframe.src
|
||||
: element.link,
|
||||
this.view.app
|
||||
);
|
||||
}
|
||||
|
||||
private actionCopyCode (element: ExcalidrawEmbeddableElement, link: string) {
|
||||
if(!element) return;
|
||||
navigator.clipboard.writeText(atob(link.split(",")[1]));
|
||||
}
|
||||
|
||||
renderButtons(appState: AppState) {
|
||||
const view = this.view;
|
||||
@@ -132,111 +242,36 @@ export class EmbeddableMenu {
|
||||
<ActionButton
|
||||
key={"MarkdownSection"}
|
||||
title={t("NARROW_TO_HEADING")}
|
||||
action={async () => {
|
||||
view.updateScene({appState: {activeEmbeddable: null}});
|
||||
const sections = (await app.metadataCache.blockCache
|
||||
.getForFile({ isCancelled: () => false },file))
|
||||
.blocks.filter((b: any) => b.display && b.node?.type === "heading")
|
||||
.filter((b: any) => !isExcalidrawFile || !MD_EX_SECTIONS.includes(b.display));
|
||||
let values, display;
|
||||
if(isExcalidrawFile) {
|
||||
values = sections.map((b: any) => `#${cleanSectionHeading(b.display)}`);
|
||||
display = sections.map((b: any) => b.display);
|
||||
} else {
|
||||
values = [""].concat(
|
||||
sections.map((b: any) => `#${cleanSectionHeading(b.display)}`)
|
||||
);
|
||||
display = [t("SHOW_ENTIRE_FILE")].concat(
|
||||
sections.map((b: any) => b.display)
|
||||
);
|
||||
}
|
||||
const newSubpath = await ScriptEngine.suggester(
|
||||
app, display, values, "Select section from document"
|
||||
);
|
||||
if(!newSubpath && newSubpath!=="") return;
|
||||
if (newSubpath !== subpath) {
|
||||
this.updateElement(newSubpath, element, file);
|
||||
}
|
||||
}}
|
||||
action={async () => this.actionMarkdownSelection(file, isExcalidrawFile, subpath, element)}
|
||||
icon={ICONS.ZoomToSection}
|
||||
view={view}
|
||||
/>
|
||||
)}
|
||||
{isMD && !isExcalidrawFile && (
|
||||
<ActionButton
|
||||
key={"MarkdownBlock"}
|
||||
title={t("NARROW_TO_BLOCK")}
|
||||
action={async () => {
|
||||
if(!file) return;
|
||||
view.updateScene({appState: {activeEmbeddable: null}});
|
||||
const paragraphs = (await app.metadataCache.blockCache
|
||||
.getForFile({ isCancelled: () => false },file))
|
||||
.blocks.filter((b: any) => b.display && b.node &&
|
||||
(b.node.type === "paragraph" || b.node.type === "blockquote" || b.node.type === "listItem" || b.node.type === "table" || b.node.type === "callout")
|
||||
);
|
||||
const values = ["entire-file"].concat(paragraphs);
|
||||
const display = [t("SHOW_ENTIRE_FILE")].concat(
|
||||
paragraphs.map((b: any) => `${b.node?.id ? `#^${b.node.id}: ` : ``}${b.display.trim()}`));
|
||||
|
||||
const selectedBlock = await ScriptEngine.suggester(
|
||||
app, display, values, "Select section from document"
|
||||
);
|
||||
if(!selectedBlock) return;
|
||||
|
||||
if(selectedBlock==="entire-file") {
|
||||
if(subpath==="") return;
|
||||
this.updateElement("", element, file);
|
||||
return;
|
||||
}
|
||||
|
||||
let blockID = selectedBlock.node.id;
|
||||
if(blockID && (`#^${blockID}` === subpath)) return;
|
||||
if (!blockID) {
|
||||
const offset = selectedBlock.node?.position?.end?.offset;
|
||||
if(!offset) return;
|
||||
blockID = nanoid();
|
||||
const fileContents = await app.vault.cachedRead(file);
|
||||
if(!fileContents) return;
|
||||
await app.vault.modify(file, fileContents.slice(0, offset) + ` ^${blockID}` + fileContents.slice(offset));
|
||||
await sleep(200); //wait for cache to update
|
||||
}
|
||||
this.updateElement(`#^${blockID}`, element, file);
|
||||
}}
|
||||
action={async () => this.actionMarkdownBlock(file, subpath, element)}
|
||||
icon={ICONS.ZoomToBlock}
|
||||
view={view}
|
||||
/>
|
||||
)}
|
||||
<ActionButton
|
||||
key={"ZoomToElement"}
|
||||
title={t("ZOOM_TO_FIT")}
|
||||
action={() => {
|
||||
if(!element) return;
|
||||
api.zoomToFit([element], 30, 0.1);
|
||||
}}
|
||||
action={() => this.actionZoomToElement(element,30)}
|
||||
icon={ICONS.ZoomToSelectedElement}
|
||||
view={view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"Properties"}
|
||||
title={t("PROPERTIES")}
|
||||
action={() => {
|
||||
if(!element) return;
|
||||
new EmbeddableSettings(view.plugin,view,file,element).open();
|
||||
}}
|
||||
action={() => this.actionProperties(element, file)}
|
||||
icon={ICONS.Properties}
|
||||
view={view}
|
||||
/>
|
||||
{isPDF && (
|
||||
<ActionButton
|
||||
key={"Crop"}
|
||||
title={t("CROP_PAGE")}
|
||||
action={() => {
|
||||
if(!element) return;
|
||||
//@ts-ignore
|
||||
view.app.commands.executeCommandById("obsidian-excalidraw-plugin:crop-image");
|
||||
}}
|
||||
action={() => this.actionCrop(element)}
|
||||
icon={ICONS.Crop}
|
||||
view={view}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -276,57 +311,34 @@ export class EmbeddableMenu {
|
||||
<ActionButton
|
||||
key={"Reload"}
|
||||
title={t("RELOAD")}
|
||||
action={()=>{
|
||||
iframe.src = link;
|
||||
}}
|
||||
action={()=> this.actionReload(iframe, link)}
|
||||
icon={ICONS.Reload}
|
||||
view={view}
|
||||
/>
|
||||
)}
|
||||
<ActionButton
|
||||
key={"Open"}
|
||||
title={t("OPEN_IN_BROWSER")}
|
||||
action={() => {
|
||||
openExternalLink(
|
||||
!iframe.src.startsWith("https://www.youtube.com") && !iframe.src.startsWith("https://player.vimeo.com")
|
||||
? iframe.src
|
||||
: element.link,
|
||||
view.app
|
||||
);
|
||||
}}
|
||||
action={() => this.actionOpen(iframe, element)}
|
||||
icon={ICONS.Globe}
|
||||
view={view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"ZoomToElement"}
|
||||
title={t("ZOOM_TO_FIT")}
|
||||
action={() => {
|
||||
if(!element) return;
|
||||
api.zoomToFit([element], view.plugin.settings.zoomToFitMaxLevel, 0.1);
|
||||
}}
|
||||
action={() => this.actionZoomToElement(element)}
|
||||
icon={ICONS.ZoomToSelectedElement}
|
||||
view={view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"Properties"}
|
||||
title={t("PROPERTIES")}
|
||||
action={() => {
|
||||
if(!element) return;
|
||||
new EmbeddableSettings(view.plugin,view,null,element).open();
|
||||
}}
|
||||
action={() => this.actionProperties(element, null)}
|
||||
icon={ICONS.Properties}
|
||||
view={view}
|
||||
/>
|
||||
{link?.startsWith("data:text/html") && (
|
||||
<ActionButton
|
||||
key={"CopyCode"}
|
||||
title={t("COPYCODE")}
|
||||
action={() => {
|
||||
if(!element) return;
|
||||
navigator.clipboard.writeText(atob(link.split(",")[1]));
|
||||
}}
|
||||
action={() => this.actionCopyCode(element, link)}
|
||||
icon={ICONS.Copy}
|
||||
view={view}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ import { t } from "src/lang/helpers";
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
|
||||
export const setPen = (pen: PenStyle, api: any) => {
|
||||
export function setPen (pen: PenStyle, api: any) {
|
||||
const st = api.getAppState();
|
||||
api.updateScene({
|
||||
appState: {
|
||||
@@ -38,7 +38,7 @@ export const setPen = (pen: PenStyle, api: any) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const resetStrokeOptions = (resetCustomPen:any, api: ExcalidrawImperativeAPI, clearCurrentStrokeOptions: boolean) => {
|
||||
export function resetStrokeOptions (resetCustomPen:any, api: ExcalidrawImperativeAPI, clearCurrentStrokeOptions: boolean) {
|
||||
api.updateScene({
|
||||
appState: {
|
||||
...resetCustomPen ? {
|
||||
@@ -57,6 +57,8 @@ export const resetStrokeOptions = (resetCustomPen:any, api: ExcalidrawImperative
|
||||
export class ObsidianMenu {
|
||||
private clickTimestamp:number[];
|
||||
private activePen: PenStyle;
|
||||
private longpressTimeout : { [key: number]: number } = {};
|
||||
private prevClickTimestamp: number = 0;
|
||||
constructor(
|
||||
private plugin: ExcalidrawPlugin,
|
||||
private toolsRef: React.MutableRefObject<any>,
|
||||
@@ -65,9 +67,98 @@ export class ObsidianMenu {
|
||||
this.clickTimestamp = Array.from({length: Object.keys(PENS).length}, () => 0);
|
||||
}
|
||||
|
||||
renderCustomPens = (isMobile: boolean, appState: AppState) => {
|
||||
private actionCustomPenLabelClick(index: number, pen: PenStyle) {
|
||||
const now = Date.now();
|
||||
const dblClick = now-this.clickTimestamp[index] < 500;
|
||||
//open pen settings on double click
|
||||
if(dblClick) {
|
||||
const penSettings = new PenSettingsModal(this.plugin,this.view,index);
|
||||
(async () => {
|
||||
await this.plugin.loadSettings();
|
||||
penSettings.open();
|
||||
})();
|
||||
return;
|
||||
}
|
||||
this.clickTimestamp[index] = now;
|
||||
|
||||
const api = this.view.excalidrawAPI;
|
||||
const st = api.getAppState();
|
||||
|
||||
//single second click to reset freedraw to default
|
||||
if(st.currentStrokeOptions === pen.penOptions && st.activeTool.type === "freedraw") {
|
||||
resetStrokeOptions(st.resetCustomPen, api, true);
|
||||
return;
|
||||
}
|
||||
|
||||
//apply pen settings to canvas
|
||||
this.activePen = {...pen};
|
||||
setPen(pen,api);
|
||||
api.setActiveTool({type:"freedraw"});
|
||||
}
|
||||
|
||||
private actionScriptButtonPonterUp(index: number, key: string) {
|
||||
if(this.longpressTimeout[index]) {
|
||||
this.view.ownerWindow.clearTimeout(this.longpressTimeout[index]);
|
||||
this.longpressTimeout[index] = 0;
|
||||
(async ()=>{
|
||||
const f = this.view.app.vault.getAbstractFileByPath(key);
|
||||
if (f && f instanceof TFile) {
|
||||
this.plugin.scriptEngine.executeScript(
|
||||
this.view,
|
||||
await this.view.app.vault.read(f),
|
||||
this.plugin.scriptEngine.getScriptName(f),
|
||||
f
|
||||
);
|
||||
}
|
||||
})()
|
||||
}
|
||||
}
|
||||
|
||||
private actionScriptButtonPointerDown(index: number, key: string) {
|
||||
const now = Date.now();
|
||||
if(this.longpressTimeout[index]>0) {
|
||||
this.view.ownerWindow.clearTimeout(this.longpressTimeout[index]);
|
||||
this.longpressTimeout[index] = 0;
|
||||
}
|
||||
if(now-this.prevClickTimestamp >= 500) {
|
||||
this.longpressTimeout[index] = this.view.ownerWindow.setTimeout(
|
||||
() => {
|
||||
this.longpressTimeout[index] = 0;
|
||||
(async () =>{
|
||||
await this.plugin.loadSettings();
|
||||
const index = this.plugin.settings.pinnedScripts.indexOf(key)
|
||||
if(index > -1) {
|
||||
this.plugin.settings.pinnedScripts.splice(index,1);
|
||||
this.view.excalidrawAPI?.setToast({message:`Pin removed: ${name}`, duration: 3000, closable: true});
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> {
|
||||
if (v.view instanceof ExcalidrawView) v.view.updatePinnedScripts()
|
||||
})
|
||||
})()
|
||||
},
|
||||
1500
|
||||
)
|
||||
}
|
||||
this.prevClickTimestamp = now;
|
||||
}
|
||||
|
||||
private actionShowHideMenu (isMobile: boolean, appState: AppState) {
|
||||
this.toolsRef.current.setTheme(appState.theme);
|
||||
this.toolsRef.current.toggleVisibility(
|
||||
appState.zenModeEnabled || isMobile,
|
||||
);
|
||||
}
|
||||
|
||||
private actionInsertAnyFile() {
|
||||
this.view.setCurrentPositionToCenter();
|
||||
const insertFileModal = new UniversalInsertFileModal(this.plugin, this.view);
|
||||
insertFileModal.open();
|
||||
}
|
||||
|
||||
public renderCustomPens (isMobile: boolean, appState: AppState) {
|
||||
return(
|
||||
appState.customPens?.map((key,index)=>{
|
||||
appState.customPens?.map((_,index)=>{
|
||||
const pen = this.plugin.settings.customPens[index]
|
||||
//Reset stroke setting when changing to a different tool
|
||||
if(
|
||||
@@ -75,7 +166,7 @@ export class ObsidianMenu {
|
||||
appState.activeTool.type !== "freedraw" &&
|
||||
appState.currentStrokeOptions === pen.penOptions
|
||||
) {
|
||||
setTimeout(()=> resetStrokeOptions(appState.resetCustomPen, this.view.excalidrawAPI, false))
|
||||
setTimeout(()=> resetStrokeOptions(appState.resetCustomPen, this.view.excalidrawAPI, false));
|
||||
}
|
||||
//if Pen settings are loaded, select custom pen when activating the freedraw element
|
||||
if (
|
||||
@@ -111,34 +202,7 @@ export class ObsidianMenu {
|
||||
"is-mobile": isMobile,
|
||||
},
|
||||
)}
|
||||
onClick={() => {
|
||||
const now = Date.now();
|
||||
const dblClick = now-this.clickTimestamp[index] < 500;
|
||||
//open pen settings on double click
|
||||
if(dblClick) {
|
||||
const penSettings = new PenSettingsModal(this.plugin,this.view,index);
|
||||
(async () => {
|
||||
await this.plugin.loadSettings();
|
||||
penSettings.open();
|
||||
})();
|
||||
return;
|
||||
}
|
||||
this.clickTimestamp[index] = now;
|
||||
|
||||
const api = this.view.excalidrawAPI;
|
||||
const st = api.getAppState();
|
||||
|
||||
//single second click to reset freedraw to default
|
||||
if(st.currentStrokeOptions === pen.penOptions && st.activeTool.type === "freedraw") {
|
||||
resetStrokeOptions(st.resetCustomPen, api, true);
|
||||
return;
|
||||
}
|
||||
|
||||
//apply pen settings to canvas
|
||||
this.activePen = {...pen};
|
||||
setPen(pen,api);
|
||||
api.setActiveTool({type:"freedraw"});
|
||||
}}
|
||||
onClick={ this.actionCustomPenLabelClick.bind(this,index, pen) }
|
||||
>
|
||||
<div
|
||||
className="ToolIcon__icon"
|
||||
@@ -157,10 +221,7 @@ export class ObsidianMenu {
|
||||
)
|
||||
}
|
||||
|
||||
private longpressTimeout : { [key: number]: number } = {};
|
||||
|
||||
renderPinnedScriptButtons = (isMobile: boolean, appState: AppState) => {
|
||||
let prevClickTimestamp = 0;
|
||||
public renderPinnedScriptButtons (isMobile: boolean, appState: AppState) {
|
||||
return (
|
||||
appState?.pinnedScripts?.map((key,index)=>{ //pinned scripts
|
||||
const scriptProp = this.plugin.scriptEngine.scriptIconMap[key];
|
||||
@@ -179,51 +240,8 @@ export class ObsidianMenu {
|
||||
"is-mobile": isMobile,
|
||||
},
|
||||
)}
|
||||
onPointerUp={() => {
|
||||
if(this.longpressTimeout[index]) {
|
||||
window.clearTimeout(this.longpressTimeout[index]);
|
||||
this.longpressTimeout[index] = 0;
|
||||
(async ()=>{
|
||||
const f = app.vault.getAbstractFileByPath(key);
|
||||
if (f && f instanceof TFile) {
|
||||
this.plugin.scriptEngine.executeScript(
|
||||
this.view,
|
||||
await app.vault.read(f),
|
||||
this.plugin.scriptEngine.getScriptName(f),
|
||||
f
|
||||
);
|
||||
}
|
||||
})()
|
||||
}
|
||||
}}
|
||||
onPointerDown={()=>{
|
||||
const now = Date.now();
|
||||
if(this.longpressTimeout[index]>0) {
|
||||
window.clearTimeout(this.longpressTimeout[index]);
|
||||
this.longpressTimeout[index] = 0;
|
||||
}
|
||||
if(now-prevClickTimestamp >= 500) {
|
||||
this.longpressTimeout[index] = window.setTimeout(
|
||||
() => {
|
||||
this.longpressTimeout[index] = 0;
|
||||
(async () =>{
|
||||
await this.plugin.loadSettings();
|
||||
const index = this.plugin.settings.pinnedScripts.indexOf(key)
|
||||
if(index > -1) {
|
||||
this.plugin.settings.pinnedScripts.splice(index,1);
|
||||
this.view.excalidrawAPI?.setToast({message:`Pin removed: ${name}`, duration: 3000, closable: true});
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> {
|
||||
if (v.view instanceof ExcalidrawView) v.view.updatePinnedScripts()
|
||||
})
|
||||
})()
|
||||
},
|
||||
1500
|
||||
)
|
||||
}
|
||||
prevClickTimestamp = now;
|
||||
}}
|
||||
onPointerUp={this.actionScriptButtonPonterUp.bind(this,index,key)}
|
||||
onPointerDown={this.actionScriptButtonPointerDown.bind(this,index,key)}
|
||||
>
|
||||
<div
|
||||
className="ToolIcon__icon"
|
||||
@@ -237,7 +255,7 @@ export class ObsidianMenu {
|
||||
)
|
||||
}
|
||||
|
||||
renderButton = (isMobile: boolean, appState: AppState) => {
|
||||
public renderButton (isMobile: boolean, appState: AppState) {
|
||||
return (
|
||||
<>
|
||||
<label
|
||||
@@ -248,12 +266,7 @@ export class ObsidianMenu {
|
||||
"is-mobile": isMobile,
|
||||
},
|
||||
)}
|
||||
onClick={() => {
|
||||
this.toolsRef.current.setTheme(appState.theme);
|
||||
this.toolsRef.current.toggleVisibility(
|
||||
appState.zenModeEnabled || isMobile,
|
||||
);
|
||||
}}
|
||||
onClick={this.actionShowHideMenu.bind(this,isMobile,appState)}
|
||||
>
|
||||
<div className="ToolIcon__icon" aria-label={t("OBSIDIAN_TOOLS_PANEL")}>
|
||||
{ICONS.obsidian}
|
||||
@@ -267,11 +280,7 @@ export class ObsidianMenu {
|
||||
"is-mobile": isMobile,
|
||||
},
|
||||
)}
|
||||
onClick={() => {
|
||||
this.view.setCurrentPositionToCenter();
|
||||
const insertFileModal = new UniversalInsertFileModal(this.plugin, this.view);
|
||||
insertFileModal.open();
|
||||
}}
|
||||
onClick={this.actionInsertAnyFile.bind(this)}
|
||||
>
|
||||
<div className="ToolIcon__icon" aria-label={t("UNIVERSAL_ADD_FILE")}>
|
||||
{ICONS["add-file"]}
|
||||
@@ -282,4 +291,19 @@ export class ObsidianMenu {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
destroy() {
|
||||
Object.values(this.longpressTimeout).forEach(
|
||||
t=>this.view.ownerWindow.clearTimeout(t)
|
||||
);
|
||||
this.longpressTimeout = {};
|
||||
this.activePen = null;
|
||||
this.plugin = null;
|
||||
this.toolsRef = null;
|
||||
this.view = null;
|
||||
this.clickTimestamp = null;
|
||||
this.renderButton = null;
|
||||
this.renderCustomPens = null;
|
||||
this.renderPinnedScriptButtons = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,9 @@ const light = '<svg style="stroke:#212529;color:#212529;fill:#212529" ';
|
||||
|
||||
type PanelProps = {
|
||||
visible: boolean;
|
||||
view: ExcalidrawView;
|
||||
view: WeakRef<ExcalidrawView>;
|
||||
centerPointer: Function;
|
||||
observer: WeakRef<ResizeObserver>;
|
||||
};
|
||||
|
||||
export type PanelState = {
|
||||
@@ -38,7 +39,7 @@ export type PanelState = {
|
||||
isDirty: boolean;
|
||||
isFullscreen: boolean;
|
||||
isPreviewMode: boolean;
|
||||
scriptIconMap: ScriptIconMap;
|
||||
scriptIconMap: ScriptIconMap | null;
|
||||
};
|
||||
|
||||
const TOOLS_PANEL_WIDTH = 228;
|
||||
@@ -55,10 +56,22 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
onRightEdge: boolean = false;
|
||||
onBottomEdge: boolean = false;
|
||||
public containerRef: React.RefObject<HTMLDivElement>;
|
||||
private view: ExcalidrawView;
|
||||
|
||||
componentWillUnmount(): void {
|
||||
if (this.containerRef.current) {
|
||||
this.props.observer.deref()?.unobserve(this.containerRef.current);
|
||||
}
|
||||
this.setState({ scriptIconMap: null });
|
||||
this.containerRef = null;
|
||||
this.view = null;
|
||||
}
|
||||
|
||||
constructor(props: PanelProps) {
|
||||
super(props);
|
||||
const react = props.view.plugin.getPackage(props.view.ownerWindow).react;
|
||||
this.view = props.view.deref();
|
||||
const react = this.view.packages.react;
|
||||
|
||||
this.containerRef = react.createRef();
|
||||
this.state = {
|
||||
visible: props.visible,
|
||||
@@ -194,6 +207,232 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
});
|
||||
}
|
||||
|
||||
actionOpenScriptInstallDialog() {
|
||||
new ScriptInstallPrompt(this.view.plugin).open();
|
||||
}
|
||||
|
||||
actionOpenReleaseNotes() {
|
||||
new ReleaseNotes(
|
||||
this.view.app,
|
||||
this.view.plugin,
|
||||
PLUGIN_VERSION,
|
||||
).open();
|
||||
}
|
||||
|
||||
actionConvertExcalidrawToMD() {
|
||||
this.view.convertExcalidrawToMD();
|
||||
}
|
||||
|
||||
actionToggleViewMode() {
|
||||
if (this.state.isPreviewMode) {
|
||||
this.view.changeTextMode(TextMode.raw);
|
||||
} else {
|
||||
this.view.changeTextMode(TextMode.parsed);
|
||||
}
|
||||
}
|
||||
|
||||
actionToggleTrayMode() {
|
||||
this.view.toggleTrayMode();
|
||||
}
|
||||
|
||||
actionToggleFullscreen() {
|
||||
if (this.state.isFullscreen) {
|
||||
this.view.exitFullscreen();
|
||||
} else {
|
||||
this.view.gotoFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
actionSearch() {
|
||||
search(this.view);
|
||||
}
|
||||
|
||||
actionOCR(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) {
|
||||
if(!this.view.plugin.settings.taskboneEnabled) {
|
||||
new Notice("Taskbone OCR is not enabled. Please go to plugins settings to enable it.",4000);
|
||||
return;
|
||||
}
|
||||
this.view.plugin.taskbone.getTextForView(this.view, {forceReScan: isWinCTRLorMacCMD(e)});
|
||||
}
|
||||
|
||||
actionOpenLink(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) {
|
||||
const event = new MouseEvent("click", {
|
||||
ctrlKey: e.ctrlKey || !(DEVICE.isIOS || DEVICE.isMacOS),
|
||||
metaKey: e.metaKey || (DEVICE.isIOS || DEVICE.isMacOS),
|
||||
shiftKey: e.shiftKey,
|
||||
altKey: e.altKey,
|
||||
});
|
||||
this.view.handleLinkClick(event);
|
||||
}
|
||||
|
||||
actionOpenLinkProperties() {
|
||||
const event = new MouseEvent("click", {
|
||||
ctrlKey: true,
|
||||
metaKey: true,
|
||||
shiftKey: false,
|
||||
altKey: false,
|
||||
});
|
||||
this.view.handleLinkClick(event);
|
||||
}
|
||||
|
||||
actionForceSave() {
|
||||
this.view.forceSave();
|
||||
}
|
||||
|
||||
actionExportLibrary() {
|
||||
this.view.plugin.exportLibrary();
|
||||
}
|
||||
|
||||
actionExportImage() {
|
||||
const view = this.view;
|
||||
if(!view.exportDialog) {
|
||||
view.exportDialog = new ExportDialog(view.plugin, view,view.file);
|
||||
view.exportDialog.createForm();
|
||||
}
|
||||
view.exportDialog.open();
|
||||
}
|
||||
|
||||
actionOpenAsMarkdown() {
|
||||
this.view.openAsMarkdown();
|
||||
}
|
||||
|
||||
actionLinkToElement(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) {
|
||||
if(isWinALTorMacOPT(e)) {
|
||||
openExternalLink("https://youtu.be/yZQoJg2RCKI", this.view.app);
|
||||
return;
|
||||
}
|
||||
this.view.copyLinkToSelectedElementToClipboard(
|
||||
isWinCTRLorMacCMD(e) ? "group=" : (isSHIFT(e) ? "area=" : "")
|
||||
);
|
||||
}
|
||||
|
||||
actionAddAnyFile() {
|
||||
this.props.centerPointer();
|
||||
const insertFileModal = new UniversalInsertFileModal(this.view.plugin, this.view);
|
||||
insertFileModal.open();
|
||||
}
|
||||
|
||||
actionInsertImage() {
|
||||
this.props.centerPointer();
|
||||
this.view.plugin.insertImageDialog.start(
|
||||
this.view,
|
||||
);
|
||||
}
|
||||
|
||||
actionInsertPDF() {
|
||||
this.props.centerPointer();
|
||||
const insertPDFModal = new InsertPDFModal(this.view.plugin, this.view);
|
||||
insertPDFModal.open();
|
||||
}
|
||||
|
||||
actionInsertMarkdown() {
|
||||
this.props.centerPointer();
|
||||
this.view.plugin.insertMDDialog.start(
|
||||
this.view,
|
||||
);
|
||||
}
|
||||
|
||||
actionInsertBackOfNote() {
|
||||
this.props.centerPointer();
|
||||
this.view.insertBackOfTheNoteCard();
|
||||
}
|
||||
|
||||
actionInsertLaTeX(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) {
|
||||
if(isWinALTorMacOPT(e)) {
|
||||
openExternalLink("https://youtu.be/r08wk-58DPk", this.view.app);
|
||||
return;
|
||||
}
|
||||
this.props.centerPointer();
|
||||
insertLaTeXToView(this.view);
|
||||
}
|
||||
|
||||
actionInsertLink() {
|
||||
this.props.centerPointer();
|
||||
this.view.plugin.insertLinkDialog.start(
|
||||
this.view.file.path,
|
||||
(text: string, fontFamily?: 1 | 2 | 3 | 4, save?: boolean) => this.view.addText (text, fontFamily, save),
|
||||
);
|
||||
}
|
||||
|
||||
actionImportSVG(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) {
|
||||
this.view.plugin.importSVGDialog.start(this.view);
|
||||
}
|
||||
|
||||
actionCropImage(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) {
|
||||
// @ts-ignore
|
||||
this.view.app.commands.executeCommandById("obsidian-excalidraw-plugin:crop-image");
|
||||
}
|
||||
|
||||
async actionRunScript(key: string) {
|
||||
const view = this.view;
|
||||
const plugin = view.plugin;
|
||||
const f = app.vault.getAbstractFileByPath(key);
|
||||
if (f && f instanceof TFile) {
|
||||
plugin.scriptEngine.executeScript(
|
||||
view,
|
||||
await app.vault.read(f),
|
||||
plugin.scriptEngine.getScriptName(f),
|
||||
f
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async actionPinScript(key: string, scriptName: string) {
|
||||
const view = this.view;
|
||||
const api = view.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
const plugin = view.plugin;
|
||||
await plugin.loadSettings();
|
||||
const index = plugin.settings.pinnedScripts.indexOf(key)
|
||||
if(index > -1) {
|
||||
plugin.settings.pinnedScripts.splice(index,1);
|
||||
api?.setToast({message:`Pin removed: ${scriptName}`, duration: 3000, closable: true});
|
||||
} else {
|
||||
plugin.settings.pinnedScripts.push(key);
|
||||
api?.setToast({message:`Pinned: ${scriptName}`, duration: 3000, closable: true})
|
||||
}
|
||||
await plugin.saveSettings();
|
||||
plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> {
|
||||
if (v.view instanceof ExcalidrawView) v.view.updatePinnedScripts()
|
||||
})
|
||||
}
|
||||
|
||||
private islandOnClick(event: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||
event.preventDefault();
|
||||
if (
|
||||
Math.abs(this.penDownX - this.pos3) > 5 ||
|
||||
Math.abs(this.penDownY - this.pos4) > 5
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.setState((prevState: PanelState) => {
|
||||
return {
|
||||
minimized: !prevState.minimized,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private islandOnPointerDown(event: React.PointerEvent) {
|
||||
const onDrag = (e: PointerEvent) => {
|
||||
e.preventDefault();
|
||||
this.pos1 = this.pos3 - e.clientX;
|
||||
this.pos2 = this.pos4 - e.clientY;
|
||||
this.pos3 = e.clientX;
|
||||
this.pos4 = e.clientY;
|
||||
this.updatePosition(this.pos2, this.pos1);
|
||||
};
|
||||
|
||||
const onPointerUp = () => {
|
||||
this.view.ownerDocument?.removeEventListener("pointerup", onPointerUp);
|
||||
this.view.ownerDocument?.removeEventListener("pointermove", onDrag);
|
||||
};
|
||||
|
||||
event.preventDefault();
|
||||
this.penDownX = this.pos3 = event.clientX;
|
||||
this.penDownY = this.pos4 = event.clientY;
|
||||
this.view.ownerDocument.addEventListener("pointerup", onPointerUp);
|
||||
this.view.ownerDocument.addEventListener("pointermove", onDrag);
|
||||
};
|
||||
|
||||
render() {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.render,"ToolsPanel.render()");
|
||||
return (
|
||||
@@ -230,41 +469,8 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
width: "100%",
|
||||
cursor: "move",
|
||||
}}
|
||||
onClick={(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
event.preventDefault();
|
||||
if (
|
||||
Math.abs(this.penDownX - this.pos3) > 5 ||
|
||||
Math.abs(this.penDownY - this.pos4) > 5
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.setState((prevState: PanelState) => {
|
||||
return {
|
||||
minimized: !prevState.minimized,
|
||||
};
|
||||
});
|
||||
}}
|
||||
onPointerDown={(event: React.PointerEvent) => {
|
||||
const onDrag = (e: PointerEvent) => {
|
||||
e.preventDefault();
|
||||
this.pos1 = this.pos3 - e.clientX;
|
||||
this.pos2 = this.pos4 - e.clientY;
|
||||
this.pos3 = e.clientX;
|
||||
this.pos4 = e.clientY;
|
||||
this.updatePosition(this.pos2, this.pos1);
|
||||
};
|
||||
|
||||
const onPointerUp = () => {
|
||||
this.props.view.ownerDocument?.removeEventListener("pointerup", onPointerUp);
|
||||
this.props.view.ownerDocument?.removeEventListener("pointermove", onDrag);
|
||||
};
|
||||
|
||||
event.preventDefault();
|
||||
this.penDownX = this.pos3 = event.clientX;
|
||||
this.penDownY = this.pos4 = event.clientY;
|
||||
this.props.view.ownerDocument.addEventListener("pointerup", onPointerUp);
|
||||
this.props.view.ownerDocument.addEventListener("pointermove", onDrag);
|
||||
}}
|
||||
onClick={this.islandOnClick.bind(this)}
|
||||
onPointerDown={this.islandOnPointerDown.bind(this)}
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
@@ -297,62 +503,39 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
<ActionButton
|
||||
key={"scriptEngine"}
|
||||
title={t("INSTALL_SCRIPT_BUTTON")}
|
||||
action={() => {
|
||||
new ScriptInstallPrompt(this.props.view.plugin).open();
|
||||
}}
|
||||
action={this.actionOpenScriptInstallDialog.bind(this)}
|
||||
icon={ICONS.scriptEngine}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"release-notes"}
|
||||
title={t("READ_RELEASE_NOTES")}
|
||||
action={() => {
|
||||
new ReleaseNotes(
|
||||
this.props.view.app,
|
||||
this.props.view.plugin,
|
||||
PLUGIN_VERSION,
|
||||
).open();
|
||||
}}
|
||||
action={this.actionOpenReleaseNotes.bind(this)}
|
||||
icon={ICONS.releaseNotes}
|
||||
view={this.props.view}
|
||||
/>
|
||||
{this.state.isPreviewMode === null ? (
|
||||
<ActionButton
|
||||
key={"convert"}
|
||||
title={t("CONVERT_FILE")}
|
||||
action={() => {
|
||||
this.props.view.convertExcalidrawToMD();
|
||||
}}
|
||||
action={(this.actionConvertExcalidrawToMD.bind(this))}
|
||||
icon={ICONS.convertFile}
|
||||
view={this.props.view}
|
||||
/>
|
||||
) : (
|
||||
<ActionButton
|
||||
key={"viewmode"}
|
||||
title={this.state.isPreviewMode ? t("PARSED") : t("RAW")}
|
||||
action={() => {
|
||||
if (this.state.isPreviewMode) {
|
||||
this.props.view.changeTextMode(TextMode.raw);
|
||||
} else {
|
||||
this.props.view.changeTextMode(TextMode.parsed);
|
||||
}
|
||||
}}
|
||||
action={this.actionToggleViewMode.bind(this)}
|
||||
icon={
|
||||
this.state.isPreviewMode
|
||||
? ICONS.rawMode
|
||||
: ICONS.parsedMode
|
||||
}
|
||||
view={this.props.view}
|
||||
/>
|
||||
)}
|
||||
<ActionButton
|
||||
key={"tray-mode"}
|
||||
title={t("TRAY_MODE")}
|
||||
action={() => {
|
||||
this.props.view.toggleTrayMode();
|
||||
}}
|
||||
action={this.actionToggleTrayMode.bind(this)}
|
||||
icon={ICONS.trayMode}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"fullscreen"}
|
||||
@@ -361,80 +544,42 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
? t("EXIT_FULLSCREEN")
|
||||
: t("GOTO_FULLSCREEN")
|
||||
}
|
||||
action={() => {
|
||||
if (this.state.isFullscreen) {
|
||||
this.props.view.exitFullscreen();
|
||||
} else {
|
||||
this.props.view.gotoFullscreen();
|
||||
}
|
||||
}}
|
||||
action={this.actionToggleFullscreen.bind(this)}
|
||||
icon={
|
||||
this.state.isFullscreen
|
||||
? ICONS.exitFullScreen
|
||||
: ICONS.gotoFullScreen
|
||||
}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"search"}
|
||||
title={t("SEARCH")}
|
||||
action={() => {
|
||||
search(this.props.view);
|
||||
}}
|
||||
action={this.actionSearch.bind(this)}
|
||||
icon={ICONS.search}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"ocr"}
|
||||
title={t("RUN_OCR")}
|
||||
action={(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
if(!this.props.view.plugin.settings.taskboneEnabled) {
|
||||
new Notice("Taskbone OCR is not enabled. Please go to plugins settings to enable it.",4000);
|
||||
return;
|
||||
}
|
||||
this.props.view.plugin.taskbone.getTextForView(this.props.view, {forceReScan: isWinCTRLorMacCMD(e)});
|
||||
}}
|
||||
action={this.actionOCR.bind(this)}
|
||||
icon={ICONS.ocr}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"openLink"}
|
||||
title={t("OPEN_LINK_CLICK")}
|
||||
action={(e) => {
|
||||
const event = new MouseEvent("click", {
|
||||
ctrlKey: e.ctrlKey || !(DEVICE.isIOS || DEVICE.isMacOS),
|
||||
metaKey: e.metaKey || (DEVICE.isIOS || DEVICE.isMacOS),
|
||||
shiftKey: e.shiftKey,
|
||||
altKey: e.altKey,
|
||||
});
|
||||
this.props.view.handleLinkClick(event);
|
||||
}}
|
||||
action={this.actionOpenLink.bind(this)}
|
||||
icon={ICONS.openLink}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"openLinkProperties"}
|
||||
title={t("OPEN_LINK_PROPS")}
|
||||
action={() => {
|
||||
const event = new MouseEvent("click", {
|
||||
ctrlKey: true,
|
||||
metaKey: true,
|
||||
shiftKey: false,
|
||||
altKey: false,
|
||||
});
|
||||
this.props.view.handleLinkClick(event);
|
||||
}}
|
||||
action={this.actionOpenLinkProperties.bind(this)}
|
||||
icon={ICONS.openLinkProperties}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"save"}
|
||||
title={t("FORCE_SAVE")}
|
||||
action={() => {
|
||||
this.props.view.forceSave();
|
||||
}}
|
||||
action={this.actionForceSave.bind(this)}
|
||||
icon={saveIcon(this.state.isDirty)}
|
||||
view={this.props.view}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -444,49 +589,26 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
<ActionButton
|
||||
key={"lib"}
|
||||
title={t("DOWNLOAD_LIBRARY")}
|
||||
action={() => {
|
||||
this.props.view.plugin.exportLibrary();
|
||||
}}
|
||||
action={this.actionExportLibrary.bind(this)}
|
||||
icon={ICONS.exportLibrary}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"exportIMG"}
|
||||
title={t("EXPORT_IMAGE")}
|
||||
action={() => {
|
||||
const view = this.props.view;
|
||||
if(!view.exportDialog) {
|
||||
view.exportDialog = new ExportDialog(view.plugin, view,view.file);
|
||||
view.exportDialog.createForm();
|
||||
}
|
||||
view.exportDialog.open();
|
||||
}}
|
||||
action={this.actionExportImage.bind(this)}
|
||||
icon={ICONS.ExportImage}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"md"}
|
||||
title={t("OPEN_AS_MD")}
|
||||
action={() => {
|
||||
this.props.view.openAsMarkdown();
|
||||
}}
|
||||
action={this.actionOpenAsMarkdown.bind(this)}
|
||||
icon={ICONS.switchToMarkdown}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"link-to-element"}
|
||||
title={t("INSERT_LINK_TO_ELEMENT")}
|
||||
action={(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
if(isWinALTorMacOPT(e)) {
|
||||
openExternalLink("https://youtu.be/yZQoJg2RCKI", this.props.view.app);
|
||||
return;
|
||||
}
|
||||
this.props.view.copyLinkToSelectedElementToClipboard(
|
||||
isWinCTRLorMacCMD(e) ? "group=" : (isSHIFT(e) ? "area=" : "")
|
||||
);
|
||||
}}
|
||||
action={this.actionLinkToElement.bind(this)}
|
||||
icon={ICONS.copyElementLink}
|
||||
view={this.props.view}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -496,104 +618,56 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
<ActionButton
|
||||
key={"anyfile"}
|
||||
title={t("UNIVERSAL_ADD_FILE")}
|
||||
action={() => {
|
||||
this.props.centerPointer();
|
||||
const insertFileModal = new UniversalInsertFileModal(this.props.view.plugin, this.props.view);
|
||||
insertFileModal.open();
|
||||
}}
|
||||
action={this.actionAddAnyFile.bind(this)}
|
||||
icon={ICONS["add-file"]}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"image"}
|
||||
title={t("INSERT_IMAGE")}
|
||||
action={() => {
|
||||
this.props.centerPointer();
|
||||
this.props.view.plugin.insertImageDialog.start(
|
||||
this.props.view,
|
||||
);
|
||||
}}
|
||||
action={this.actionInsertImage.bind(this)}
|
||||
icon={ICONS.insertImage}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"pdf"}
|
||||
title={t("INSERT_PDF")}
|
||||
action={() => {
|
||||
this.props.centerPointer();
|
||||
const insertPDFModal = new InsertPDFModal(this.props.view.plugin, this.props.view);
|
||||
insertPDFModal.open();
|
||||
}}
|
||||
action={this.actionInsertPDF.bind(this)}
|
||||
icon={ICONS.insertPDF}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"insertMD"}
|
||||
title={t("INSERT_MD")}
|
||||
action={() => {
|
||||
this.props.centerPointer();
|
||||
this.props.view.plugin.insertMDDialog.start(
|
||||
this.props.view,
|
||||
);
|
||||
}}
|
||||
action={this.actionInsertMarkdown.bind(this)}
|
||||
icon={ICONS.insertMD}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"insertBackOfNote"}
|
||||
title={t("INSERT_CARD")}
|
||||
action={() => {
|
||||
this.props.centerPointer();
|
||||
this.props.view.insertBackOfTheNoteCard();
|
||||
}}
|
||||
action={this.actionInsertBackOfNote.bind(this)}
|
||||
icon={ICONS.BackOfNote}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"latex"}
|
||||
title={t("INSERT_LATEX")}
|
||||
action={(e) => {
|
||||
if(isWinALTorMacOPT(e)) {
|
||||
openExternalLink("https://youtu.be/r08wk-58DPk", this.props.view.app);
|
||||
return;
|
||||
}
|
||||
this.props.centerPointer();
|
||||
insertLaTeXToView(this.props.view);
|
||||
}}
|
||||
action={this.actionInsertLaTeX.bind(this)}
|
||||
icon={ICONS.insertLaTeX}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"link"}
|
||||
title={t("INSERT_LINK")}
|
||||
action={() => {
|
||||
this.props.centerPointer();
|
||||
this.props.view.plugin.insertLinkDialog.start(
|
||||
this.props.view.file.path,
|
||||
(text: string, fontFamily?: 1 | 2 | 3 | 4, save?: boolean) => this.props.view.addText (text, fontFamily, save),
|
||||
);
|
||||
}}
|
||||
action={this.actionInsertLink.bind(this)}
|
||||
icon={ICONS.insertLink}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"import-svg"}
|
||||
title={t("IMPORT_SVG")}
|
||||
action={(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
this.props.view.plugin.importSVGDialog.start(this.props.view);
|
||||
}}
|
||||
action={this.actionImportSVG.bind(this)}
|
||||
icon={ICONS.importSVG}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"crop-image"}
|
||||
title={t("CROP_IMAGE")}
|
||||
action={(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
// @ts-ignore
|
||||
this.props.view.app.commands.executeCommandById("obsidian-excalidraw-plugin:crop-image");
|
||||
}}
|
||||
action={this.actionCropImage.bind(this)}
|
||||
icon={ICONS.Crop}
|
||||
view={this.props.view}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -612,7 +686,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
return "";
|
||||
}
|
||||
|
||||
const downloadedScriptsRoot = `${this.props.view.plugin.settings.scriptFolderPath}/${SCRIPT_INSTALL_FOLDER}/`;
|
||||
const downloadedScriptsRoot = `${this.view.plugin.settings.scriptFolderPath}/${SCRIPT_INSTALL_FOLDER}/`;
|
||||
|
||||
const filterCondition = (key: string): boolean =>
|
||||
isDownloaded
|
||||
@@ -647,45 +721,15 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
<ActionButton
|
||||
key={key}
|
||||
title={value.name}
|
||||
action={async () => {
|
||||
const view = this.props.view;
|
||||
const plugin = view.plugin;
|
||||
const f = app.vault.getAbstractFileByPath(key);
|
||||
if (f && f instanceof TFile) {
|
||||
plugin.scriptEngine.executeScript(
|
||||
view,
|
||||
await app.vault.read(f),
|
||||
plugin.scriptEngine.getScriptName(f),
|
||||
f
|
||||
);
|
||||
}
|
||||
}}
|
||||
longpress={async () => {
|
||||
const view = this.props.view;
|
||||
const api = view.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
const plugin = view.plugin;
|
||||
await plugin.loadSettings();
|
||||
const index = plugin.settings.pinnedScripts.indexOf(key)
|
||||
if(index > -1) {
|
||||
plugin.settings.pinnedScripts.splice(index,1);
|
||||
api?.setToast({message:`Pin removed: ${value.name}`, duration: 3000, closable: true});
|
||||
} else {
|
||||
plugin.settings.pinnedScripts.push(key);
|
||||
api?.setToast({message:`Pinned: ${value.name}`, duration: 3000, closable: true})
|
||||
}
|
||||
await plugin.saveSettings();
|
||||
app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> {
|
||||
if (v.view instanceof ExcalidrawView) v.view.updatePinnedScripts()
|
||||
})
|
||||
}}
|
||||
action={this.actionRunScript.bind(this,key)}
|
||||
longpress={this.actionPinScript.bind(this,key, value.name)}
|
||||
icon={
|
||||
value.svgString
|
||||
new WeakRef(value.svgString
|
||||
? stringToSVG(value.svgString)
|
||||
: (
|
||||
ICONS.cog
|
||||
)
|
||||
)).deref()
|
||||
}
|
||||
view={this.props.view}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,10 @@ export default class Taskbone {
|
||||
) {
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.plugin = null;
|
||||
}
|
||||
|
||||
public async initialize(save:boolean = true):Promise<string> {
|
||||
if(this.plugin.settings.taskboneAPIkey !== "") return;
|
||||
const response = await requestUrl({
|
||||
@@ -95,11 +99,13 @@ export default class Taskbone {
|
||||
));
|
||||
if(viewElements.length === 0) {
|
||||
new Notice ("Aborting OCR because there are no image or freedraw elements on the canvas.",4000);
|
||||
ea.destroy();
|
||||
return;
|
||||
}
|
||||
const fe = new FrontmatterEditor(view.data);
|
||||
if(addToFrontmatter && fe.hasKey("taskbone-ocr") && !forceReScan) {
|
||||
new Notice ("The drawing has already been processed, you will find the result in the frontmatter in markdown view mode. If you ran the command from the Obsidian Panel in Excalidraw then you can CTRL(CMD)+click the command to force the rescaning.",4000)
|
||||
ea.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -113,6 +119,7 @@ export default class Taskbone {
|
||||
window.navigator.clipboard.writeText(text);
|
||||
new Notice(`I placed the recognized text onto the system clipboard${addToFrontmatter?" and to document properties":""}.`);
|
||||
}
|
||||
ea.destroy();
|
||||
}
|
||||
|
||||
private async getTextForImage(image: Blob):Promise<string> {
|
||||
|
||||
@@ -2,18 +2,19 @@ import {
|
||||
App,
|
||||
ButtonComponent,
|
||||
DropdownComponent,
|
||||
getIcon,
|
||||
normalizePath,
|
||||
PluginSettingTab,
|
||||
Setting,
|
||||
TextComponent,
|
||||
TFile,
|
||||
} from "obsidian";
|
||||
import { DEVICE, GITHUB_RELEASES, VIEW_TYPE_EXCALIDRAW } from "./constants/constants";
|
||||
import { GITHUB_RELEASES, VIEW_TYPE_EXCALIDRAW } from "./constants/constants";
|
||||
import ExcalidrawView from "./ExcalidrawView";
|
||||
import { t } from "./lang/helpers";
|
||||
import type ExcalidrawPlugin from "./main";
|
||||
import { PenStyle } from "./PenTypes";
|
||||
import { DynamicStyle } from "./types";
|
||||
import { DynamicStyle } from "./types/types";
|
||||
import { PreviewImageType } from "./utils/UtilTypes";
|
||||
import { setDynamicStyle } from "./utils/DynamicStyling";
|
||||
import {
|
||||
@@ -36,6 +37,8 @@ import { ModifierKeySettingsComponent } from "./dialogs/ModifierKeySettings";
|
||||
import { ANNOTATED_PREFIX, CROPPED_PREFIX } from "./utils/CarveOut";
|
||||
import { EDITOR_FADEOUT } from "./CodeMirrorExtension/EditorHandler";
|
||||
import { setDebugging } from "./utils/DebugHelper";
|
||||
import { link } from "fs";
|
||||
import { Rank } from "./menu/ActionIcons";
|
||||
|
||||
export interface ExcalidrawSettings {
|
||||
folder: string;
|
||||
@@ -47,6 +50,7 @@ export interface ExcalidrawSettings {
|
||||
compress: boolean;
|
||||
decompressForMDView: boolean;
|
||||
onceOffCompressFlagReset: boolean; //used to reset compress to true in 2.2.0
|
||||
onceOffGPTVersionReset: boolean; //used to reset GPT version in 2.2.11
|
||||
autosave: boolean;
|
||||
autosaveIntervalDesktop: number;
|
||||
autosaveIntervalMobile: number;
|
||||
@@ -120,9 +124,11 @@ export interface ExcalidrawSettings {
|
||||
experimentalFileTag: string;
|
||||
experimentalLivePreview: boolean;
|
||||
fadeOutExcalidrawMarkup: boolean;
|
||||
loadPropertySuggestions: boolean;
|
||||
experimentalEnableFourthFont: boolean;
|
||||
experimantalFourthFont: string;
|
||||
addDummyTextElement: boolean;
|
||||
zoteroCompatibility: boolean;
|
||||
fieldSuggester: boolean;
|
||||
//loadCount: number; //version 1.2 migration counter
|
||||
drawingOpenCount: number;
|
||||
@@ -194,6 +200,7 @@ export interface ExcalidrawSettings {
|
||||
longPressDesktop: number;
|
||||
longPressMobile: number;
|
||||
isDebugMode: boolean;
|
||||
rank: Rank;
|
||||
}
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
@@ -208,6 +215,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
compress: true,
|
||||
decompressForMDView: false,
|
||||
onceOffCompressFlagReset: false,
|
||||
onceOffGPTVersionReset: false,
|
||||
autosave: true,
|
||||
autosaveIntervalDesktop: 15000,
|
||||
autosaveIntervalMobile: 10000,
|
||||
@@ -280,9 +288,11 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
experimentalFileTag: "✏️",
|
||||
experimentalLivePreview: true,
|
||||
fadeOutExcalidrawMarkup: false,
|
||||
loadPropertySuggestions: true,
|
||||
experimentalEnableFourthFont: false,
|
||||
experimantalFourthFont: "Virgil",
|
||||
addDummyTextElement: false,
|
||||
zoteroCompatibility: false,
|
||||
fieldSuggester: true,
|
||||
compatibilityMode: false,
|
||||
//loadCount: 0,
|
||||
@@ -356,7 +366,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
startupScriptPath: "",
|
||||
openAIAPIToken: "",
|
||||
openAIDefaultTextModel: "gpt-3.5-turbo-1106",
|
||||
openAIDefaultVisionModel: "gpt-4-vision-preview",
|
||||
openAIDefaultVisionModel: "gpt-4o",
|
||||
openAIDefaultImageGenerationModel: "dall-e-3",
|
||||
openAIURL: "https://api.openai.com/v1/chat/completions",
|
||||
openAIImageGenerationURL: "https://api.openai.com/v1/images/generations",
|
||||
@@ -447,6 +457,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
longPressDesktop: 500,
|
||||
longPressMobile: 500,
|
||||
isDebugMode: false,
|
||||
rank: "Bronze",
|
||||
};
|
||||
|
||||
export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
@@ -535,6 +546,46 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
},
|
||||
});
|
||||
coffeeImg.height = 45;
|
||||
|
||||
const iconLinks = [
|
||||
{
|
||||
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"></path><path d="M9 18c-4.51 2-5-2-7-2"></path></svg>`,
|
||||
href: "https://github.com/zsviczian/obsidian-excalidraw-plugin/issues",
|
||||
aria: "Report bugs and raise feature requsts on the plugin's GitHub page",
|
||||
text: "Bugs and Feature Requests",
|
||||
},
|
||||
{
|
||||
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19c-2.3 0-6.4-.2-8.1-.6-.7-.2-1.2-.7-1.4-1.4-.3-1.1-.5-3.4-.5-5s.2-3.9.5-5c.2-.7.7-1.2 1.4-1.4C5.6 5.2 9.7 5 12 5s6.4.2 8.1.6c.7.2 1.2.7 1.4 1.4.3 1.1.5 3.4.5 5s-.2 3.9-.5 5c-.2.7-.7 1.2-1.4 1.4-1.7.4-5.8.6-8.1.6 0 0 0 0 0 0z"></path><polygon points="10 15 15 12 10 9"></polygon></svg>`,
|
||||
href: "https://www.youtube.com/@VisualPKM",
|
||||
aria: "Check out my YouTube channel to learn about Visual Thinking and Excalidraw",
|
||||
text: "Visual PKM on YouTube",
|
||||
},
|
||||
{
|
||||
icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" stroke="none" strokeWidth="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 640 512"><path d="M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z"/></svg>`,
|
||||
href: "https://discord.gg/DyfAXFwUHc",
|
||||
aria: "Join the Visual Thinking Workshop Discord Server",
|
||||
text: "Community on Discord",
|
||||
},
|
||||
{
|
||||
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"></path></svg>`,
|
||||
href: "https://twitter.com/zsviczian",
|
||||
aria: "Follow me on Twitter",
|
||||
text: "Follow me on Twitter",
|
||||
},
|
||||
{
|
||||
icon: getIcon("graduation-cap").outerHTML,
|
||||
href: "https://visual-thinking-workshop.com",
|
||||
aria: "Learn about Visual PKM, Excalidraw, Obsidian, ExcaliBrain and more",
|
||||
text: "Join the Visual Thinking Workshop",
|
||||
}
|
||||
];
|
||||
|
||||
const linksEl = containerEl.createDiv("setting-item-description excalidraw-settings-links-container");
|
||||
iconLinks.forEach(({ icon, href, aria, text }) => {
|
||||
linksEl.createEl("a",{href, attr: { "aria-label": aria }}, (a)=> {
|
||||
a.innerHTML = icon + text;
|
||||
});
|
||||
});
|
||||
|
||||
// ------------------------------------------------
|
||||
// Saving
|
||||
@@ -2133,7 +2184,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
d.addOption("Assistant", "Assistant");
|
||||
this.app.vault
|
||||
.getFiles()
|
||||
.filter((f) => ["ttf", "woff", "woff2"].contains(f.extension))
|
||||
.filter((f) => ["ttf", "woff", "woff2", "otf"].contains(f.extension))
|
||||
.forEach((f: TFile) => {
|
||||
d.addOption(f.path, f.name);
|
||||
});
|
||||
@@ -2219,7 +2270,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
areaZoomText.innerText = ` ${value.toString()}`;
|
||||
this.plugin.settings.areaZoomLimit = value;
|
||||
this.applySettingsUpdate();
|
||||
this.plugin.excalidrawConfig.updateValues();
|
||||
this.plugin.excalidrawConfig.updateValues(this.plugin);
|
||||
}),
|
||||
)
|
||||
.settingEl.createDiv("", (el) => {
|
||||
@@ -2377,6 +2428,18 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("EXCALIDRAW_PROPERTIES_NAME"))
|
||||
.setDesc(fragWithHTML(t("EXCALIDRAW_PROPERTIES_DESC")))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.loadPropertySuggestions)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.loadPropertySuggestions = value;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
detailsEl = experimentalDetailsEl.createEl("details");
|
||||
detailsEl.createEl("summary", {
|
||||
text: t("TASKBONE_HEAD"),
|
||||
@@ -2518,6 +2581,18 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("PRESERVE_TEXT_AFTER_DRAWING_NAME"))
|
||||
.setDesc(fragWithHTML(t("PRESERVE_TEXT_AFTER_DRAWING_DESC")))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.zoteroCompatibility)
|
||||
.onChange((value) => {
|
||||
this.plugin.settings.zoteroCompatibility = value;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
new Setting(detailsEl)
|
||||
.setName(t("DEBUGMODE_NAME"))
|
||||
|
||||
@@ -30,6 +30,15 @@ export const svgToExcalidraw = (svgString: string): ConversionResult => {
|
||||
|
||||
walk({ tw, scene, groups, root: svgDOM }, tw.nextNode());
|
||||
|
||||
const hasVisibleElements = Boolean(scene.elements.find((el)=>el.opacity !== 0));
|
||||
if (!hasVisibleElements) {
|
||||
scene.elements.forEach((el) => {
|
||||
el.opacity = 100;
|
||||
});
|
||||
}
|
||||
scene.elements.forEach((el) => {
|
||||
if(el.opacity <= 1) el.opacity = 100;
|
||||
});
|
||||
content = scene.elements; //scene.toExJSON();
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ const SUPPORTED_TAGS = [
|
||||
"rect",
|
||||
"polyline",
|
||||
"polygon",
|
||||
"switch",
|
||||
];
|
||||
|
||||
const nodeValidator = (node: Element): number => {
|
||||
@@ -120,6 +121,18 @@ const walkers = {
|
||||
walk(args, args.tw.nextNode());
|
||||
},
|
||||
|
||||
switch: (args: WalkerArgs) => {
|
||||
const nextArgs = {
|
||||
...args,
|
||||
tw: createTreeWalker(args.tw.currentNode),
|
||||
groups: [...args.groups, new Group(args.tw.currentNode as Element)],
|
||||
};
|
||||
|
||||
walk(nextArgs, nextArgs.tw.nextNode());
|
||||
|
||||
walk(args, args.tw.nextSibling());
|
||||
},
|
||||
|
||||
g: (args: WalkerArgs) => {
|
||||
const nextArgs = {
|
||||
...args,
|
||||
|
||||
163
src/types.d.ts → src/types/types.d.ts
vendored
163
src/types.d.ts → src/types/types.d.ts
vendored
@@ -1,82 +1,83 @@
|
||||
import { ExcalidrawAutomate } from "./ExcalidrawAutomate";
|
||||
|
||||
export type ConnectionPoint = "top" | "bottom" | "left" | "right" | null;
|
||||
|
||||
export type Packages = {
|
||||
react: any,
|
||||
reactDOM: any,
|
||||
excalidrawLib: any,
|
||||
}
|
||||
|
||||
export type ValueOf<T> = T[keyof T];
|
||||
|
||||
export type DynamicStyle = "none" | "gray" | "colorful";
|
||||
|
||||
export type DeviceType = {
|
||||
isDesktop: boolean,
|
||||
isPhone: boolean,
|
||||
isTablet: boolean,
|
||||
isMobile: boolean,
|
||||
isLinux: boolean,
|
||||
isMacOS: boolean,
|
||||
isWindows: boolean,
|
||||
isIOS: boolean,
|
||||
isAndroid: boolean
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ExcalidrawAutomate: ExcalidrawAutomate;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "obsidian" {
|
||||
interface App {
|
||||
internalPlugins: any;
|
||||
isMobile(): boolean;
|
||||
getObsidianUrl(file:TFile): string;
|
||||
metadataTypeManager: {
|
||||
setType(name:string, type:string): void;
|
||||
};
|
||||
}
|
||||
interface Keymap {
|
||||
getRootScope(): Scope;
|
||||
}
|
||||
interface Scope {
|
||||
keys: any[];
|
||||
}
|
||||
interface Workspace {
|
||||
on(
|
||||
name: "hover-link",
|
||||
callback: (e: MouseEvent) => any,
|
||||
ctx?: any,
|
||||
): EventRef;
|
||||
}
|
||||
interface DataAdapter {
|
||||
url: {
|
||||
pathToFileURL(path: string): URL;
|
||||
},
|
||||
basePath: string;
|
||||
}
|
||||
interface FoldPosition {
|
||||
from: number;
|
||||
to: number;
|
||||
}
|
||||
|
||||
interface FoldInfo {
|
||||
folds: FoldPosition[];
|
||||
lines: number;
|
||||
}
|
||||
|
||||
interface MarkdownSubView {
|
||||
applyFoldInfo(foldInfo: FoldInfo): void;
|
||||
getFoldInfo(): FoldInfo | null;
|
||||
}
|
||||
/*interface Editor {
|
||||
insertText(data: string): void;
|
||||
}*/
|
||||
interface MetadataCache {
|
||||
getBacklinksForFile(file: TFile): any;
|
||||
getLinks(): { [id: string]: Array<{ link: string; displayText: string; original: string; position: any }> };
|
||||
}
|
||||
import { ExcalidrawAutomate } from "../ExcalidrawAutomate";
|
||||
import { ExcalidrawLib } from "../ExcalidrawLib";
|
||||
|
||||
export type ConnectionPoint = "top" | "bottom" | "left" | "right" | null;
|
||||
|
||||
export type Packages = {
|
||||
react: any,
|
||||
reactDOM: any,
|
||||
excalidrawLib: typeof ExcalidrawLib,
|
||||
}
|
||||
|
||||
export type ValueOf<T> = T[keyof T];
|
||||
|
||||
export type DynamicStyle = "none" | "gray" | "colorful";
|
||||
|
||||
export type DeviceType = {
|
||||
isDesktop: boolean,
|
||||
isPhone: boolean,
|
||||
isTablet: boolean,
|
||||
isMobile: boolean,
|
||||
isLinux: boolean,
|
||||
isMacOS: boolean,
|
||||
isWindows: boolean,
|
||||
isIOS: boolean,
|
||||
isAndroid: boolean
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ExcalidrawAutomate: ExcalidrawAutomate;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "obsidian" {
|
||||
interface App {
|
||||
internalPlugins: any;
|
||||
isMobile(): boolean;
|
||||
getObsidianUrl(file:TFile): string;
|
||||
metadataTypeManager: {
|
||||
setType(name:string, type:string): void;
|
||||
};
|
||||
}
|
||||
interface Keymap {
|
||||
getRootScope(): Scope;
|
||||
}
|
||||
interface Scope {
|
||||
keys: any[];
|
||||
}
|
||||
interface Workspace {
|
||||
on(
|
||||
name: "hover-link",
|
||||
callback: (e: MouseEvent) => any,
|
||||
ctx?: any,
|
||||
): EventRef;
|
||||
}
|
||||
interface DataAdapter {
|
||||
url: {
|
||||
pathToFileURL(path: string): URL;
|
||||
},
|
||||
basePath: string;
|
||||
}
|
||||
interface FoldPosition {
|
||||
from: number;
|
||||
to: number;
|
||||
}
|
||||
|
||||
interface FoldInfo {
|
||||
folds: FoldPosition[];
|
||||
lines: number;
|
||||
}
|
||||
|
||||
interface MarkdownSubView {
|
||||
applyFoldInfo(foldInfo: FoldInfo): void;
|
||||
getFoldInfo(): FoldInfo | null;
|
||||
}
|
||||
/*interface Editor {
|
||||
insertText(data: string): void;
|
||||
}*/
|
||||
interface MetadataCache {
|
||||
getBacklinksForFile(file: TFile): any;
|
||||
getLinks(): { [id: string]: Array<{ link: string; displayText: string; original: string; position: any }> };
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ export interface ObsidianCanvasNode {
|
||||
child: any;
|
||||
isEditing: boolean;
|
||||
file: TFile;
|
||||
detach: Function;
|
||||
}
|
||||
|
||||
export class CanvasNodeFactory {
|
||||
@@ -41,6 +42,7 @@ export class CanvasNodeFactory {
|
||||
nodes = new Map<string, ObsidianCanvasNode>();
|
||||
initialized: boolean = false;
|
||||
public isInitialized = () => this.initialized;
|
||||
private observer: CustomMutationObserver | MutationObserver;
|
||||
|
||||
constructor(
|
||||
private view: ExcalidrawView,
|
||||
@@ -110,11 +112,11 @@ export class CanvasNodeFactory {
|
||||
}
|
||||
}
|
||||
};
|
||||
const observer = DEBUGGING
|
||||
this.observer = DEBUGGING
|
||||
? new CustomMutationObserver(nodeObserverFn, "CanvasNodeFactory")
|
||||
: new MutationObserver(nodeObserverFn);
|
||||
|
||||
observer.observe(node.child.editor.containerEl.parentElement.parentElement, { attributes: true });
|
||||
this.observer.observe(node.child.editor.containerEl.parentElement.parentElement, { attributes: true });
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -127,12 +129,29 @@ export class CanvasNodeFactory {
|
||||
node.child.showPreview();
|
||||
}
|
||||
|
||||
removeNode(node: ObsidianCanvasNode) {
|
||||
if(!this.initialized || !node) return;
|
||||
this.nodes.delete(node.file.path);
|
||||
this.canvas.removeNode(node);
|
||||
node.detach();
|
||||
}
|
||||
|
||||
public purgeNodes() {
|
||||
if(!this.initialized) return;
|
||||
this.nodes.forEach(node => {
|
||||
this.canvas.removeNode(node);
|
||||
this.canvas.removeNode(node);
|
||||
node.detach();
|
||||
});
|
||||
this.nodes.clear();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.purgeNodes();
|
||||
this.initialized = false; //calling after purgeNodes becaues purge nodes checks for initialized
|
||||
this.observer?.disconnect();
|
||||
this.view = null;
|
||||
this.canvas = null;
|
||||
this.leaf = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,10 @@ export const carveOutImage = async (sourceEA: ExcalidrawAutomate, viewImageEl: E
|
||||
targetEA.copyViewElementsToEAforEditing([viewImageEl],true);
|
||||
const {height, width} = await sourceEA.getOriginalImageSize(viewImageEl);
|
||||
|
||||
if(!height || !width || height === 0 || width === 0) return;
|
||||
if(!height || !width || height === 0 || width === 0) {
|
||||
targetEA.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
const newImage = targetEA.getElement(viewImageEl.id) as Mutable<ExcalidrawImageElement>;
|
||||
newImage.x = 0;
|
||||
@@ -47,7 +50,10 @@ export const carveOutImage = async (sourceEA: ExcalidrawAutomate, viewImageEl: E
|
||||
const {folderpath, filename} = await getCropFileNameAndFolder(sourceEA.plugin,sourceEA.targetView.file.path,fname);
|
||||
|
||||
const file = await createImageCropperFile(targetEA, newImage.id, imageLink, folderpath, filename);
|
||||
if(!file) return;
|
||||
if(!file) {
|
||||
targetEA.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
//console.log(await app.vault.read(file));
|
||||
sourceEA.clear();
|
||||
@@ -61,7 +67,8 @@ export const carveOutImage = async (sourceEA: ExcalidrawAutomate, viewImageEl: E
|
||||
replacingImage.height = sourceImageEl.height;
|
||||
replacingImage.scale = scale;
|
||||
replacingImage.angle = angle;
|
||||
sourceEA.addElementsToView(false, true, true);
|
||||
await sourceEA.addElementsToView(false, true, true);
|
||||
targetEA.destroy();
|
||||
}
|
||||
|
||||
export const carveOutPDF = async (sourceEA: ExcalidrawAutomate, embeddableEl: ExcalidrawEmbeddableElement, pdfPathWithPage: string, pdfFile: TFile) => {
|
||||
@@ -71,7 +78,10 @@ export const carveOutPDF = async (sourceEA: ExcalidrawAutomate, embeddableEl: Ex
|
||||
|
||||
let {height, width} = embeddableEl;
|
||||
|
||||
if(!height || !width || height === 0 || width === 0) return;
|
||||
if(!height || !width || height === 0 || width === 0) {
|
||||
targetEA.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
const imageId = await targetEA.addImage(0,0, pdfPathWithPage);
|
||||
const newImage = targetEA.getElement(imageId) as Mutable<ExcalidrawImageElement>;
|
||||
@@ -85,7 +95,10 @@ export const carveOutPDF = async (sourceEA: ExcalidrawAutomate, embeddableEl: Ex
|
||||
const {folderpath, filename} = await getCropFileNameAndFolder(sourceEA.plugin,sourceEA.targetView.file.path,fname);
|
||||
|
||||
const file = await createImageCropperFile(targetEA, newImage.id, imageLink, folderpath, filename);
|
||||
if(!file) return;
|
||||
if(!file) {
|
||||
targetEA.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
//console.log(await app.vault.read(file));
|
||||
sourceEA.clear();
|
||||
@@ -100,7 +113,8 @@ export const carveOutPDF = async (sourceEA: ExcalidrawAutomate, embeddableEl: Ex
|
||||
replacingImage.width = replacingImage.height * imageAspectRatio;
|
||||
}
|
||||
replacingImage.angle = angle;
|
||||
sourceEA.addElementsToView(false, true, true);
|
||||
await sourceEA.addElementsToView(false, true, true);
|
||||
targetEA.destroy();
|
||||
}
|
||||
|
||||
|
||||
@@ -169,41 +183,5 @@ export const createImageCropperFile = async (targetEA: ExcalidrawAutomate, image
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
//wait for the new ExcalidrawView to open and initialize
|
||||
counter = 0;
|
||||
let newView = workspace.getActiveViewOfType(ExcalidrawView) as ExcalidrawView;
|
||||
while(
|
||||
(workspace.getActiveFile() !== file ||
|
||||
newView?.file !== file ||
|
||||
!newView?.isLoaded ||
|
||||
!Boolean(newView?.excalidrawAPI)) &&
|
||||
counter < 100
|
||||
) {
|
||||
await sleep(100);
|
||||
newView = workspace.getActiveViewOfType(ExcalidrawView) as ExcalidrawView;
|
||||
counter++;
|
||||
}
|
||||
//console.log({counter});
|
||||
if(newView?.file !== file || !newView?.isLoaded ||!Boolean(newView?.excalidrawAPI)) {
|
||||
new Notice("View did not initialize. NewExcalidraw Drawing is taking too long to open. Please try again.");
|
||||
return;
|
||||
}
|
||||
|
||||
//wait for the image to load to the new view
|
||||
const api = newView.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
counter = 0;
|
||||
while(Object.keys(api.getFiles()).length === 0 && counter < 100) {
|
||||
await sleep(100);
|
||||
counter++;
|
||||
}
|
||||
|
||||
if(Object.keys(api.getFiles()).length === 0) {
|
||||
new Notice("Image did not load to the view. NewExcalidraw Drawing is taking too long to load. Please try again.");
|
||||
return;
|
||||
}
|
||||
*/
|
||||
//console.log({counter, path: workspace.getActiveFile()?.path, newView, files: api.getFiles()});
|
||||
|
||||
return file;
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import { Notice } from "obsidian";
|
||||
import { getEA } from "src";
|
||||
import { ExcalidrawAutomate, cloneElement } from "src/ExcalidrawAutomate";
|
||||
import { ExportSettings } from "src/ExcalidrawView";
|
||||
import { embedFontsInSVG } from "./Utils";
|
||||
import { nanoid } from "src/constants/constants";
|
||||
|
||||
export class CropImage {
|
||||
@@ -45,6 +44,15 @@ export class CropImage {
|
||||
})
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.imageEA.destroy();
|
||||
this.maskEA.destroy();
|
||||
this.imageEA = null;
|
||||
this.maskEA = null;
|
||||
this.elements = null;
|
||||
this.bbox = null;
|
||||
}
|
||||
|
||||
private setBoundingEl(ea: ExcalidrawAutomate, bgColor: string) {
|
||||
const {topX, topY, width, height} = this.bbox;
|
||||
ea.style.backgroundColor = bgColor;
|
||||
@@ -83,7 +91,7 @@ export class CropImage {
|
||||
isMask: false,
|
||||
}
|
||||
|
||||
const maskSVG = await this.maskEA.createSVG(null,false,exportSettings,null,null,0);
|
||||
const maskSVG = await this.maskEA.createSVG(null,true,exportSettings,null,null,0);
|
||||
const defs = maskSVG.querySelector("defs");
|
||||
const styleEl = maskSVG.querySelector("style");
|
||||
const style = styleEl ? styleEl.outerHTML : "";
|
||||
@@ -129,7 +137,7 @@ export class CropImage {
|
||||
async getCroppedPNG(): Promise<Blob> {
|
||||
//@ts-ignore
|
||||
const PLUGIN = app.plugins.plugins["obsidian-excalidraw-plugin"];
|
||||
const svg = embedFontsInSVG(await this.buildSVG(), PLUGIN);
|
||||
const svg = await this.buildSVG();
|
||||
return new Promise((resolve, reject) => {
|
||||
const svgData = new XMLSerializer().serializeToString(svg);
|
||||
const canvas = document.createElement('canvas');
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/
|
||||
import { ColorMaster } from "colormaster";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
import ExcalidrawView from "src/ExcalidrawView";
|
||||
import { DynamicStyle } from "src/types";
|
||||
import { DynamicStyle } from "src/types/types";
|
||||
import { cloneElement } from "src/ExcalidrawAutomate";
|
||||
import { ExcalidrawFrameElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { addAppendUpdateCustomData } from "./Utils";
|
||||
@@ -23,11 +23,9 @@ export const setDynamicStyle = (
|
||||
toolsStyle = toolsStyle.replace(/\-\-color\-primary.*/,"");
|
||||
toolspanel.setAttribute("style",toolsStyle);
|
||||
}
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
const doc = view.ownerDocument;
|
||||
//const doc = view.ownerDocument;
|
||||
const isLightTheme =
|
||||
view?.excalidrawAPI?.getAppState?.()?.theme === "light" ||
|
||||
view?.excalidrawData?.scene?.appState?.theme === "light";
|
||||
@@ -115,7 +113,7 @@ export const setDynamicStyle = (
|
||||
[`--h3-color`]: str(text),
|
||||
[`--h4-color`]: str(text),
|
||||
[`color`]: str(text),
|
||||
['--excalidraw-caret-color']: str(text),
|
||||
['--excalidraw-caret-color']: str(isLightTheme ? text : cmBG()),
|
||||
[`--select-highlight-color`]: str(gray1()),
|
||||
[`--color-gray-80`]: str(isDark?text.darkerBy(40):text.lighterBy(40)), //frame
|
||||
};
|
||||
@@ -129,9 +127,22 @@ export const setDynamicStyle = (
|
||||
styleString
|
||||
)*/
|
||||
|
||||
const toolspanel = view.toolsPanelRef?.current?.containerRef?.current;
|
||||
if(toolspanel) {
|
||||
let toolsStyle = toolspanel.getAttribute("style");
|
||||
toolsStyle = toolsStyle.replace(/\-\-color\-primary.*/,"");
|
||||
toolspanel.setAttribute("style",toolsStyle+styleString);
|
||||
}
|
||||
|
||||
setTimeout(()=>{
|
||||
const api = view.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
if(!api) return;
|
||||
if(!api) {
|
||||
view = null;
|
||||
ea = null;
|
||||
color = null;
|
||||
dynamicStyle = null;
|
||||
return;
|
||||
}
|
||||
const frameColor = {
|
||||
stroke: str(isDark?gray2().lighterBy(15):gray2().darkerBy(15)),
|
||||
fill: str((isDark?gray2().lighterBy(30):gray2().darkerBy(30)).alphaTo(0.2)),
|
||||
@@ -158,11 +169,9 @@ export const setDynamicStyle = (
|
||||
dynamicStyle: styleObject
|
||||
}
|
||||
});
|
||||
view = null;
|
||||
ea = null;
|
||||
color = null;
|
||||
dynamicStyle = null;
|
||||
});
|
||||
const toolspanel = view.toolsPanelRef?.current?.containerRef?.current;
|
||||
if(toolspanel) {
|
||||
let toolsStyle = toolspanel.getAttribute("style");
|
||||
toolsStyle = toolsStyle.replace(/\-\-color\-primary.*/,"");
|
||||
toolspanel.setAttribute("style",toolsStyle+styleString);
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,13 @@ export class ExcalidrawConfig {
|
||||
public areaLimit: number = 16777216;
|
||||
public widthHeightLimit: number = 32767;
|
||||
|
||||
constructor(private plugin: ExcalidrawPlugin) {
|
||||
this.updateValues();
|
||||
constructor(plugin: ExcalidrawPlugin) {
|
||||
this.updateValues(plugin);
|
||||
}
|
||||
|
||||
updateValues() {
|
||||
updateValues(plugin: ExcalidrawPlugin) {
|
||||
if(DEVICE.isIOS) return;
|
||||
this.areaLimit = 16777216*this.plugin.settings.areaZoomLimit; //this.plugin.settings.areaLimit;
|
||||
this.widthHeightLimit = 32767*this.plugin.settings.areaZoomLimit; //his.plugin.settings.widthHeightLimit;
|
||||
this.areaLimit = 16777216*plugin.settings.areaZoomLimit; //this.plugin.settings.areaLimit;
|
||||
this.widthHeightLimit = 32767*plugin.settings.areaZoomLimit; //his.plugin.settings.widthHeightLimit;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
import { MAX_IMAGE_SIZE, IMAGE_TYPES, ANIMATED_IMAGE_TYPES, MD_EX_SECTIONS } from "src/constants/constants";
|
||||
import { App, TFile } from "obsidian";
|
||||
import { App, Notice, TFile, WorkspaceLeaf } from "obsidian";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK, getExcalidrawMarkdownHeaderSection } from "src/ExcalidrawData";
|
||||
import ExcalidrawView from "src/ExcalidrawView";
|
||||
@@ -10,13 +10,15 @@ import { cleanSectionHeading } from "./ObsidianUtils";
|
||||
import { getEA } from "src";
|
||||
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { EmbeddableMDCustomProps } from "src/dialogs/EmbeddableSettings";
|
||||
import { nanoid } from "nanoid";
|
||||
import { t } from "src/lang/helpers";
|
||||
|
||||
export const insertImageToView = async (
|
||||
export async function insertImageToView(
|
||||
ea: ExcalidrawAutomate,
|
||||
position: { x: number, y: number },
|
||||
file: TFile | string,
|
||||
scale?: boolean,
|
||||
):Promise<string> => {
|
||||
):Promise<string> {
|
||||
ea.clear();
|
||||
ea.style.strokeColor = "transparent";
|
||||
ea.style.backgroundColor = "transparent";
|
||||
@@ -32,17 +34,17 @@ export const insertImageToView = async (
|
||||
return id;
|
||||
}
|
||||
|
||||
export const insertEmbeddableToView = async (
|
||||
export async function insertEmbeddableToView (
|
||||
ea: ExcalidrawAutomate,
|
||||
position: { x: number, y: number },
|
||||
file?: TFile,
|
||||
link?: string,
|
||||
):Promise<string> => {
|
||||
):Promise<string> {
|
||||
ea.clear();
|
||||
ea.style.strokeColor = "transparent";
|
||||
ea.style.backgroundColor = "transparent";
|
||||
if(file && (IMAGE_TYPES.contains(file.extension) || ea.isExcalidrawFile(file)) && !ANIMATED_IMAGE_TYPES.contains(file.extension)) {
|
||||
return await insertImageToView(ea, position, file);
|
||||
return await insertImageToView(ea, position, link??file);
|
||||
} else {
|
||||
const id = ea.addEmbeddable(
|
||||
position.x,
|
||||
@@ -57,7 +59,7 @@ export const insertEmbeddableToView = async (
|
||||
}
|
||||
}
|
||||
|
||||
export const getLinkTextFromLink = (text: string): string => {
|
||||
export function getLinkTextFromLink (text: string): string {
|
||||
if (!text) return;
|
||||
if (text.match(REG_LINKINDEX_HYPERLINK)) return;
|
||||
|
||||
@@ -70,7 +72,7 @@ export const getLinkTextFromLink = (text: string): string => {
|
||||
return linktext;
|
||||
}
|
||||
|
||||
export const openTagSearch = (link:string, app: App, view?: ExcalidrawView) => {
|
||||
export function openTagSearch (link:string, app: App, view?: ExcalidrawView) {
|
||||
const tags = link
|
||||
.matchAll(/#([\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+)/gu)
|
||||
.next();
|
||||
@@ -91,21 +93,70 @@ export const openTagSearch = (link:string, app: App, view?: ExcalidrawView) => {
|
||||
return;
|
||||
}
|
||||
|
||||
export const openExternalLink = (link:string, app: App, element?: ExcalidrawElement):boolean => {
|
||||
function getLinkFromMarkdownLink(link: string): string {
|
||||
const result = /^\[[^\]]*]\(([^\)]*)\)/.exec(link);
|
||||
return result ? result[1] : link;
|
||||
}
|
||||
|
||||
export function openExternalLink (link:string, app: App, element?: ExcalidrawElement):boolean {
|
||||
link = getLinkFromMarkdownLink(link);
|
||||
if (link.match(/^cmd:\/\/.*/)) {
|
||||
const cmd = link.replace("cmd://", "");
|
||||
//@ts-ignore
|
||||
app.commands.executeCommandById(cmd);
|
||||
return true;
|
||||
}
|
||||
if (link.match(REG_LINKINDEX_HYPERLINK)) {
|
||||
window.open(link, "_blank");
|
||||
if (!link.startsWith("obsidian://") && link.match(REG_LINKINDEX_HYPERLINK)) {
|
||||
window.open(link, "_blank");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export const getExcalidrawFileForwardLinks = (app: App, excalidrawFile: TFile, secondOrderLinksSet: Set<string>):string => {
|
||||
/**
|
||||
*
|
||||
* @param link
|
||||
* @param app
|
||||
* @param returnWikiLink
|
||||
* @returns
|
||||
* false if the link is not an obsidian link,
|
||||
* true if the link is an obsidian link and it was opened (i.e. it is a link to another Vault or not a file link e.g. plugin link), or
|
||||
* the link to the file path. By default as a wiki link, or as a file path if returnWikiLink is false.
|
||||
*/
|
||||
export function parseObsidianLink(link: string, app: App, returnWikiLink: boolean = true): boolean | string {
|
||||
link = getLinkFromMarkdownLink(link);
|
||||
if (!link.startsWith("obsidian://")) {
|
||||
return false;
|
||||
}
|
||||
const url = new URL(link);
|
||||
const action = url.pathname.slice(2); // Remove leading '//'
|
||||
|
||||
const props: {[key: string]: string} = {};
|
||||
url.searchParams.forEach((value, key) => {
|
||||
props[key] = decodeURIComponent(value);
|
||||
});
|
||||
|
||||
if (action === "open" && props.vault === app.vault.getName()) {
|
||||
const file = props.file;
|
||||
const f = app.metadataCache.getFirstLinkpathDest(file, "");
|
||||
if (f && f instanceof TFile) {
|
||||
if (returnWikiLink) {
|
||||
return `[[${f.path}]]`;
|
||||
} else {
|
||||
return f.path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.open(link, "_blank");
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getExcalidrawFileForwardLinks (
|
||||
app: App, excalidrawFile: TFile,
|
||||
secondOrderLinksSet: Set<string>,
|
||||
):string {
|
||||
let secondOrderLinks = "";
|
||||
const forwardLinks = app.metadataCache.getLinks()[excalidrawFile.path];
|
||||
if(forwardLinks && forwardLinks.length > 0) {
|
||||
@@ -124,7 +175,10 @@ export const getExcalidrawFileForwardLinks = (app: App, excalidrawFile: TFile, s
|
||||
return secondOrderLinks;
|
||||
}
|
||||
|
||||
export const getFrameBasedOnFrameNameOrId = (frameName: string, elements: ExcalidrawElement[]): ExcalidrawFrameElement | null => {
|
||||
export function getFrameBasedOnFrameNameOrId(
|
||||
frameName: string,
|
||||
elements: ExcalidrawElement[],
|
||||
): ExcalidrawFrameElement | null {
|
||||
const frames = elements
|
||||
.filter((el: ExcalidrawElement)=>el.type==="frame")
|
||||
.map((el: ExcalidrawFrameElement, idx: number)=>{
|
||||
@@ -135,7 +189,13 @@ export const getFrameBasedOnFrameNameOrId = (frameName: string, elements: Excali
|
||||
return frames.length === 1 ? frames[0] : null;
|
||||
}
|
||||
|
||||
export const addBackOfTheNoteCard = async (view: ExcalidrawView, title: string, activate: boolean = true, cardBody?: string, embeddableCustomData?: EmbeddableMDCustomProps):Promise<string> => {
|
||||
export async function addBackOfTheNoteCard(
|
||||
view: ExcalidrawView,
|
||||
title: string,
|
||||
activate: boolean = true,
|
||||
cardBody?: string,
|
||||
embeddableCustomData?: EmbeddableMDCustomProps,
|
||||
):Promise<string> {
|
||||
const data = view.data;
|
||||
const header = getExcalidrawMarkdownHeaderSection(data);
|
||||
const body = data.split(header)[1];
|
||||
@@ -181,5 +241,143 @@ export const addBackOfTheNoteCard = async (view: ExcalidrawView, title: string,
|
||||
if(found) view.getEmbeddableLeafElementById(el.id)?.editNode?.();
|
||||
});
|
||||
}
|
||||
ea.destroy();
|
||||
return el.id;
|
||||
}
|
||||
|
||||
export function renderContextMenuAction(
|
||||
React: any,
|
||||
label: string,
|
||||
action: Function,
|
||||
onClose: (callback?: () => void) => void,
|
||||
) {
|
||||
return React.createElement (
|
||||
"li",
|
||||
{
|
||||
key: nanoid(),
|
||||
onClick: () => {
|
||||
onClose(()=>action())
|
||||
},
|
||||
},
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
className: "context-menu-item",
|
||||
},
|
||||
React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "context-menu-item__label",
|
||||
},
|
||||
label,
|
||||
),
|
||||
React.createElement(
|
||||
"kbd",
|
||||
{
|
||||
className: "context-menu-item__shortcut",
|
||||
},
|
||||
"", //this is where the shortcut may go in the future
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function tmpBruteForceCleanup (view: ExcalidrawView) {
|
||||
window.setTimeout(()=>{
|
||||
if(!view) return;
|
||||
// const cleanupHTMLElement = (el: Element) => {
|
||||
// //console.log(el);
|
||||
// while(el.firstElementChild) {
|
||||
// cleanupHTMLElement(el.firstElementChild);
|
||||
// el.removeChild(el.firstElementChild);
|
||||
// }
|
||||
// Object.keys(el).forEach((key) => {
|
||||
// //@ts-ignore
|
||||
// delete el[key];
|
||||
// });
|
||||
// el.empty();
|
||||
// }
|
||||
|
||||
// const cleanupLeaf = (l:any) => {
|
||||
// l.detach?.();
|
||||
// l.resizeObserver?.disconnect?.();
|
||||
// l.view?.unload?.();
|
||||
// l.component?.unload?.();
|
||||
// Object.keys(l).forEach((key) => {
|
||||
// const obj = l[key];
|
||||
// if (obj instanceof Element) {
|
||||
// // Recursively empty the DOM element's children
|
||||
// while (obj.firstChild) {
|
||||
// cleanupHTMLElement(obj.firstElementChild);
|
||||
// obj.removeChild(obj.firstElementChild);
|
||||
// }
|
||||
// obj.empty();
|
||||
// delete l[key];
|
||||
// return;
|
||||
// }
|
||||
// //@ts-ignore
|
||||
// delete l[key];
|
||||
// });
|
||||
// }
|
||||
|
||||
// //@ts-ignore
|
||||
// if(view.leaf && !view.leaf.parent) {
|
||||
// if(view.containerEl) {
|
||||
// cleanupHTMLElement(view.containerEl);
|
||||
// }
|
||||
// const leaves = new Set();
|
||||
// leaves.add(view.leaf);
|
||||
// while(leaves.has(view.leaf.getContainer())) {
|
||||
// leaves.add(view.leaf.getContainer());
|
||||
// }
|
||||
// const roots = new Set();
|
||||
// roots.add(view.leaf.getRoot());
|
||||
// leaves.forEach((leaf:WorkspaceLeaf) => {
|
||||
// roots.add(leaf.getRoot());
|
||||
// });
|
||||
// leaves.forEach((l:any) => cleanupLeaf(l));
|
||||
// leaves.clear();
|
||||
// roots.forEach((root:any) => cleanupLeaf(root));
|
||||
// roots.clear();
|
||||
// }
|
||||
|
||||
Object.keys(view).forEach((key) => {
|
||||
//@ts-ignore
|
||||
delete view[key];
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the text matches the transclusion pattern and if so,
|
||||
* check if the link in the transclusion can be resolved to a file in the vault.
|
||||
* if yes, call the callback function with the link and the file.
|
||||
* @param text
|
||||
* @param callback
|
||||
* @returns true if text is a transclusion and the link can be resolved to a file in the vault, false otherwise.
|
||||
*/
|
||||
export function isTextImageTransclusion (
|
||||
text: string,
|
||||
view: ExcalidrawView,
|
||||
callback: (link: string, file: TFile)=>void
|
||||
): boolean {
|
||||
const REG_TRANSCLUSION = /^!\[\[([^|\]]*)?.*?]]$|^!\[[^\]]*?]\((.*?)\)$/g;
|
||||
const match = text.trim().matchAll(REG_TRANSCLUSION).next(); //reset the iterator
|
||||
if(match?.value?.[0]) {
|
||||
const link = match.value[1] ?? match.value[2];
|
||||
const file = view.app.metadataCache.getFirstLinkpathDest(link?.split("#")[0], view.file.path);
|
||||
if(view.file === file) {
|
||||
new Notice(t("RECURSIVE_INSERT_ERROR"));
|
||||
return false;
|
||||
}
|
||||
if(file && file instanceof TFile) {
|
||||
if (file.extension !== "md" || view.plugin.isExcalidrawFile(file)) {
|
||||
callback(link, file);
|
||||
return true;
|
||||
} else {
|
||||
new Notice(t("USE_INSERT_FILE_MODAL"),5000);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { getAttachmentsFolderAndFilePath } from "./ObsidianUtils";
|
||||
/**
|
||||
* Splits a full path including a folderpath and a filename into separate folderpath and filename components
|
||||
* @param filepath
|
||||
* @returns folderpath will be normalized. This means "/" for root folder and no trailing "/" for other folders
|
||||
*/
|
||||
type ImageExtension = keyof typeof IMAGE_MIME_TYPES;
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export type ImageKey = {
|
||||
|
||||
const getKey = (key: ImageKey): string =>
|
||||
`${key.filepath}#${key.blockref??""}#${key.sectionref??""}#${key.isDark ? 1 : 0}#${
|
||||
key.hasGroupref}#${key.hasArearef}#${key.hasFrameref}#${key.hasSectionref}#${
|
||||
key.hasGroupref}#${key.hasArearef}#${key.hasFrameref}#${key.hasClippedFrameref}#${key.hasSectionref}#${
|
||||
key.previewImageType === PreviewImageType.SVGIMG
|
||||
? 1
|
||||
: key.previewImageType === PreviewImageType.PNG
|
||||
@@ -43,6 +43,19 @@ class ImageCache {
|
||||
private app: App;
|
||||
public initializationNotice: boolean = false;
|
||||
private obsidanURLCache = new Map<string, string>();
|
||||
private purgeInvalidCacheTimer: number = null;
|
||||
private purgeInvalidBackupTimer: number = null;
|
||||
|
||||
public destroy(): void {
|
||||
this.isInitializing = true;
|
||||
if(this.purgeInvalidCacheTimer) clearTimeout(this.purgeInvalidCacheTimer);
|
||||
if(this.purgeInvalidBackupTimer) clearTimeout(this.purgeInvalidBackupTimer);
|
||||
this.db = null;
|
||||
this.plugin = null;
|
||||
this.app = null;
|
||||
this.obsidanURLCache.clear();
|
||||
this.obsidanURLCache = null;
|
||||
}
|
||||
|
||||
constructor(dbName: string, cacheStoreName: string, backupStoreName: string) {
|
||||
this.dbName = dbName;
|
||||
@@ -129,8 +142,15 @@ class ImageCache {
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(async ()=>this.purgeInvalidCacheFiles(), 60000);
|
||||
setTimeout(async ()=>this.purgeInvalidBackupFiles(), 120000);
|
||||
this.purgeInvalidCacheTimer = window.setTimeout(async ()=>{
|
||||
this.purgeInvalidCacheTimer = null;
|
||||
this.purgeInvalidCacheFiles();
|
||||
}, 60000);
|
||||
|
||||
this.purgeInvalidBackupTimer = window.setTimeout(async ()=>{
|
||||
this.purgeInvalidBackupTimer = null;
|
||||
this.purgeInvalidBackupFiles();
|
||||
}, 120000);
|
||||
} finally {
|
||||
this.isInitializing = false;
|
||||
if(this.initializationNotice) {
|
||||
@@ -152,7 +172,7 @@ class ImageCache {
|
||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue | null>).result;
|
||||
if(cursor) {
|
||||
const key = cursor.key as string;
|
||||
const isLegacyKey = key.replaceAll(/[^#]/g,"").length < 9; // introduced hasGroupref, etc. in 1.9.28
|
||||
const isLegacyKey = key.split("#").length-1 < 11; // introduced hasGroupref, etc. in 1.9.28 // introduced hasClippedFrameref in 2.2.10
|
||||
const filepath = key.split("#")[0];
|
||||
const fileExists = files.some((f: TFile) => f.path === filepath);
|
||||
const file = fileExists ? files.find((f: TFile) => f.path === filepath) : null;
|
||||
@@ -321,6 +341,7 @@ class ImageCache {
|
||||
return this.getBackupData(filepath);
|
||||
}
|
||||
|
||||
//cache SVG should have the width and height parameters and not the embedded font
|
||||
public addImageToCache(key_: ImageKey, obsidianURL: string, image: Blob|SVGSVGElement): void {
|
||||
if (!this.isReady()) {
|
||||
return; // Database not initialized yet
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { ExcalidrawElement, ExcalidrawImageElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { requireApiVersion } from "obsidian";
|
||||
|
||||
export const getMermaidImageElements = (elements: ExcalidrawElement[]):ExcalidrawImageElement[] =>
|
||||
elements
|
||||
export function getMermaidImageElements (elements: ExcalidrawElement[]):ExcalidrawImageElement[] {
|
||||
return elements
|
||||
? elements.filter((element) =>
|
||||
element.type === "image" && element.customData?.mermaidText
|
||||
) as ExcalidrawImageElement[]
|
||||
: [];
|
||||
}
|
||||
|
||||
export const getMermaidText = (element: ExcalidrawElement):string =>
|
||||
element.customData?.mermaidText;
|
||||
export function getMermaidText (element: ExcalidrawElement):string {
|
||||
return element.customData?.mermaidText;
|
||||
}
|
||||
|
||||
export const shouldRenderMermaid = ():boolean => requireApiVersion("1.4.14");
|
||||
export function shouldRenderMermaid():boolean {
|
||||
return requireApiVersion("1.4.14");
|
||||
}
|
||||
@@ -3,7 +3,30 @@ import ExcalidrawPlugin from "src/main";
|
||||
import { getAllWindowDocuments } from "./ObsidianUtils";
|
||||
import { DEBUGGING, debug } from "./DebugHelper";
|
||||
|
||||
const STYLE_VARIABLES = ["--background-modifier-cover","--background-primary-alt","--background-secondary","--background-secondary-alt","--background-modifier-border","--text-normal","--text-muted","--text-accent","--text-accent-hover","--text-faint","--text-highlight-bg","--text-highlight-bg-active","--text-selection","--interactive-normal","--interactive-hover","--interactive-accent","--interactive-accent-hover","--scrollbar-bg","--scrollbar-thumb-bg","--scrollbar-active-thumb-bg"];
|
||||
const STYLE_VARIABLES = [
|
||||
"--background-modifier-cover",
|
||||
"--background-primary-alt",
|
||||
"--background-secondary",
|
||||
"--background-secondary-alt",
|
||||
"--background-modifier-border",
|
||||
"--text-normal",
|
||||
"--text-muted",
|
||||
"--text-accent",
|
||||
"--text-accent-hover",
|
||||
"--text-faint",
|
||||
"--text-highlight-bg",
|
||||
"--text-highlight-bg-active",
|
||||
"--text-selection",
|
||||
"--interactive-normal",
|
||||
"--interactive-hover",
|
||||
"--interactive-accent",
|
||||
"--interactive-accent-hover",
|
||||
"--scrollbar-bg",
|
||||
"--scrollbar-thumb-bg",
|
||||
"--scrollbar-active-thumb-bg",
|
||||
"--tab-container-background",
|
||||
"--titlebar-background-focused",
|
||||
];
|
||||
const EXCALIDRAW_CONTAINER_CLASS = "excalidraw__embeddable__outer";
|
||||
|
||||
export class StylesManager {
|
||||
@@ -14,47 +37,43 @@ export class StylesManager {
|
||||
|
||||
constructor(plugin: ExcalidrawPlugin) {
|
||||
this.plugin = plugin;
|
||||
const self = this;
|
||||
plugin.app.workspace.onLayoutReady(async () => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(undefined, "StylesManager.constructor > app.workspace.onLayoutReady", self);
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(undefined, "StylesManager.constructor > app.workspace.onLayoutReady", this);
|
||||
await this.harvestStyles();
|
||||
getAllWindowDocuments(plugin.app).forEach(doc => {
|
||||
this.copyPropertiesToTheme(doc);
|
||||
})
|
||||
getAllWindowDocuments(plugin.app).forEach(doc => this.copyPropertiesToTheme(doc));
|
||||
|
||||
//initialize
|
||||
plugin.registerEvent(
|
||||
plugin.app.workspace.on("css-change", async () => {
|
||||
await this.harvestStyles();
|
||||
getAllWindowDocuments(plugin.app).forEach(doc => {
|
||||
this.copyPropertiesToTheme(doc);
|
||||
})
|
||||
}),
|
||||
plugin.app.workspace.on("css-change", ()=>this.onCSSChange()),
|
||||
)
|
||||
|
||||
plugin.registerEvent(
|
||||
plugin.app.workspace.on("window-open", (win: WorkspaceWindow, window: Window) => {
|
||||
this.stylesMap.set(win.doc, {
|
||||
light: document.head.querySelector(`style[id="excalidraw-embedded-light"]`),
|
||||
dark: document.head.querySelector(`style[id="excalidraw-embedded-dark"]`)
|
||||
});
|
||||
//this.copyPropertiesToTheme(win.doc);
|
||||
}),
|
||||
plugin.app.workspace.on("window-open", (win)=>this.onWindowOpen(win)),
|
||||
)
|
||||
|
||||
plugin.registerEvent(
|
||||
plugin.app.workspace.on("window-open", (win: WorkspaceWindow, window: Window) => {
|
||||
this.stylesMap.delete(win.doc);
|
||||
}),
|
||||
plugin.app.workspace.on("window-close", (win)=>this.onWindowClose(win)),
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
public unload() {
|
||||
for (const [doc, styleTags] of this.stylesMap) {
|
||||
doc.head.removeChild(styleTags.light);
|
||||
doc.head.removeChild(styleTags.dark);
|
||||
}
|
||||
private async onCSSChange () {
|
||||
await this.harvestStyles();
|
||||
getAllWindowDocuments(this.plugin.app).forEach(doc => {
|
||||
this.copyPropertiesToTheme(doc);
|
||||
})
|
||||
}
|
||||
|
||||
private onWindowOpen (win: WorkspaceWindow) {
|
||||
this.stylesMap.set(win.doc, {
|
||||
light: document.head.querySelector(`style[id="excalidraw-embedded-light"]`),
|
||||
dark: document.head.querySelector(`style[id="excalidraw-embedded-dark"]`)
|
||||
});
|
||||
//this.copyPropertiesToTheme(win.doc);
|
||||
}
|
||||
|
||||
private onWindowClose (win: WorkspaceWindow) {
|
||||
this.stylesMap.delete(win.doc);
|
||||
}
|
||||
|
||||
private async harvestStyles() {
|
||||
@@ -127,4 +146,13 @@ export class StylesManager {
|
||||
this.stylesMap.set(doc, {light: lightStyleTag, dark: darkStyleTag});
|
||||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
for (const [doc, styleTags] of this.stylesMap) {
|
||||
doc.head.removeChild(styleTags.light);
|
||||
doc.head.removeChild(styleTags.dark);
|
||||
}
|
||||
this.plugin = null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,8 +5,9 @@ export type FILENAMEPARTS = {
|
||||
hasTaskbone: boolean,
|
||||
hasArearef: boolean,
|
||||
hasFrameref: boolean,
|
||||
blockref: string,
|
||||
hasClippedFrameref: boolean,
|
||||
hasSectionref: boolean,
|
||||
blockref: string,
|
||||
sectionref: string,
|
||||
linkpartReference: string,
|
||||
linkpartAlias: string
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
import {
|
||||
App,
|
||||
Notice,
|
||||
request,
|
||||
requestUrl,
|
||||
request,requestUrl,
|
||||
TFile,
|
||||
TFolder,
|
||||
} from "obsidian";
|
||||
import { Random } from "roughjs/bin/math";
|
||||
import { BinaryFileData, DataURL} from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import {
|
||||
ASSISTANT_FONT,
|
||||
CASCADIA_FONT,
|
||||
VIRGIL_FONT,
|
||||
} from "src/constants/constFonts";
|
||||
import {
|
||||
exportToSvg,
|
||||
exportToBlob,
|
||||
@@ -33,7 +27,7 @@ import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
|
||||
import { cleanBlockRef, cleanSectionHeading, getFileCSSClasses } from "./ObsidianUtils";
|
||||
import { updateElementLinksToObsidianLinks } from "src/ExcalidrawAutomate";
|
||||
import { CropImage } from "./CropImage";
|
||||
|
||||
import opentype from 'opentype.js';
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
declare var LZString: any;
|
||||
@@ -50,8 +44,9 @@ declare module "obsidian" {
|
||||
}
|
||||
}
|
||||
|
||||
export let versionUpdateCheckTimer: number = null;
|
||||
let versionUpdateChecked = false;
|
||||
export const checkExcalidrawVersion = async (app: App) => {
|
||||
export async function checkExcalidrawVersion() {
|
||||
if (versionUpdateChecked) {
|
||||
return;
|
||||
}
|
||||
@@ -61,12 +56,13 @@ export const checkExcalidrawVersion = async (app: App) => {
|
||||
const gitAPIrequest = async () => {
|
||||
return JSON.parse(
|
||||
await request({
|
||||
url: `https://api.github.com/repos/zsviczian/obsidian-excalidraw-plugin/releases?per_page=5&page=1`,
|
||||
url: `https://api.github.com/repos/zsviczian/obsidian-excalidraw-plugin/releases?per_page=15&page=1`,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const latestVersion = (await gitAPIrequest())
|
||||
.filter((el: any) => !el.draft && !el.prerelease)
|
||||
.map((el: any) => {
|
||||
return {
|
||||
version: el.tag_name,
|
||||
@@ -84,12 +80,17 @@ export const checkExcalidrawVersion = async (app: App) => {
|
||||
} catch (e) {
|
||||
errorlog({ where: "Utils/checkExcalidrawVersion", error: e });
|
||||
}
|
||||
setTimeout(() => (versionUpdateChecked = false), 28800000); //reset after 8 hours
|
||||
versionUpdateCheckTimer = window.setTimeout(() => {
|
||||
versionUpdateChecked = false;
|
||||
versionUpdateCheckTimer = null;
|
||||
}, 28800000); //reset after 8 hours
|
||||
};
|
||||
|
||||
|
||||
const random = new Random(Date.now());
|
||||
export const randomInteger = () => Math.floor(random.next() * 2 ** 31);
|
||||
export function randomInteger () {
|
||||
return Math.floor(random.next() * 2 ** 31)
|
||||
};
|
||||
|
||||
//https://macromates.com/blog/2006/wrapping-text-with-regular-expressions/
|
||||
export function wrapTextAtCharLength(
|
||||
@@ -154,44 +155,17 @@ const rotate = (
|
||||
centerY,
|
||||
];
|
||||
|
||||
export const rotatedDimensions = (
|
||||
export function rotatedDimensions (
|
||||
element: ExcalidrawElement,
|
||||
): [number, number, number, number] => {
|
||||
): [number, number, number, number] {
|
||||
const bb = getCommonBoundingBox([element]);
|
||||
return [bb.minX, bb.minY, bb.maxX - bb.minX, bb.maxY - bb.minY];
|
||||
|
||||
//removed with 2.1.5... will delete later
|
||||
if (element.angle === 0) {
|
||||
return [element.x, element.y, element.width, element.height];
|
||||
}
|
||||
const centerX = element.x + element.width / 2;
|
||||
const centerY = element.y + element.height / 2;
|
||||
const [left, top] = rotate(
|
||||
element.x,
|
||||
element.y,
|
||||
centerX,
|
||||
centerY,
|
||||
element.angle,
|
||||
);
|
||||
const [right, bottom] = rotate(
|
||||
element.x + element.width,
|
||||
element.y + element.height,
|
||||
centerX,
|
||||
centerY,
|
||||
element.angle,
|
||||
);
|
||||
return [
|
||||
left < right ? left : right,
|
||||
top < bottom ? top : bottom,
|
||||
Math.abs(left - right),
|
||||
Math.abs(top - bottom),
|
||||
];
|
||||
};
|
||||
|
||||
export const getDataURL = async (
|
||||
export async function getDataURL(
|
||||
file: ArrayBuffer,
|
||||
mimeType: string,
|
||||
): Promise<DataURL> => {
|
||||
): Promise<DataURL> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
@@ -203,19 +177,47 @@ export const getDataURL = async (
|
||||
});
|
||||
};
|
||||
|
||||
export const getFontDataURL = async (
|
||||
export async function getFontDataURL (
|
||||
app: App,
|
||||
fontFileName: string,
|
||||
sourcePath: string,
|
||||
name?: string,
|
||||
): Promise<{ fontDef: string; fontName: string; dataURL: string }> => {
|
||||
): Promise<{ fontDef: string; fontName: string; dataURL: string }> {
|
||||
let fontDef: string = "";
|
||||
let fontName = "";
|
||||
let dataURL = "";
|
||||
const f = app.metadataCache.getFirstLinkpathDest(fontFileName, sourcePath);
|
||||
if (f) {
|
||||
const ab = await app.vault.readBinary(f);
|
||||
const mimeType = f.extension.startsWith("woff")
|
||||
let mimeType = "";
|
||||
let format = "";
|
||||
|
||||
switch (f.extension) {
|
||||
case "woff":
|
||||
mimeType = "application/font-woff";
|
||||
format = "woff";
|
||||
break;
|
||||
case "woff2":
|
||||
mimeType = "font/woff2";
|
||||
format = "woff2";
|
||||
break;
|
||||
case "ttf":
|
||||
mimeType = "font/ttf";
|
||||
format = "truetype";
|
||||
break;
|
||||
case "otf":
|
||||
mimeType = "font/otf";
|
||||
format = "opentype";
|
||||
break;
|
||||
default:
|
||||
mimeType = "application/octet-stream"; // Fallback if file type is unexpected
|
||||
}
|
||||
fontName = name ?? f.basename;
|
||||
dataURL = await getDataURL(ab, mimeType);
|
||||
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;
|
||||
@@ -223,23 +225,23 @@ export const getFontDataURL = async (
|
||||
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]}`;
|
||||
fontDef = `${split[0]};charset=utf-8;base64,${split[1]}`;*/
|
||||
}
|
||||
return { fontDef, fontName, dataURL };
|
||||
};
|
||||
|
||||
export const base64StringToBlob = (base64String: string, mimeType: string): Blob => {
|
||||
export function base64StringToBlob (base64String: string, mimeType: string): Blob {
|
||||
const buffer = Buffer.from(base64String, 'base64');
|
||||
return new Blob([buffer], { type: mimeType });
|
||||
};
|
||||
|
||||
export const svgToBase64 = (svg: string): string => {
|
||||
export function svgToBase64 (svg: string): string {
|
||||
return `data:image/svg+xml;base64,${btoa(
|
||||
unescape(encodeURIComponent(svg.replaceAll(" ", " "))),
|
||||
)}`;
|
||||
};
|
||||
|
||||
export const getBinaryFileFromDataURL = async (dataURL: string): Promise<ArrayBuffer> => {
|
||||
export async function getBinaryFileFromDataURL (dataURL: string): Promise<ArrayBuffer> {
|
||||
if (!dataURL) {
|
||||
return null;
|
||||
}
|
||||
@@ -262,12 +264,12 @@ export const getBinaryFileFromDataURL = async (dataURL: string): Promise<ArrayBu
|
||||
return bytes.buffer;
|
||||
};
|
||||
|
||||
export const getSVG = async (
|
||||
export async function getSVG (
|
||||
scene: any,
|
||||
exportSettings: ExportSettings,
|
||||
padding: number,
|
||||
srcFile: TFile|null, //if set, will replace markdown links with obsidian links
|
||||
): Promise<SVGSVGElement> => {
|
||||
): Promise<SVGSVGElement> {
|
||||
let elements:ExcalidrawElement[] = scene.elements;
|
||||
if(elements.some(el => el.type === "embeddable")) {
|
||||
elements = JSON.parse(JSON.stringify(elements));
|
||||
@@ -288,20 +290,25 @@ export const getSVG = async (
|
||||
if(exportSettings.isMask) {
|
||||
const cropObject = new CropImage(elements, scene.files);
|
||||
svg = await cropObject.getCroppedSVG();
|
||||
cropObject.destroy();
|
||||
} else {
|
||||
svg = await exportToSvg({
|
||||
elements: elements.filter((el:ExcalidrawElement)=>el.isDeleted !== true),
|
||||
appState: {
|
||||
...scene.appState,
|
||||
exportBackground: exportSettings.withBackground,
|
||||
exportWithDarkMode: exportSettings.withTheme
|
||||
? scene.appState?.theme !== "light"
|
||||
: false,
|
||||
...scene.appState,
|
||||
...exportSettings.frameRendering
|
||||
? {frameRendering: exportSettings.frameRendering}
|
||||
: {},
|
||||
},
|
||||
files: scene.files,
|
||||
exportPadding: padding,
|
||||
exportPadding: exportSettings.frameRendering ? 0 : padding,
|
||||
exportingFrame: null,
|
||||
renderEmbeddables: true,
|
||||
skipInliningFonts: exportSettings.skipInliningFonts,
|
||||
});
|
||||
}
|
||||
if(svg) {
|
||||
@@ -329,29 +336,34 @@ export function filterFiles(files: Record<ExcalidrawElement["id"], BinaryFileDat
|
||||
return filteredFiles;
|
||||
}
|
||||
|
||||
export const getPNG = async (
|
||||
export async function getPNG (
|
||||
scene: any,
|
||||
exportSettings: ExportSettings,
|
||||
padding: number,
|
||||
scale: number = 1,
|
||||
): Promise<Blob> => {
|
||||
): Promise<Blob> {
|
||||
try {
|
||||
if(exportSettings.isMask) {
|
||||
const cropObject = new CropImage(scene.elements, scene.files);
|
||||
return await cropObject.getCroppedPNG();
|
||||
const blob = await cropObject.getCroppedPNG();
|
||||
cropObject.destroy();
|
||||
return blob;
|
||||
}
|
||||
|
||||
return await exportToBlob({
|
||||
elements: scene.elements.filter((el:ExcalidrawElement)=>el.isDeleted !== true),
|
||||
appState: {
|
||||
...scene.appState,
|
||||
exportBackground: exportSettings.withBackground,
|
||||
exportWithDarkMode: exportSettings.withTheme
|
||||
? scene.appState?.theme !== "light"
|
||||
: false,
|
||||
...scene.appState,
|
||||
...exportSettings.frameRendering
|
||||
? {frameRendering: exportSettings.frameRendering}
|
||||
: {},
|
||||
},
|
||||
files: filterFiles(scene.files),
|
||||
exportPadding: padding,
|
||||
exportPadding: exportSettings.frameRendering ? 0 : padding,
|
||||
mimeType: "image/png",
|
||||
getDimensions: (width: number, height: number) => ({
|
||||
width: width * scale,
|
||||
@@ -360,16 +372,17 @@ export const getPNG = async (
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
new Notice("Error exporting PNG - PNG too large, try a smaller resolution");
|
||||
errorlog({ where: "Utils.getPNG", error });
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const getQuickImagePreview = async (
|
||||
export async function getQuickImagePreview (
|
||||
plugin: ExcalidrawPlugin,
|
||||
path: string,
|
||||
extension: "png" | "svg",
|
||||
): Promise<any> => {
|
||||
): Promise<any> {
|
||||
if (!plugin.settings.displayExportedImageIfAvailable) {
|
||||
return null;
|
||||
}
|
||||
@@ -386,38 +399,10 @@ export const getQuickImagePreview = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const embedFontsInSVG = (
|
||||
svg: SVGSVGElement,
|
||||
plugin: ExcalidrawPlugin,
|
||||
localOnly: boolean = false,
|
||||
): SVGSVGElement => {
|
||||
//replace font references with base64 fonts)
|
||||
const includesVirgil = !localOnly &&
|
||||
svg.querySelector("text[font-family^='Virgil']") !== null;
|
||||
const includesCascadia = !localOnly &&
|
||||
svg.querySelector("text[font-family^='Cascadia']") !== null;
|
||||
const includesAssistant = !localOnly &&
|
||||
svg.querySelector("text[font-family^='Assistant']") !== null;
|
||||
const includesLocalFont =
|
||||
svg.querySelector("text[font-family^='LocalFont']") !== null;
|
||||
const defs = svg.querySelector("defs");
|
||||
if (defs && (includesCascadia || includesVirgil || includesLocalFont || includesAssistant)) {
|
||||
let style = defs.querySelector("style");
|
||||
if (!style) {
|
||||
style = document.createElement("style");
|
||||
defs.appendChild(style);
|
||||
}
|
||||
style.innerHTML = `${includesVirgil ? VIRGIL_FONT : ""}${
|
||||
includesCascadia ? CASCADIA_FONT : ""}${
|
||||
includesAssistant ? ASSISTANT_FONT : ""
|
||||
}${includesLocalFont ? plugin.fourthFontDef : ""}`;
|
||||
}
|
||||
return svg;
|
||||
};
|
||||
|
||||
export const getImageSize = async (
|
||||
export async function getImageSize (
|
||||
src: string,
|
||||
): Promise<{ height: number; width: number }> => {
|
||||
): Promise<{ height: number; width: number }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
@@ -429,7 +414,7 @@ export const getImageSize = async (
|
||||
});
|
||||
};
|
||||
|
||||
export const addAppendUpdateCustomData = (el: Mutable<ExcalidrawElement>, newData: any): ExcalidrawElement => {
|
||||
export function addAppendUpdateCustomData (el: Mutable<ExcalidrawElement>, newData: any): ExcalidrawElement {
|
||||
if(!newData) return el;
|
||||
if(!el.customData) el.customData = {};
|
||||
for (const key in newData) {
|
||||
@@ -439,10 +424,10 @@ export const addAppendUpdateCustomData = (el: Mutable<ExcalidrawElement>, newDat
|
||||
return el;
|
||||
};
|
||||
|
||||
export const scaleLoadedImage = (
|
||||
export function scaleLoadedImage (
|
||||
scene: any,
|
||||
files: any
|
||||
): { dirty: boolean; scene: any } => {
|
||||
): { dirty: boolean; scene: any } {
|
||||
let dirty = false;
|
||||
if (!files || !scene) {
|
||||
return { dirty, scene };
|
||||
@@ -491,7 +476,7 @@ export const scaleLoadedImage = (
|
||||
return { dirty, scene };
|
||||
};
|
||||
|
||||
export const setDocLeftHandedMode = (isLeftHanded: boolean, ownerDocument:Document) => {
|
||||
export function setDocLeftHandedMode(isLeftHanded: boolean, ownerDocument:Document) {
|
||||
const newStylesheet = ownerDocument.createElement("style");
|
||||
newStylesheet.id = "excalidraw-left-handed";
|
||||
newStylesheet.textContent = `.excalidraw .App-bottom-bar{justify-content:flex-end;}`;
|
||||
@@ -504,7 +489,7 @@ export const setDocLeftHandedMode = (isLeftHanded: boolean, ownerDocument:Docume
|
||||
}
|
||||
}
|
||||
|
||||
export const setLeftHandedMode = (isLeftHanded: boolean) => {
|
||||
export function setLeftHandedMode (isLeftHanded: boolean) {
|
||||
const visitedDocs = new Set<Document>();
|
||||
app.workspace.iterateAllLeaves((leaf) => {
|
||||
const ownerDocument = DEVICE.isMobile?document:leaf.view.containerEl.ownerDocument;
|
||||
@@ -525,7 +510,7 @@ export type LinkParts = {
|
||||
page: number;
|
||||
};
|
||||
|
||||
export const getLinkParts = (fname: string, file?: TFile): LinkParts => {
|
||||
export function getLinkParts (fname: string, file?: TFile): LinkParts {
|
||||
// 1 2 3 4 5
|
||||
const REG = /(^[^#\|]*)#?(\^)?([^\|]*)?\|?(\d*)x?(\d*)/;
|
||||
const parts = fname.match(REG);
|
||||
@@ -543,18 +528,18 @@ export const getLinkParts = (fname: string, file?: TFile): LinkParts => {
|
||||
};
|
||||
};
|
||||
|
||||
export const compress = (data: string): string => {
|
||||
export function compress (data: string): string {
|
||||
return LZString.compressToBase64(data).replace(/(.{256})/g, "$1\n\n");
|
||||
};
|
||||
|
||||
export const decompress = (data: string): string => {
|
||||
export function decompress (data: string): string {
|
||||
return LZString.decompressFromBase64(data.replaceAll("\n", "").replaceAll("\r", ""));
|
||||
};
|
||||
|
||||
export const isMaskFile = (
|
||||
export function isMaskFile (
|
||||
plugin: ExcalidrawPlugin,
|
||||
file: TFile,
|
||||
): boolean => {
|
||||
): boolean {
|
||||
if (file) {
|
||||
const fileCache = plugin.app.metadataCache.getFileCache(file);
|
||||
if (
|
||||
@@ -568,10 +553,10 @@ export const isMaskFile = (
|
||||
return false;
|
||||
};
|
||||
|
||||
export const hasExportTheme = (
|
||||
export function hasExportTheme (
|
||||
plugin: ExcalidrawPlugin,
|
||||
file: TFile,
|
||||
): boolean => {
|
||||
): boolean {
|
||||
if (file) {
|
||||
const fileCache = plugin.app.metadataCache.getFileCache(file);
|
||||
if (
|
||||
@@ -585,11 +570,11 @@ export const hasExportTheme = (
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getExportTheme = (
|
||||
export function getExportTheme (
|
||||
plugin: ExcalidrawPlugin,
|
||||
file: TFile,
|
||||
theme: string,
|
||||
): string => {
|
||||
): string {
|
||||
if (file) {
|
||||
const fileCache = plugin.app.metadataCache.getFileCache(file);
|
||||
if (
|
||||
@@ -605,10 +590,10 @@ export const getExportTheme = (
|
||||
return plugin.settings.exportWithTheme ? theme : "light";
|
||||
};
|
||||
|
||||
export const shouldEmbedScene = (
|
||||
export function shouldEmbedScene (
|
||||
plugin: ExcalidrawPlugin,
|
||||
file: TFile
|
||||
): boolean => {
|
||||
): boolean {
|
||||
if (file) {
|
||||
const fileCache = plugin.app.metadataCache.getFileCache(file);
|
||||
if (
|
||||
@@ -622,10 +607,10 @@ export const shouldEmbedScene = (
|
||||
return plugin.settings.exportEmbedScene;
|
||||
};
|
||||
|
||||
export const hasExportBackground = (
|
||||
export function hasExportBackground (
|
||||
plugin: ExcalidrawPlugin,
|
||||
file: TFile,
|
||||
): boolean => {
|
||||
): boolean {
|
||||
if (file) {
|
||||
const fileCache = plugin.app.metadataCache.getFileCache(file);
|
||||
if (
|
||||
@@ -639,10 +624,10 @@ export const hasExportBackground = (
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getWithBackground = (
|
||||
export function getWithBackground (
|
||||
plugin: ExcalidrawPlugin,
|
||||
file: TFile,
|
||||
): boolean => {
|
||||
): boolean {
|
||||
if (file) {
|
||||
const fileCache = plugin.app.metadataCache.getFileCache(file);
|
||||
if (
|
||||
@@ -656,10 +641,10 @@ export const getWithBackground = (
|
||||
return plugin.settings.exportWithBackground;
|
||||
};
|
||||
|
||||
export const getExportPadding = (
|
||||
export function getExportPadding (
|
||||
plugin: ExcalidrawPlugin,
|
||||
file: TFile,
|
||||
): number => {
|
||||
): number {
|
||||
if (file) {
|
||||
const fileCache = plugin.app.metadataCache.getFileCache(file);
|
||||
if(!fileCache?.frontmatter) return plugin.settings.exportPaddingSVG;
|
||||
@@ -693,7 +678,7 @@ export const getExportPadding = (
|
||||
return plugin.settings.exportPaddingSVG;
|
||||
};
|
||||
|
||||
export const getPNGScale = (plugin: ExcalidrawPlugin, file: TFile): number => {
|
||||
export function getPNGScale (plugin: ExcalidrawPlugin, file: TFile): number {
|
||||
if (file) {
|
||||
const fileCache = plugin.app.metadataCache.getFileCache(file);
|
||||
if (
|
||||
@@ -712,7 +697,7 @@ export const getPNGScale = (plugin: ExcalidrawPlugin, file: TFile): number => {
|
||||
return plugin.settings.pngExportScale;
|
||||
};
|
||||
|
||||
export const isVersionNewerThanOther = (version: string, otherVersion: string): boolean => {
|
||||
export function isVersionNewerThanOther (version: string, otherVersion: string): boolean {
|
||||
const v = version.match(/(\d*)\.(\d*)\.(\d*)/);
|
||||
const o = otherVersion.match(/(\d*)\.(\d*)\.(\d*)/);
|
||||
|
||||
@@ -727,9 +712,9 @@ export const isVersionNewerThanOther = (version: string, otherVersion: string):
|
||||
)
|
||||
}
|
||||
|
||||
export const getEmbeddedFilenameParts = (fname:string): FILENAMEPARTS => {
|
||||
export function getEmbeddedFilenameParts (fname:string): FILENAMEPARTS {
|
||||
// 0 1 23 4 5 6 7 8 9
|
||||
const parts = fname?.match(/([^#\^]*)((#\^)(group=|area=|frame=|taskbone)?([^\|]*)|(#)(group=|area=|frame=|taskbone)?([^\^\|]*))(.*)/);
|
||||
const parts = fname?.match(/([^#\^]*)((#\^)(group=|area=|frame=|clippedframe=|taskbone)?([^\|]*)|(#)(group=|area=|frame=|clippedframe=|taskbone)?([^\^\|]*))(.*)/);
|
||||
if(!parts) {
|
||||
return {
|
||||
filepath: fname,
|
||||
@@ -738,6 +723,7 @@ export const getEmbeddedFilenameParts = (fname:string): FILENAMEPARTS => {
|
||||
hasTaskbone: false,
|
||||
hasArearef: false,
|
||||
hasFrameref: false,
|
||||
hasClippedFrameref: false,
|
||||
blockref: "",
|
||||
hasSectionref: false,
|
||||
sectionref: "",
|
||||
@@ -752,6 +738,7 @@ export const getEmbeddedFilenameParts = (fname:string): FILENAMEPARTS => {
|
||||
hasTaskbone: (parts[4]==="taskbone") || (parts[7]==="taskbone"),
|
||||
hasArearef: (parts[4]==="area=") || (parts[7]==="area="),
|
||||
hasFrameref: (parts[4]==="frame=") || (parts[7]==="frame="),
|
||||
hasClippedFrameref: (parts[4]==="clippedframe=") || (parts[7]==="clippedframe="),
|
||||
blockref: parts[5],
|
||||
hasSectionref: Boolean(parts[6]),
|
||||
sectionref: parts[8],
|
||||
@@ -760,14 +747,17 @@ export const getEmbeddedFilenameParts = (fname:string): FILENAMEPARTS => {
|
||||
}
|
||||
}
|
||||
|
||||
export const fragWithHTML = (html: string) =>
|
||||
createFragment((frag) => (frag.createDiv().innerHTML = html));
|
||||
export function fragWithHTML (html: string) {
|
||||
return createFragment((frag) => (frag.createDiv().innerHTML = html));
|
||||
}
|
||||
|
||||
export const errorlog = (data: {}) => {
|
||||
export function errorlog (data: {}) {
|
||||
console.error({ plugin: "Excalidraw", ...data });
|
||||
};
|
||||
|
||||
export const sleep = async (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
export async function sleep (ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**REACT 18
|
||||
//see also: https://github.com/zsviczian/obsidian-excalidraw-plugin/commit/b67d70c5196f30e2968f9da919d106ee66f2a5eb
|
||||
@@ -778,12 +768,12 @@ export const awaitNextAnimationFrame = async () => new Promise(requestAnimationF
|
||||
//export const debug = function(){};
|
||||
|
||||
|
||||
export const _getContainerElement = (
|
||||
export function _getContainerElement (
|
||||
element:
|
||||
| (ExcalidrawElement & { containerId: ExcalidrawElement["id"] | null })
|
||||
| null,
|
||||
scene: any,
|
||||
) => {
|
||||
) {
|
||||
if (!element || !scene?.elements || element.type !== "text") {
|
||||
return null;
|
||||
}
|
||||
@@ -798,9 +788,9 @@ export const _getContainerElement = (
|
||||
* Transforms array of objects containing `id` attribute,
|
||||
* or array of ids (strings), into a Map, keyd by `id`.
|
||||
*/
|
||||
export const arrayToMap = <T extends { id: string } | string>(
|
||||
export function arrayToMap <T extends { id: string } | string>(
|
||||
items: readonly T[] | Map<string, T>,
|
||||
) => {
|
||||
) {
|
||||
if (items instanceof Map) {
|
||||
return items;
|
||||
}
|
||||
@@ -810,7 +800,7 @@ export const arrayToMap = <T extends { id: string } | string>(
|
||||
}, new Map());
|
||||
};
|
||||
|
||||
export const updateFrontmatterInString = (data:string, keyValuePairs?: [string,string][]):string => {
|
||||
export function updateFrontmatterInString(data:string, keyValuePairs?: [string,string][]):string {
|
||||
if(!data || !keyValuePairs) return data;
|
||||
for(const kvp of keyValuePairs) {
|
||||
const r = new RegExp(`${kvp[0]}:\\s.*\\n`,"g");
|
||||
@@ -821,22 +811,27 @@ export const updateFrontmatterInString = (data:string, keyValuePairs?: [string,s
|
||||
return data;
|
||||
}
|
||||
|
||||
const isHyperLink = (link:string) => link && !link.includes("\n") && !link.includes("\r") && link.match(/^https?:(\d*)?\/\/[^\s]*$/);
|
||||
function isHyperLink (link:string) {
|
||||
return link && !link.includes("\n") && !link.includes("\r") && link.match(/^https?:(\d*)?\/\/[^\s]*$/);
|
||||
}
|
||||
|
||||
export const isContainer = (el: ExcalidrawElement) => el.type!=="arrow" && el.boundElements?.map((e) => e.type).includes("text");
|
||||
export function isContainer (el: ExcalidrawElement) {
|
||||
return el.type!=="arrow" && el.boundElements?.map((e) => e.type).includes("text");
|
||||
}
|
||||
|
||||
export const hyperlinkIsImage = (data: string):boolean => {
|
||||
export function hyperlinkIsImage (data: string):boolean {
|
||||
if(!isHyperLink(data)) false;
|
||||
const corelink = data.split("?")[0];
|
||||
return IMAGE_TYPES.contains(corelink.substring(corelink.lastIndexOf(".")+1));
|
||||
}
|
||||
|
||||
export const hyperlinkIsYouTubeLink = (link:string): boolean =>
|
||||
isHyperLink(link) &&
|
||||
export function hyperlinkIsYouTubeLink (link:string): boolean {
|
||||
return isHyperLink(link) &&
|
||||
(link.startsWith("https://youtu.be") || link.startsWith("https://www.youtube.com") || link.startsWith("https://youtube.com") || link.startsWith("https//www.youtu.be")) &&
|
||||
link.match(/(youtu.be\/|v=)([^?\/\&]*)/)!==null
|
||||
}
|
||||
|
||||
export const getYouTubeThumbnailLink = async (youtubelink: string):Promise<string> => {
|
||||
export async function getYouTubeThumbnailLink (youtubelink: string):Promise<string> {
|
||||
//https://stackoverflow.com/questions/2068344/how-do-i-get-a-youtube-video-thumbnail-from-the-youtube-api
|
||||
//https://youtu.be/z8UkHGpykYU?t=60
|
||||
//https://www.youtube.com/watch?v=z8UkHGpykYU&ab_channel=VerbaltoVisual
|
||||
@@ -860,7 +855,7 @@ export const getYouTubeThumbnailLink = async (youtubelink: string):Promise<strin
|
||||
return `https://i.ytimg.com/vi/${videoId}/default.jpg`;
|
||||
}
|
||||
|
||||
export const isCallerFromTemplaterPlugin = (stackTrace:string) => {
|
||||
export function isCallerFromTemplaterPlugin (stackTrace:string) {
|
||||
const lines = stackTrace.split("\n");
|
||||
for (const line of lines) {
|
||||
if (line.trim().startsWith("at Templater.")) {
|
||||
@@ -870,7 +865,7 @@ export const isCallerFromTemplaterPlugin = (stackTrace:string) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
export const convertSVGStringToElement = (svg: string): SVGSVGElement => {
|
||||
export function convertSVGStringToElement (svg: string): SVGSVGElement {
|
||||
const divElement = document.createElement("div");
|
||||
divElement.innerHTML = svg;
|
||||
const firstChild = divElement.firstChild;
|
||||
@@ -880,9 +875,11 @@ export const convertSVGStringToElement = (svg: string): SVGSVGElement => {
|
||||
return;
|
||||
}
|
||||
|
||||
export const escapeRegExp = (str:string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
export function escapeRegExp (str:string) {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
|
||||
export const addIframe = (containerEl: HTMLElement, link:string, startAt?: number, style:string = "settings") => {
|
||||
export function addIframe (containerEl: HTMLElement, link:string, startAt?: number, style:string = "settings") {
|
||||
const wrapper = containerEl.createDiv({cls: `excalidraw-videoWrapper ${style}`})
|
||||
wrapper.createEl("iframe", {
|
||||
attr: {
|
||||
@@ -894,4 +891,34 @@ export const addIframe = (containerEl: HTMLElement, link:string, startAt?: numbe
|
||||
sandbox: "allow-forms allow-presentation allow-same-origin allow-scripts allow-modals",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export interface FontMetrics {
|
||||
unitsPerEm: number;
|
||||
ascender: number;
|
||||
descender: number;
|
||||
lineHeight: number;
|
||||
fontName: string;
|
||||
}
|
||||
|
||||
export async function getFontMetrics(fontUrl: string, name: string): Promise<FontMetrics | null> {
|
||||
try {
|
||||
const font = await opentype.load(fontUrl);
|
||||
const unitsPerEm = font.unitsPerEm;
|
||||
const ascender = font.ascender;
|
||||
const descender = font.descender;
|
||||
const lineHeight = (ascender - descender) / unitsPerEm;
|
||||
const fontName = font.names.fontFamily.en ?? name;
|
||||
|
||||
return {
|
||||
unitsPerEm,
|
||||
ascender,
|
||||
descender,
|
||||
lineHeight,
|
||||
fontName,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error loading font:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
29
src/utils/WeakArray.ts
Normal file
29
src/utils/WeakArray.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export class WeakArray<T extends object> {
|
||||
private weakArray: WeakRef<T>[] = [];
|
||||
|
||||
constructor() {}
|
||||
|
||||
push(obj: T) {
|
||||
this.weakArray.push(new WeakRef(obj));
|
||||
}
|
||||
|
||||
forEach(callback: (obj: T, index: number) => void) {
|
||||
this.weakArray.forEach((ref, index) => {
|
||||
const obj = ref.deref();
|
||||
if (obj) {
|
||||
callback(obj, index);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeObjects(objectsToRemove: Set<T>) {
|
||||
this.weakArray = this.weakArray.filter((ref) => {
|
||||
const obj = ref.deref();
|
||||
return obj && !objectsToRemove.has(obj);
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.weakArray = [];
|
||||
}
|
||||
}
|
||||
31
styles.css
31
styles.css
@@ -594,4 +594,35 @@ root {
|
||||
|
||||
textarea.excalidraw-wysiwyg, .excalidraw input {
|
||||
caret-color: var(--excalidraw-caret-color);
|
||||
}
|
||||
|
||||
.excalidraw-settings-links-container {
|
||||
display: flex; /* Align SVG and text horizontally */
|
||||
align-items: center; /* Center SVG and text vertically */
|
||||
text-decoration: none; /* Remove underline from links */
|
||||
color: inherit; /* Inherit text color */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.excalidraw-settings-links-container a {
|
||||
display: flex; /* Align children horizontally */
|
||||
align-items: center; /* Center items vertically */
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.excalidraw-settings-links-container svg {
|
||||
margin-right: 8px;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.excalidraw-rank {
|
||||
text-align: center;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.excalidraw-rank svg {
|
||||
height: 8rem;
|
||||
width: 8rem;
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"baseUrl": ".",
|
||||
"sourceMap": false,
|
||||
"module": "ES2015",
|
||||
"target": "es2017", //es2017 because script engine requires for async execution
|
||||
"target": "es2018", //es2017 because script engine requires for async execution
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
@@ -18,11 +18,11 @@
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"jsx": "react",
|
||||
"inlineSourceMap": true
|
||||
"inlineSourceMap": true,
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx", "src/Dialogs/OpenDrawing.ts",
|
||||
"src/types.d.ts"
|
||||
"src/types/types.d.ts",
|
||||
]
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"baseUrl": ".",
|
||||
"sourceMap": false,
|
||||
"module": "es2015",
|
||||
"target": "es2017", //es2017 because script engine requires for async execution
|
||||
"target": "es2018", //es2017 because script engine requires for async execution //es2018 for named capture groups
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
@@ -14,14 +14,14 @@
|
||||
"dom",
|
||||
"scripthost",
|
||||
"es2015",
|
||||
"esnext",
|
||||
"ESNext",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"jsx": "react"
|
||||
"jsx": "react",
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx", "src/Dialogs/OpenDrawing.ts",
|
||||
"src/types.d.ts"
|
||||
"src/types/types.d.ts",
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user