Compare commits

...

110 Commits

Author SHA1 Message Date
zsviczian
dfdca90ca5 2.4.0-rc-2 2024-08-29 23:18:01 +02:00
zsviczian
6a8e1735db update nvmrc to 18 2024-08-29 20:29:54 +02:00
zsviczian
c0e9a0553e minor build changes 2024-08-29 20:24:59 +02:00
zsviczian
e1501165d9 Merge pull request #1971 from dmscode/master
Update zh-cn.ts to hotekey override, hide spalsh, save active tool, r…
2024-08-29 15:04:15 +02:00
dmscode
3b0f706059 Update zh-cn.ts to hotekey override, hide spalsh, save active tool, remove comments in p… … 2024-08-29 18:27:50 +08:00
zsviczian
7d19662f68 Merge pull request #1970 from zsviczian/hotkey-override
hotekey override, hide spalsh, save active tool, remove comments in p…
2024-08-29 11:58:30 +02:00
zsviczian
5c949dc71c hotekey override, hide spalsh, save active tool, remove comments in prod build 2024-08-29 09:57:53 +00:00
zsviczian
0439d67a0c Merge pull request #1966 from dmscode/master
Update zh-cn.ts to 2.4.0-rc-1
2024-08-29 09:00:59 +02:00
dmscode
d3446a20b1 Update zh-cn.ts to 2.4.0-rc-1 2024-08-28 20:45:21 +08:00
zsviczian
5b37dc2e38 save on contentEl mouseleave 2024-08-28 12:34:56 +02:00
zsviczian
eee264918e 2.4.0-rc-1 2024-08-27 21:43:21 +02:00
zsviczian
89172a88f1 fix android preview render bug 2024-08-27 08:56:57 +02:00
zsviczian
ffdb054291 2.4.0-beta-10 2024-08-26 22:40:45 +02:00
zsviczian
200d39c408 Merge pull request #1960 from dmscode/master
Update zh-cn.ts and add Readme.zh-cn
2024-08-26 22:27:07 +02:00
zsviczian
4306574ace Update Excalidraw Writing Machine 2024-08-26 15:04:58 +02:00
zsviczian
12e3b90458 support wikilinks 2024-08-26 15:04:05 +02:00
dmscode
03364b5d2e Readme.zh-cn 2024-08-26 15:43:28 +08:00
dmscode
4e268991dc Update zh-cn.ts to 2.4.0-beta-9 2024-08-26 14:32:46 +08:00
zsviczian
429c84f940 updated rollup, tsconfig, rollup versions in package.json 2024-08-25 16:53:15 +02:00
zsviczian
e890e4489b replace json.stringify with proper processing, fix small issues with Ephemral state, added worker (inactive) 2024-08-25 16:08:12 +02:00
zsviczian
8466c42217 update Excalidraw package -42 is corrupted 2024-08-24 20:30:51 +02:00
zsviczian
353732f597 2.4.0-beta-9 2024-08-24 13:59:49 +02:00
zsviczian
5599d2507f update writing machine data 2024-08-23 12:43:27 +02:00
zsviczian
70cf6ffe70 Support embeddables 2024-08-23 12:41:33 +02:00
zsviczian
61c9277097 writing machine 2024-08-22 21:59:06 +02:00
zsviczian
401052efd3 svg icon take 2 2024-08-22 20:36:00 +02:00
zsviczian
a57a0e797d update writing machine svg 2024-08-22 20:33:52 +02:00
zsviczian
8f48853e2c updated writing machine script 2024-08-22 17:58:42 +02:00
zsviczian
a62148dc07 publish Excalidraw Writing Machine 2024-08-21 21:53:59 +02:00
zsviczian
3ae890bd86 2.4.0-beta-8 2024-08-19 15:57:23 +02:00
zsviczian
65a4cd4ba5 Merge pull request #1939 from dmscode/master
Update zh-cn.ts 2.4.0-beta-7
2024-08-19 15:38:56 +02:00
dmscode
f63b473bc1 Update zh-cn.ts to the commit "fixed getTemplate, migrade gridSize, gridStep" 2024-08-19 09:42:37 +08:00
稻米鼠
859a5ba03a Merge branch 'zsviczian:master' into master 2024-08-19 09:37:07 +08:00
zsviczian
832b97b179 fixed getTemplate, migrade gridSize, gridStep, 2024-08-18 17:15:03 +02:00
dmscode
e98d688d36 Update zh-cn.ts 2.4.0-beta-7 2024-08-16 15:53:39 +08:00
zsviczian
39318337fe updated set stroke width script 2024-08-16 08:13:55 +02:00
zsviczian
f21215be84 2.4.0-beta-7 2024-08-15 17:45:49 +02:00
zsviczian
0690525af8 Merge pull request #1937 from dmscode/master
Update zh-cn.ts to 2.4.0-beta-6
2024-08-15 17:41:32 +02:00
dmscode
b3176425c5 Merge branch 'master' of https://github.com/dmscode/obsidian-excalidraw-plugin 2024-08-15 18:56:30 +08:00
dmscode
9f2c18b6b6 Update zh-cn.ts to 2.4.0-beta-6 2024-08-15 18:56:20 +08:00
zsviczian
d529a04f48 2.4.0-beta-6, pdf++ crop support, double tap eraser disable 2024-08-14 20:02:41 +02:00
zsviczian
8786c5aa99 2.4.0-beta-5 2024-08-14 10:07:55 +02:00
zsviczian
013279ab60 2.2.4-beta-4, markdown post processor, PDF frames, selectFrameElements 2024-08-13 22:53:03 +02:00
zsviczian
06193b6d49 2.4.0-beta-3 2024-08-11 09:22:13 +02:00
zsviczian
ac6f4af5d6 Merge pull request #1928 from mProjectsCode/patch-1
Fix `authorUrl` in manifest
2024-08-11 08:42:11 +02:00
zsviczian
bf148adc68 2.4.0-beta-2 2024-08-09 23:36:20 +02:00
Moritz Jung
0f9dafb01d Fix authorUrl in manifest 2024-08-09 22:16:07 +02:00
zsviczian
9fc0452b70 2.4.0-beta-1 2024-08-08 23:27:57 +02:00
zsviczian
83eda9b3f5 Update README.md 2024-08-04 11:01:12 +02:00
zsviczian
9bfbf47963 2.3.0 2024-08-04 10:37:06 +02:00
zsviczian
252bf411b1 2.3.0-beta-1 2024-08-03 21:26:47 +02:00
zsviczian
5622c019dd rebuild view DEVICE.isDesktop 2024-08-02 17:55:23 +02:00
zsviczian
b32fab7865 2.2.13-1 2024-08-01 22:18:16 +02:00
zsviczian
cafdad1f7a onWindowMigrated 2024-07-30 18:54:54 +02:00
zsviczian
9da40944ab 2.2.13 2024-07-29 18:13:13 +02:00
zsviczian
f678203a64 Merge pull request #1908 from dmscode/master
Update zh-cn.ts to 2.2.11
2024-07-29 17:32:14 +02:00
zsviczian
a9572e08e9 Merge branch 'master' into master 2024-07-29 17:31:58 +02:00
dmscode
ee9b042cdf Update zh-cn.ts to 2.2.11 2024-07-28 20:42:41 +08:00
zsviczian
bc138fa78a fixed storeAction, replaced "none" with "update" #1906 2024-07-28 10:06:31 +02:00
zsviczian
67dbe256f7 2.2.12 2024-07-28 06:55:27 +02:00
zsviczian
8b066d46e2 fix rename and skip inline fonts to cache key 2024-07-28 06:24:30 +02:00
zsviczian
1769a65a82 2.2.11 2024-07-27 21:25:23 +02:00
zsviczian
941eb56769 updated ExcaliAI 2024-07-27 07:09:19 +02:00
zsviczian
a317613ef4 Merge pull request #1859 from Saik0s/patch-1
Update ExcaliAI.md to fix incorrect request json for openai api
2024-07-27 07:05:58 +02:00
zsviczian
173571846f 2.2.10-2 2024-07-26 20:11:20 +02:00
zsviczian
ab1078d393 Fourth Font 2.2.10-1 2024-07-25 21:59:42 +02:00
zsviczian
681321a595 2.2.10-1 fontpicker beta 2024-07-25 21:52:47 +02:00
zsviczian
fcd50d4bc2 Merge pull request #1897 from dmscode/master
Updated zh-cn.ts to 2.2.10
2024-07-23 17:49:00 +02:00
dmscode
7e214e5aaa Updated zh-cn.ts to 2.2.10 2024-07-23 09:54:43 +08:00
zsviczian
de39053857 2.2.10 2024-07-22 22:01:36 +02:00
zsviczian
f543e3218e clippedframe, framesettings 2024-07-21 17:54:00 +02:00
zsviczian
e668aea214 Merge pull request #1890 from dmscode/master
Fixed link path
2024-07-20 08:33:09 +02:00
zsviczian
d810daa735 bump excalidraw version 2024-07-19 12:20:21 +02:00
zsviczian
b94b3118eb improved error message when getPNG fails 2024-07-19 12:19:48 +02:00
dmscode
06cbd0c92d Fixed link path 2024-07-19 12:44:50 +08:00
zsviczian
96ebbbf11d tolerate ! for embedded files 2024-07-17 21:05:01 +02:00
zsviczian
fc1467b05b Support for area, group, frame references in image embeds 2024-07-17 20:31:36 +02:00
zsviczian
53c27f2a59 2.2.9 2024-07-13 17:34:19 +02:00
zsviczian
db80f5c715 allows opacity 0% 2024-07-09 16:45:06 +02:00
zsviczian
405c98ca50 2.2.8 2024-07-05 07:00:42 +02:00
zsviczian
4892aed9e6 Merge pull request #1825 from dmscode/master
Updated and formated zh-cn.ts
2024-07-04 20:33:30 +02:00
zsviczian
1b04b94db2 Merge pull request #1843 from tobybaratta/patch-1
Documentation: Update Custom Pens documentation links
2024-07-04 20:33:01 +02:00
zsviczian
4a430f5fe7 pre-2.2.8 2024-07-04 20:15:33 +02:00
zsviczian
325bfd825f 2.2.7-5 beta 2024-07-04 09:23:49 +02:00
zsviczian
471553f913 2.2.7-4 2024-07-03 00:46:55 +02:00
zsviczian
a43d0689d8 2.2.7-3 without slob 2024-07-01 20:57:47 +02:00
zsviczian
afacb8a94b 2.2.7-2 2024-07-01 20:51:01 +02:00
zsviczian
afe5300e00 2.2.7-1 beta release after refactoring 2024-06-30 23:19:12 +02:00
zsviczian
ef85d0e323 select similar elements 2024-06-30 11:37:22 +02:00
Igor Tarasenko
d3aeedb9f6 Update ExcaliAI.md to fix incorrect request json for openai api 2024-06-28 21:03:58 +02:00
Toby Baratta
7718f69269 Fixing links to file names with spaces
commonmark spec thank you for working with spaces now
2024-06-20 19:17:00 -07:00
Toby Baratta
af50b0c5c6 Update Custom Pens documentation links
**Problem:**
- The README right now contains a broken link to a not-on-main file documenting how to add custom pens. (That document was archived -- I found the original via searching and tried to find the corresponding PRs for the details on the replacement functionality but couldn't find that PR.) 
- Pens are now handled in settings with a different YouTube video showing how to set them up (I think). 
- Adds links to the Hardware Eraser functionality & Auto Draw for Pen. 

Question:
- I think it might be useful to link to the freestyle docs here still, or have some sort of master "Alternative Pens" link that still shows how to do the JSON setup? Not sure if necessary.
2024-06-20 18:55:35 -07:00
dmscode
b576faad82 Updated zh-cn.ts to 2.2.7 2024-06-21 09:34:15 +08:00
稻米鼠
4583e603e9 Merge branch 'zsviczian:master' into master 2024-06-21 09:24:12 +08:00
zsviczian
7ee316a605 2.2.7 2024-06-19 20:56:34 +02:00
dmscode
7dca225691 Updated and formated zh-cn.ts
- Updated to 2.2.6
- Some punctuation to Chinese punctuation
- Add space between Chinese and English
- Change keyname to just first letter upper
- Remove space at line end
2024-06-13 08:23:22 +08:00
zsviczian
8a27d0240a 2.2.6 2024-06-12 22:23:32 +02:00
zsviczian
939bd6fd91 Merge pull request #1815 from dmscode/master
Update zh-cn.ts to 2.2.4
2024-06-12 22:20:20 +02:00
zsviczian
dfbd385de7 2.2.5 2024-06-09 16:34:48 +02:00
dmscode
0d791070dd Update zh-cn.ts to 2.2.4 2024-06-05 19:02:20 +08:00
zsviczian
94fbac38bf 2.2.4 2024-06-02 12:28:52 +02:00
zsviczian
73cf8e75d3 Merge pull request #1679 from evolutioned/patch-1
Update templater_mindmap.md grammar
2024-06-02 06:44:32 +02:00
zsviczian
395cbc104c Merge pull request #1805 from jmhammond/master
Remove autosaveInterval variable but preserve functionality
2024-06-02 06:43:31 +02:00
zsviczian
9a24db8379 Merge pull request #1810 from dmscode/master
Update zh-cn language to 2.2.3
2024-06-02 06:40:29 +02:00
zsviczian
934a5f4838 this to self 2024-06-02 06:39:55 +02:00
dmscode
9022500087 Update zh-cn language to 2.2.3 2024-06-01 12:03:23 +08:00
John Hammond
795807b6cf Remove autosaveInterval variable but preserve functionality 2024-05-29 17:15:14 -05:00
zsviczian
9eff79733c 2.2.3 2024-05-26 22:34:59 +02:00
zsviczian
c4e95d9207 replace commitToHistory with storeAction 2024-05-22 21:22:39 +02:00
evolutioned
d5cf4ace21 Update templater_mindmap.md
Small grammar update
2024-04-02 06:40:55 +11:00
88 changed files with 7268 additions and 2952 deletions

2
.nvmrc
View File

@@ -1 +1 @@
16
18

View File

@@ -2,31 +2,3 @@ The project runs with `node 18`.
After running `npm -i` you'll need to make two manual changes:
## postprocess
postprocess is used in rollup.config.js.
However, the version available on npmjs does not work, after installing packages you need this update:
`npm install brettz9/rollup-plugin-postprocess#update --save-dev``
More info here: https://github.com/developit/rollup-plugin-postprocess/issues/10
## colormaster
1.2.1 misses 3 plugin references after installing the package you need to update
`node_modules/colormaster/package.json` adding the following to the `exports:` section:
```typescript
,
"./plugins/luv": {
"import": "./plugins/luv.mjs",
"require": "./plugins/luv.js",
"default": "./plugins/luv.mjs"
},
"./plugins/uvw": {
"import": "./plugins/uvw.mjs",
"require": "./plugins/uvw.js",
"default": "./plugins/uvw.mjs"
},
"./plugins/ryb": {
"import": "./plugins/ryb.mjs",
"require": "./plugins/ryb.js",
"default": "./plugins/ryb.mjs"
}
```

View File

@@ -1,5 +1,7 @@
# Excalidraw
[简体中文](./README.zh-cn.md)
The Obsidian-Excalidraw plugin integrates [Excalidraw](https://excalidraw.com/), a feature rich sketching tool, into Obsidian. You can store and edit Excalidraw files in your vault, you can embed drawings into your documents, and you can link to documents and other drawings to/and from Excalidraw. For a showcase of Excalidraw features, please read my blog post [here](https://www.zsolt.blog/2021/03/showcasing-excalidraw.html) and/or watch the videos below.
## Video Walkthrough
@@ -63,6 +65,13 @@ The Obsidian-Excalidraw plugin integrates [Excalidraw](https://excalidraw.com/),
<a href="https://youtu.be/4N6efq1DtH0" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/158008902-12c6a851-237e-4edd-a631-d48e81c904b2.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Eraser, left-handed mode, improved filename configuration</a><br>
</details>
### Beta testing
The plugin follows a monthly release schedule. If you want to receive more frequent updates with new features (e.g. shiny new stuff available on excalidraw.com, but not yet in Obsidian) and minor bug fixes, then join the beta community.
[![Thumbnail - 20240803 Excalidraw Release Approach (Custom)](https://github.com/user-attachments/assets/ab40648c-f73f-4bda-a416-52839f918f2a)](https://youtu.be/2poSS-Z91lY)
[![Excalidraw Plugin Release Strategy (Phone)](https://github.com/user-attachments/assets/87f1f379-782c-4c32-8b5b-d27fe2d3ac4b)](https://github.com/user-attachments/assets/120a0790-7239-48ae-bfbd-eb249f8b518d)
---
## Features
@@ -243,11 +252,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

285
README.zh-cn.md Normal file
View File

@@ -0,0 +1,285 @@
# Excalidraw
> 此说明当前更新至 2.4.0-beta-9。
Obsidian-Excalidraw 插件将 [Excalidraw](https://excalidraw.com/) 这一功能丰富的草图工具集成到 Obsidian 中。您可以在您的库中存储和编辑 Excalidraw 文件,可以将图形嵌入到文档中,还可以在 Excalidraw 中链接到文档和其他图形。有关 Excalidraw 功能的展示,请查看我的博客文章 [这里](https://www.zsolt.blog/2021/03/showcasing-excalidraw.html) 或观看以下视频。
## 视频演示
<a href="https://youtu.be/P_Q6avJGoWI" target="_blank"><img src="https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/da34bb33-7610-45e6-b36f-cb7a02a9141b" width="300"/></a>
<a href="https://youtu.be/o0exK-xFP3k" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/156931370-aa4d88de-c4a8-46cc-aeb2-dc09aa0bea39.jpg" width="300"/></a>
<a href="https://youtu.be/QKnQgSjJVuc" target="_blank"><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/thumbnail-getting-started.jpg" width="300"/></a>
### 这是我完整的视频目录:
<a href="https://excalidraw-obsidian.online/Hobbies/Excalidraw+Blog/Catalogue+of+Videos"><img width="380" alt="image" src="https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/2577e5ad-7a21-4c62-acd5-4fe80c8a8a95"></a>
<br>
<details><summary>10 部分(稍微过时)视频演示</summary>
<a href="https://youtu.be/sY4FoflGaiM" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160304-7f211180-e17c-11eb-8363-c52723de1ffd.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;1 入门</a><br>
<a href="https://youtu.be/Iy_oVTq12Gw" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160312-8a743d00-e17c-11eb-9fa2-490ef4cbd59e.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;2 基本形状和功能</a><br>
<a href="https://youtu.be/QOL1KF7-kdc" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160323-96f89580-e17c-11eb-9bce-8eb1067a51bb.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;3 元素分组</a><br>
<a href="https://youtu.be/aSgcbfspvfo" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160332-9f50d080-e17c-11eb-98e9-fec60fe147d9.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;4 模板库</a><br>
<a href="https://youtu.be/MaJ5jJwBRWs" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160341-a546b180-e17c-11eb-9de8-d87fdc844c9c.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;5 嵌入</a><br>
<a href="https://youtu.be/MXzeCOEExNo" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160346-aa0b6580-e17c-11eb-930b-4024807040d1.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;6 链接</a><br>
<a href="https://youtu.be/R0IAg0s-wQE" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160354-b2fc3700-e17c-11eb-81af-9e71e461f6dd.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;7 Markdown</a><br>
<a href="https://youtu.be/ibdS7ykwpW4" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160360-b8f21800-e17c-11eb-8bd8-79d4e3f6e92d.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;8 模板</a><br>
<a href="https://youtu.be/VRZVujfVab0" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160367-bdb6cc00-e17c-11eb-92f1-6f59faea85fd.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;9 Excalidraw 自动化</a><br>
<a href="https://youtu.be/D1iBYo1_jjc" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160374-c3141680-e17c-11eb-8cc2-dfaffd903d15.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;10 杂项</a><br>
</details>
<details><summary>将内容嵌入 Excalidraw</summary>
<a href="https://www.youtube.com/watch?v=_c_0zpBJ4Xc&" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/138607067-ccb62f92-48a4-4880-ac6e-68c1bf86ac2c.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;图像元素</a><br>
<a href="https://youtu.be/r08wk-58DPk" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/143732412-1c65227e-4381-406d-847a-b001ab3506ca.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;LaTeX 演示</a><br>
<a href="https://youtu.be/tsecSfnTMow" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/143732440-90bfa029-8615-462e-ada3-c903d71a82c9.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Markdown 嵌入</a><br>
<a href="https://youtu.be/K6qZkTz8GHs" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/143783906-15cee494-c6d5-4495-a2ca-74634e4e7355.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Markdown 嵌入高级功能</a><br>
<a href="https://youtu.be/Etskjw7a5zo" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/156931461-0979b821-315a-41dd-86f1-31d169b7c127.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;链接到元素、垂直文本对齐、Markdown 样式</a><br>
</details>
<details><summary>脚本引擎商店 - Excalidraw 自动化</summary>
<a href="https://youtu.be/hePJcObHIso" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/145684531-8d9c2992-59ac-4ebc-804a-4cce1777ded2.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;介绍脚本引擎</a><br>
<a href="https://youtu.be/lzYdOQ6z8F0" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/147889174-6c306d0d-2d29-46cc-a53f-3f0013cf14de.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;脚本引擎商店</a><br>
</details>
<details><summary>使用颜色</summary>
<a href="https://youtu.be/6PLGHBH9VZ4" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/194773147-5418a0ab-6be5-4eb0-a8e4-d6af21b1b483.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;颜色 - Excalidraw 基础(自定义)</a><br>
<a href="https://youtu.be/epYNx2FSf2w" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/194773211-9e871be7-0795-4dc7-947e-c6c275e690d0.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Excalidraw 调色板(自定义)</a><br>
<a href="https://youtu.be/Amhlv6r9WvM" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/194773268-400cfb1b-6bde-45e0-9e4b-91bbaa461cf0.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;“艺术”颜色渐变</a><br>
<a href="https://youtu.be/r9oB1SlK1GU" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/194773527-ef35c8b9-1a6d-4415-9c7e-b667fb17535d.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;美丽草图的简单规则</a><br>
<a href="https://youtu.be/7gJDwNgQ6NU" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/195988535-a133a9b9-d094-45ba-ba64-c994b9a1e0ef.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;ColorMaster 脚本编写</a><br>
</details>
<details><summary>链接和块引用</summary>
<a href="https://youtu.be/qiKuqMcNWgU" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/171635214-30533c45-94fa-436e-83a9-b2ec99f190e2.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;链接视觉思维的 6 种策略 v4</a><br>
<a href="https://youtu.be/yZQoJg2RCKI" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/185791706-3d9983ab-7cb1-4b27-a016-30c039d84e34.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;图像的块引用部分</a><br>
<a href="https://youtu.be/Etskjw7a5zo" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/156931461-0979b821-315a-41dd-86f1-31d169b7c127.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;链接到元素、垂直文本对齐、Markdown 样式</a><br>
<a href="https://youtu.be/2Y8OhkGiTHg" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/152585752-7eb0371f-0bab-40f6-a194-3b48e5811735.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Excalidraw 原生超链接使用指南</a><br>
</details>
<details><summary>强大工具</summary>
<a href="https://youtu.be/NOuddK6xrr8" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/147283367-e5689385-ea51-4983-81a3-04d810d39f62.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;便签(自动换行)</a><br>
<a href="https://youtu.be/eKFmrSQhFA4" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/149659524-2a4e0a24-40c9-4e66-a6b1-c92f3b88ecd5.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;本地字体</a><br>
<a href="https://youtu.be/vlC1-iBvIfo" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/199207784-8bbe14e0-7d10-47d7-971d-20dce8dbd659.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;SVG 导入</a><br>
<a href="https://youtu.be/7gu4ETx7zro" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/202916770-28f2fa64-1ba2-4b40-a7fe-d721b42634f7.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;OCR</a><br>
<a href="https://youtu.be/U2LkBRBk4LY" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/159369910-6371f08d-b5fa-454d-9c6c-948f7e7a7d26.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;绑定/解绑文本与容器,前置标签自定义导出</a><br>
<a href="https://youtu.be/uZz5MgzWXiM" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/211054371-8872e01a-77d6-4afc-a0c2-86a55410a8d3.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;自定义笔支持</a><br>
</details>
<details><summary>生活质量改善</summary>
<a href="https://youtu.be/qbPIAZguJeo" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/151705333-54e9ffd2-0bd7-4d02-b99e-0bd4e4708d4d.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;移动支持</a><br>
<a href="https://youtu.be/2v9TZmQNO8c" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/153676009-6f86b2d7-c248-49a2-b802-be21c6999e4f.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;托盘模式和可自定义调色板</a><br>
<a href="https://youtu.be/xHPGWR3m0c8" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/154821232-a404b6cf-72fb-4ce4-9d53-619132dce491.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;压缩 JSON 和改进的保存/同步支持</a><br>
<a href="https://youtu.be/gMIKXyhS-dM" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/156931428-b2269fd9-87bd-43ab-8558-5572f40dff93.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Obsidian 工具面板</a><br>
<a href="https://youtu.be/4N6efq1DtH0" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/158008902-12c6a851-237e-4edd-a631-d48e81c904b2.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;橡皮擦、左利手模式、改进的文件名配置</a><br>
</details>
### Beta 测试
该插件遵循每月发布的计划。如果您希望获得更频繁的更新包括新功能例如excalidraw.com 上的新内容,但尚未在 Obsidian 中提供)和小的 bug 修复,请加入 beta 社区。
[![缩略图 - 20240803 Excalidraw 发布方法(自定义)](https://github.com/user-attachments/assets/ab40648c-f73f-4bda-a416-52839f918f2a)](https://youtu.be/2poSS-Z91lY)
[![Excalidraw 插件发布策略(手机)](https://github.com/user-attachments/assets/87f1f379-782c-4c32-8b5b-d27fe2d3ac4b)](https://github.com/user-attachments/assets/120a0790-7239-48ae-bfbd-eb249f8b518d)
---
## 功能
- 该插件将 Excalidraw 无缝集成到 Obsidian 中,包括命令面板操作、文件资源管理器功能、选项菜单命令和工具栏按钮。
- 在工具栏按钮或文件资源管理器中 <kbd>CTRL/CMD+鼠标左键</kbd> 以在新面板中创建/打开绘图。
### 设置
设置将允许您根据需要自定义 Excalidraw。该插件提供了大量设置。我尝试为这些设置添加有意义的解释所以请耐心查找对于大多数请求已经存在相关设置。
插件设置分为以下几个部分:
- **基本设置**:例如使用的默认文件夹。
- **保存**:压缩和自动保存间隔。
- **文件名**:配置自动生成的 Excalidraw 文件名。
- **显示**:影响 Excalidraw 处理的设置(例如:左利手模式、主题设置、鼠标滚轮和捏合缩放设置、适应缩放设置)。
- **链接和嵌入**:影响链接和嵌入项在 Excalidraw 画布上行为的设置。
- **Markdown 嵌入设置**:这些设置控制从您的 Vault 嵌入到 Excalidraw 绘图中的 Markdown 文档的行为。
- **嵌入与导出**:控制 Excalidraw 图像在嵌入到 Markdown 文档时的显示方式的设置。
- **自动导出设置**:您可以配置 Excalidraw 在每次保存时创建绘图的 PNG 或 SVG 副本。
- **兼容性功能**:如果您在 Obsidian 之外编辑 Excalidraw 绘图(例如在 LogSeq、Visual Studio、网页等请检查这些设置。
- **实验性功能**:有一些高级功能作为“巧妙”的 hacks 实现,包括定义本地字体、添加自定义图标以区分 Obsidian 文件资源管理器中的 Excalidraw 文件、OCR 设置等。
- **已安装脚本的设置**:从脚本库安装的一些脚本附带设置。脚本设置在您第一次运行脚本时安装。因此,要访问脚本的设置,请安装脚本,首次运行后在插件设置中查找设置。
#### 模板
- 新绘图的模板。该模板将恢复笔画属性。这意味着您可以在模板中设置笔画颜色、笔画宽度、不透明度、字体系列、字体大小、填充样式、笔画样式等的默认值。这同样适用于 ExcalidrawAutomate。
- 通过模板,您可以自定义 Excalidraw 使用的调色板。
- 切换到 Markdown 视图。
- 滚动到文件底部,找到 `"AppState": {`
- 在 AppState 部分末尾找到 `"customColorPalette": {`
- 您可以通过添加以下三个变量中的任何一个或全部来指定 Excalidraw 使用的 3 个调色板:
- `"canvasBackground":[], "elementBackground":[], "elementStroke": []`
- 在每个变量的数组中添加有效 HTML 颜色的逗号分隔列表(例如,`#FF0000` 表示红色)。
- 有关更多帮助,请查看我上面的录像。
#### 导出
- 如果便携性对您很重要:
- 自动导出 SVG 和/或 PNG 文件,包括同步保持功能,这样您可以将 SVG/PNG 嵌入到文档中,而不是嵌入 Excalidraw 文件。
- 您可以通过添加 `excalidraw-autoexport` 前置字段键来覆盖单个文件的导出设置。该键的有效值为 `none``both``png``svg`
- 指定嵌入绘图的默认宽度。
- 兼容性功能以自动导出和保持同步 Markdown Excalidraw 文件及旧版 `.excalidraw` 文件。
- 实验性功能可在文件资源管理器中添加自定义标签以标记绘图文件。
- 启用/禁用自动保存。
### 将您的绘图嵌入到 Markdown 文档中
- 您可以使用以下格式自定义嵌入图像的大小和位置:
- `![[image.excalidraw|100]]`
- `![[image.excalidraw|100x100]]`
- `![[image.excalidraw|100|left]]`
- `![[image.excalidraw|right-wrap]]`
- `![[<filename.excalidraw>|<width>x<height>|<alignment>]]`
- 您可以通过 CSS 添加自定义 [对齐方式](https://www.scaler.com/topics/align-image-in-html/)。
- 出现在 `<alignment>` 中的任何文本将被添加到渲染的 SVG 元素样式和包装 DIV 元素中。
- 有关更多信息,请参见 [styles.css](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/styles.css)。
- Excalidraw 绘图在 Obsidian Publish 中不显示。如果您希望在发布的文档中使用 Excalidraw可以在插件设置中的 `Embed & Export` 下进行配置,以便在创建新文件时自动将绘图的 PNG 或 SVG 版本插入到文档中。请参见 `type of file to insert into document`
-`Export settings` 下,您还可以配置自动导出图像的深色和浅色版本,以便您的发布网站支持深色和浅色模式。
### 超链接和拖放支持
![](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/images/excalidraw-modifiers.png)
#### 超链接
- 支持超链接,例如:
- `https://zsolt.blog`
- `[Obsidian](https://obsidian.md)`,以及
- 内部链接,例如在绘图文本中使用 `[[My file in vault|Alias]]`
- 如果您启用了 Obsidian 设置中的“文件与链接/自动更新内部链接”,则文件移动或重命名时链接会自动更新。
- 绘图中的链接会出现在文档的反向链接中。
- 支持嵌入:
- `![[myfile#^blockref]]` 将绘图转换为该块的嵌入文本。
- `![[myfile#section]]` 也有效,这将嵌入该部分。
- 您还可以通过在嵌入后加上最大字符数的花括号来指定嵌入文本的换行,例如 `![[myfile#^blockref]]{40}` 将在 40 个字符处换行。
- 为了方便,您还可以使用命令面板将链接插入到绘图中。
- <kbd>CTRL/CMD + 鼠标悬停</kbd> 可以调出链接的 Obsidian 快速预览。(在 Mac 上为 <kbd>CTRL+CMD+鼠标悬停</kbd>)。
- 使用块引用,您还可以在其他文档中引用和嵌入绘图中出现的文本。
#### 拖放支持
- 您可以从 Obsidian 文件资源管理器中拖动文件,它们将成为 Excalidraw 中指向这些文件的链接。有关各种修饰键组合,请参见上面的表格。
- 注意:将图像锚定到其 100% 尺寸是一个非常小众的功能,具有非常特定的行为,我主要是为自己开发的。
- (甚至 Excalidraw Obsidian 中的其他功能更是如此 - 也是主要为自己开发的 😉)。
- 每次打开 Excalidraw 绘图时,这将重置您嵌入的图像为 100% 尺寸,或者如果您在画布上嵌入了使用此功能插入的 Excalidraw 绘图,每次更新嵌入的绘图时,它将缩放回 100% 尺寸。
- 这意味着即使您在绘图中调整了图像的大小,下次打开文件或修改原始嵌入对象时,它也会重置为 100%。此功能在将绘图分解为单独的 Excalidraw 文件时非常有用,但当它们组合到单个画布上时,您希望各个部分保持其实际大小。我使用此功能从原子绘图构建“一页书”摘要。
- 您可以将文本从 Markdown 视图拖放到 Excalidraw 中。
- 您可以从浏览器中拖放网页地址,它们将成为链接。
- 您可以拖放 YouTube 链接和缩略图,它们将在 Excalidraw 中成为带缩略图的 YouTube 链接。
### LaTeX
使用命令面板操作“插入 LaTeX 公式”插入 LaTeX 公式。您可以在 Markdown 视图中编辑公式,或者通过 <kbd>CTRL/CMD + 鼠标左键</kbd> 点击公式进行编辑。
### 图像支持
- 在 iOS 和 Android 上,您可以通过按下 Excalidraw 中的添加图像按钮从相机添加图像。
- 您可以将图像复制/粘贴到绘图中。图像将保存在您的 Vault 中。
- 您可以按照上面的说明拖放图像。
- URL 链接到网络上的图像:您可以从网页将图像拖放到 Excalidraw。如果在将图像拖放到 Excalidraw 时按住 CTRL 键,图像将不会保存到您的 Vault 中。Excalidraw 将从 URL 加载图像。请注意,如果您没有互联网连接,或者这些图像从互联网上被删除,它们也会从您的绘图中消失。
- 如果您将图像 URL 粘贴到 Excalidraw只需点击 URL 复制,然后在 Excalidraw 画布上点击粘贴),图像将以链接形式插入到网络图像上。同样,图像不会保存到您的 Vault 中,只有链接会被保存。
- 如果您拖放 YouTube 视频链接,它将转换为一个缩略图,并带有指向视频的元素链接。
### 引用图像部分的块
有关更多详细信息,请参见此 [视频](https://youtu.be/yZQoJg2RCKI)。
- 当通过链接引用 Excalidraw 文件中的画布元素时,可以使用:
- 元素 ID 或章节标题(即包含 `# <章节标题>` 的文本元素)
- 例如 `[[file#^elementID]]`
- 您可以添加 `group=` 前缀,
- 例如 `[[file#^group=elementID]]`,或
- `area=` 前缀,
- 例如 `[[file#area=Section heading]]`
- 如果找到 `group=` 前缀Excalidraw 将选择与通过元素 ID块引用或章节标题引用的元素在同一组中的元素。
- 如果找到 `area=` 前缀Excalidraw 将在引用的元素周围插入图像的剪切部分。
- 请注意,当将 Excalidraw 嵌入为 PNG 到您的 Markdown 文档时,不支持 `area=` 选择器。
- 引用文本元素的元素 ID 而不带 `group=``area=` 前缀将以普通文本嵌入该元素。引用非文本元素(例如矩形、椭圆等)而不带 `group=``area=` 前缀将导致 Obsidian 错误,因为这些元素 ID 在 Excalidraw Markdown 文件中不能够作为块引用。
### Markdown
- 从 1.2.0 版本开始,绘图文件存储在 Markdown 文件中。
- 您可以为绘图添加标签。
- 您可以在绘图的 YAML 前置字段中添加元数据。
- 您在前置字段和 `# Text Elements` 标题之间添加的任何内容将被 Excalidraw 忽略,即您可以在这里添加任何内容,它将作为文档的一部分被保留。
- Excalidraw 文档现在会在图形视图中显示。
- 以下前置字段键将自定义绘图的显示方式 - 覆盖一般设置:
- `excalidraw-link-prefix: "📍"` 内部链接的预览前缀
- `excalidraw-url-prefix: "🌐"` 外部链接的预览前缀
- `excalidraw-link-brackets: true|false` 是否在预览中显示链接周围的括号
- `excalidraw-default-mode: view|zen` 默认以查看模式或禅模式打开此文档。默认查看模式非常适合演示幻灯片。
- 前置字段标签用于在文件级别自定义图像导出 [519](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/519)。如果这些键存在,它们将覆盖默认的 Excalidraw 嵌入和导出设置。
- `excalidraw-export-transparent: true` true == 透明 / false == 有背景。
- `excalidraw-export-dark` true == 深色模式 / false == 浅色模式。
- `excalidraw-export-padding`:指定图像的导出边距。
- `excalidraw-export-pngscale`:这仅影响导出为 PNG。指定图像的导出比例。典型范围在 0.5 到 5 之间,但您也可以尝试其他值。
### 将完整的 Markdown 文件嵌入到您的绘图中
从 Obsidian 文件资源管理器中拖动所需文件,同时按住 <kbd>SHIFT</kbd> 将文件放到画布上。
- 使用命令面板操作:`从 Vault 插入 Markdown 文件`
- 使用自定义的 woff、woff2 或 TTF 字体来显示文档,您可以在 Excalidraw 设置中设置默认字体。
- 您可以为渲染 Markdown 文档的快照图像设置自定义 CSS。仅支持操作系统标准字体作为字体系列[Win10](https://docs.microsoft.com/en-us/typography/fonts/windows_10_font_list)、[Mac & iOS](https://developer.apple.com/fonts/system-fonts/)),此外,您可以使用上述设置添加一个额外的自定义字体。
- (要查看演示,请观看此 [视频](https://youtu.be/K6qZkTz8GHs) 并查看此
- [示例 CSS](https://github.com/zsviczian/obsidian-excalidraw-plugin/discussions/281))。
- 为了帮助样式设置,您可以查看 Excalidraw 创建的 Markdown 文档的 SVG 快照。
- 打开 Obsidian 开发者控制台 (<kbd>CTRL+Shift+i</kbd>/<kbd>CMD+OPT+i</kbd>),并
- 执行以下命令:`ExcalidrawAutomate.mostRecentMarkdownSVG`
- 您可以通过将以下前置字段键添加到您的 Markdown 文档,按文件控制嵌入 Markdown 文件的外观:
- `excalidraw-font: Virgil|Cascadia|font_file_name.extension`
- `excalidraw-font-color: css-color-name|#HEXcolor|any-other-html-standard-format`
- 您可以在 [这里](https://www.w3schools.com/colors/colors_names.asp) 找到 CSS 颜色名称。
- `excalidraw-border-color: css-color-name|#HEXcolor|any-other-html-standard-format`
- `excalidraw-css: "css-filename|css snippet"`
- 切换到 Markdown 视图或使用 <kbd>WIN+CTRL</kbd>/<kbd>CMD+CTRL</kbd> 点击图像以编辑嵌入的属性:
- `[[filename#^blockref|WIDTHxMAXHEIGHT]]`
### 自定义字体、自定义笔、OCR 支持、SVG 导入
- 在插件设置中,您可以添加自定义的本地字体。有关更多详细信息,请参见此 [视频](https://youtu.be/eKFmrSQhFA4)。
- 该插件包括使用 Taskbone OCR 的 OCR 支持。有关更多详细信息,请参见此 [视频](https://youtu.be/7gu4ETx7zro)。
- 您可以将 SVG 文件转换为 Excalidraw 绘图(有一些限制)。有关更多详细信息,请参见此 [视频](https://youtu.be/vlC1-iBvIfo)。
- 您可以定义自定义笔和荧光笔,并将其固定到侧边栏。有关更多详细信息,请参见此 [视频](https://youtu.be/OjNhjaH2KjI)。使用 ExcalidrawAutomate您可以添加对 [自动切换](<ea-scripts/Auto Draw for Pen.md>) 笔的支持,以及对 [硬件橡皮擦按钮](<ea-scripts/Hardware Eraser Support.md>) 的支持。
### 脚本引擎
- 从 1.5.0 版本开始,您可以轻松执行 ExcalidrawAutomate 宏,并为它们分配命令面板快捷键,使用脚本引擎。您可以在 [这里](ea-scripts/README.md) 找到介绍视频和不断增加的可安装脚本库。
- 您可以通过将脚本和随附的 SVG 图标文件移动到文件夹中,将脚本组织成组,放在 Excalidraw 的 Obsidian 工具面板中。请参见演示 [视频](https://youtu.be/wTtaXmRJ7wg?t=16)。
### 其他
- 左利手模式
- 包含完整的
- [QuickAdd](https://github.com/chhoumann/quickadd)
- [Templater](https://silentvoid13.github.io/Templater/) 和
- [Dataview](https://blacksmithgu.github.io/obsidian-dataview/docs/api/intro/) 支持,通过 ExcalidrawAutomate 实现。
- 查看 [详细帮助 + 示例](https://zsviczian.github.io/obsidian-excalidraw-plugin/)。
- 我还有一个 [YouTube ExcalidrawAutomate 播放列表](https://www.youtube.com/playlist?list=PL6mqgtMZ4NP1IR4nXxSlMA4PA5E-qpyHZ),里面有很多示例。
- 需要 Obsidian Sync 订阅:完整的绘图文件历史记录和设备之间的同步。
- 多语言支持:如果您想通过翻译插件来帮助,请与我联系。
## 反馈、问题、想法、问题
请在 [forum.obsidian.md](https://forum.obsidian.md/t/excalidraw-full-featured-sketching-plugin-in-obsidian) 上参与关于 Excalidraw 插件的讨论。
请前往 [GitHub](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues) 报告错误或请求增强功能。
---
## 感谢支持
如果您喜欢 Excalidraw请通过在 [https://ko-fi/zsolt](https://ko-fi.com/zsolt) 上请我喝杯咖啡来支持我的工作和热情。
请通过在 Twitter、Reddit 或其他您常用的社交媒体平台上分享 Obsidian Excalidraw 插件来帮助传播消息。
您可以在 Twitter 上找到我 [@zsviczian](https://twitter.com/zsviczian),以及我的博客 [zsolt.blog](https://zsolt.blog)。
[<img style="float:left" src="https://user-images.githubusercontent.com/14358394/115450238-f39e8100-a21b-11eb-89d0-fa4b82cdbce8.png" width="200">](https://ko-fi.com/zsolt)
---
## Excalidraw 的朋友们
如果您喜欢 Excalidraw可以考虑尝试 [ExcaliBrain](https://github.com/zsviczian/excalibrain)(也可通过 Obsidian 社区插件获得)。
<a href="https://youtu.be/gOkniMkDPyM" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/169708346-9e41289d-9536-43ec-8f70-2d2ad2d369d6.png" width="300"/></a>

View File

@@ -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
@@ -422,6 +422,7 @@ export declare class ExcalidrawAutomate {
appState?: AppState;
files?: BinaryFileData;
commitToHistory?: boolean;
storeAction?: "capture" | "none" | "update";
}, restore?: boolean): void;
/**
* connect an object to the selected element in the view

View File

@@ -1,6 +1,6 @@
# [◀ Excalidraw Automate How To](../readme.md)
## Generating a simple mindmap from a text outline
This is a slightly more elaborate example. This will generate an a mindmap from a tabulated outline.
This is a slightly more elaborate example. This will generate a mindmap from a tabulated outline.
### Output
![Drawing 2021-05-05 20 52 34](https://user-images.githubusercontent.com/14358394/117194124-00a69d00-ade4-11eb-8b75-5e18a9cbc3cd.png)

View File

@@ -19,10 +19,10 @@ if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.0.25")) {
// -------------------------------
const excalidrawTemplates = ea.getListOfTemplateFiles();
if(typeof window.ExcalidrawDeconstructElements === "undefined") {
window.ExcalidrawDeconstructElements = {
openDeconstructedImage: true,
templatePath: excalidrawTemplates?.[0].path??""
};
window.ExcalidrawDeconstructElements = {
openDeconstructedImage: true,
templatePath: excalidrawTemplates?.[0].path??""
};
}
const splitFolderAndFilename = (filepath) => {
@@ -36,13 +36,13 @@ const splitFolderAndFilename = (filepath) => {
let settings = ea.getScriptSettings();
//set default values on first run
if(!settings["Templates"]) {
settings = {
"Templates" : {
value: "",
settings = {
"Templates" : {
value: "",
description: "Comma-separated list of template filepaths"
}
};
await ea.setScriptSettings(settings);
}
};
await ea.setScriptSettings(settings);
}
if(!settings["Default file name"]) {
@@ -80,31 +80,31 @@ ea.getElements().filter(el=>el.type==="image").forEach(el=>{
const img = ea.targetView.excalidrawData.getFile(el.fileId);
const path = (img?.linkParts?.original)??(img?.file?.path);
if(img && path) {
ea.imagesDict[el.fileId] = {
mimeType: img.mimeType,
id: el.fileId,
dataURL: img.img,
created: img.mtime,
file: path,
hasSVGwithBitmap: img.isSVGwithBitmap,
latex: null,
};
return;
}
const equation = ea.targetView.excalidrawData.getEquation(el.fileId);
eqImg = ea.targetView.getScene()?.files[el.fileId]
if(equation && eqImg) {
ea.imagesDict[el.fileId] = {
mimeType: eqImg.mimeType,
id: el.fileId,
dataURL: eqImg.dataURL,
created: eqImg.created,
file: null,
hasSVGwithBitmap: null,
latex: equation.latex,
};
return;
}
mimeType: img.mimeType,
id: el.fileId,
dataURL: img.img,
created: img.mtime,
file: path,
hasSVGwithBitmap: img.isSVGwithBitmap,
latex: null,
};
return;
}
const equation = ea.targetView.excalidrawData.getEquation(el.fileId);
eqImg = ea.targetView.getScene()?.files[el.fileId]
if(equation && eqImg) {
ea.imagesDict[el.fileId] = {
mimeType: eqImg.mimeType,
id: el.fileId,
dataURL: eqImg.dataURL,
created: eqImg.created,
file: null,
hasSVGwithBitmap: null,
latex: equation.latex,
};
return;
}
});

View File

@@ -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,

View File

@@ -0,0 +1,259 @@
/*
Generates a hierarchical Markdown document out of a visual layout of an article.
Watch this video to understand how the script is intended to work:
![Excalidraw Writing Machine YouTube Video](https://youtu.be/zvRpCOZAUSs)
You can download the sample Obsidian Templater file from [here](https://gist.github.com/zsviczian/bf49d4b2d401f5749aaf8c2fa8a513d9)
You can download the demo PDF document showcased in the video from [here](https://zsviczian.github.io/DemoArticle-AtomicHabits.pdf)
```js*/
const selectedElements = ea.getViewSelectedElements();
if (selectedElements.length !== 1 || selectedElements[0].type === "arrow") {
new Notice("Select a single element that is not an arrow and not a frame");
return;
}
const visited = new Set(); // Avoiding recursive infinite loops
delete window.ewm;
await ea.targetView.save();
//------------------
// Load Settings
//------------------
let settings = ea.getScriptSettings();
//set default values on first run
let didSettingsChange = false;
if(!settings["Template path"]) {
settings = {
"Template path" : {
value: "",
description: "The template file path that will receive the concatenated text. If the file includes <<<REPLACE ME>>> then it will be replaced with the generated text, if <<<REPLACE ME>>> is not present in the file the hierarchical markdown generated from the diagram will be added to the end of the template."
},
"ZK '# Summary' section": {
value: "Summary",
description: "The section in your visual zettelkasten file that contains the short written summary of the idea. This is the text that will be included in the hierarchical markdown file if visual ZK cards are included in your flow"
},
"ZK '# Source' section": {
value: "Source",
description: "The section in your visual zettelkasten file that contains the reference to your source. If present in the file, this text will be included in the output file as a reference"
},
"Embed image links": {
value: true,
description: "Should the resulting markdown document include the ![[embedded images]]?"
}
};
didSettingsChange = true;
}
if(!settings["Generate ![markdown](links)"]) {
settings["Generate ![markdown](links)"] = {
value: true,
description: "If you turn this off the script will generate ![[wikilinks]] for images"
}
didSettingsChange = true;
}
if(didSettingsChange) {
await ea.setScriptSettings(settings);
}
const ZK_SOURCE = settings["ZK '# Source' section"].value;
const ZK_SECTION = settings["ZK '# Summary' section"].value;
const INCLUDE_IMG_LINK = settings["Embed image links"].value;
const MARKDOWN_LINKS = settings["Generate ![markdown](links)"].value;
let templatePath = settings["Template path"].value;
//------------------
// Select template file
//------------------
const MSG = "Select another file"
let selection = MSG;
if(templatePath && app.vault.getAbstractFileByPath(templatePath)) {
selection = await utils.suggester([templatePath, MSG],[templatePath, MSG], "Use previous template or select another?");
if(!selection) {
new Notice("process aborted");
return;
}
}
if(selection === MSG) {
const files = app.vault.getMarkdownFiles().map(f=>f.path);
selection = await utils.suggester(files,files,"Select the template to use. ESC to not use a tempalte");
}
if(selection && selection !== templatePath) {
settings["Template path"].value = selection;
await ea.setScriptSettings(settings);
}
templatePath = selection;
//------------------
// supporting functions
//------------------
function getNextElementFollowingArrow(el, arrow) {
if (arrow.startBinding?.elementId === el.id) {
return ea.getViewElements().find(x => x.id === arrow.endBinding?.elementId);
}
if (arrow.endBinding?.elementId === el.id) {
return ea.getViewElements().find(x => x.id === arrow.startBinding?.elementId);
}
return null;
}
function getImageLink(f) {
if(MARKDOWN_LINKS) {
return `![${f.basename}](${encodeURI(f.path)})`;
}
return `![[${f.path}|${f.basename}]]`;
}
function getBoundText(el) {
const textId = el.boundElements?.find(x => x.type === "text")?.id;
const text = ea.getViewElements().find(x => x.id === textId)?.originalText;
return text ? text + "\n" : "";
}
async function getSectionText(file, section) {
const content = await app.vault.cachedRead(file);
const metadata = app.metadataCache.getFileCache(file);
if (!metadata || !metadata.headings) {
return null;
}
const targetHeading = metadata.headings.find(h => h.heading === section);
if (!targetHeading) {
return null;
}
const startPos = targetHeading.position.start.offset;
let endPos = content.length;
const nextHeading = metadata.headings.find(h => h.position.start.offset > startPos);
if (nextHeading) {
endPos = nextHeading.position.start.offset;
}
let sectionContent = content.slice(startPos, endPos).trim();
sectionContent = sectionContent.substring(sectionContent.indexOf('\n') + 1).trim();
// Remove Markdown comments enclosed in %%
sectionContent = sectionContent.replace(/%%[\s\S]*?%%/g, '').trim();
return sectionContent;
}
async function getBlockText(file, blockref) {
const content = await app.vault.cachedRead(file);
const blockPattern = new RegExp(`\\^${blockref}\\b`, 'g');
let blockPosition = content.search(blockPattern);
if (blockPosition === -1) {
return "";
}
const startPos = content.lastIndexOf('\n', blockPosition) + 1;
let endPos = content.indexOf('\n', blockPosition);
if (endPos === -1) {
endPos = content.length;
} else {
const nextBlockOrHeading = content.slice(endPos).search(/(^# |^\^|\n)/gm);
if (nextBlockOrHeading !== -1) {
endPos += nextBlockOrHeading;
} else {
endPos = content.length;
}
}
let blockContent = content.slice(startPos, endPos).trim();
blockContent = blockContent.replace(blockPattern, '').trim();
blockContent = blockContent.replace(/%%[\s\S]*?%%/g, '').trim();
return blockContent;
}
async function getElementText(el) {
if (el.type === "text") {
return el.originalText;
}
if (el.type === "image") {
const f = ea.getViewFileForImageElement(el);
if(!ea.isExcalidrawFile(f)) return f.name + (INCLUDE_IMG_LINK ? `\n${getImageLink(f)}\n` : "");
let source = await getSectionText(f, ZK_SOURCE);
source = source ? ` (source:: ${source})` : "";
const summary = await getSectionText(f, ZK_SECTION) ;
if(summary) return (INCLUDE_IMG_LINK ? `${getImageLink(f)}\n${summary + source}` : summary + source) + "\n";
return f.name + (INCLUDE_IMG_LINK ? `\n${getImageLink(f)}\n` : "");
}
if (el.type === "embeddable") {
const linkWithRef = el.link.match(/\[\[([^\]]*)]]/)?.[1];
if(!linkWithRef) return "";
const path = linkWithRef.split("#")[0];
const f = app.metadataCache.getFirstLinkpathDest(path, ea.targetView.file.path);
if(!f) return "";
if(f.extension !== "md") return f.name;
const ref = linkWithRef.split("#")[1];
if(!ref) return await app.vault.read(f);
if(ref.startsWith("^")) {
return await getBlockText(f, ref.substring(1));
} else {
return await getSectionText(f, ref);
}
}
return getBoundText(el);
}
//------------------
// Navigating the hierarchy
//------------------
async function crawl(el, level, isFirst = false) {
visited.add(el.id);
let result = await getElementText(el) + "\n";
// Process all arrows connected to this element
const boundElementsData = el.boundElements.filter(x => x.type === "arrow");
const isFork = boundElementsData.length > (isFirst ? 1 : 2);
if(isFork) level++;
for(const bindingData of boundElementsData) {
const arrow = ea.getViewElements().find(x=> x.id === bindingData.id);
const nextEl = getNextElementFollowingArrow(el, arrow);
if (nextEl && !visited.has(nextEl.id)) {
if(isFork) result += `\n${"#".repeat(level)} `;
const arrowLabel = getBoundText(arrow);
if (arrowLabel) {
// If the arrow has a label, add it as an additional level
result += arrowLabel + "\n";
result += await crawl(nextEl, level);
} else {
// If no label, continue to the next element
result += await crawl(nextEl, level);
}
}
};
return result;
}
window.ewm = "## " + await crawl(selectedElements[0], 2, true);
const outputPath = await ea.getAttachmentFilepath(`EWM - ${ea.targetView.file.name}.md`);
let result = templatePath
? await app.vault.read(app.vault.getAbstractFileByPath(templatePath))
: "";
if(result.match("<<<REPLACE ME>>>")) {
result = result.replaceAll("<<<REPLACE ME>>>",window.ewm);
} else {
result += window.ewm;
}
const outfile = await app.vault.create(outputPath,result);
setTimeout(()=>{
ea.openFileInNewOrAdjacentLeaf(outfile);
}, 250);

View File

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="CurrentColor" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-keyboard">
<path stroke-width="2" d="M10 8h.01"/>
<path stroke-width="2" d="M12 12h.01"/>
<path stroke-width="2" d="M14 8h.01"/>
<path stroke-width="2" d="M16 12h.01"/>
<path stroke-width="2" d="M18 8h.01"/>
<path stroke-width="2" d="M6 8h.01"/>
<path stroke-width="2" d="M7 16h10"/>
<path stroke-width="2" d="M8 12h.01"/>
<path fill="none" stroke-width="2" d="M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 611 B

View File

@@ -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();

View File

@@ -8,6 +8,46 @@ https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.h
```javascript
*/
if(ea.verifyMinimumPluginVersion && ea.verifyMinimumPluginVersion("2.4.0")) {
const api = ea.getExcalidrawAPI();
let appState = api.getAppState();
let gridFrequency = appState.gridStep;;
const customControls = (container) => {
new ea.obsidian.Setting(container)
.setName(`Major grid frequency`)
.addDropdown(dropdown => {
[2,3,4,5,6,7,8,9,10].forEach(grid=>dropdown.addOption(grid,grid));
dropdown
.setValue(gridFrequency)
.onChange(value => {
gridFrequency = value;
})
})
}
const gridSize = parseInt(await utils.inputPrompt(
"Grid size?",
null,
appState.GridSize?.toString()??"20",
null,
1,
false,
customControls
));
if(isNaN(gridSize)) return; //this is to avoid passing an illegal value to Excalidraw
const gridStep = isNaN(parseInt(gridFrequency)) ? appState.gridStep : parseInt(gridFrequency);
api.updateScene({
appState : {gridSize, gridStep, gridModeEnabled:true},
commitToHistory:false
});
}
// ----------------
// old script
// ----------------
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.9.19")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;

View File

@@ -9,7 +9,11 @@ https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.h
```javascript
*/
let width = (ea.getViewSelectedElement().strokeWidth??1).toString();
width = await utils.inputPrompt("Width?","number",width);
width = parseFloat(await utils.inputPrompt("Width?","number",width));
if(isNaN(width)) {
new Notice("Invalid number");
return;
}
const elements=ea.getViewSelectedElements();
ea.copyViewElementsToEAforEditing(elements);
ea.getElements().forEach((el)=>el.strokeWidth=width);

File diff suppressed because one or more lines are too long

View File

@@ -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)
---
@@ -119,6 +119,7 @@ I would love to include your contribution in the script library. If you have a s
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Custom%20Zoom.svg"/></div>|[[#Custom Zoom]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Copy%20Selected%20Element%20Styles%20to%20Global.svg"/></div>|[[#Copy Selected Element Styles to Global]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/ExcaliAI.svg"/></div>|[[#ExcaliAI]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Excalidraw%20Writing%20Machine.svg"/></div>|[[#Excalidraw Writing Machine]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/GPT-Draw-a-UI.svg"/></div>|[[#GPT Draw-a-UI]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Hardware%20Eraser%20Support.svg"/></div>|[[#Hardware Eraser Support]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Palette%20loader.svg"/></div>|[[#Palette Loader]]|
@@ -389,6 +390,12 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/ExcaliAI.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Various AI features based on GPT Vision.<br><iframe width="400" height="225" src="https://www.youtube.com/embed/A1vrSGBbWgo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-draw-a-ui.jpg'></td></tr></table>
## Excalidraw Writing Machine
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Excalidraw%20Writing%20Machine.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Excalidraw%20Writing%20Machine.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Creates a hierarchical Markdown document out of a visual layout of an article that can be fed to Templater and converted into an article using AI for Templater.<br>Watch this video to understand how the script is intended to work:<br><iframe width="400" height="225" src="https://www.youtube.com/embed/zvRpCOZAUSs" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><br>You can download the sample Obsidian Templater file from <a href="https://gist.github.com/zsviczian/bf49d4b2d401f5749aaf8c2fa8a513d9">here</a>. You can download the demo PDF document showcased in the video from <a href="https://zsviczian.github.io/DemoArticle-AtomicHabits.pdf">here</a>.</td></tr></table>
## GPT Draw-a-UI
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/GPT-Draw-a-UI.md

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 KiB

After

Width:  |  Height:  |  Size: 861 KiB

View File

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

View File

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

View File

@@ -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,61 +19,63 @@
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@zsviczian/excalidraw": "0.17.1-obsidian-23",
"@zsviczian/excalidraw": "0.17.1-obsidian-45",
"chroma-js": "^2.4.2",
"clsx": "^2.0.0",
"colormaster": "^1.2.1",
"@zsviczian/colormaster": "^1.2.2",
"gl-matrix": "^3.4.3",
"js-yaml": "^4.1.0",
"lucide-react": "^0.263.1",
"mathjax-full": "^3.2.2",
"monkey-around": "^2.3.0",
"nanoid": "^4.0.2",
"opentype.js": "^1.3.4",
"polybooljs": "^1.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"roughjs": "^4.5.2",
"js-yaml": "^4.1.0"
"woff2sfnt-sfnt2woff": "^1.0.0"
},
"devDependencies": {
"dotenv": "^16.4.5",
"@babel/core": "^7.22.9",
"@babel/preset-env": "^7.22.10",
"@babel/preset-react": "^7.22.5",
"@excalidraw/eslint-config": "^1.0.3",
"@excalidraw/prettier-config": "^1.0.2",
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-commonjs": "^24.1.0",
"@rollup/plugin-node-resolve": "^15.2.1",
"@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-typescript": "^11.1.2",
"@types/chroma-js": "^2.4.0",
"@types/js-beautify": "^1.14.0",
"@types/node": "^20.10.5",
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18",
"@types/js-yaml": "^4.0.9",
"@zerollup/ts-transform-paths": "^1.7.18",
"cross-env": "^7.0.3",
"cssnano": "^6.0.2",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"lz-string": "^1.5.0",
"obsidian": "^1.4.0",
"prettier": "^3.0.1",
"rollup": "^2.70.1",
"rollup-plugin-copy": "^3.5.0",
"rollup-plugin-postprocess": "github:brettz9/rollup-plugin-postprocess#update",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.34.1",
"rollup-plugin-web-worker-loader": "^1.6.1",
"tslib": "^2.6.1",
"ttypescript": "^1.5.15",
"typescript": "^5.2.2",
"@codemirror/commands": "^6.3.3",
"@codemirror/language": "^6.10.0",
"@codemirror/search": "^6.5.5",
"@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.23.0"
"@codemirror/view": "^6.23.0",
"@excalidraw/eslint-config": "^1.0.3",
"@excalidraw/prettier-config": "^1.0.2",
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-typescript": "^11.1.6",
"@types/chroma-js": "^2.4.0",
"@types/js-beautify": "^1.14.0",
"@types/js-yaml": "^4.0.9",
"@types/node": "^20.10.5",
"@types/opentype.js": "^1.3.8",
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18",
"@zerollup/ts-transform-paths": "^1.7.18",
"cross-env": "^7.0.3",
"cssnano": "^6.0.2",
"dotenv": "^16.4.5",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"lz-string": "^1.5.0",
"obsidian": "1.5.7-1",
"prettier": "^3.0.1",
"rollup": "^2.70.1",
"rollup-plugin-copy": "^3.5.0",
"@zsviczian/rollup-plugin-postprocess": "^1.0.3",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.34.1",
"tslib": "^2.6.1",
"ttypescript": "^1.5.15",
"typescript": "^5.2.2"
},
"resolutions": {
"@typescript-eslint/typescript-estree": "5.3.0"

View File

@@ -1,21 +1,19 @@
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import babel from '@rollup/plugin-babel';
import replace from "@rollup/plugin-replace";
import { terser } from "rollup-plugin-terser";
import copy from "rollup-plugin-copy";
import typescript2 from "rollup-plugin-typescript2";
import webWorker from "rollup-plugin-web-worker-loader";
import fs from 'fs';
import LZString from 'lz-string';
import postprocess from 'rollup-plugin-postprocess';
import postprocess from '@zsviczian/rollup-plugin-postprocess';
import cssnano from 'cssnano';
// Load environment variables
import dotenv from 'dotenv';
dotenv.config();
const DIST_FOLDER = 'dist';
const DIST_FOLDER = 'dist';
const isProd = (process.env.NODE_ENV === "production");
const isLib = (process.env.NODE_ENV === "lib");
console.log(`Running: ${process.env.NODE_ENV}`);
@@ -54,11 +52,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',
@@ -83,9 +81,12 @@ const BASE_CONFIG = {
const getRollupPlugins = (tsconfig, ...plugins) => [
typescript2(tsconfig),
nodeResolve({ browser: true }),
replace({
preventAssignment: true,
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
}),
commonjs(),
webWorker({ inline: true, forceInline: true, targetPlatform: "browser" }),
nodeResolve({ browser: true, preferBuiltins: false }),
].concat(plugins);
const BUILD_CONFIG = {
@@ -96,48 +97,28 @@ const BUILD_CONFIG = {
format: 'cjs',
exports: 'default',
},
plugins: [
typescript2({
tsconfig: isProd ? "tsconfig.json" : "tsconfig.dev.json",
}),
replace({
preventAssignment: true,
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
}),
babel({
presets: [['@babel/preset-env', {
targets: {
esmodules: true,
},
}]],
exclude: "node_modules/**",
}),
commonjs(),
nodeResolve({ browser: true, preferBuiltins: false }),
plugins: getRollupPlugins(
{tsconfig: isProd ? "tsconfig.json" : "tsconfig.dev.json"},
...(isProd ? [
terser({
toplevel: false,
compress: { passes: 2 },
format: {
comments: false, // Remove all comments
},
}),
//!postprocess - the version available on npmjs does not work, need this update:
// npm install brettz9/rollup-plugin-postprocess#update --save-dev
// https://github.com/developit/rollup-plugin-postprocess/issues/10
postprocess([
[/React=require\("react"\),state=require\("@codemirror\/state"\),view=require\("@codemirror\/view"\)/,
`state=require("@codemirror/state"),view=require("@codemirror/view")` + packageString],
]),
] : [
postprocess([
[/var React = require\('react'\);/, packageString],
]),
postprocess([ [/var React = require\('react'\);/, packageString] ]),
]),
copy({
targets: [
{ src: 'manifest.json', dest: DIST_FOLDER },
],
verbose: true, // Optional: To display copied files in the console
targets: [ { src: 'manifest.json', dest: DIST_FOLDER } ],
verbose: true,
}),
],
),
};
const LIB_CONFIG = {

View File

@@ -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);

View File

@@ -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,
getCSSFontDefinition,
} from "./constants/constants";
import { createSVG } from "./ExcalidrawAutomate";
import { ExcalidrawData, getTransclusion } from "./ExcalidrawData";
@@ -38,12 +34,14 @@ import {
LinkParts,
svgToBase64,
isMaskFile,
getEmbeddedFilenameParts,
cropCanvas,
} 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 +142,6 @@ const replaceSVGColors = (svg: SVGSVGElement | string, colorMap: ColorMap | null
return svg;
}
export class EmbeddedFile {
public file: TFile = null;
public isSVGwithBitmap: boolean = false;
@@ -156,6 +152,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 +200,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 +211,9 @@ export class EmbeddedFile {
5000,
);
}
} else {
this.filenameparts = getEmbeddedFilenameParts(imgPath);
this.filenameparts.filepath = this.file.path;
}
}
@@ -359,25 +359,36 @@ export class EmbeddedFilesLoader {
: false,
withTheme: !!forceTheme,
isMask,
skipInliningFonts: false,
};
const shouldUseCache = this.plugin.settings.allowImageCacheInScene && file && imageCache.isReady();
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,
inlineFonts: !exportSettings.skipInliningFonts,
}: {
filepath: file.path,
hasBlockref: false,
hasGroupref: false,
hasTaskbone: false,
hasArearef: false,
hasFrameref: false,
hasClippedFrameref: false,
hasSectionref: false,
inlineFonts: !exportSettings.skipInliningFonts,
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
@@ -388,9 +399,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,
@@ -423,8 +442,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};
};
@@ -443,6 +473,8 @@ export class EmbeddedFilesLoader {
return null;
}
const app = this.plugin.app;
const isHyperLink = inFile instanceof EmbeddedFile ? inFile.isHyperLink : false;
const isLocalLink = inFile instanceof EmbeddedFile ? inFile.isLocalLink : false;
const hyperlink = inFile instanceof EmbeddedFile ? inFile.hyperlink : "";
@@ -543,7 +575,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,
@@ -716,6 +749,8 @@ export class EmbeddedFilesLoader {
}
const pageNum = isNaN(linkParts.page) ? 1 : (linkParts.page??1);
const scale = this.plugin.settings.pdfScale;
const cropRect = linkParts.ref.split("rect=")[1]?.split(",").map(x=>parseInt(x));
const validRect = cropRect && cropRect.length === 4 && cropRect.every(x=>!isNaN(x));
// Render the page
const renderPage = async (num:number) => {
@@ -736,6 +771,23 @@ export class EmbeddedFilesLoader {
};
await page.render(renderCtx).promise;
if(validRect) {
const [left, bottom, _, top] = page.view;
const pageHeight = top - bottom;
width = (cropRect[2] - cropRect[0]) * scale;
height = (cropRect[3] - cropRect[1]) * scale;
const crop = validRect ? {
left: (cropRect[0] - left) * scale,
top: (bottom + pageHeight - cropRect[3]) * scale,
width,
height,
} : undefined;
if(crop) {
return cropCanvas(canvas, crop);
}
}
return canvas;
};
@@ -786,13 +838,29 @@ export class EmbeddedFilesLoader {
}
switch (fontName) {
case "Virgil":
fontDef = VIRGIL_FONT;
fontDef = await getCSSFontDefinition(1);
break;
case "Cascadia":
fontDef = CASCADIA_FONT;
fontDef = await getCSSFontDefinition(3);
break;
case "Assistant":
fontDef = ASSISTANT_FONT;
case "Assistant":
case "Helvetica":
fontDef = await getCSSFontDefinition(2);
break;
case "Excalifont":
fontDef = await getCSSFontDefinition(5);
break;
case "Nunito":
fontDef = await getCSSFontDefinition(6);
break;
case "Lilita One":
fontDef = await getCSSFontDefinition(7);
break;
case "Comic Shanns":
fontDef = await getCSSFontDefinition(8);
break;
case "Liberation Sans":
fontDef = await getCSSFontDefinition(9);
break;
case "":
fontDef = "";
@@ -875,12 +943,14 @@ export class EmbeddedFilesLoader {
mdDIV.style.display = "block";
mdDIV.style.color = fontColor && fontColor !== "" ? fontColor : "initial";
await MarkdownRenderer.renderMarkdown(text, mdDIV, file.path, plugin);
//await MarkdownRenderer.renderMarkdown(text, mdDIV, file.path, plugin);
await MarkdownRenderer.render(this.plugin.app,text,mdDIV,file.path,this.plugin);
mdDIV
.querySelectorAll(":scope > *[class^='frontmatter']")
.forEach((el) => mdDIV.removeChild(el));
await replaceBlobWithBase64(mdDIV); //because image cache returns a blob
const internalEmbeds = Array.from(mdDIV.querySelectorAll("span[class='internal-embed']"))
for(let i=0;i<internalEmbeds.length;i++) {
const el = internalEmbeds[i];
@@ -992,7 +1062,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);
@@ -1007,4 +1077,53 @@ 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;
};
const replaceBlobWithBase64 = async (divElement: HTMLDivElement): Promise<void> => {
const images = divElement.querySelectorAll<HTMLImageElement>('img[src^="blob:app://obsidian.md"]');
for (let img of images) {
const blobUrl = img.src;
try {
const response = await fetch(blobUrl);
const blob = await response.blob();
const base64 = await blobToBase64(blob);
img.src = `data:${blob.type};base64,${base64}`;
} catch (error) {
console.error(`Failed to fetch or convert blob: ${blobUrl}`, error);
}
}
};

View File

@@ -11,7 +11,9 @@ import {
StrokeRoundness,
RoundnessType,
ExcalidrawFrameElement,
ExcalidrawTextContainer,
} from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { MimeType } from "./EmbeddedFileLoader";
import { Editor, normalizePath, Notice, OpenViewState, RequestUrlResponse, TFile, TFolder, WorkspaceLeaf } from "obsidian";
import * as obsidian_module from "obsidian";
import ExcalidrawView, { ExportSettings, TextMode, getTextMode } from "src/ExcalidrawView";
@@ -26,7 +28,7 @@ import {
GITHUB_RELEASES,
determineFocusDistance,
getCommonBoundingBox,
getDefaultLineHeight,
getLineHeight,
getMaximumGroups,
intersectElementWithLine,
measureText,
@@ -35,11 +37,12 @@ import {
REG_LINKINDEX_INVALIDCHARS,
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,
@@ -59,22 +62,22 @@ 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 CM, { ColorMaster, extendPlugins } from "colormaster";
import HarmonyPlugin from "colormaster/plugins/harmony";
import MixPlugin from "colormaster/plugins/mix"
import A11yPlugin from "colormaster/plugins/accessibility"
import NamePlugin from "colormaster/plugins/name"
import LCHPlugin from "colormaster/plugins/lch";
import LUVPlugin from "colormaster/plugins/luv";
import LABPlugin from "colormaster/plugins/lab";
import UVWPlugin from "colormaster/plugins/uvw";
import XYZPlugin from "colormaster/plugins/xyz";
import HWBPlugin from "colormaster/plugins/hwb";
import HSVPlugin from "colormaster/plugins/hsv";
import RYBPlugin from "colormaster/plugins/ryb";
import CMYKPlugin from "colormaster/plugins/cmyk";
import { TInput } from "colormaster/types";
import { ConnectionPoint, DeviceType } from "src/types/types";
import CM, { ColorMaster, extendPlugins } from "@zsviczian/colormaster";
import HarmonyPlugin from "@zsviczian/colormaster/plugins/harmony";
import MixPlugin from "@zsviczian/colormaster/plugins/mix"
import A11yPlugin from "@zsviczian/colormaster/plugins/accessibility"
import NamePlugin from "@zsviczian/colormaster/plugins/name"
import LCHPlugin from "@zsviczian/colormaster/plugins/lch";
import LUVPlugin from "@zsviczian/colormaster/plugins/luv";
import LABPlugin from "@zsviczian/colormaster/plugins/lab";
import UVWPlugin from "@zsviczian/colormaster/plugins/uvw";
import XYZPlugin from "@zsviczian/colormaster/plugins/xyz";
import HWBPlugin from "@zsviczian/colormaster/plugins/hwb";
import HSVPlugin from "@zsviczian/colormaster/plugins/hsv";
import RYBPlugin from "@zsviczian/colormaster/plugins/ryb";
import CMYKPlugin from "@zsviczian/colormaster/plugins/cmyk";
import { TInput } from "@zsviczian/colormaster/types";
import {ConversionResult, svgToExcalidraw} from "src/svgToExcalidraw/parser"
import { ROUNDNESS } from "src/constants/constants";
import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard";
@@ -88,8 +91,9 @@ import {
extractCodeBlocks as _extractCodeBlocks,
} from "./utils/AIUtils";
import { EXCALIDRAW_AUTOMATE_INFO, EXCALIDRAW_SCRIPTENGINE_INFO } from "./dialogs/SuggesterInfo";
import { getFrameBasedOnFrameNameOrId } from "./utils/ExcalidrawViewUtils";
import { addBackOfTheNoteCard, getFrameBasedOnFrameNameOrId } from "./utils/ExcalidrawViewUtils";
import { log } from "./utils/DebugHelper";
import { ExcalidrawLib } from "./ExcalidrawLib";
extendPlugins([
HarmonyPlugin,
@@ -109,6 +113,7 @@ extendPlugins([
declare const PLUGIN_VERSION:string;
declare var LZString: any;
declare const excalidrawLib: typeof ExcalidrawLib;
const GAP = 4;
@@ -273,11 +278,11 @@ export class ExcalidrawAutomate {
return getNewUniqueFilepath(app.vault, filename, folderAndPath.folder);
}
public compressToBase64(str:string):string {
public compressToBase64(str:string): string {
return LZString.compressToBase64(str);
}
public decompressFromBase64(str:string):string {
public decompressFromBase64(str:string): string {
return LZString.decompressFromBase64(str);
}
@@ -384,7 +389,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
@@ -712,6 +717,12 @@ export class ExcalidrawAutomate {
this.style.roundness ? "round":"sharp",
gridSize: template?.appState?.gridSize ?? this.canvas.gridSize,
colorPalette: template?.appState?.colorPalette ?? this.colorPalette,
...template?.appState?.frameRendering
? {frameRendering: template.appState.frameRendering}
: {},
...template?.appState?.objectsSnapModeEnabled
? {objectsSnapModeEnabled: template.appState.objectsSnapModeEnabled}
: {},
},
files: template?.files ?? {},
};
@@ -1022,6 +1033,22 @@ export class ExcalidrawAutomate {
return id;
};
/**
* Add elements to frame
* @param frameId
* @param elementIDs to add
* @returns void
*/
addElementsToFrame(frameId: string, elementIDs: string[]):void {
if(!this.getElement(frameId)) return;
elementIDs.forEach(elID => {
const el = this.getElement(elID);
if(el) {
el.frameId = frameId;
}
});
}
/**
*
* @param topX
@@ -1185,14 +1212,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;
}
@@ -1211,7 +1238,8 @@ export class ExcalidrawAutomate {
topY: number,
text: string,
formatting?: {
wrapAt?: number;
autoResize?: boolean; //Default is true. Setting this to false will wrap the text in the text element without the need for the containser. If set to false, you must set a width value as well.
wrapAt?: number; //wrapAt is ignored if autoResize is set to false (and width is provided)
width?: number;
height?: number;
textAlign?: "left" | "center" | "right";
@@ -1224,20 +1252,24 @@ export class ExcalidrawAutomate {
): string {
id = id ?? nanoid();
const originalText = text;
text = formatting?.wrapAt ? this.wrapText(text, formatting.wrapAt) : text;
const { w, h, baseline } = _measureText(
const autoresize = ((typeof formatting?.width === "undefined") || formatting?.box)
? true
: (formatting?.autoResize ?? true)
text = (formatting?.wrapAt && autoresize) ? this.wrapText(text, formatting.wrapAt) : text;
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;
let boxId: string = null;
const boxPadding = formatting?.boxPadding ?? 30;
const strokeColor = this.style.strokeColor;
this.style.strokeColor = formatting?.boxStrokeColor ?? strokeColor;
const boxPadding = formatting?.boxPadding ?? 30;
if (formatting?.box) {
switch (formatting.box) {
case "ellipse":
@@ -1283,12 +1315,12 @@ export class ExcalidrawAutomate {
? formatting.textAlign
: this.style.textAlign ?? "left",
verticalAlign: formatting?.textVerticalAlign ?? this.style.verticalAlign,
baseline,
...this.boxedElement(id, "text", topX, topY, width, height),
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") {
this.addToGroup([id, boxId]);
@@ -1300,6 +1332,25 @@ export class ExcalidrawAutomate {
}
box.boundElements.push({ type: "text", id });
}
const textElement = this.getElement(id) as Mutable<ExcalidrawTextElement>;
const container = (boxId && formatting.box !== "blob") ? this.getElement(boxId) as Mutable<ExcalidrawTextContainer>: undefined;
const dimensions = refreshTextDimensions(
textElement,
container,
arrayToMap(this.getElements()),
originalText,
);
if(dimensions) {
textElement.width = dimensions.width;
textElement.height = dimensions.height;
textElement.x = dimensions.x;
textElement.y = dimensions.y;
textElement.text = dimensions.text;
if(container) {
container.width = dimensions.width + 2 * boxPadding;
container.height = dimensions.height + 2 * boxPadding;
}
}
return boxId ?? id;
};
@@ -1543,6 +1594,26 @@ export class ExcalidrawAutomate {
return id;
};
/**
* returns the base64 dataURL of the LaTeX equation rendered as an SVG
* @param tex The LaTeX equation as string
* @param scale of the image, default value is 4
* @returns
*/
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1930
async tex2dataURL(
tex: string,
scale: number = 4 // Default scale value, adjust as needed
): Promise<{
mimeType: MimeType;
fileId: FileId;
dataURL: DataURL;
created: number;
size: { height: number; width: number };
}> {
return await tex2dataURL(tex,scale);
};
/**
*
* @param objectA
@@ -1833,12 +1904,30 @@ export class ExcalidrawAutomate {
this.targetView.updateScene({
elements: el.filter((e: ExcalidrawElement) => !elToDelete.includes(e)),
appState: st,
commitToHistory: true,
storeAction: "capture",
});
//this.targetView.save();
return true;
};
/**
* Adds a back of the note card to the current active view
* @param sectionTitle: string
* @param activate:boolean = true; if true, the new Embedded Element will be activated after creation
* @param sectionBody?: string;
* @param embeddableCustomData?: EmbeddableMDCustomProps; formatting of the embeddable element
* @returns embeddable element id
*/
async addBackOfTheCardNoteToView(sectionTitle: string, activate: boolean = false, sectionBody?: string, embeddableCustomData?: EmbeddableMDCustomProps): Promise<string> {
//@ts-ignore
if (!this.targetView || !this.targetView?._loaded) {
errorMessage("targetView not set", "addBackOfTheCardNoteToView()");
return null;
}
await this.targetView.forceSave(true);
return addBackOfTheNoteCard(this.targetView, sectionTitle, activate, sectionBody, embeddableCustomData);
}
/**
* get the selected element in the view, if more are selected, get the first
* @returns
@@ -1850,15 +1939,16 @@ export class ExcalidrawAutomate {
/**
*
* @param includeFrameChildren
* @returns
*/
getViewSelectedElements(): any[] {
getViewSelectedElements(includeFrameChildren:boolean = true): any[] {
//@ts-ignore
if (!this.targetView || !this.targetView?._loaded) {
errorMessage("targetView not set", "getViewSelectedElements()");
return [];
}
return this.targetView.getViewSelectedElements();
return this.targetView.getViewSelectedElements(includeFrameChildren);
};
/**
@@ -1951,7 +2041,7 @@ export class ExcalidrawAutomate {
appState: {
viewModeEnabled: !isFullscreen,
},
commitToHistory: false,
storeAction: "update",
});
this.targetView.toolsPanelRef?.current?.setExcalidrawViewMode(!isFullscreen);
}
@@ -1970,7 +2060,7 @@ export class ExcalidrawAutomate {
return;
}
const view = this.targetView as ExcalidrawView;
view.updateScene({appState:{viewModeEnabled: enabled}});
view.updateScene({appState:{viewModeEnabled: enabled}, storeAction: "update"});
view.toolsPanelRef?.current?.setExcalidrawViewMode(enabled);
}
@@ -1986,6 +2076,7 @@ export class ExcalidrawAutomate {
appState?: AppState,
files?: BinaryFileData,
commitToHistory?: boolean,
storeAction?: "capture" | "none" | "update",
},
restore: boolean = false,
):void {
@@ -1994,7 +2085,16 @@ export class ExcalidrawAutomate {
errorMessage("targetView not set", "viewToggleFullScreen()");
return;
}
this.targetView.updateScene(scene,restore);
if (!Boolean(scene.storeAction)) {
scene.storeAction = scene.commitToHistory ? "capture" : "update";
}
this.targetView.updateScene({
elements: scene.elements,
appState: scene.appState,
files: scene.files,
storeAction: scene.storeAction,
},restore);
}
/**
@@ -2340,35 +2440,61 @@ export class ExcalidrawAutomate {
* @param elements - typically all the non-deleted elements in the scene
* @returns
*/
getElementsInTheSameGroupWithElement(element: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[] {
getElementsInTheSameGroupWithElement(
element: ExcalidrawElement,
elements: ExcalidrawElement[],
includeFrameElements: boolean = false,
): ExcalidrawElement[] {
if(!element || !elements) return [];
const container = (element.type === "text" && element.containerId)
? elements.filter(el=>el.id === element.containerId)
: [];
if(element.groupIds.length === 0) {
if(includeFrameElements && element.type === "frame") {
return this.getElementsInFrame(element,elements,true);
}
if(container.length === 1) return [element,container[0]];
return [element];
}
if(container.length === 1) {
return elements.filter(el=>
el.groupIds.some(id=>element.groupIds.includes(id)) ||
el === container[0]
);
const conditionFN = container.length === 1
? (el: ExcalidrawElement) => el.groupIds.some(id=>element.groupIds.includes(id)) || el === container[0]
: (el: ExcalidrawElement) => el.groupIds.some(id=>element.groupIds.includes(id));
if(!includeFrameElements) {
return elements.filter(el=>conditionFN(el));
} else {
//I use the set and the filter at the end to preserve scene layer seqeuence
//adding frames could potentially mess up the sequence otherwise
const elementIDs = new Set<string>();
elements
.filter(el=>conditionFN(el))
.forEach(el=>{
if(el.type === "frame") {
this.getElementsInFrame(el,elements,true).forEach(el=>elementIDs.add(el.id))
} else {
elementIDs.add(el.id);
}
});
return elements.filter(el=>elementIDs.has(el.id));
}
return elements.filter(el=>el.groupIds.some(id=>element.groupIds.includes(id)));
}
/**
* 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));
}
/**
@@ -2434,16 +2560,17 @@ 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 };
};
/**
* Returns the size of the image element at 100% (i.e. the original size)
* Returns the size of the image element at 100% (i.e. the original size), or undefined if the data URL is not available
* @param imageElement an image element from the active scene on targetView
* @param shouldWaitForImage if true, the function will wait for the image to load before returning the size
*/
async getOriginalImageSize(imageElement: ExcalidrawImageElement): Promise<{width: number; height: number}> {
async getOriginalImageSize(imageElement: ExcalidrawImageElement, shouldWaitForImage: boolean=false): Promise<{width: number; height: number}> {
//@ts-ignore
if (!this.targetView || !this.targetView?._loaded) {
errorMessage("targetView not set", "getOriginalImageSize()");
@@ -2459,10 +2586,59 @@ export class ExcalidrawAutomate {
return null;
}
const isDark = this.getExcalidrawAPI().getAppState().theme === "dark";
const dataURL = ef.getImage(isDark);
let dataURL = ef.getImage(isDark);
if(!dataURL && !shouldWaitForImage) return;
if(!dataURL) {
let watchdog = 0;
while(!dataURL && watchdog < 50) {
await sleep(100);
dataURL = ef.getImage(isDark);
watchdog++;
}
if(!dataURL) return;
}
return await getImageSize(dataURL);
}
/**
* Resets the image to its original aspect ratio.
* If the image is resized then the function returns true.
* If the image element is not in EA (only in the view), then if image is resized, the element is copied to EA for Editing using copyViewElementsToEAforEditing([imgEl]).
* Note you need to run await ea.addElementsToView(false); to add the modified image to the view.
* @param imageElement - the EA image element to be resized
* returns true if image was changed, false if image was not changed
*/
async resetImageAspectRatio(imgEl: ExcalidrawImageElement): Promise<boolean> {
//@ts-ignore
if (!this.targetView || !this.targetView?._loaded) {
errorMessage("targetView not set", "resetImageAspectRatio()");
return null;
}
const size = await this.getOriginalImageSize(imgEl, true);
if (size) {
const originalArea = imgEl.width * imgEl.height;
const originalAspectRatio = size.width / size.height;
let newWidth = Math.sqrt(originalArea * originalAspectRatio);
let newHeight = Math.sqrt(originalArea / originalAspectRatio);
const centerX = imgEl.x + imgEl.width / 2;
const centerY = imgEl.y + imgEl.height / 2;
if (newWidth !== imgEl.width || newHeight !== imgEl.height) {
if(!this.getElement(imgEl.id)) {
this.copyViewElementsToEAforEditing([imgEl]);
}
const eaEl = this.getElement(imgEl.id);
eaEl.width = newWidth;
eaEl.height = newHeight;
eaEl.x = centerX - newWidth / 2;
eaEl.y = centerY - newHeight / 2;
return true;
}
}
return false;
}
/**
* verifyMinimumPluginVersion returns true if plugin version is >= than required
* recommended use:
@@ -2555,7 +2731,7 @@ export class ExcalidrawAutomate {
elements.splice(newZIndex, 0, elements.splice(oldZIndex, 1)[0]);
this.targetView.updateScene({
elements,
commitToHistory: true,
storeAction: "capture",
});
};
@@ -2647,6 +2823,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(
@@ -2659,14 +2849,10 @@ 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 },
) {
): number[][] {
const p = [];
const [x, y] = points[0];
for (let i = 0; i < points.length; i++) {
@@ -2675,7 +2861,9 @@ function normalizeLinePoints(
return p;
}
function getLineBox(points: [x: number, y: number][]) {
function getLineBox(
points: [x: number, y: number][]
):{x:number, y:number, w: number, h:number} {
const [x1, y1, x2, y2] = estimateLineBound(points);
return {
x: x1,
@@ -2685,23 +2873,16 @@ 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";
}
function getFontFamily(id: number):string {
return 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():Promise<void> {
await excalidrawLib.registerFontsInCSS();
const fonts = excalidrawLib.getFontFamilies();
for(let i=0;i<fonts.length;i++) {
if(fonts[i] !== "Local Font") await (document as any).fonts.load(`16px ${fonts[i]}`);
};
}
export function _measureText(
@@ -2709,21 +2890,21 @@ export function _measureText(
fontSize: number,
fontFamily: number,
lineHeight: number,
) {
): {w: number, h:number} {
//following odd error with mindmap on iPad while synchornizing with desktop.
if (!fontSize) {
fontSize = 20;
}
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(
@@ -2808,18 +2989,17 @@ async function getTemplate(
? getTextElementsMatchingQuery(scene.elements,["# "+filenameParts.sectionref],true)
: scene.elements.filter((el: ExcalidrawElement)=>el.id===filenameParts.blockref);
if(el.length > 0) {
groupElements = plugin.ea.getElementsInTheSameGroupWithElement(el[0],scene.elements)
groupElements = plugin.ea.getElementsInTheSameGroupWithElement(el[0],scene.elements,true)
}
}
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" ||
@@ -2828,6 +3008,8 @@ async function getTemplate(
));
}
excalidrawData.destroy();
const filehead = data.substring(0, trimLocation);
return {
elements: convertMarkdownLinksToObsidianURLs
? updateElementLinksToObsidianLinks({
@@ -2835,7 +3017,7 @@ async function getTemplate(
hostFile: file,
}) : groupElements,
appState: scene.appState,
frontmatter: data.substring(0, trimLocation),
frontmatter: filehead.match(/^---\n.*\n---\n/ms)?.[0] ?? filehead,
files: scene.files,
hasSVGwithBitmap,
};
@@ -2867,7 +3049,7 @@ export async function createPNG(
depth: number,
padding?: number,
imagesDict?: any,
) {
): Promise<Blob> {
if (!loader) {
loader = new EmbeddedFilesLoader(plugin);
}
@@ -2896,6 +3078,7 @@ export async function createPNG(
theme: forceTheme ?? template?.appState?.theme ?? canvasTheme,
viewBackgroundColor:
template?.appState?.viewBackgroundColor ?? canvasBackgroundColor,
...template?.appState?.frameRendering ? {frameRendering: template.appState.frameRendering} : {},
},
files,
},
@@ -2950,7 +3133,7 @@ export const updateElementLinksToObsidianLinks = ({elements, hostFile}:{
})
}
function addFilterToForeignObjects(svg:SVGSVGElement) {
function addFilterToForeignObjects(svg:SVGSVGElement):void {
const foreignObjects = svg.querySelectorAll("foreignObject");
foreignObjects.forEach((foreignObject) => {
foreignObject.setAttribute("filter", THEME_FILTER);
@@ -2975,6 +3158,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;
@@ -2991,6 +3177,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
@@ -3002,6 +3189,7 @@ export async function createSVG(
theme,
viewBackgroundColor:
template?.appState?.viewBackgroundColor ?? canvasBackgroundColor,
...template?.appState?.frameRendering ? {frameRendering: template.appState.frameRendering} : {},
},
files,
},
@@ -3010,6 +3198,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,
@@ -3017,9 +3208,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
@@ -3041,7 +3231,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] {
@@ -3092,7 +3282,7 @@ export function repositionElementsToCursor(
return restore({elements}, null, null).elements;
}
function errorMessage(message: string, source: string) {
function errorMessage(message: string, source: string):void {
switch (message) {
case "targetView not set":
errorlog({
@@ -3145,7 +3335,7 @@ export const search = async (view: ExcalidrawView) => {
const ea = view.plugin.ea;
ea.reset();
ea.setView(view);
const elements = ea.getViewElements().filter((el) => el.type === "text" || el.type === "frame");
const elements = ea.getViewElements().filter((el) => el.type === "text" || el.type === "frame" || el.link || el.type === "image");
if (elements.length === 0) {
return;
}
@@ -3239,6 +3429,62 @@ export const getFrameElementsMatchingQuery = (
}));
}
/**
*
* @param elements
* @param query
* @param exactMatch - when searching for section header exactMatch should be set to true
* @returns the elements matching the query
*/
export const getElementsWithLinkMatchingQuery = (
elements: ExcalidrawElement[],
query: string[],
exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
): ExcalidrawElement[] => {
if (!elements || elements.length === 0 || !query || query.length === 0) {
return [];
}
return elements.filter((el: any) =>
el.link &&
query.some((q) => {
const text = el.link.toLowerCase().trim();
return exactMatch
? (text === q.toLowerCase())
: text.match(q.toLowerCase());
}));
}
/**
*
* @param elements
* @param query
* @param exactMatch - when searching for section header exactMatch should be set to true
* @returns the elements matching the query
*/
export const getImagesMatchingQuery = (
elements: ExcalidrawElement[],
query: string[],
excalidrawData: ExcalidrawData,
exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
): ExcalidrawElement[] => {
if (!elements || elements.length === 0 || !query || query.length === 0) {
return [];
}
return elements.filter((el: ExcalidrawElement) =>
el.type === "image" &&
query.some((q) => {
const filename = excalidrawData.getFile(el.fileId)?.file?.basename.toLowerCase().trim();
const equation = excalidrawData.getEquation(el.fileId)?.latex?.toLocaleLowerCase().trim();
const text = filename ?? equation;
if(!text) return false;
return exactMatch
? (text === q.toLowerCase())
: text.match(q.toLowerCase());
}));
}
export const cloneElement = (el: ExcalidrawElement):any => {
const newEl = JSON.parse(JSON.stringify(el));
newEl.version = el.version + 1;

View File

@@ -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 {
@@ -36,6 +35,7 @@ import {
updateFrontmatterInString,
wrapTextAtCharLength,
arrayToMap,
compressAsync,
} from "./utils/Utils";
import { cleanBlockRef, cleanSectionHeading, getAttachmentsFolderAndFilePath, isObsidianThemeDark } from "./utils/ObsidianUtils";
import {
@@ -50,6 +50,7 @@ import { ConfirmationPrompt } from "./dialogs/Prompt";
import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
import { DEBUGGING, debug } from "./utils/DebugHelper";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import { updateElementIdsInScene } from "./utils/ExcalidrawSceneUtils";
type SceneDataWithFiles = SceneData & { files: BinaryFiles };
@@ -69,10 +70,34 @@ export enum AutoexportPreference {
inherit
}
export const REGEX_TAGS = {
// #[\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+
// 1
EXPR: /(#[\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+)/gu,
getResList: (text: string): IteratorResult<RegExpMatchArray, any>[] => {
const res = text.matchAll(REGEX_TAGS.EXPR);
let parts: IteratorResult<RegExpMatchArray, any>;
const resultList = [];
while (!(parts = res.next()).done) {
resultList.push(parts);
}
return resultList;
},
getTag: (parts: IteratorResult<RegExpMatchArray, any>): string => {
return parts.value[1];
},
isTag: (parts: IteratorResult<RegExpMatchArray, any>): boolean => {
return parts.value[1]?.startsWith("#")
},
};
export const REGEX_LINK = {
//![[link|alias]] [alias](link){num}
// 1 2 3 4 5 67 8 9
EXPR: /(!)?(\[\[([^|\]]+)\|?([^\]]+)?]]|\[([^\]]*)]\(([^)]*)\))(\{(\d+)\})?/g, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187
//EXPR: /(!)?(\[\[([^|\]]+)\|?([^\]]+)?]]|\[([^\]]*)]\(([^)]*)\))(\{(\d+)\})?/g, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187
// 1 2 3 4 5 67 8 9
EXPR: /(!)?(\[\[([^|\]]+)\|?([^\]]+)?]]|\[([^\]]*)]\(((?:[^\(\)]|\([^\(\)]*\))*)\))(\{(\d+)\})?/g, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1963
getResList: (text: string): IteratorResult<RegExpMatchArray, any>[] => {
const res = text.matchAll(REGEX_LINK.EXPR);
let parts: IteratorResult<RegExpMatchArray, any>;
@@ -198,15 +223,28 @@ export function getJSON(data: string): { scene: string; pos: number } {
return { scene: data, pos: parts.value ? parts.value.index : 0 };
}
export function getMarkdownDrawingSection(
export async function getMarkdownDrawingSectionAsync (
jsonString: string,
compressed: boolean,
) {
return compressed
const result = compressed
? `## Drawing\n\x60\x60\x60compressed-json\n${await compressAsync(
jsonString,
)}\n\x60\x60\x60\n%%`
: `## Drawing\n\x60\x60\x60json\n${jsonString}\n\x60\x60\x60\n%%`;
return result;
}
export function getMarkdownDrawingSection(
jsonString: string,
compressed: boolean,
): string {
const result = compressed
? `## Drawing\n\x60\x60\x60compressed-json\n${compress(
jsonString,
)}\n\x60\x60\x60\n%%`
: `## Drawing\n\x60\x60\x60json\n${jsonString}\n\x60\x60\x60\n%%`;
return result;
}
/**
@@ -240,26 +278,26 @@ const wrap = (text: string, lineLen: number) =>
lineLen ? wrapTextAtCharLength(text, lineLen, false, 0) : text;
//WITHSECTION refers to back of the card note (see this.inputEl.onkeyup in SelectCard.ts)
const RE_EXCALIDRAWDATA_WITHSECTION_OK = new RegExp(`^#\n%%\n# Excalidraw Data(?:\n|$)`, "m");
const RE_EXCALIDRAWDATA_WITHSECTION_NOTOK = new RegExp(`#\n%%\n# Excalidraw Data(?:\n|$)`, "m");
const RE_EXCALIDRAWDATA_NOSECTION_OK = new RegExp(`^(%%\n)?# Excalidraw Data(?:\n|$)`, "m");
const RE_EXCALIDRAWDATA_WITHSECTION_OK = /^(#\n+)%%\n+# Excalidraw Data(?:\n|$)/m;
const RE_EXCALIDRAWDATA_WITHSECTION_NOTOK = /#\n+%%\n+# Excalidraw Data(?:\n|$)/m;
const RE_EXCALIDRAWDATA_NOSECTION_OK = /^(%%\n+)?# Excalidraw Data(?:\n|$)/m;
//WITHSECTION refers to back of the card note (see this.inputEl.onkeyup in SelectCard.ts)
const RE_TEXTELEMENTS_WITHSECTION_OK = new RegExp(`^#\n%%\n##? Text Elements(?:\n|$)`, "m");
const RE_TEXTELEMENTS_WITHSECTION_NOTOK = new RegExp(`#\n%%\n##? Text Elements(?:\n|$)`, "m");
const RE_TEXTELEMENTS_NOSECTION_OK = new RegExp(`^(%%\n)?##? Text Elements(?:\n|$)`, "m");
const RE_TEXTELEMENTS_WITHSECTION_OK = /^#\n+%%\n+##? Text Elements(?:\n|$)/m;
const RE_TEXTELEMENTS_WITHSECTION_NOTOK = /#\n+%%\n+##? Text Elements(?:\n|$)/m;
const RE_TEXTELEMENTS_NOSECTION_OK = /^(%%\n+)?##? Text Elements(?:\n|$)/m;
//The issue is that when editing in markdown embeds the user can delete the last enter causing two sections
//to collide. This is particularly problematic when the user is editing the last section before # Text Elements
const RE_EXCALIDRAWDATA_FALLBACK_1 = new RegExp(`(.*)%%\n# Excalidraw Data(?:\n|$)`, "m");
const RE_EXCALIDRAWDATA_FALLBACK_2 = new RegExp(`(.*)# Excalidraw Data(?:\n|$)`, "m");
const RE_EXCALIDRAWDATA_FALLBACK_1 = /(.*)%%\n+# Excalidraw Data(?:\n|$)/m;
const RE_EXCALIDRAWDATA_FALLBACK_2 = /(.*)# Excalidraw Data(?:\n|$)/m;
const RE_TEXTELEMENTS_FALLBACK_1 = new RegExp(`(.*)%%\n##? Text Elements(?:\n|$)`, "m");
const RE_TEXTELEMENTS_FALLBACK_2 = new RegExp(`(.*)##? Text Elements(?:\n|$)`, "m");
const RE_TEXTELEMENTS_FALLBACK_1 = /(.*)%%\n+##? Text Elements(?:\n|$)/m;
const RE_TEXTELEMENTS_FALLBACK_2 = /(.*)##? Text Elements(?:\n|$)/m;
const RE_DRAWING = new RegExp(`^(%%\n)?##? Drawing\n`, "m");
const RE_DRAWING = /^(%%\n+)?##? Drawing\n/m;
export const getExcalidrawMarkdownHeaderSection = (data:string, keys?:[string,string][]):string => {
//The base case scenario is at the top, continued with fallbacks in order of likelihood and file structure
@@ -278,10 +316,11 @@ export const getExcalidrawMarkdownHeaderSection = (data:string, keys?:[string,st
data = data.substring(0, drawingTrimLocation);
}
let trimLocation = data.search(RE_EXCALIDRAWDATA_WITHSECTION_OK);
const m1 = data.match(RE_EXCALIDRAWDATA_WITHSECTION_OK);
let trimLocation = m1?.index ?? -1; //data.search(RE_EXCALIDRAWDATA_WITHSECTION_OK);
let shouldFixTrailingHashtag = false;
if(trimLocation > 0) {
trimLocation += 2; //accounts for the "#\n" which I want to leave there untouched
trimLocation += m1[1].length; //accounts for the "(#\n\s*)" which I want to leave there untouched
}
/* Expected markdown structure (this happens when the user deletes the last empty line of the last back-of-the-card note):
@@ -416,7 +455,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;
@@ -428,6 +466,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
@@ -438,12 +477,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
*/
@@ -527,7 +588,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")) {
@@ -698,6 +759,24 @@ export class ExcalidrawData {
this.scene.appState.theme = isObsidianThemeDark() ? "dark" : "light";
}
//girdSize, gridStep, previousGridSize, gridModeEnabled migration
if(this.scene.appState.hasOwnProperty("previousGridSize")) { //if previousGridSize was present this is legacy data
if(this.scene.appState.gridSize === null) {
this.scene.appState.gridSize = this.scene.appState.previousGridSize;
this.scene.appState.gridModeEnabled = false;
} else {
this.scene.appState.gridModeEnabled = true;
}
delete this.scene.appState.previousGridSize;
}
if(this.scene.appState?.gridColor?.hasOwnProperty("MajorGridFrequency")) { //if this is present, this is legacy data
if(this.scene.appState.gridColor.MajorGridFrequency>1) {
this.scene.gridStep = this.scene.appState.gridColor.MajorGridFrequency;
}
delete this.scene.appState.gridColor.MajorGridFrequency;
}
//once off migration of legacy scenes
if(this.scene?.elements?.some((el:any)=>el.type==="iframe" && !el.customData)) {
const prompt = new ConfirmationPrompt(
@@ -766,8 +845,8 @@ export class ExcalidrawData {
return true; //Text Elements header does not exist
}
data = data.slice(position);
const normalMatch = data.match(/^((%%\n)?# Excalidraw Data\n## Text Elements(?:\n|$))/m)
??data.match(/^((%%\n)?##? Text Elements(?:\n|$))/m);
const normalMatch = data.match(/^((%%\n*)?# Excalidraw Data\n## Text Elements(?:\n|$))/m)
?? data.match(/^((%%\n*)?##? Text Elements(?:\n|$))/m);
const textElementsMatch = normalMatch
? normalMatch[0]
@@ -788,7 +867,7 @@ export class ExcalidrawData {
? data.substring(indexOfNewElementLinks + lengthOfNewElementLinks)
: data.substring(indexOfOldElementLinks + lengthOfOldElementLinks);
//Load Embedded files
const RE_ELEMENT_LINKS = /^(.{8}):\s*(\[\[[^\]]*]])$/gm;
const RE_ELEMENT_LINKS = /^(.{8}):\s*(.*)$/gm;
const linksRes = elementLinksData.matchAll(RE_ELEMENT_LINKS);
while (!(parts = linksRes.next()).done) {
elementLinkMap.set(parts.value[1], parts.value[2]);
@@ -865,7 +944,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(
@@ -1021,7 +1100,7 @@ export class ExcalidrawData {
return (
el.type !== "text" &&
el.link &&
el.link.startsWith("[[") &&
//el.link.startsWith("[[") &&
!this.elementLinks.has(el.id)
);
});
@@ -1029,8 +1108,6 @@ export class ExcalidrawData {
return result;
}
let jsonString = JSON.stringify(this.scene);
let id: string; //will be used to hold the new 8 char long ID for textelements that don't yet appear under # Text Elements
for (const el of elements) {
@@ -1041,11 +1118,10 @@ export class ExcalidrawData {
if (el.id.length > 8) {
result = true;
id = nanoid();
jsonString = jsonString.replaceAll(el.id, id); //brute force approach to replace all occurrences (e.g. links, groups,etc.)
updateElementIdsInScene(this.scene, el, id);
}
this.elementLinks.set(id, el.link);
}
this.scene = JSON.parse(jsonString);
return result;
}
@@ -1057,9 +1133,7 @@ export class ExcalidrawData {
//console.log("Excalidraw.Data.findNewTextElementsInScene()");
//get scene text elements
this.selectedElementIds = selectedElementIds;
const texts = this.scene.elements?.filter((el: any) => el.type === "text");
let jsonString = JSON.stringify(this.scene);
const texts = this.scene.elements?.filter((el: any) => el.type === "text") as ExcalidrawTextElement[];
let dirty: boolean = false; //to keep track if the json has changed
let id: string; //will be used to hold the new 8 char long ID for textelements that don't yet appear under # Text Elements
@@ -1075,7 +1149,7 @@ export class ExcalidrawData {
delete this.selectedElementIds[te.id];
this.selectedElementIds[id] = true;
}
jsonString = jsonString.replaceAll(te.id, id); //brute force approach to replace all occurrences (e.g. links, groups,etc.)
updateElementIdsInScene(this.scene, te, id);
if (this.textElements.has(te.id)) {
//element was created with onBeforeTextSubmit
const text = this.textElements.get(te.id);
@@ -1097,11 +1171,6 @@ export class ExcalidrawData {
}
}
if (dirty) {
//reload scene json in case it has changed
this.scene = JSON.parse(jsonString);
}
return dirty;
}
@@ -1112,8 +1181,8 @@ export class ExcalidrawData {
(el: any) =>
el.type !== "text" &&
el.id === key &&
el.link &&
el.link.startsWith("[["),
el.link, //&&
//el.link.startsWith("[["),
);
if (el.length === 0) {
this.elementLinks.delete(key); //if no longer in the scene, delete the text element
@@ -1342,19 +1411,22 @@ export class ExcalidrawData {
* @returns markdown string
*/
disableCompression: boolean = false;
generateMD(deletedElements: ExcalidrawElement[] = []): string {
generateMDBase(deletedElements: ExcalidrawElement[] = []) {
let outString = this.textElementCommentedOut ? "%%\n" : "";
outString += `# Excalidraw Data\n## Text Elements\n`;
if (this.plugin.settings.addDummyTextElement) {
outString += `\n^_dummy!_\n\n`;
}
const textElementLinks = new Map<string, string>();
for (const key of this.textElements.keys()) {
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/566
const element = this.scene.elements.filter((el:any)=>el.id===key);
let elementString = this.textElements.get(key).raw;
if(element && element.length===1 && element[0].link && element[0].rawText === element[0].originalText) {
if(element[0].link.match(/^\[\[[^\]]*]]$/g)) { //apply this only to markdown links
//if(element[0].link.match(/^\[\[[^\]]*]]$/g)) { //apply this only to markdown links
textElementLinks.set(key, element[0].link);
//elementString = `%%***>>>text element-link:${element[0].link}<<<***%%` + elementString;
}
//}
}
outString += `${elementString} ^${key}\n\n`;
}
@@ -1406,14 +1478,33 @@ export class ExcalidrawData {
appState: this.scene.appState,
files: this.scene.files
}, null, "\t");
return (
return { outString, sceneJSONstring };
}
async generateMDAsync(deletedElements: ExcalidrawElement[] = []): Promise<string> {
const { outString, sceneJSONstring } = this.generateMDBase(deletedElements);
const result = (
outString +
(this.textElementCommentedOut ? "" : "%%\n") +
getMarkdownDrawingSection(
(await getMarkdownDrawingSectionAsync(
sceneJSONstring,
this.disableCompression ? false : this.plugin.settings.compress,
)
))
);
return result;
}
generateMDSync(deletedElements: ExcalidrawElement[] = []): string {
const { outString, sceneJSONstring } = this.generateMDBase(deletedElements);
const result = (
outString +
(this.textElementCommentedOut ? "" : "%%\n") +
(getMarkdownDrawingSection(
sceneJSONstring,
this.disableCompression ? false : this.plugin.settings.compress,
))
);
return result;
}
public async saveDataURLtoVault(dataURL: DataURL, mimeType: MimeType, key: FileId) {
@@ -1963,7 +2054,7 @@ export class ExcalidrawData {
}
public getEquationEntries() {
return this.equations.entries();
return this.equations?.entries();
}
public deleteEquation(fileId: FileId) {

View File

@@ -1,8 +1,9 @@
import { RestoredDataState } from "@zsviczian/excalidraw/types/excalidraw/data/restore";
import { ImportedDataState } from "@zsviczian/excalidraw/types/excalidraw/data/types";
import { BoundingBox } from "@zsviczian/excalidraw/types/excalidraw/element/bounds";
import { ElementsMap, ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameElement, 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 { ElementsMap, ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameElement, ExcalidrawFrameLikeElement, ExcalidrawTextContainer, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, NonDeletedExcalidrawElement, Theme } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { FontMetadata } from "@zsviczian/excalidraw/types/excalidraw/fonts/metadata";
import { AppState, BinaryFiles, DataURL, GenerateDiagramToCode, Point, Zoom } from "@zsviczian/excalidraw/types/excalidraw/types";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
type EmbeddedLink =
@@ -26,6 +27,7 @@ declare namespace ExcalidrawLib {
appState?: Partial<Omit<AppState, "offsetTop" | "offsetLeft">>;
files: BinaryFiles | null;
maxWidthOrHeight?: number;
exportingFrame?: ExcalidrawFrameLikeElement | null;
getDimensions?: (
width: number,
height: number,
@@ -46,6 +48,7 @@ declare namespace ExcalidrawLib {
exportPadding?: number;
exportingFrame: ExcalidrawFrameElement | null | undefined;
renderEmbeddables?: boolean;
skipInliningFonts?: boolean;
}): Promise<SVGSVGElement>;
function sceneCoordsToViewportCoords(
@@ -114,10 +117,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 +130,13 @@ declare namespace ExcalidrawLib {
fontFamily: FontFamilyValues;
}): FontString;
function getFontFamilyString ({
fontFamily,
}: {
fontFamily: number;
}): string;
function getBoundTextMaxWidth(container: ExcalidrawElement): number;
function exportToBlob(
@@ -155,4 +164,27 @@ 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;
var DiagramToCodePlugin: (props: {
generate: GenerateDiagramToCode;
}) => any;
function getDataURL(file: Blob | File): Promise<DataURL>;
function destroyObsidianUtils(): void;
function registerLocalFont(fontMetrics: FontMetadata, uri: string): void;
function getFontFamilies(): string[];
function registerFontsInCSS(): Promise<void>;
function getCSSFontDefinition(fontFamily: number): Promise<string>;
function getTextFromElements (
elements: readonly ExcalidrawElement[],
separator?: string,
): string;
function safelyParseJSON (json: string): Record<string, any> | null;
}

File diff suppressed because it is too large Load Diff

View File

@@ -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();

View File

@@ -1,4 +1,5 @@
import {
App,
MarkdownPostProcessorContext,
MetadataCache,
PaneType,
@@ -12,7 +13,6 @@ import { ExportSettings } from "./ExcalidrawView";
import ExcalidrawPlugin from "./main";
import {getIMGFilename,} from "./utils/FileUtils";
import {
embedFontsInSVG,
getEmbeddedFilenameParts,
getExportTheme,
getQuickImagePreview,
@@ -26,7 +26,7 @@ import { getParentOfClass, isObsidianThemeDark, getFileCSSClasses } from "./util
import { linkClickModifierType } from "./utils/ModifierkeyHelper";
import { ImageKey, imageCache } from "./utils/ImageCache";
import { FILENAMEPARTS, PreviewImageType } from "./utils/UtilTypes";
import { CustomMutationObserver, DEBUGGING } from "./utils/DebugHelper";
import { CustomMutationObserver, debug, DEBUGGING } from "./utils/DebugHelper";
import { getExcalidrawFileForwardLinks } from "./utils/ExcalidrawViewUtils";
import { linkPrompt } from "./dialogs/Prompt";
@@ -39,8 +39,11 @@ interface imgElementAttributes {
}
let plugin: ExcalidrawPlugin;
let app: App;
let vault: Vault;
let metadataCache: MetadataCache;
const DEBUGGING_MPP = false;
const getDefaultWidth = (plugin: ExcalidrawPlugin): string => {
const width = parseInt(plugin.settings.width);
@@ -61,8 +64,9 @@ const getDefaultHeight = (plugin: ExcalidrawPlugin): string => {
export const initializeMarkdownPostProcessor = (p: ExcalidrawPlugin) => {
plugin = p;
vault = p.app.vault;
metadataCache = p.app.metadataCache;
app = plugin.app;
vault = app.vault;
metadataCache = app.metadataCache;
};
const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader}:{
@@ -75,6 +79,7 @@ const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,ex
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLImageElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getPNG, `MarkdownPostProcessor.ts > _getPNG`);
const width = parseInt(imgAttributes.fwidth);
const scale = width >= 2400
? 5
@@ -86,7 +91,14 @@ const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,ex
? 2
: 1;
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.PNG, scale, isTransparent: !exportSettings.withBackground};
const cacheKey = {
...filenameParts,
isDark: theme==="dark",
previewImageType: PreviewImageType.PNG,
scale,
isTransparent: !exportSettings.withBackground,
inlineFonts: true, //though for PNG this makes no difference, but the key requires it
};
if(cacheReady) {
const src = await imageCache.getImageFromCache(cacheKey);
@@ -105,11 +117,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,
@@ -132,6 +146,7 @@ const setStyle = ({element,imgAttributes,onCanvas}:{
onCanvas: boolean,
}
) => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(setStyle, `MarkdownPostProcessor.ts > setStyle`);
let style = "";
if(imgAttributes.fwidth) {
style = `max-width:${imgAttributes.fwidth}${imgAttributes.fwidth.match(/\d$/) ? "px":""}; `; //width:100%;`; //removed !important https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/886
@@ -163,7 +178,17 @@ const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSetting
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLImageElement> => {
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.SVGIMG, scale:1, isTransparent: !exportSettings.withBackground};
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getSVGIMG, `MarkdownPostProcessor.ts > _getSVGIMG`);
exportSettings.skipInliningFonts = false;
const cacheKey = {
...filenameParts,
isDark: theme==="dark",
previewImageType: PreviewImageType.SVGIMG,
scale:1,
isTransparent: !exportSettings.withBackground,
inlineFonts: !exportSettings.skipInliningFonts,
};
if(cacheReady) {
const src = await imageCache.getImageFromCache(cacheKey);
if(src && typeof src === "string") {
@@ -182,13 +207,15 @@ const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSetting
}
}
let svg = convertSVGStringToElement((
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 +231,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");
@@ -220,20 +246,31 @@ const _getSVGNative = async ({filenameParts,theme,cacheReady,containerElement,fi
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLDivElement> => {
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.SVG, scale:1, isTransparent: !exportSettings.withBackground};
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getSVGNative, `MarkdownPostProcessor.ts > _getSVGNative`);
exportSettings.skipInliningFonts = false;
const cacheKey = {
...filenameParts,
isDark: theme==="dark",
previewImageType: PreviewImageType.SVG,
scale:1,
isTransparent: !exportSettings.withBackground,
inlineFonts: !exportSettings.skipInliningFonts,
};
let maybeSVG;
if(cacheReady) {
maybeSVG = await imageCache.getImageFromCache(cacheKey);
}
let svg = (maybeSVG && (maybeSVG instanceof SVGSVGElement))
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 +287,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;
}
@@ -269,6 +309,7 @@ const getIMG = async (
imgAttributes: imgElementAttributes,
onCanvas: boolean = false,
): Promise<HTMLImageElement | HTMLDivElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(getIMG, `MarkdownPostProcessor.ts > getIMG`, imgAttributes);
let file = imgAttributes.file;
if (!imgAttributes.file) {
const f = vault.getAbstractFileByPath(imgAttributes.fname?.split("#")[0]);
@@ -316,22 +357,23 @@ const getIMG = async (
case PreviewImageType.PNG: {
const img = createEl("img");
setStyle({element:img,imgAttributes,onCanvas});
return _getPNG({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader});
return await _getPNG({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader});
}
case PreviewImageType.SVGIMG: {
const img = createEl("img");
setStyle({element:img,imgAttributes,onCanvas});
return _getSVGIMG({filenameParts,theme,cacheReady,img,file,exportSettings,loader});
return await _getSVGIMG({filenameParts,theme,cacheReady,img,file,exportSettings,loader});
}
case PreviewImageType.SVG: {
const img = createEl("div");
setStyle({element:img,imgAttributes,onCanvas});
return _getSVGNative({filenameParts,theme,cacheReady,containerElement: img,file,exportSettings,loader});
return await _getSVGNative({filenameParts,theme,cacheReady,containerElement: img,file,exportSettings,loader});
}
}
};
const addSVGToImgSrc = (img: HTMLImageElement, svg: SVGSVGElement, cacheReady: boolean, cacheKey: ImageKey):HTMLImageElement => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(addSVGToImgSrc, `MarkdownPostProcessor.ts > addSVGToImgSrc`);
const svgString = new XMLSerializer().serializeToString(svg);
const blob = new Blob([svgString], { type: 'image/svg+xml' });
const blobUrl = URL.createObjectURL(blob);
@@ -344,6 +386,7 @@ const createImgElement = async (
attr: imgElementAttributes,
onCanvas: boolean = false,
) :Promise<HTMLElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(createImgElement, `MarkdownPostProcessor.ts > createImgElement`);
const imgOrDiv = await getIMG(attr,onCanvas);
if(!imgOrDiv) {
return null;
@@ -358,7 +401,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 +454,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 +463,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);
@@ -471,6 +514,7 @@ const createImageDiv = async (
attr: imgElementAttributes,
onCanvas: boolean = false
): Promise<HTMLDivElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(createImageDiv, `MarkdownPostProcessor.ts > createImageDiv`);
const img = await createImgElement(attr, onCanvas);
return createDiv(attr.style.join(" "), (el) => el.append(img));
};
@@ -479,6 +523,7 @@ const processReadingMode = async (
embeddedItems: NodeListOf<Element> | [HTMLElement],
ctx: MarkdownPostProcessorContext,
) => {
(process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(processReadingMode, `MarkdownPostProcessor.ts > processReadingMode`);
//We are processing a non-excalidraw file in reading mode
//Embedded files will be displayed in an .internal-embed container
@@ -510,6 +555,7 @@ const processReadingMode = async (
};
const processInternalEmbed = async (internalEmbedEl: Element, file: TFile ):Promise<HTMLDivElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(processInternalEmbed, `MarkdownPostProcessor.ts > processInternalEmbed`, internalEmbedEl);
const attr: imgElementAttributes = {
fname: "",
fheight: "",
@@ -546,6 +592,7 @@ const processAltText = (
alt:string,
attr: imgElementAttributes
) => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(processAltText, `MarkdownPostProcessor.ts > processAltText`);
if (alt && !alt.startsWith(fname)) {
//2:width, 3:height, 4:style 12 3 4
const parts = alt.match(/[^\|\d]*\|?((\d*%?)x?(\d*%?))?\|?(.*)/);
@@ -565,17 +612,22 @@ const processAltText = (
}
const isTextOnlyEmbed = (internalEmbedEl: Element):boolean => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(isTextOnlyEmbed, `MarkdownPostProcessor.ts > isTextOnlyEmbed`);
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)
}
const tmpObsidianWYSIWYG = async (
el: HTMLElement,
ctx: MarkdownPostProcessorContext,
isPrinting: boolean,
isMarkdownReadingMode: boolean,
isHoverPopover: boolean,
) => {
(process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(tmpObsidianWYSIWYG, `MarkdownPostProcessor.ts > tmpObsidianWYSIWYG`);
const file = app.vault.getAbstractFileByPath(ctx.sourcePath);
if(!(file instanceof TFile)) return;
if(!plugin.isExcalidrawFile(file)) return;
@@ -593,11 +645,11 @@ const tmpObsidianWYSIWYG = async (
//@ts-ignore
const containerEl = ctx.containerEl;
if(!plugin.settings.renderImageInMarkdownReadingMode && containerEl.parentElement?.parentElement?.hasClass("markdown-reading-view")) {
if(!plugin.settings.renderImageInMarkdownReadingMode && isMarkdownReadingMode) { // containerEl.parentElement?.parentElement?.hasClass("markdown-reading-view")) {
return;
}
if(!plugin.settings.renderImageInMarkdownToPDF && containerEl.parentElement?.hasClass("print")) {
if(!plugin.settings.renderImageInMarkdownToPDF && isPrinting) { //containerEl.parentElement?.hasClass("print")) {
return;
}
@@ -614,7 +666,7 @@ const tmpObsidianWYSIWYG = async (
) {
internalEmbedDiv = internalEmbedDiv.parentElement;
}
if(
internalEmbedDiv.hasClass("dataview") ||
internalEmbedDiv.hasClass("cm-preview-code-block") ||
@@ -623,7 +675,16 @@ const tmpObsidianWYSIWYG = async (
return; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/835
}
const isPrinting = Boolean(internalEmbedDiv.hasClass("print"));
if(!plugin.settings.renderImageInHoverPreviewForMDNotes) {
//const isHoverPopover = internalEmbedDiv.parentElement?.hasClass("hover-popover");
const shouldOpenMD = Boolean(ctx.frontmatter?.["excalidraw-open-md"]);
if(isHoverPopover && shouldOpenMD) {
return;
}
}
//const isPrinting = Boolean(internalEmbedDiv.hasClass("print"));
const attr: imgElementAttributes = {
fname: ctx.sourcePath,
@@ -635,7 +696,7 @@ const tmpObsidianWYSIWYG = async (
attr.file = file;
const markdownEmbed = internalEmbedDiv.hasClass("markdown-embed");
const markdownReadingView = internalEmbedDiv.hasClass("markdown-reading-view") || isPrinting;
const markdownReadingView = isPrinting || isMarkdownReadingMode; //internalEmbedDiv.hasClass("markdown-reading-view")
if (!internalEmbedDiv.hasClass("internal-embed") && (markdownEmbed || markdownReadingView)) {
if(isPrinting) {
internalEmbedDiv = containerEl;
@@ -649,7 +710,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;
@@ -699,15 +760,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);
@@ -722,6 +783,7 @@ const tmpObsidianWYSIWYG = async (
});
};
const docIDs = new Set<string>();
/**
*
* @param el
@@ -731,12 +793,43 @@ export const markdownPostProcessor = async (
el: HTMLElement,
ctx: MarkdownPostProcessorContext,
) => {
const isPrinting = Boolean(document.body.querySelectorAll("body > .print").length>0);
//firstElementChild: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1956
const isFrontmatter = el.hasClass("mod-frontmatter") || el.firstElementChild?.hasClass("frontmatter");
if(isPrinting && isFrontmatter) {
return;
}
//@ts-ignore
const containerEl = ctx.containerEl;
(process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(markdownPostProcessor, `MarkdownPostProcessor.ts > markdownPostProcessor`, ctx, el);
//check to see if we are rendering in editing mode or live preview
//if yes, then there should be no .internal-embed containers
//if yes, then there should be no .internal-embed containers
const isMarkdownReadingMode = Boolean(containerEl && getParentOfClass(containerEl, "markdown-reading-view"));
const isHoverPopover = Boolean(containerEl && getParentOfClass(containerEl, "hover-popover"));
const isPreview = (isHoverPopover && Boolean(ctx?.frontmatter?.["excalidraw-open-md"]) && !plugin.settings.renderImageInHoverPreviewForMDNotes);
const embeddedItems = el.querySelectorAll(".internal-embed");
if (embeddedItems.length === 0) {
tmpObsidianWYSIWYG(el, ctx);
if(isPrinting && plugin.settings.renderImageInMarkdownToPDF) {
await tmpObsidianWYSIWYG(el, ctx, isPrinting, isMarkdownReadingMode, isHoverPopover);
return;
}
if (!isPreview && embeddedItems.length === 0) {
if(isFrontmatter) {
docIDs.add(ctx.docId);
} else {
if(docIDs.has(ctx.docId) && !el.hasChildNodes()) {
docIDs.delete(ctx.docId);
}
const isAreaGroupFrameRef = el.querySelectorAll('[data-heading^="Unable to find"]').length === 1;
if(!isAreaGroupFrameRef) {
return;
}
}
await tmpObsidianWYSIWYG(el, ctx, isPrinting, isMarkdownReadingMode, isHoverPopover);
return;
}
@@ -745,8 +838,7 @@ export const markdownPostProcessor = async (
//transcluded text element or some other transcluded content inside the Excalidraw file
//in reading mode these elements should be hidden
const excalidrawFile = Boolean(ctx.frontmatter?.hasOwnProperty("excalidraw-plugin"));
const isPrinting = Boolean(document.body.querySelectorAll("body > .print"));
if (excalidrawFile && !isPrinting) {
if (!(isPreview || isMarkdownReadingMode || isPrinting) && excalidrawFile) {
el.style.display = "none";
return;
}

View File

@@ -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

View File

@@ -0,0 +1,3 @@
export const TAG_PDFEXPORT = "PDFExport";
export const TAG_MDREADINGMODE = "MDReadingMode";
export const TAG_AUTOEXPORT = "Autoexport";

View File

@@ -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";
@@ -9,6 +9,11 @@ export let EXCALIDRAW_PLUGIN: ExcalidrawPlugin = null;
export const setExcalidrawPlugin = (plugin: ExcalidrawPlugin) => {
EXCALIDRAW_PLUGIN = plugin;
};
export const THEME = {
LIGHT: "light",
DARK: "dark",
} as const;
const MD_EXCALIDRAW = "# Excalidraw Data";
const MD_TEXTELEMENTS = "## Text Elements";
const MD_ELEMENTLINKS = "## Element Links";
@@ -85,7 +90,7 @@ export const {
getCommonBoundingBox,
getMaximumGroups,
measureText,
getDefaultLineHeight,
getLineHeight,
wrapText,
getFontString,
getBoundTextMaxWidth,
@@ -94,10 +99,14 @@ export const {
mutateElement,
restore,
mermaidToExcalidraw,
getFontFamilyString,
getContainerElement,
refreshTextDimensions,
getCSSFontDefinition,
} = excalidrawLib;
export const FONTS_STYLE_ID = "excalidraw-custom-fonts";
export function JSON_parse(x: string): any {
return JSON.parse(x.replaceAll("&#91;", "["));
}
@@ -112,7 +121,7 @@ export const DEVICE: DeviceType = {
isMacOS: document.body.hasClass("mod-macos") && ! document.body.hasClass("is-ios"),
isWindows: document.body.hasClass("mod-windows"),
isIOS: document.body.hasClass("is-ios"),
isAndroid: document.body.hasClass("is-android")
isAndroid: document.body.hasClass("is-android"),
};
export const ROOTELEMENTSIZE = (() => {

View File

@@ -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;
}
@@ -406,8 +416,14 @@ function RenderObsidianView(
}
} else if (leafRef.current?.node) {
//Handle canvas node
containerRef.current?.removeClasses(["is-editing", "is-focused"]);
view.canvasNodeFactory.stopEditing(leafRef.current.node);
if(view.plugin.settings.markdownNodeOneClickEditing && !containerRef.current?.hasClass("is-editing")) {
const newTheme = getTheme(view, themeRef.current);
containerRef.current?.addClasses(["is-editing", "is-focused"]);
view.canvasNodeFactory.startEditing(leafRef.current.node, newTheme);
} else {
containerRef.current?.removeClasses(["is-editing", "is-focused"]);
view.canvasNodeFactory.stopEditing(leafRef.current.node);
}
}
}, [
containerRef,
@@ -418,7 +434,8 @@ function RenderObsidianView(
element,
view,
isEditingRef,
view.canvasNodeFactory
view.canvasNodeFactory,
themeRef.current
]);
return null;
@@ -426,8 +443,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 (

View File

@@ -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>;
@@ -174,16 +177,24 @@ export class EmbeddableSettings extends Modal {
const fnparts = splitFolderAndFilename(newPathWithExt);
const newPath = getNewUniqueFilepath(
this.app.vault,
fnparts.folderpath,
fnparts.filename,
fnparts.folderpath,
);
await this.app.vault.rename(this.file,newPath);
el.link = this.element.link.replace(
/(\[\[)([^#\]]*)([^\]]*]])/,`$1${
this.plugin.app.metadataCache.fileToLinktext(
this.file,this.view.file.path,true)
}$3`);
dirty = true;
if(this.app.vault.getAbstractFileByPath(newPath)) {
new Notice("File rename failed. A file with this name already exists.\n"+newPath,10000);
} else {
try {
await this.app.fileManager.renameFile(this.file,newPath);
el.link = this.element.link.replace(
/(\[\[)([^#\]]*)([^\]]*]])/,`$1${
this.plugin.app.metadataCache.fileToLinktext(
this.file,this.view.file.path,true)
}$3`);
dirty = true;
} catch(e) {
new Notice("File rename failed. "+e,10000);
}
}
}
}
if(this.isYouTube && this.youtubeStart !== getYouTubeStartAt(this.element.link)) {
@@ -215,11 +226,12 @@ export class EmbeddableSettings extends Modal {
(async() => {
await this.ea.addElementsToView();
//@ts-ignore
this.ea.viewUpdateScene({appState: {}});
this.ea.viewUpdateScene({appState: {}, storeAction: "update"});
this.close(); //close should only run once update scene is done
})();
} else {
this.close();
}
this.close();
};
}

View File

@@ -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`);

View File

@@ -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) {

View File

@@ -0,0 +1,83 @@
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
},
storeAction: "update",
});
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" : "";
}

162
src/dialogs/HotkeyEditor.ts Normal file
View File

@@ -0,0 +1,162 @@
import { BaseComponent, Setting, Modifier } from 'obsidian';
import { DEVICE } from 'src/constants/constants';
import { t } from 'src/lang/helpers';
import { ExcalidrawSettings } from 'src/settings';
import { modifierLabel } from 'src/utils/ModifierkeyHelper';
import { fragWithHTML } from 'src/utils/Utils';
export class HotkeyEditor extends BaseComponent {
private settings: ExcalidrawSettings;
private containerEl: HTMLElement;
private capturing: boolean = false;
private activeModifiers: Modifier[] = [];
public isDirty: boolean = false;
private applySettingsUpdate: Function;
// Store bound event handlers
private boundKeydownHandler: (event: KeyboardEvent) => void;
private boundKeyupHandler: (event: KeyboardEvent) => void;
constructor(containerEl: HTMLElement, settings: ExcalidrawSettings, applySettingsUpdate: Function) {
super();
this.containerEl = containerEl.createDiv();
this.settings = settings;
this.applySettingsUpdate = applySettingsUpdate;
// Bind the event handlers once in the constructor
this.boundKeydownHandler = this.onKeydown.bind(this);
this.boundKeyupHandler = this.onKeyup.bind(this);
}
onload(): void {
this.render();
}
private render(): void {
// Clear previous content
this.containerEl.empty();
// Render current overrides
this.settings.modifierKeyOverrides.forEach((override, index) => {
const key = override.key.toUpperCase();
new Setting(this.containerEl)
.setDesc(fragWithHTML(`<b>Code:</b> <kbd>${override.modifiers.join("+")} + ${key}</kbd> | ` +
`<b>Apple:</b> <kbd>${modifierLabel(override.modifiers, "Mac")} + ${key}</kbd> | ` +
`<b>Windows:</b> <kbd>${modifierLabel(override.modifiers, "Other")} + ${key}</kbd>`))
.addButton((button) =>
button
.setButtonText(t("HOTKEY_BUTTON_REMOVE"))
.setCta()
.onClick(() => {
this.settings.modifierKeyOverrides.splice(index, 1);
this.isDirty = true;
this.applySettingsUpdate();
this.render();
})
);
});
// Render Add New Override or Capture Instruction
if (this.capturing) {
new Setting(this.containerEl)
.setName(t("HOTKEY_PRESS_COMBO_NANE"))
.setDesc(t("HOTKEY_PRESS_COMBO_DESC"))
.controlEl.style.cursor = 'pointer';
} else {
new Setting(this.containerEl)
.addButton((button) =>
button
.setButtonText(t("HOTKEY_BUTTON_ADD_OVERRIDE"))
.setCta()
.onClick(() => this.startCapture())
);
}
}
private startCapture(): void {
this.capturing = true;
this.activeModifiers = [];
this.render();
// Use the pre-bound handlers
window.addEventListener('keydown', this.boundKeydownHandler);
window.addEventListener('keyup', this.boundKeyupHandler);
}
private onKeydown(event: KeyboardEvent): void {
event.preventDefault();
event.stopPropagation();
const modifiers = this.getModifiersFromEvent(event);
// If only modifiers are pressed, update activeModifiers and continue listening
if (['Control', 'Shift', 'Alt', 'Meta'].includes(event.key)) {
this.activeModifiers = modifiers;
return;
}
const key = event.key.length === 1 ? event.key.toLowerCase() : event.key;
// Check for duplicate overrides
const exists = this.settings.modifierKeyOverrides.some(
(override) =>
override.key === key &&
override.modifiers.length === modifiers.length &&
override.modifiers.every((mod) => modifiers.includes(mod))
);
if (!exists) {
this.settings.modifierKeyOverrides.push({ modifiers, key });
this.isDirty = true;
this.applySettingsUpdate();
}
this.stopCapture();
}
private onKeyup(event: KeyboardEvent): void {
// If all modifier keys are released, stop capturing
if (!event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey) {
this.stopCapture();
}
}
private stopCapture(): void {
this.capturing = false;
// Use the pre-bound handlers for removal
window.removeEventListener('keydown', this.boundKeydownHandler);
window.removeEventListener('keyup', this.boundKeyupHandler);
this.render();
}
public unload(): void {
// Ensure listeners are removed when the component is unloaded
this.stopCapture();
}
private getModifiersFromEvent(event: KeyboardEvent): Modifier[] {
const modifiers: Modifier[] = [];
if (DEVICE.isMacOS && event.metaKey) {
modifiers.push('Mod');
} else if (!DEVICE.isMacOS && event.ctrlKey) {
modifiers.push('Mod');
}
if (DEVICE.isMacOS && event.ctrlKey) {
modifiers.push('Ctrl');
}
if (!DEVICE.isMacOS && event.metaKey) {
modifiers.push('Meta');
}
if (event.shiftKey) {
modifiers.push('Shift');
}
if (event.altKey) {
modifiers.push('Alt');
}
return modifiers;
}
}

View File

@@ -7,10 +7,15 @@ import { getEA } from "src";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
export class ImportSVGDialog 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;
@@ -38,14 +43,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) {

View File

@@ -3,9 +3,13 @@ import { REG_LINKINDEX_INVALIDCHARS } from "../constants/constants";
import { t } from "../lang/helpers";
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 +36,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();
}
}

View File

@@ -4,12 +4,27 @@ 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 +70,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();
})();
}

View File

@@ -5,10 +5,15 @@ import ExcalidrawPlugin from "src/main";
import { getLink } from "src/utils/FileUtils";
export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
public app: App;
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 +56,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;

View File

@@ -2,12 +2,18 @@ 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 +40,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 +52,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();
}
}

View File

@@ -1,4 +1,4 @@
import { ButtonComponent, TFile } from "obsidian";
import { ButtonComponent, TFile, ToggleComponent } from "obsidian";
import ExcalidrawView from "../ExcalidrawView";
import ExcalidrawPlugin from "../main";
import { getPDFDoc } from "src/utils/FileUtils";
@@ -7,9 +7,11 @@ import { FileSuggestionModal } from "./FolderSuggester";
import { getEA } from "src";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
import { t } from "src/lang/helpers";
export class InsertPDFModal extends Modal {
private borderBox: boolean = true;
private frame: boolean = false;
private gapSize:number = 20;
private groupPages: boolean = false;
private direction: "down" | "right" = "right";
@@ -28,7 +30,7 @@ export class InsertPDFModal extends Modal {
private plugin: ExcalidrawPlugin,
private view: ExcalidrawView,
) {
super(app);
super(plugin.app);
}
open (file?: TFile) {
@@ -48,18 +50,24 @@ export class InsertPDFModal extends Modal {
if(this.dirty) {
this.plugin.settings.pdfImportScale = this.importScale;
this.plugin.settings.pdfBorderBox = this.borderBox;
this.plugin.settings.pdfFrame = this.frame;
this.plugin.settings.pdfGapSize = this.gapSize;
this.plugin.settings.pdfGroupPages = this.groupPages;
this.plugin.settings.pdfNumColumns = this.numColumns;
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) {
@@ -115,6 +123,7 @@ export class InsertPDFModal extends Modal {
async createForm() {
await this.plugin.loadSettings();
this.borderBox = this.plugin.settings.pdfBorderBox;
this.frame = this.plugin.settings.pdfFrame;
this.gapSize = this.plugin.settings.pdfGapSize;
this.groupPages = this.plugin.settings.pdfGroupPages;
this.numColumns = this.plugin.settings.pdfNumColumns;
@@ -133,13 +142,13 @@ export class InsertPDFModal extends Modal {
const importButtonMessages = () => {
if(!this.pdfDoc) {
importMessage.innerText = "Please select a PDF file";
importMessage.innerText = t("IPM_SELECT_PDF");
importButton.buttonEl.style.display="none";
return;
}
if(this.pagesToImport.length === 0) {
importButton.buttonEl.style.display="none";
importMessage.innerText = "Please select pages to import";
importMessage.innerText = t("IPM_SELECT_PAGES_TO_IMPORT");
return
}
if(Math.max(...this.pagesToImport) <= this.pdfDoc.numPages) {
@@ -156,7 +165,7 @@ export class InsertPDFModal extends Modal {
const numPagesMessages = () => {
if(numPages === 0) {
numPagesMessage.innerText = "Please select a PDF file";
numPagesMessage.innerText = t("IPM_SELECT_PDF");
return;
}
numPagesMessage.innerHTML = `There are <b>${numPages}</b> pages in the selected document.`;
@@ -206,7 +215,7 @@ export class InsertPDFModal extends Modal {
numPagesMessage = ce.createEl("p", {text: ""});
numPagesMessages();
new Setting(ce)
.setName("Pages to import")
.setName(t("IPM_PAGES_TO_IMPORT_NAME"))
.setDesc("e.g.: 1,3-5,7,9-10")
.addText(text => {
pageRangesTextComponent = text;
@@ -217,18 +226,52 @@ export class InsertPDFModal extends Modal {
})
importPagesMessage = ce.createEl("p", {text: ""});
new Setting(ce)
.setName("Add border box")
.addToggle(toggle => toggle
.setValue(this.borderBox)
.onChange((value) => {
this.borderBox = value;
this.dirty = true;
}))
let bbToggle: ToggleComponent;
let fToggle: ToggleComponent;
let laiToggle: ToggleComponent;
this.frame = this.borderBox ? false : this.frame;
new Setting(ce)
.setName("Group pages")
.setDesc("This will group all pages into a single group. This is recommended if you are locking the pages after import, because the group will be easier to unlock later rather than unlocking one by one.")
.setName(t("IPM_ADD_BORDER_BOX_NAME"))
.addToggle(toggle => {
bbToggle = toggle;
toggle
.setValue(this.borderBox)
.onChange((value) => {
this.borderBox = value;
if(value) {
this.frame = false;
fToggle.setValue(false);
}
this.dirty = true;
})
})
new Setting(ce)
.setName(t("IPM_ADD_FRAME_NAME"))
.setDesc(t("IPM_ADD_FRAME_DESC"))
.addToggle(toggle => {
fToggle = toggle;
toggle
.setValue(this.frame)
.onChange((value) => {
this.frame = value;
if(value) {
this.borderBox = false;
bbToggle.setValue(false);
if(!this.lockAfterImport) {
this.lockAfterImport = true;
laiToggle.setValue(true);
}
}
this.dirty = true;
})
})
new Setting(ce)
.setName(t("IPM_GROUP_PAGES_NAME"))
.setDesc(t("IPM_GROUP_PAGES_DESC"))
.addToggle(toggle => toggle
.setValue(this.groupPages)
.onChange((value) => {
@@ -239,12 +282,15 @@ export class InsertPDFModal extends Modal {
new Setting(ce)
.setName("Lock pages on canvas after import")
.addToggle(toggle => toggle
.setValue(this.lockAfterImport)
.onChange((value) => {
this.lockAfterImport = value
this.dirty = true;
}))
.addToggle(toggle => {
laiToggle = toggle;
toggle
.setValue(this.lockAfterImport)
.onChange((value) => {
this.lockAfterImport = value
this.dirty = true;
})
})
let numColumnsSetting: Setting;
let numRowsSetting: Setting;
@@ -386,6 +432,12 @@ export class InsertPDFModal extends Modal {
if(this.lockAfterImport) imgEl.locked = true;
ea.addToGroup([boxID,imageID]);
if(this.frame) {
const frameID = ea.addFrame(topX, topY,imgWidth,imgHeight,`${page}`);
ea.addElementsToFrame(frameID, [boxID,imageID]);
ea.getElement(frameID).link = this.pdfFile.path + `#page=${page}`;
}
switch(this.direction) {
case "right":
@@ -399,7 +451,9 @@ export class InsertPDFModal extends Modal {
}
}
if(this.groupPages) {
const ids = ea.getElements().map(el => el.id);
const ids = ea.getElements()
.filter(el=>!this.frame || (el.type === "frame"))
.map(el => el.id);
ea.addToGroup(ids);
}
await ea.addElementsToView(true,true,false);
@@ -408,6 +462,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;

View File

@@ -17,6 +17,179 @@ 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.3.0": `
I am moving to a new release approach aiming to publish one update per month to the Obsidian script store. If you want to continue to receive more frequent updates with new features and minor bug fixes, then join the beta testing team. [#1912](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1912)
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/2poSS-Z91lY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## New
- Elbow connectors: https://x.com/excalidraw/status/1819084086222393554
## Fixed
- Convert Markdown to Excalidraw did not work correctly when there was ${String.fromCharCode(96)}---${String.fromCharCode(96)} anywhere in the file, but no frontmatter (e.g. a table)
- Fixed Obsidian move tab to new window
- Fixed duplicating bound arrows without its bound elements throwing error [#8315](https://github.com/excalidraw/excalidraw/issues/8315)
`,
"2.2.13": `
## Fixed
- Could not undo element after pasting [#1906](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1906)
- Links broke after renaming an Excalidraw file using the F2 shortcut [#1907](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1907)
- Unable to open or convert very large ${String.fromCharCode(96)}.excalidraw${String.fromCharCode(96)} file, e.g. BoaPs you can download from [here](https://ko-fi.com/zsolt/shop)
`,
"2.2.12": `
## Fixed
- Rename moved files to root folder [#1905](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1905)
- Fonts not displaying correctly in cached image previews
`,
"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">
<img src="https://private-user-images.githubusercontent.com/14358394/335857018-c4f5c4c7-9b8f-427f-aa6f-8c1b189610af.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTczMjQxNDksIm5iZiI6MTcxNzMyMzg0OSwicGF0aCI6Ii8xNDM1ODM5NC8zMzU4NTcwMTgtYzRmNWM0YzctOWI4Zi00MjdmLWFhNmYtOGMxYjE4OTYxMGFmLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA2MDIlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwNjAyVDEwMjQwOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdhZGUwNDRkZmM2NmJjNTNiYjUwNjMxMmU2MGEyZTQwZGQwNmUyZmI5ZDFhNzMwMzg2OThjMjhmZmJkNzNiZDkmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.wZfkXmBRcXwz0pN6q0EEvmwtxVAB9ymPk9a9upmGXYE" referrerpolicy="no-referrer" style="width: 150px; margin: 0 auto;">
</a>
</div>
## New from Excalidraw.com
- You can now set the text width even during creation. Simply drag with the text tool. Note, there's a minimum distance before the text enters the wrapped mode so there aren't false positives. [See example here](https://x.com/excalidraw/status/1795468201335075000)
## New
- Updated zh-cn translation. Thank you @dmscode
- New context menu and command palette action: "Move back-of-note card to File". This is only active when an eligible embeddable element is selected.
## Fixed
- Setting different autosaveIntervals on Desktop and Mobile will no longer trigger unnecessary commits to settings each time you open Excalidraw on a different device. Thanks @jmhammond for the contribution! [#1805](https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/1805), [#1652](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1652), [#888](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/888)
## Fixed in ExcalidrawAutomate
- ${String.fromCharCode(96)}getCM(color)${String.fromCharCode(96)} was missing from ${String.fromCharCode(96)}ea.help()${String.fromCharCode(96)}. It is now added. getCM returns a ColorMaster object. ColorMaster is a powerful library should you want to create scripts to manipulate colors. Check out my [Scripting Colors](https://youtu.be/7gJDwNgQ6NU) video should you want to learn more. [#1806](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1806)
`,
"2.2.3":`
## Fixed
- Undo history was not properly initialized [#1791](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1785)
- Excalidraw did not save edits when switching to markdown view mode with a hotkey or terminating the popout window
- SVG export did not maintain the aspect ratio of manually distorted images [#1780](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1780)
## New
- In pen mode, double tapping the screen will toggle the eraser tool when using freedraw tool, or one of the other tools in locked mode.
- New setting under "Excalidraw appearance and behavior" to disable rendering of Excalidraw drawings in hover previews, in case the file has the ${String.fromCharCode(96)}excalidraw-open-md: true${String.fromCharCode(96)} frontmatter property [#1795](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1795)
- Additional foolproofing of ${String.fromCharCode(96)}# Excalidraw Data${String.fromCharCode(96)}. The file is now more resilient to automated linting and other changes. There is also a new setting under "Compatibility Features" to add a dummy first text element to ${String.fromCharCode(96)}## Text Elements${String.fromCharCode(96)}. You can use this feature if your auto-linter adds empty lines after section headings.
- Pasting markdown code blocks will create a back-of-the-note card with the code block. CTRL+SHIFT+V will paste the text as a normal text element. When copying code from Chat GPT the markdown code fence (triple backtick) is missing. In this case, you may use the new context menu action "Paste code block" to create a back of the note card with the code block.
- Pasting long text will be wrapped in the text element.
## New in ExcalidrawAutomate
- Updated viewUpdateScene: This now implements the [new Excalidraw API](https://github.com/excalidraw/excalidraw/pull/7898)
${String.fromCharCode(96, 96, 96)}ts
viewUpdateScene (
scene: {
elements?: ExcalidrawElement[],
appState?: AppState,
files?: BinaryFileData,
commitToHistory?: boolean,
storeAction?: "capture" | "none" | "update",
},
restore: boolean = false,
):void ;
${String.fromCharCode(96, 96, 96)}
- Updated addText. The function now supports the new text-wrapping feature
${String.fromCharCode(96, 96, 96)}ts
addText(
topX: number,
topY: number,
text: string,
formatting?: {
autoResize?: boolean; //Default is true. Setting this to false will wrap the text in the text element without the need for the container. If set to false, you must set a width value as well.
wrapAt?: number; //wrapAt is ignored if autoResize is set to false (and width is provided)
width?: number;
height?: number;
textAlign?: "left" | "center" | "right";
box?: boolean | "box" | "blob" | "ellipse" | "diamond";
boxPadding?: number;
boxStrokeColor?: string;
textVerticalAlign?: "top" | "middle" | "bottom";
},
id?: string,
): string
${String.fromCharCode(96, 96, 96)}
`,
"2.2.2":`
## Fixed
- ExcaliBrain stopped working with 2.2.0

View File

@@ -9,11 +9,17 @@ export enum openDialogAction {
}
export class OpenFileDialog extends FuzzySuggestModal<TFile> {
public app: App;
private plugin: ExcalidrawPlugin;
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;

View File

@@ -20,9 +20,9 @@ import { t } from "src/lang/helpers";
import { ExcalidrawElement, getEA } from "src";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import { MAX_IMAGE_SIZE, REG_LINKINDEX_INVALIDCHARS } from "src/constants/constants";
import { REGEX_LINK } from "src/ExcalidrawData";
import { REGEX_LINK, REGEX_TAGS } 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 };
@@ -211,15 +211,15 @@ export class GenericInputPrompt extends Modal {
}, 30);
}
textComponent.inputEl.addEventListener("keydown", this.keyDownCallback);
textComponent.inputEl.addEventListener('keyup', checkcaret); // Every character written
textComponent.inputEl.addEventListener('pointerup', checkcaret); // Click down
textComponent.inputEl.addEventListener('touchend', checkcaret); // Click down
textComponent.inputEl.addEventListener('input', checkcaret); // Other input events
textComponent.inputEl.addEventListener('paste', checkcaret); // Clipboard actions
textComponent.inputEl.addEventListener('cut', checkcaret);
textComponent.inputEl.addEventListener('select', checkcaret); // Some browsers support this event
textComponent.inputEl.addEventListener('selectionchange', checkcaret);// Some browsers support this event
textComponent.inputEl.addEventListener("keydown", this.keyDownCallback.bind(this));
textComponent.inputEl.addEventListener('keyup', checkcaret.bind(this)); // Every character written
textComponent.inputEl.addEventListener('pointerup', checkcaret.bind(this)); // Click down
textComponent.inputEl.addEventListener('touchend', checkcaret.bind(this)); // Click down
textComponent.inputEl.addEventListener('input', checkcaret.bind(this)); // Other input events
textComponent.inputEl.addEventListener('paste', checkcaret.bind(this)); // Clipboard actions
textComponent.inputEl.addEventListener('cut', checkcaret.bind(this));
textComponent.inputEl.addEventListener('select', checkcaret.bind(this)); // Some browsers support this event
textComponent.inputEl.addEventListener('selectionchange', checkcaret.bind(this));// Some browsers support this event
return textComponent;
}
@@ -272,18 +272,18 @@ export class GenericInputPrompt extends Modal {
this.createButton(
actionButtonContainer,
"✅",
this.submitClickCallback,
this.submitClickCallback.bind(this),
).setCta().buttonEl.style.marginRight = "0";
}
this.createButton(actionButtonContainer, "❌", this.cancelClickCallback, t("PROMPT_BUTTON_CANCEL"));
this.createButton(actionButtonContainer, "❌", this.cancelClickCallback.bind(this), t("PROMPT_BUTTON_CANCEL"));
if(this.displayEditorButtons) {
this.createButton(editorButtonContainer, "⏎", ()=>this.insertStringBtnClickCallback("\n"), t("PROMPT_BUTTON_INSERT_LINE"), "0");
this.createButton(editorButtonContainer, "⌫", this.delBtnClickCallback, "Delete");
this.createButton(editorButtonContainer, "⌫", this.delBtnClickCallback.bind(this), "Delete");
this.createButton(editorButtonContainer, "⎵", ()=>this.insertStringBtnClickCallback(" "), t("PROMPT_BUTTON_INSERT_SPACE"));
if(this.view) {
this.createButton(editorButtonContainer, "🔗", this.linkBtnClickCallback, t("PROMPT_BUTTON_INSERT_LINK"));
this.createButton(editorButtonContainer, "🔗", this.linkBtnClickCallback.bind(this), t("PROMPT_BUTTON_INSERT_LINK"));
}
this.createButton(editorButtonContainer, "🔠", this.uppercaseBtnClickCallback, t("PROMPT_BUTTON_UPPERCASE"));
this.createButton(editorButtonContainer, "🔠", this.uppercaseBtnClickCallback.bind(this), t("PROMPT_BUTTON_UPPERCASE"));
}
}
@@ -342,8 +342,13 @@ export class GenericInputPrompt extends Modal {
this.inputComponent.inputEl.setSelectionRange(this.selectionStart, this.selectionEnd);
}
private submitClickCallback = () => this.submit();
private cancelClickCallback = () => this.cancel();
private submitClickCallback () {
this.submit();
}
private cancelClickCallback () {
this.cancel();
}
private keyDownCallback = (evt: KeyboardEvent) => {
if ((evt.key === "Enter" && this.lines === 1) || (isWinCTRLorMacCMD(evt) && evt.key === "Enter")) {
@@ -527,6 +532,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 +598,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();
};
@@ -662,10 +673,10 @@ export class ConfirmationPrompt extends Modal {
buttonContainer.style.display = "flex";
buttonContainer.style.justifyContent = "flex-end";
const cancelButton = this.createButton(buttonContainer, t("PROMPT_BUTTON_CANCEL"), this.cancelClickCallback);
const cancelButton = this.createButton(buttonContainer, t("PROMPT_BUTTON_CANCEL"), this.cancelClickCallback.bind(this));
cancelButton.buttonEl.style.marginRight = "0.5rem";
const confirmButton = this.createButton(buttonContainer, t("PROMPT_BUTTON_OK"), this.confirmClickCallback);
const confirmButton = this.createButton(buttonContainer, t("PROMPT_BUTTON_OK"), this.confirmClickCallback.bind(this));
confirmButton.buttonEl.style.marginRight = "0";
cancelButton.buttonEl.focus();
@@ -677,12 +688,12 @@ export class ConfirmationPrompt extends Modal {
return button;
}
private cancelClickCallback = () => {
private cancelClickCallback() {
this.didConfirm = false;
this.close();
};
private confirmClickCallback = () => {
private confirmClickCallback() {
this.didConfirm = true;
this.close();
};
@@ -702,19 +713,34 @@ 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]> => {
const partsArray = REGEX_LINK.getResList(linkText);
export async function linkPrompt (
linkText:string,
app: App,
view?: ExcalidrawView,
message: string = "Select link to open",
):Promise<[file:TFile, linkText:string, subpath: string]> {
const linksArray = REGEX_LINK.getResList(linkText);
const tagsArray = REGEX_TAGS.getResList(linkText);
let subpath: string = null;
let file: TFile = null;
let parts = partsArray[0];
if (partsArray.length > 1) {
let parts = linksArray[0] ?? tagsArray[0];
const itemsDisplay = [
...linksArray.filter(p=> Boolean(p.value)).map(p => {
const alias = REGEX_LINK.getAliasOrLink(p);
return alias === "100%" ? REGEX_LINK.getLink(p) : alias;
}),
...tagsArray.filter(x=> Boolean(x.value)).map(x => REGEX_TAGS.getTag(x)),
];
const items = [
...linksArray.filter(p=>Boolean(p.value)),
...tagsArray.filter(x=> Boolean(x.value)),
];
if (items.length>1) {
parts = await ScriptEngine.suggester(
app,
partsArray.filter(p=>Boolean(p.value)).map(p => {
const alias = REGEX_LINK.getAliasOrLink(p);
return alias === "100%" ? REGEX_LINK.getLink(p) : alias;
}),
partsArray.filter(p=>Boolean(p.value)),
itemsDisplay,
items,
message,
);
if(!parts) return;
@@ -724,13 +750,16 @@ export const linkPrompt = async (linkText:string, app: App, view?: ExcalidrawVie
return;
}
if (!parts.value) {
openTagSearch(linkText, app);
if (REGEX_TAGS.isTag(parts)) {
openTagSearch(REGEX_TAGS.getTag(parts), app);
return;
}
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);
@@ -748,8 +777,6 @@ export const linkPrompt = async (linkText:string, app: App, view?: ExcalidrawVie
return [file, linkText, subpath];
}
export const templatePromt = async (files: TFile[], app: App): Promise<TFile> => {
if(files.length === 1) return files[0];
return ((await linkPrompt(

File diff suppressed because one or more lines are too long

View File

@@ -3,10 +3,8 @@ import { t } from "../lang/helpers";
import ExcalidrawView from "src/ExcalidrawView";
import { getEA } from "src";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import { getExcalidrawMarkdownHeaderSection } from "src/ExcalidrawData";
import { MD_EX_SECTIONS } from "src/constants/constants";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
import { cleanSectionHeading } from "src/utils/ObsidianUtils";
import { addBackOfTheNoteCard } from "src/utils/ExcalidrawViewUtils";
export class SelectCard extends FuzzySuggestModal<string> {
@@ -32,48 +30,8 @@ export class SelectCard extends FuzzySuggestModal<string> {
new Notice(t("INVALID_SECTION_NAME"));
this.close();
return;
}
(async () => {
const data = view.data;
const header = getExcalidrawMarkdownHeaderSection(data);
const body = data.split(header)[1];
const shouldAddHashtag = body && body.startsWith("%%");
const shouldRemoveTrailingHashtag = header.endsWith("#\n");
view.data = data.replace(
header,
(shouldRemoveTrailingHashtag ? header.substring(0,header.length-2) : header) +
`\n# ${item}\n\n${shouldAddHashtag ? "#\n" : ""}`);
await view.forceSave(true);
let watchdog = 0;
await sleep(200);
let found:string;
while (watchdog++ < 10 && !(found=(await this.app.metadataCache.blockCache
.getForFile({ isCancelled: () => false },view.file))
.blocks.filter((b: any) => b.display && b.node?.type === "heading")
.filter((b: any) => !MD_EX_SECTIONS.includes(b.display))
.map((b: any) => cleanSectionHeading(b.display))
.find((b: any) => b === item))) {
await sleep(200);
}
const ea = getEA(this.view) as ExcalidrawAutomate;
const id = ea.addEmbeddable(
0,0,400,500,
`[[${this.view.file.path}#${item}]]`
);
await ea.addElementsToView(true, false, true);
const api = view.excalidrawAPI as ExcalidrawImperativeAPI;
const el = ea.getViewElements().find(el=>el.id === id);
api.selectElements([el]);
setTimeout(()=>{
api.updateScene({appState: {activeEmbeddable: {element: el, state: "active"}}});
if(found) view.getEmbeddableLeafElementById(el.id)?.editNode?.();
});
})();
//create new section
//`# ${this.inputEl.value}\n\n`;
//Do not allow MD_EX_SECTIONS
}
addBackOfTheNoteCard(this.view, item);
this.close();
}
}
@@ -97,6 +55,7 @@ export class SelectCard extends FuzzySuggestModal<string> {
(async () => {
await ea.addElementsToView(true, false, true);
ea.selectElementsInView([id]);
ea.destroy();
})();
}

View File

@@ -9,6 +9,9 @@ const hyperlink = (url: string, text: string) => {
return `<a onclick='window.open("${url}")'>${text}</a>`;
}
const EMBEDDABLE_MDCUSTOMPROPS = `type EmbeddableMDCustomProps = {<br>useObsidianDefaults: boolean;<br>backgroundMatchCanvas: boolean;<br>backgroundMatchElement: boolean;<br>backgroundColor: string;<br>backgroundOpacity: number;<br>borderMatchElement: boolean;<br>borderColor: string;<br>borderOpacity: number;<br>filenameVisible: boolean;<br>};<br>`;
export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
{
field: "help",
@@ -97,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: "",
},
{
@@ -230,6 +233,18 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: null,
after: "",
},
{
field: "addElementsToFrame",
code: "addElementsToFrame(frameId: string, elementIDs: string[]):void;",
desc: null,
after: "",
},
{
field: "addFrame",
code: "addFrame(topX: number, topY: number, width: number, height: number, name?: string): string;",
desc: null,
after: "",
},
{
field: "addRect",
code: "addRect(topX: number, topY: number, width: number, height: number, id?:string): string;",
@@ -262,8 +277,10 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "addText",
code: 'addText(topX: number, topY: number, text: string, formatting?: {wrapAt?: number; width?: number; height?: number; textAlign?: "left" | "center" | "right"; textVerticalAlign: "top" | "middle" | "bottom"; box?: boolean | "box" | "blob" | "ellipse" | "diamond"; boxPadding?: number; boxStrokeColor?: string;}, id?: string,): string;',
desc: "If box is !null, then text will be boxed\nThe function returns the id of the TextElement. If the text element is boxed i.e. it is a sticky note, then the id of the container object",
code: 'addText(topX: number, topY: number, text: string, formatting?: {autoResize?: boolean; wrapAt?: number; width?: number; height?: number; textAlign?: "left" | "center" | "right"; textVerticalAlign: "top" | "middle" | "bottom"; box?: boolean | "box" | "blob" | "ellipse" | "diamond"; boxPadding?: number; boxStrokeColor?: string;}, id?: string,): string;',
desc: "If box is !null, then text will be boxed\nThe function returns the id of the TextElement. If the text element is boxed i.e. it is a sticky note, then the id of the container object.\n"+
"Default value for autoResize is true. Setting autoResize to false will wrap the text in the text element without the need for the container. If set to false, you must provide a width value as well.\n" +
"wrapAt will be ignored if autoResize is set to false (and a width is also provided)",
after: "",
},
{
@@ -286,8 +303,9 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "addEmbeddable",
code: "addEmbeddable(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string;",
desc: "Adds an iframe/webview (depending on content and platform) to the drawing. If url is not null then the iframe/webview will be loaded from the url. The url maybe a markdown link to an note in the Vault or a weblink. If url is null then the iframe/webview will be loaded from the file. Both the url and the file may not be null.",
code: "addEmbeddable(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile, embeddableCustomData?: EmbeddableMDCustomProps): string;",
desc: "Adds an iframe/webview (depending on content and platform) to the drawing. If url is not null then the iframe/webview will be loaded from the url. The url maybe a markdown link to an note in the Vault or a weblink. " +
"If url is null then the iframe/webview will be loaded from the file. Both the url and the file may not be null.<br>" + EMBEDDABLE_MDCUSTOMPROPS,
after: "",
},
{
@@ -305,6 +323,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: "This is an async function, you need to avait the results. Adds a LaTex element to the drawing. The tex string is the LaTex code. The function returns the id of the created element.",
after: "",
},
{
field: "tex2dataURL",
code: "async tex2dataURL(tex: string, scale: number = 4): Promise<{mimeType: MimeType;fileId: FileId;dataURL: DataURL;created: number;size: { height: number; width: number };}> ",
desc: "returns the base64 dataURL of the LaTeX equation rendered as an SVG. tex is the LaTeX equation string",
after: "",
},
{
field: "connectObjects",
code: "connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?: {numberOfPoints?: number; startArrowHead?: string; endArrowHead?: string; padding?: number;},): string;",
@@ -365,6 +389,14 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: null,
after: "",
},
{
field: "addBackOfTheCardNoteToView",
code: "async addBackOfTheCardNoteToView(sectionTitle: string, activate: boolean = false, sectionBody?: string, embeddableCustomData?: EmbeddableMDCustomProps): Promise<string>",
desc: "Adds a back of the note card to the current active view. If <b>body</b> is provided the note will be created with the body text, otherwise the note will be created with the title only.<br>Returns the id of the created element.<br>" +
"If <b>activate</b> is true, the embedded note will be activated for editing.<br>" +
"This is an async function, if you need the element ID of the created element, the function should be awaited.<br>" + EMBEDDABLE_MDCUSTOMPROPS,
after: "",
},
{
field: "getViewSelectedElement",
code: "getViewSelectedElement(): ExcalidrawElement;",
@@ -373,8 +405,8 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "getViewSelectedElements",
code: "getViewSelectedElements(): ExcalidrawElement[];",
desc: null,
code: "getViewSelectedElements(includeFrameChildren: boolean = true): ExcalidrawElement[];",
desc: "If a frame is selected this function will return the frame and all its elements unless includeFrameChildren is set to false",
after: "",
},
{
@@ -428,7 +460,19 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
{
field: "getExportSettings",
code: "getExportSettings(withBackground: boolean, withTheme: boolean,): ExportSettings;",
desc: "Utility function to generate ExportSettings object",
desc: "Utility function to generate ExportSettings object\n" +
"export interface ExportSettings {\n" +
" withBackground: boolean;\n" +
" withTheme: boolean;\n" +
" isMask: boolean; //if true elements will be processed as mask, clipping, etc.\n" +
" frameRendering?: { //optional, overrides relevant appState settings for rendering the frame\n" +
" enabled: boolean;\n" +
" name: boolean;\n" +
" outline: boolean;\n" +
" clip: boolean;\n" +
" };\n" +
" skipInliningFonts?: boolean;\n" +
"}",
after: "",
},
{
@@ -463,10 +507,17 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "getElementsInTheSameGroupWithElement",
code: "getElementsInTheSameGroupWithElement(element: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[];",
desc: "Gets all the elements from elements[] that share one or more groupIds with element.",
code: "getElementsInTheSameGroupWithElement(element: ExcalidrawElement, elements: ExcalidrawElement[], includeFrameElements: boolean = false): ExcalidrawElement[];",
desc: "Gets all the elements from elements[] that share one or more groupIds with element.<br>" +
"If includeFrameElements is true, then if the frame is part of the group all the elements that are in the frame will also be included in the result set",
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;",
@@ -497,6 +548,23 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: "Measures text size based on current style settings",
after: "",
},
{
field: "getOriginalImageSize",
code: "async getOriginalImageSize(imageElement: ExcalidrawImageElement, shouldWaitForImage: boolean=false): Promise<{width: number; height: number}>",
desc: "Returns the size of the image element at 100% (i.e. the original size) or undefined if the data URL is not available.\n"+
"If shouldWaitForImage is true, the function will wait for the view to load the image before returning the size.\n"+
"This is an async function, you need to await the result.",
after: "",
},
{
field: "resetImageAspectRatio",
code: "async resetImageAspectRatio(imgEl: ExcalidrawImageElement): Promise<boolean>",
desc: "Resets the image to its original aspect ratio.\n" +
"If the image is resized then the function returns true.\n" +
"If the image element is not in EA (only in the view), then if the image is resized, the element is copied to EA for Editing using copyViewElementsToEAforEditing([imgEl]).\n" +
"Note you need to run await ea.addElementsToView(false); to add the modified image to the view.",
after: "",
},
{
field: "verifyMinimumPluginVersion",
code: "verifyMinimumPluginVersion(requiredVersion: string): boolean;",
@@ -557,6 +625,14 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: "Converts a CSS color name to its HEX color equivalent. 'White' to #FFFFFF",
after: "",
},
{
field: "getCM",
code: "getCM(color:TInput): ColorMaster;",
desc: `Returns a ${hyperlink("https://github.com/lbragile/ColorMaster", "ColorMaster")} object. ` +
"The function also accepts css color names. Under the hood, before calling ColorMaster it uses " +
"colorNameToHex to convert the color name to a HEX color.",
after: "",
},
{
field: "obsidian",
code: "obsidian",
@@ -686,8 +762,9 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "viewUpdateScene",
code: "viewUpdateScene(scene:{elements?:ExcalidrawElement[],appState?: AppState,files?: BinaryFileData,commitToHistory?: boolean,},restore:boolean=false):void",
desc: "Calls the ExcalidrawAPI updateScene function for the targetView. When restore=true, excalidraw will try to correct errors in the scene such as setting default values to missing element properties.",
code: "viewUpdateScene(scene:{elements?:ExcalidrawElement[],appState?: AppState,files?: BinaryFileData,commitToHistory?: boolean,storeAction?: 'capture' | 'none' | 'update'},restore:boolean=false):void",
desc: "Calls the ExcalidrawAPI updateScene function for the targetView. When restore=true, excalidraw will try to correct errors in the scene such as setting default values to missing element properties. " +
`Note that commitToHistory has been deprecated in Excalidraw and is no longer used. You should use storeAction instead. See ${hyperlink("https://github.com/excalidraw/excalidraw/pull/7898", "ExcalidrawAPI")} documentation for more information.`,
after: "",
},
{

View File

@@ -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;
}
}

View File

@@ -2,6 +2,7 @@ import {
DEVICE,
FRONTMATTER_KEYS,
} from "src/constants/constants";
import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/constants/constSettingsTags";
import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/utils/ModifierkeyHelper";
// English
@@ -36,6 +37,7 @@ export default {
TRANSCLUDE: "Embed a drawing",
TRANSCLUDE_MOST_RECENT: "Embed the most recently edited drawing",
TOGGLE_LEFTHANDED_MODE: "Toggle left-handed mode",
TOGGLE_SPLASHSCREEN: "Show splash screen in new drawings",
FLIP_IMAGE: "Open the back-of-the-note of the selected excalidraw image",
NEW_IN_NEW_PANE: "Create new drawing - IN AN ADJACENT WINDOW",
NEW_IN_NEW_TAB: "Create new drawing - IN A NEW TAB",
@@ -50,17 +52,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.`,
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",
@@ -72,8 +77,11 @@ export default {
INSERT_PDF: "Insert PDF file from vault",
UNIVERSAL_ADD_FILE: "Insert ANY file",
INSERT_CARD: "Add back-of-note card",
CONVERT_CARD_TO_FILE: "Move back-of-note card to File",
ERROR_TRY_AGAIN: "Please try again.",
PASTE_CODEBLOCK: "Paste code block",
INSERT_LATEX:
`Insert LaTeX formula (e.g. \\binom{n}{k} = \\frac{n!}{k!(n-k)!}). ${labelALT()}+CLICK to watch a help video.`,
`Insert LaTeX formula (e.g. \\binom{n}{k} = \\frac{n!}{k!(n-k)!}).`,
ENTER_LATEX: "Enter a valid LaTeX expression",
READ_RELEASE_NOTES: "Read latest release notes",
RUN_OCR: "OCR full drawing: Grab text from freedraw + images to clipboard and doc.props",
@@ -86,10 +94,33 @@ export default {
ANNOTATE_IMAGE : "Annotate image in Excalidraw",
INSERT_ACTIVE_PDF_PAGE_AS_IMAGE: "Insert active PDF page as image",
RESET_IMG_TO_100: "Set selected image element size to 100% of original",
RESET_IMG_ASPECT_RATIO: "Reset selected image element aspect ratio",
TEMPORARY_DISABLE_AUTOSAVE: "Disable autosave until next time Obsidian starts (only set this if you know what you are doing)",
TEMPORARY_ENABLE_AUTOSAVE: "Enable autosave",
//ExcalidrawView.ts
NO_SEARCH_RESULT: "Didn't find a matching element in the drawing",
FORCE_SAVE_ABORTED: "Force Save aborted because saving is in progress",
LINKLIST_SECOND_ORDER_LINK: "Second Order Link",
MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT_TITLE: "Customize the Embedded File link",
MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT: "Do not add [[square brackets]] around the filename!<br>" +
"For markdown-page images follow this format when editing your link: <mark>filename#^blockref|WIDTHxMAXHEIGHT</mark><br>" +
"You can anchor Excalidraw images to 100% of their size by adding <code>|100%</code> to the end of the link.<br>" +
"You can change the PDF page by changing <code>#page=1</code> to <code>#page=2</code> etc.<br>" +
"PDF rect crop values are: <code>left, bottom, right, top</code>. Eg.: <code>#rect=0,0,500,500</code><br>",
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",
@@ -116,6 +147,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",
@@ -206,7 +238,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",
@@ -298,21 +330,33 @@ FILENAME_HEAD: "Filename",
DEFAULT_PEN_MODE_NAME: "Pen mode",
DEFAULT_PEN_MODE_DESC:
"Should pen mode be automatically enabled when opening Excalidraw?",
DISABLE_DOUBLE_TAP_ERASER_NAME: "Enable double-tap eraser in pen mode",
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_NAME: "Show (+) crosshair in pen mode",
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_DESC:
"Show crosshair in pen mode when using the freedraw tool. <b><u>Toggle ON:</u></b> SHOW <b><u>Toggle OFF:</u></b> HIDE<br>"+
"The effect depends on the device. Crosshair is typically visible on drawing tablets, MS Surface, but not on iOS.",
SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME: "Render image when in markdown reading mode",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_NAME: "Render Excalidraw file as an image in hover preview...",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_DESC:
"...even if the file has the <b>excalidraw-open-md: true</b> frontmatter key.<br>" +
"When this setting is off and the file is set to open in md by default, the hover preview will show the " +
"markdown side of the document.",
SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME: "Render as image when in markdown reading mode of an Excalidraw file",
SHOW_DRAWING_OR_MD_IN_READING_MODE_DESC:
"Must close the active excalidraw/markdown file and reopen it for this change to take effect.<br>When you are in markdown reading mode (aka. reading the back side of the drawing), should the Excalidraw drawing be rendered as an image? " +
"This setting will not affect the display of the drawing when you are in Excalidraw mode, when you embed the drawing into a markdown document or when rendering hover preview.<br><ul>" +
"<li>See other related setting for <b>PDF Export</b> under 'Embedding and Exporting' further below.</li>" +
"<li>Be sure to check out the <b>Fade Out setting</b> in the 'Miscellaneous fetures' section.</li></ul>",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME: "Render image when EXPORT TO PDF in markdown mode",
"When you are in markdown reading mode (aka. reading the back side of the drawing) should the Excalidraw drawing be rendered as an image? " +
"This setting will not affect the display of the drawing when you are in Excalidraw mode or when you embed the drawing into a markdown document or when rendering hover preview.<br><ul>" +
"<li>See other related setting for <a href='#"+TAG_PDFEXPORT+"'>PDF Export</a> under 'Embedding and Exporting' further below.</li></ul><br>" +
"You must close the active excalidraw/markdown file and reopen it for this change to take effect.",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME: "Render the file as an image when exporting an Excalidraw file to PDF",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_DESC:
"Must close the active excalidraw/markdown file and reopen for this change to take effect.<br>When you are printing the markdown side of the note to PDF (aka. the back side of the drawing), should the Excalidraw drawing be rendered as an image?<br><ul>" +
"<li>See other related setting for <b>Markdown Reading Mode</b> under 'Appearnace and Behavior' further above.</li>" +
"<li>Be sure to check out the <b>Fade Out setting</b> in the 'Miscellaneous fetures' section.</li></ul>",
"This setting controls the behavior of Excalidraw when exporting an Excalidraw file to PDF in markdown view mode using Obsidian's <b>Export to PDF</b> feature.<br>" +
"<ul><li>When <b>enabled</b> the PDF will show the Excalidraw drawing only;</li>" +
"<li>When <b>disabled</b> the PDF will show the markdown side of the document.</li></ul>" +
"See the other related setting for <a href='#"+TAG_MDREADINGMODE+"'>Markdown Reading Mode</a> under 'Appearnace and Behavior' further above.<br>" +
"⚠️ Note, you must close the active excalidraw/markdown file and reopen for this change to take effect. ⚠️",
HOTKEY_OVERRIDE_HEAD: "Hotkey overrides",
HOTKEY_OVERRIDE_DESC: `Some of the Excalidraw hotkeys such as <code>${labelCTRL()}+Enter</code> to edit text or <code>${labelCTRL()}+K</code> to create an element link ` +
"conflict with Obsidian hotkey settings. The hotkey combinations you add below will override Obsidian's hotkey settings while useing Excalidraw, thus " +
`you can add <code>${labelCTRL()}+G</code> if you want to default to Group Object in Excalidraw instead of opening Graph View.`,
THEME_HEAD: "Theme and styling",
ZOOM_HEAD: "Zoom",
DEFAULT_PINCHZOOM_NAME: "Allow pinch zoom in pen mode",
@@ -438,7 +482,11 @@ FILENAME_HEAD: "Filename",
EMBED_TOEXCALIDRAW_DESC: "In the Embed Files section of Excalidraw Settings, you can configure how various files are embedded into Excalidraw. This includes options for embedding interactive markdown files, PDFs, and markdown files as images.",
MD_HEAD: "Embed markdown into Excalidraw as image",
MD_EMBED_CUSTOMDATA_HEAD_NAME: "Interactive Markdown Files",
MD_EMBED_CUSTOMDATA_HEAD_DESC: `These settings will only effect future embeds. Current embeds remain unchanged. The theme setting of embedded frames is under the "Excalidraw appearance and behavior" section.`,
MD_EMBED_CUSTOMDATA_HEAD_DESC: `The below settings will only effect future embeds. Current embeds remain unchanged. The theme setting of embedded frames is under the "Excalidraw appearance and behavior" section.`,
MD_EMBED_SINGLECLICK_EDIT_NAME: "Single click to edit embedded markdown",
MD_EMBED_SINGLECLICK_EDIT_DESC:
"Single click on an embedded markdown file to edit it. " +
"When turned off, the markdown file will first open in preview mode, then switch to edit mode when you click on it again.",
MD_TRANSCLUDE_WIDTH_NAME: "Default width of a transcluded markdown document",
MD_TRANSCLUDE_WIDTH_DESC:
"The width of the markdown page. This affects the word wrapping when transcluding longer paragraphs, and the width of " +
@@ -499,7 +547,7 @@ FILENAME_HEAD: "Filename",
EMBED_REUSE_EXPORTED_IMAGE_NAME:
"If found, use the already exported image for preview",
EMBED_REUSE_EXPORTED_IMAGE_DESC:
"This setting works in conjunction with the Auto-export SVG/PNG setting. If an exported image that matches the file name of the drawing " +
"This setting works in conjunction with the <a href='#"+TAG_AUTOEXPORT+"'>Auto-export SVG/PNG</a> setting. If an exported image that matches the file name of the drawing " +
"is available, use that image instead of generating a preview image on the fly. This will result in faster previews especially when you have many embedded objects in the drawing, however, " +
"it may happen that your latest changes are not displayed and that the image will not automatically match your Obsidian theme in case you have changed the " +
"Obsidian theme since the export was created. This setting only applies to embedding images into markdown documents. " +
@@ -530,7 +578,7 @@ FILENAME_HEAD: "Filename",
EMBED_TYPE_NAME: "Type of file to insert into the document",
EMBED_TYPE_DESC:
"When you embed an image into a document using the command palette this setting will specify if Excalidraw should embed the original Excalidraw file " +
"or a PNG or an SVG copy. You need to enable auto-export PNG / SVG (see below under Export Settings) for those image types to be available in the dropdown. For drawings that do not have a " +
"or a PNG or an SVG copy. You need to enable <a href='#"+TAG_AUTOEXPORT+"'>auto-export PNG / SVG</a> (see below under Export Settings) for those image types to be available in the dropdown. For drawings that do not have a " +
"a corresponding PNG or SVG readily available the command palette action will insert a broken link. You need to open the original drawing and initiate export manually. " +
"This option will not autogenerate PNG/SVG files, but will simply reference the already existing files.",
EMBED_MARKDOWN_COMMENT_NAME: "Embed link to drawing as comment",
@@ -548,7 +596,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",
@@ -580,6 +628,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: "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>",
@@ -611,7 +668,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. " +
@@ -656,16 +713,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.",
@@ -682,6 +744,12 @@ FILENAME_HEAD: "Filename",
"the developer of Taskbone (as you can imagine, there is no such thing as 'free', providing this awesome OCR service costs some money to the developer of Taskbone), you can " +
"purchase a paid API key from <a href='https://www.taskbone.com/' target='_blank'>taskbone.com</a>. In case you have purchased a key, simply overwrite this auto generated free-tier API-key with your paid key.",
//HotkeyEditor
HOTKEY_PRESS_COMBO_NANE: "Press your hotkey combination",
HOTKEY_PRESS_COMBO_DESC: "Please press the desired key combination",
HOTKEY_BUTTON_ADD_OVERRIDE: "Add New Override",
HOTKEY_BUTTON_REMOVE: "Remove",
//openDrawings.ts
SELECT_FILE: "Select a file then press enter.",
SELECT_COMMAND: "Select a command then press enter.",
@@ -726,7 +794,7 @@ FILENAME_HEAD: "Filename",
TOGGLE_FRAME_RENDERING: "Toggle frame rendering",
TOGGLE_FRAME_CLIPPING: "Toggle frame clipping",
OPEN_LINK_CLICK: "Open Link",
OPEN_LINK_PROPS: "Open markdown-embed properties or open link in new window",
OPEN_LINK_PROPS: "Open the image-link or LaTeX-formula editor",
//IFrameActionsMenu.tsx
NARROW_TO_HEADING: "Narrow to heading...",
@@ -787,4 +855,23 @@ 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",
//InsertPDFModal.ts
IPM_PAGES_TO_IMPORT_NAME: "Pages to import",
IPM_SELECT_PAGES_TO_IMPORT: "Please select pages to import",
IPM_ADD_BORDER_BOX_NAME: "Add border box",
IPM_ADD_FRAME_NAME: "Add page to frame",
IPM_ADD_FRAME_DESC: "For easier handling I recommend to lock the page inside the frame. " +
"If, however, you do lock the page inside the frame then the only way to unlock it is to right-click the frame, select remove elements from frame, then unlock the page.",
IPM_GROUP_PAGES_NAME: "Group pages",
IPM_GROUP_PAGES_DESC: "This will group all pages into a single group. This is recommended if you are locking the pages after import, because the group will be easier to unlock later rather than unlocking one by one.",
IPM_SELECT_PDF: "Please select a PDF file",
};

View File

@@ -2,6 +2,7 @@ import {
DEVICE,
FRONTMATTER_KEYS,
} from "src/constants/constants";
import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/constants/constSettingsTags";
import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/utils/ModifierkeyHelper";
// 简体中文
@@ -9,7 +10,8 @@ export default {
// main.ts
CONVERT_URL_TO_FILE: "从 URL 下载图像到本地",
UNZIP_CURRENT_FILE: "解压当前 Excalidraw 文件",
PUBLISH_SVG_CHECK: "Obsidian Publish: 搜索过期的 SVG 和 PNG 导出文件",
ZIP_CURRENT_FILE: "压缩当前 Excalidraw 文件",
PUBLISH_SVG_CHECK: "Obsidian Publish搜索过期的 SVG 和 PNG 导出文件",
EMBEDDABLE_PROPERTIES: "Embeddable 元素设置",
EMBEDDABLE_RELATIVE_ZOOM: "使元素的缩放等级等于当前画布的缩放等级",
OPEN_IMAGE_SOURCE: "打开 Excalidraw 绘图文件",
@@ -35,6 +37,8 @@ export default {
TRANSCLUDE: "嵌入绘图(形如 ![[drawing]])到当前 Markdown 文档中",
TRANSCLUDE_MOST_RECENT: "嵌入最近编辑过的绘图(形如 ![[drawing]])到当前 Markdown 文档中",
TOGGLE_LEFTHANDED_MODE: "切换为左手模式",
TOGGLE_SPLASHSCREEN: "在新绘图中显示启动画面",
FLIP_IMAGE: "打开当前所选 excalidraw 图像的“背景笔记”",
NEW_IN_NEW_PANE: "新建绘图 - 于新面板",
NEW_IN_NEW_TAB: "新建绘图 - 于新页签",
NEW_IN_ACTIVE_PANE: "新建绘图 - 于当前面板",
@@ -48,14 +52,18 @@ export default {
NEW_IN_POPOUT_WINDOW_EMBED: "新建绘图 - 于新窗口 - 并将其嵌入(形如 ![[drawing]])到当前 Markdown 文档中",
TOGGLE_LOCK: "文本元素原文模式RAW⟺ 预览模式PREVIEW",
DELETE_FILE: "从库中删除所选图像(或以图像形式嵌入绘图中的 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()} 可观看视频演示。`,
`复制所选元素为内部链接(形如 [[file#^id]] )。\n按住 ${labelCTRL()} 可复制元素所在分组为内部链接(形如 [[file#^group=id]] )。\n按住 ${labelSHIFT()} 可复制所选元素所在区域为内部链接(形如 [[file#^area=id]] )。`,
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: "未选择画布里的单个元素",
@@ -64,27 +72,60 @@ export default {
INSERT_COMMAND: "插入 Obsidian 命令(以内部链接形式嵌入)到当前绘图中",
INSERT_IMAGE: "插入图像或 Excalidraw 绘图(以图像形式嵌入)到当前绘图中",
IMPORT_SVG: "从 SVG 文件导入图形元素到当前绘图中(暂不支持文本元素)",
IMPORT_SVG_CONTEXTMENU: "转换 SVG 到线条 - 有限制",
INSERT_MD: "插入 Markdown 文档(以图像形式嵌入)到当前绘图中",
INSERT_PDF: "插入 PDF 文档(以图像形式嵌入)到当前绘图中",
UNIVERSAL_ADD_FILE: "插入任意文件(以交互形式嵌入,或者以图像形式嵌入)到当前绘图中",
INSERT_CARD: "插入“背景笔记”卡片",
CONVERT_CARD_TO_FILE: "将“背景笔记”卡片保存到文件",
ERROR_TRY_AGAIN: "请重试。",
PASTE_CODEBLOCK: "粘贴代码块",
INSERT_LATEX:
`插入 LaTeX 公式到当前绘图`,
`插入 LaTeX 公式(例如:\\binom{n}{k} = \\frac{n!}{k!(n-k)!})。`,
ENTER_LATEX: "输入 LaTeX 表达式",
READ_RELEASE_NOTES: "阅读本插件的更新说明",
RUN_OCR: "OCR识别涂鸦和图片里的文本并复制到剪贴板",
RUN_OCR: "OCR 完整画布:识别涂鸦和图片里的文本并复制到剪贴板和文档属性中",
RERUN_OCR: "重新运行 OCR 完整画笔:识别涂鸦和图片里的文本并复制到剪贴板和文档属性中",
RUN_OCR_ELEMENTS: "OCR 选中的元素:识别涂鸦和图片里的文本并复制到剪贴板",
TRAY_MODE: "绘图工具属性页:面板模式 ⟺ 托盘模式",
SEARCH: "搜索文本",
CROP_IMAGE: "裁剪与蒙版",
RESET_IMG_TO_100: "重设图像元素的尺寸为 100%",
CROP_PAGE: "对所选页面裁剪并添加蒙版",
CROP_IMAGE: "对图片裁剪并添加蒙版",
ANNOTATE_IMAGE : "在 Excalidraw 中标注图像",
INSERT_ACTIVE_PDF_PAGE_AS_IMAGE: "将当前激活的的 PDF 页面作为图片插入",
RESET_IMG_TO_100: "重置图像元素的尺寸为 100%",
RESET_IMG_ASPECT_RATIO: "重置所选图像元素的纵横比",
TEMPORARY_DISABLE_AUTOSAVE: "临时禁用自动保存功能,直到本次 Obsidian 退出(小白慎用!)",
TEMPORARY_ENABLE_AUTOSAVE: "启用自动保存功能",
//ExcalidrawView.ts
NO_SEARCH_RESULT: "在绘图中未找到匹配的元素",
FORCE_SAVE_ABORTED: "自动保存被中止,因为文件正在保存中",
LINKLIST_SECOND_ORDER_LINK: "二级链接",
MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT_TITLE: "自定义嵌入文件链接",
MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT: "请不要在文件名周围添加 [[方括号]]<br>" +
"对于 markdown 图像,在编辑链接时请遵循以下格式:<mark>文件名#^块引用|宽度x最大高度</mark><br>" +
"您可以通过在链接末尾添加 <code>|100%</code> 来将 Excalidraw 图像锚定为 100% 的大小。<br>" +
"您可以通过将 <code>#page=1</code> 更改为 <code>#page=2</code> 等来更改 PDF 页码。<br>" +
"PDF 矩形裁剪值为:<code>左, 下, 右, 上</code>。例如:<code>#rect=0,0,500,500</code><br>",
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:
"请选择一个含有链接的图形或文本元素。",
@@ -103,10 +144,15 @@ export default {
BACKUP_RESTORED: "已恢复备份",
CACHE_NOT_READY: "抱歉,加载绘图文件时出错。<br><br><mark>现在有耐心,将来更省心。</mark><br><br>该插件有备份机制,但您似乎刚刚打开 Obsidian需要等待一分钟或更长的时间来读取缓存。缓存读取完毕时您将会在右上角收到提示。<br><br>请点击 OK 并耐心等待缓存,或者选择点击取消后手动修复你的文件。<br>",
OBSIDIAN_TOOLS_PANEL: "Obsidian 工具面板",
ERROR_SAVING_IMAGE: "获取图像时发生未知错误",
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: "移除文字元素链接",
LASER_ON: "启用激光笔",
LASER_OFF: "关闭激光笔",
//settings.ts
RELEASE_NOTES_NAME: "显示更新说明",
@@ -123,6 +169,23 @@ export default {
FOLDER_NAME: "Excalidraw 文件夹",
FOLDER_DESC:
"新绘图的默认存储路径。若为空,将在库的根目录中创建新绘图。",
CROP_PREFIX_NAME: "剪贴文件的前缀",
CROP_PREFIX_DESC:
"当剪贴图片进来时保存的文件名的前缀。 " +
"留空则使用 'cropped_'",
ANNOTATE_PREFIX_NAME: "标注文件的前缀",
ANNOTATE_PREFIX_DESC:
"在标注图像时创建新绘图的文件名的第一部分。" +
"留空则使用'annotated_'",
ANNOTATE_PRESERVE_SIZE_NAME: "在标注时保留图像尺寸",
ANNOTATE_PRESERVE_SIZE_DESC:
"当在 Markdown 中标注图像时,替换后的图像链接将包含原始图像的宽度。",
CROP_FOLDER_NAME: "剪贴文件文件夹",
CROP_FOLDER_DESC:
"剪贴图像时创建新绘图的默认存储路径。如果留空,将按照 Vault 附件设置创建。",
ANNOTATE_FOLDER_NAME: "图片标注文件文件夹",
ANNOTATE_FOLDER_DESC:
"创建图片标注是的默认存储路径。如果留空,将按照 Vault 附件设置创建。",
FOLDER_EMBED_NAME:
"将 Excalidraw 文件夹用于“新建绘图”系列命令",
FOLDER_EMBED_DESC:
@@ -131,11 +194,14 @@ export default {
"<b>开启:</b>使用上面的 Excalidraw 文件夹。 <br><b>关闭:</b>使用 Obsidian 设置的新附件默认位置。",
TEMPLATE_NAME: "Excalidraw 模板文件",
TEMPLATE_DESC:
"Excalidraw 模板文件的存储路径。<br>" +
"如果您的模板在默认的 Excalidraw 文件夹中且文件名是 " +
"Template.md则此项应设为 Excalidraw/Template.md也可省略 .md 扩展名,即 Excalidraw/Template<br>" +
"Excalidraw 模板文件(文件夹)的存储路径。<br>" +
"<b>模板文件:</b>比如:如果您的模板在默认的 Excalidraw 文件夹中且文件名是 " +
"Template.md则此项应设为 Excalidraw/Template.md也可省略 .md 扩展名,即 Excalidraw/Template。" +
"如果您在兼容模式下使用 Excalidraw那么您的模板文件也必须是旧的 *.excalidraw 格式," +
"例如 Excalidraw/Template.excalidraw。",
"例如 Excalidraw/Template.excalidraw。<br><b>模板文件夹:</b> 你还可以将文件夹设置为模板。 " +
"在这种情况下,创建新绘图时将提示您选择使用哪个模板。<br>" +
"<b>专业提示:</b> 如果您正在使用 Obsidian Templater 插件,您可以将 Templater 代码添加到不同的" +
"Excalidraw 模板中,以自动配置您的绘图",
SCRIPT_FOLDER_NAME: "Excalidraw 自动化脚本的文件夹(大小写敏感!)",
SCRIPT_FOLDER_DESC:
"此文件夹用于存放 Excalidraw 自动化脚本。" +
@@ -171,8 +237,8 @@ export default {
AI_OPENAI_DEFAULT_API_URL_DESC:
"默认的 OpenAI API URL。请填写有效的 OpenAI API URL。" +
"Excalidraw 会通过该 URL 发送 API 请求给 OpenAI。我没有对此选项做任何错误处理请谨慎修改。",
AI_OPENAI_DEFAULT_IMAGE_API_URL_NAME: "OpenAI Image Generation API URL",
AI_OPENAI_DEFAULT_VISION_MODEL_PLACEHOLDER: "gpt-4-vision-preview",
AI_OPENAI_DEFAULT_IMAGE_API_URL_NAME: "OpenAI 图像生成 API URL",
AI_OPENAI_DEFAULT_VISION_MODEL_PLACEHOLDER: "输入你的默认 AI 模型名称例如gpt-4o",
SAVING_HEAD: "保存",
SAVING_DESC: "包括:压缩,自动保存的时间间隔,文件的命名格式和扩展名等的设置。",
COMPRESS_NAME: "压缩 Excalidraw JSON",
@@ -186,6 +252,14 @@ export default {
"而当您切换回 Excalidraw 模式时,数据就会被再次编码。<br>" +
"开启此项后,对于之前已存在但未压缩的绘图文件," +
"需要重新打开并保存才能生效。",
DECOMPRESS_FOR_MD_NAME: "在 Markdown 视图中解压缩 Excalidraw JSON",
DECOMPRESS_FOR_MD_DESC:
"通过启用此功能Excalidraw 将在切换到 Markdown 视图时自动解压缩绘图 JSON。" +
"这将使您能够轻松阅读和编辑 JSON 字符串。" +
"一旦您切换回 Excalidraw 视图并保存绘图Ctrl+S绘图将再次被压缩。<br>" +
"我建议关闭此功能,因为这可以获得更小的文件尺寸,并避免在 Obsidian 搜索中出现不必要的结果。 " +
"您始终可以使用命令面板中的“Excalidraw: 解压缩当前 Excalidraw 文件”命令"+
"在需要阅读或编辑时手动解压缩绘图 JSON。",
AUTOSAVE_INTERVAL_DESKTOP_NAME: "桌面端自动保存时间间隔",
AUTOSAVE_INTERVAL_DESKTOP_DESC:
"每隔多长时间自动保存一次(如果绘图文件没有发生改变,将不会保存)。" +
@@ -235,7 +309,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>" +
@@ -256,12 +330,39 @@ FILENAME_HEAD: "文件名",
DEFAULT_PEN_MODE_NAME: "触控笔模式Pen mode",
DEFAULT_PEN_MODE_DESC:
"打开绘图时,是否自动开启触控笔模式?",
DISABLE_DOUBLE_TAP_ERASER_NAME: "启用手写模式下的双击橡皮擦功能",
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_NAME: "在触控笔模式下显示十字准星(+",
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_DESC:
"在触控笔模式下使用涂鸦功能会显示十字准星 <b><u>打开:</u></b> 显示 <b><u>关闭:</u></b> 隐藏<br>"+
"效果取决于设备。十字准星通常在绘图板、MS Surface 上可见。但在 iOS 上不可见。",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_NAME: "在鼠标悬停预览时将 Excalidraw 文件渲染文图片",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_DESC:
"...即使文件具有 `<b>excalidraw-open-md: true</b>` frontmatter 属性。<br>" +
"当此设置关闭且文件默认设置为以 md 格式打开时,悬停预览将显示文档的 Markdown 部分(背景笔记)。" +
"",
SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME: "Excalidraw 文件在 Markdown 阅读模式下渲染为图片",
SHOW_DRAWING_OR_MD_IN_READING_MODE_DESC:
"当您处于 Markdown 阅读模式即查看绘图的背景笔记Excalidraw 绘图是否应该渲染为图像?" +
"此设置不会影响您在 Excalidraw 模式下的绘图显示,或者在将绘图嵌入 Markdown 文档时,或在渲染悬停预览时。<br><ul>" +
"<li>请参阅下面‘嵌入和导出’部分的 <a href='#"+TAG_PDFEXPORT+"'>PDF 导出</a> 相关设置。</li></ul><br>" +
"您必须关闭当前的 Excalidraw/Markdown 文件并重新打开,以使此更改生效。",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME: "在将 Excalidraw 文件导出为 PDF 时将文件渲染为图像",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_DESC:
"处于 Markdown 视图模式时,此设置控制 Excalidraw 在使用 Obsidian 的 <b>导出为 PDF</b> 功能时,将 Excalidraw 文件导出为 PDF 的行为。<br>" +
"<ul><li>当 <b>启用</b> 时PDF 将仅显示 Excalidraw 绘图;</li>" +
"<li>当 <b>禁用</b> 时PDF 将显示文档的 Markdown 部分(背景笔记)。</li></ul>" +
"请参阅上面‘外观和行为’部分的 <<a href='#"+TAG_MDREADINGMODE+"'>>Markdown 阅读模式</a> 相关设置。" +
"⚠️ 注意,您必须关闭当前的 Excalidraw/Markdown 文件并重新打开,以使此更改生效。⚠️",
HOTKEY_OVERRIDE_HEAD: "热键覆盖",
HOTKEY_OVERRIDE_DESC: `一些 Excalidraw 的热键,例如 ${labelCTRL()}+Enter 用于编辑文本,或 ${labelCTRL()}+K 用于创建元素链接。` +
"与 Obsidian 的热键设置发生冲突。您在下面添加的热键组合将在使用 Excalidraw 时覆盖 Obsidian 的热键设置," +
`因此如果您希望在 Excalidraw 中默认选择“组合对象”,而不是打开“图形视图”,您可以添加 ${labelCTRL()}+G。`,
THEME_HEAD: "主题和样式",
ZOOM_HEAD: "缩放",
DEFAULT_PINCHZOOM_NAME: "允许在触控笔模式下进行双指缩放",
DEFAULT_PINCHZOOM_DESC:
"在触控笔模式下使用自由画笔工具时,双指缩放可能造成干扰。<br>" +
"<b>开启: </b>允许在触控笔模式下进行双指缩放<br><b>关闭: </b>禁止在触控笔模式下进行双指缩放",
"<b>开启:</b>允许在触控笔模式下进行双指缩放<br><b>关闭: </b>禁止在触控笔模式下进行双指缩放",
DEFAULT_WHEELZOOM_NAME: "鼠标滚轮缩放页面",
DEFAULT_WHEELZOOM_DESC:
@@ -287,7 +388,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>。",
@@ -296,7 +397,19 @@ 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: "长按打开(移动端)",
LONG_PRESS_MOBILE_DESC: "长按(以毫秒为单位)打开在 Markdown 文件中嵌入的 Excalidraw 绘图。",
FOCUS_ON_EXISTING_TAB_NAME: "聚焦于当前标签页",
FOCUS_ON_EXISTING_TAB_DESC: "当打开一个链接时如果该文件已经打开Excalidraw 将会聚焦到现有的标签页上 " +
"启用这个设置会在文件已经打开的情况下覆盖“重用相邻窗格”的设置。",
SECOND_ORDER_LINKS_NAME: "显示二级链接",
SECOND_ORDER_LINKS_DESC: "在 Excalidraw 中点击链接时显示链接。二级链接是指指向被点击链接的反向链接" +
"当使用图标连接相似的笔记时,二级链接可以让你直接到达相关笔记,而不需要两次点击。" +
"请观看 <a href='https://youtube.com/shorts/O_1ls9c6wBY?feature=share'>这个 YouTube Shorts 视频</a> 以了解更多信息。",
ADJACENT_PANE_NAME: "在相邻面板中打开",
ADJACENT_PANE_DESC:
`按住 ${labelCTRL()}+${labelSHIFT()} 并点击绘图里的内部链接时,插件默认会在新面板中打开该链接。<br>` +
@@ -306,7 +419,7 @@ FILENAME_HEAD: "文件名",
MAINWORKSPACE_PANE_NAME: "在主工作区中打开",
MAINWORKSPACE_PANE_DESC:
`按住 ${labelCTRL()}+${labelSHIFT()} 并点击绘图里的内部链接时,插件默认会在当前窗口的新面板中打开该链接。<br>` +
"若开启此项Excalidraw 会在主工作区的面板中打开该链接。",
"若开启此项Excalidraw 会在主工作区的面板中打开该链接。",
LINK_BRACKETS_NAME: "在链接的两侧显示 <code>[[中括号]]</code>",
LINK_BRACKETS_DESC: `${
"文本元素处于预览PREVIEW模式时在内部链接的两侧显示中括号。<br>" +
@@ -331,7 +444,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:
@@ -340,7 +453,7 @@ FILENAME_HEAD: "文件名",
`按住 ${labelCTRL()} 并点击含有 [[链接]] 或 [别名](链接) 的文本来打开链接`,
LINK_CTRL_CLICK_DESC:
"如果此功能影响到您使用某些原版 Excalidraw 功能,可将其关闭。" +
"关闭后,您只能通过绘图面板标题栏中的链接按钮来打开链接。",
"关闭后,您可以使用 ${labelCTRL()} + ${labelMETA()} 或者元素右上角的链接指示器来打开链接。",
TRANSCLUSION_WRAP_NAME: "MD-Transclusion 的折行方式",
TRANSCLUSION_WRAP_DESC:
"中的 number 表示嵌入的文本溢出时,在第几个字符处进行折行。<br>" +
@@ -369,17 +482,21 @@ FILENAME_HEAD: "文件名",
EMBED_TOEXCALIDRAW_DESC: "包括:以图像形式嵌入到绘图中的 PDF 文档、以交互形式嵌入到绘图中的 Markdown 文档MD-Embeddable、以图像形式嵌入的 Markdown 文档MD-Embed等。",
MD_HEAD: "以图像形式嵌入到绘图中的 Markdown 文档MD-Embed",
MD_EMBED_CUSTOMDATA_HEAD_NAME: "以交互形式嵌入到绘图中的 Markdown 文档MD-Embeddable",
MD_EMBED_CUSTOMDATA_HEAD_DESC: `这些选项不会影响到已存在的 MD-Embeddable。MD-Embeddable 的主题风格在“显示 & 行为”小节设置`,
MD_EMBED_CUSTOMDATA_HEAD_DESC: `以下设置只会影响以后的嵌入。已存在的嵌入保持不变。嵌入框的主题设置位于 “Excalidraw 外观和行为” 部分`,
MD_EMBED_SINGLECLICK_EDIT_NAME: "单击以编辑嵌入的 markdown。",
MD_EMBED_SINGLECLICK_EDIT_DESC:
"单击嵌入的 markdown 文件以进行编辑。 " +
"当此功能关闭时markdown 文件将首先以预览模式打开,然后在您再次单击时切换到编辑模式。",
MD_TRANSCLUDE_WIDTH_NAME: "MD-Embed 的默认宽度",
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:
@@ -400,11 +517,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 文件并保持与绘图文件状态同步的设置。`,
@@ -420,13 +537,17 @@ FILENAME_HEAD: "文件名",
EMBED_IMAGE_CACHE_NAME: "为嵌入到 Markdown 文档中的绘图创建预览图缓存",
EMBED_IMAGE_CACHE_DESC: "可提高下次嵌入的速度。" +
"但如果绘图中又嵌入了子绘图,当子绘图改变时,您需要打开子绘图并手动保存,才能够更新父绘图的预览图。",
SCENE_IMAGE_CACHE_NAME: "缓存场景中嵌套的 Excalidraw",
SCENE_IMAGE_CACHE_DESC: "缓存场景中嵌套的 Excalidraw 以加快场景渲染速度。这将加快渲染过程,特别是在您的场景中有深度嵌套的 Excalidraw 时。" +
"Excalidraw 将智能地尝试识别嵌套 Excalidraw 的子元素是否发生变化,并更新缓存。 " +
"如果您怀疑缓存未能正确更新,您可能需要关闭此功能。",
EMBED_IMAGE_CACHE_CLEAR: "清除缓存",
BACKUP_CACHE_CLEAR: "清除备份",
BACKUP_CACHE_CLEAR_CONFIRMATION: "该操作将删除所有绘图文件的备份。备份是绘图文件损坏时的一种补救手段。每次您打开 Obsidian 时,本插件会自动清理无用的备份。您确定要现在删除所有备份吗?",
EMBED_REUSE_EXPORTED_IMAGE_NAME:
"将之前已导出的图像作为预览图",
EMBED_REUSE_EXPORTED_IMAGE_DESC:
"该选项与自动导出 SVG/PNG 副本选项配合使用。如果嵌入到 Markdown 文档中的绘图文件存在同名的 SVG/PNG 副本,则将其作为预览图,而不再重新生成。<br>" +
"该选项与<a href='#"+TAG_AUTOEXPORT+"'>自动导出 SVG/PNG 副本</a>选项配合使用。如果嵌入到 Markdown 文档中的绘图文件存在同名的 SVG/PNG 副本,则将其作为预览图,而不再重新生成。<br>" +
"该选项能够提高 Markdown 文档的打开速度,尤其是当嵌入到 Markdown 文档中的绘图文件中含有大量图像或 MD-Embed 时。" +
"但是,该选项也可能导致预览图无法立即响应你对绘图文件或者 Obsidian 主题风格的修改。<br>" +
"该选项仅作用于嵌入到 Markdown 文档中的绘图。" +
@@ -439,7 +560,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>" +
@@ -449,17 +570,22 @@ FILENAME_HEAD: "文件名",
"嵌入到 Markdown 文档中的绘图的预览图的默认宽度。该选项也适用于鼠标悬停时浮现的预览图。<br>" +
"您可为某个要嵌入到 Markdown 文档中的绘图文件单独设置此项," +
"方法是修改相应的内部链接格式为形如 <code>![[drawing.excalidraw|100]]</code> 或 <code>[[drawing.excalidraw|100x100]]</code>。",
EMBED_HEIGHT_NAME: "预览图的默认高度",
EMBED_HEIGHT_DESC:
"嵌入到 Markdown 文档中的绘图的预览图得默认高度。该选项也适用于实时预览编辑和阅读模式,以及悬停预览。" +
"您可以在使用 <code>![[drawing.excalidraw|100]]</code> 或者 <code>[[drawing.excalidraw|100x100]]</code>" +
"格式在嵌入图像时指定自定义高度。",
EMBED_TYPE_NAME: "“嵌入绘图到当前 Markdown 文档中”系列命令的源文件类型",
EMBED_TYPE_DESC:
"在命令面板中执行“嵌入绘图到当前 Markdown 文档中”系列命令时,要嵌入绘图文件本身,还是嵌入其 PNG 或 SVG 副本。<br>" +
"如果您想选择 PNG 或 SVG 副本,需要先开启下方的“自动导出 PNG 副本”或“自动导出 SVG 副本。<br>" +
"如果您想选择 PNG 或 SVG 副本,需要先开启下方的<a href='#"+TAG_AUTOEXPORT+"'>自动导出 PNG / SVG 副本</a>。<br>" +
"如果您选择了 PNG 或 SVG 副本,当副本不存在时,该命令将会插入一条损坏的链接,您需要打开绘图文件并手动导出副本才能修复 —— " +
"也就是说,该选项不会自动帮您生成 PNG/SVG 副本,而只会引用已有的 PNG/SVG 副本。",
EMBED_MARKDOWN_COMMENT_NAME: "Embed link to drawing as comment",
EMBED_MARKDOWN_COMMENT_NAME: "将链接作为注释嵌入",
EMBED_MARKDOWN_COMMENT_DESC:
"Embed the link to the original Excalidraw file as a markdown link under the image, e.g.:<code>%%[[drawing.excalidraw]]%%</code>.<br>" +
"Instead of adding a markdown comment you may also select the embedded SVG or PNG line and use the command palette action: " +
"'<code>Excalidraw: Open Excalidraw drawing</code>' to open the drawing.",
"在图像下方以 Markdown 链接的形式嵌入原始 Excalidraw 文件的链接,例如:<code>%%[[drawing.excalidraw]]%%</code><br>" +
"除了添加 Markdown 注释之外,您还可以选择嵌入的 SVG PNG,并使用命令面板:" +
"'<code>Excalidraw: 打开 Excalidraw 绘图</code>'来打开该绘图",
EMBED_WIKILINK_NAME: "“嵌入绘图到当前 Markdown 文档中”系列命令产生的内部链接类型",
EMBED_WIKILINK_DESC:
"<b>开启:</b>将产生 <code>![[Wiki 链接]]</code>。<b>关闭:</b>将产生 <code>![](Markdown 链接)</code>。",
@@ -470,13 +596,17 @@ 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: "导出的图像匹配主题",
EXPORT_THEME_DESC:
"导出与绘图的黑暗/明亮主题匹配的图像。" +
"如果关闭,在黑暗主题下导出的图像将和明亮主题一样。",
EXPORT_EMBED_SCENE_NAME: "在导出的图片中嵌入场景",
EXPORT_EMBED_SCENE_DESC:
"在导出的图像中嵌入 Excalidraw 场景。可以通过在文件级别添加 <code>excalidraw-export-embed-scene: true/false</code> frontmatter 元数据键来覆盖此设置。" +
"此设置仅在您下次(重新)打开绘图时生效。",
EXPORT_HEAD: "导出设置",
EXPORT_SYNC_NAME:
"保持 SVG/PNG 文件名与绘图文件同步",
@@ -497,7 +627,19 @@ FILENAME_HEAD: "文件名",
EXPORT_BOTH_DARK_AND_LIGHT_DESC: "若开启Excalidraw 将导出两个文件filename.dark.png或 filename.dark.svg和 filename.light.png或 filename.light.svg。<br>"+
"该选项可作用于“自动导出 SVG 副本”、“自动导出 PNG 副本”,以及其他的手动的导出命令。",
COMPATIBILITY_HEAD: "兼容性设置",
COMPATIBILITY_DESC: "如果没有特殊原因(例如您想同时在 VSCode / Logseq 和 Obsidian 中使用 Excalidraw建议您使用 markdown 格式的绘图文件,而不是旧的 excalidraw.com 格式,因为本插件的很多功能在旧格式中无法使用。",
COMPATIBILITY_DESC: "如果没有特殊原因(例如您想同时在 VSCode / Logseq 和 Obsidian 中使用 Excalidraw建议您使用 Markdown 格式的绘图文件,而不是旧的 excalidraw.com 格式,因为本插件的很多功能在旧格式中无法使用。",
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> 上提出的问题中包含控制台日志。",
SLIDING_PANES_NAME: "Sliding panes 插件支持",
SLIDING_PANES_DESC:
"设置此项后需要重启 Obsidian 才能生效。<br>" +
@@ -525,8 +667,13 @@ FILENAME_HEAD: "文件名",
LATEX_DEFAULT_NAME: "插入 LaTeX 时的默认表达式",
LATEX_DEFAULT_DESC: "允许留空。允许使用类似 <code>\\color{white}</code> 的格式化表达式。",
NONSTANDARD_HEAD: "非 Excalidraw.com 官方支持的特性",
NONSTANDARD_DESC: `这些特性不受 Excalidraw.com 官方支持。如果 Excalidraw.com 格式导出绘图,这些特性将会发生不可预知的变化。
NONSTANDARD_DESC: `这些特性不受 Excalidraw.com 官方支持。如果 Excalidraw.com 导入绘图,这些特性将会发生不可预知的变化。
包括:自定义画笔工具的数量,自定义字体等。`,
RENDER_TWEAK_HEAD: "渲染优化",
MAX_IMAGE_ZOOM_IN_NAME: "最大图像放大倍数",
MAX_IMAGE_ZOOM_IN_DESC: "为了节省内存,并且因为 Apple Safari (Obsidian on iOS) 有一些硬编码的限制Excalidraw.com 在放大时会限制图像和大型对象的最大分辨率。您可以使用乘数来覆盖这个限制。" +
"这意味着将乘以 Excalidraw 默认设置的限制,乘数越大,图像放大分辨率就越高,但也会消耗更多内存。" +
"我建议尝试多个值来设置这个参数。当您放大一个较大的 PNG 图像时,如果图像突然从视图中消失,那就说明您已经达到了极限。默认值为 1。此设置对 iOS 无效。",
CUSTOM_PEN_HEAD: "自定义画笔",
CUSTOM_PEN_NAME: "自定义画笔工具的数量",
CUSTOM_PEN_DESC: "在画布上的 Obsidian 菜单按钮旁边切换自定义画笔。长按画笔按钮可以修改其样式。",
@@ -561,7 +708,17 @@ FILENAME_HEAD: "文件名",
"开启此项,则可在 Obsidian 实时预览模式的编辑视图下,用形如 <code>![[绘图|宽度|样式]]</code> 的语法来嵌入绘图。<br>" +
"该选项不会在已打开的文档中立刻生效 —— " +
"你需要重新打开此文档来使其生效。",
CUSTOM_FONT_HEAD: "自定义字体",
FADE_OUT_EXCALIDRAW_MARKUP_NAME: "淡化 Excalidraw 标记",
FADE_OUT_EXCALIDRAW_MARKUP_DESC: "在 Markdown 视图模式下,在 Markdown 注释 %% " +
"之后的部分会淡化。文本仍然存在,但视觉杂乱感会减少。请注意,您可以将 %% 放在 # Text Elements 行的上一行," +
"这样,整个 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:
"开启此项后,文本元素的属性面板里会多出一个本地字体按钮。<br>" +
@@ -570,7 +727,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 自动化脚本包含设置项,当执行这些脚本时,它们会在该列表下添加设置项。",
@@ -581,16 +738,22 @@ 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 服务)您可以" +
"到 <a href='https://www.taskbone.com/' target='_blank'>taskbone.com</a> 购买一个商用 API key。购买后请将它填写到旁边这个文本框里替换掉原本自动生成的免费 API key。",
//HotkeyEditor
HOTKEY_PRESS_COMBO_NANE: "按下您的组合键",
HOTKEY_PRESS_COMBO_DESC: "请按下所需的组合键",
HOTKEY_BUTTON_ADD_OVERRIDE: "添加新的(热键)覆写",
HOTKEY_BUTTON_REMOVE: "移除",
//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: "选择要插入(以内部链接形式嵌入)到当前绘图中的文件。",
@@ -605,6 +768,13 @@ FILENAME_HEAD: "文件名",
PDF_PAGES_HEADER: "页码范围",
PDF_PAGES_DESC: "示例1, 3-5, 7, 9-11",
//SelectCard.ts
TYPE_SECTION: "输入章节名称(标题)进行选择",
SELECT_SECTION_OR_TYPE_NEW:
"选择现有章节(标题)或输入新章节(标题)的名称,然后按 Enter。",
INVALID_SECTION_NAME: "无效的章节名称(标题)",
EMPTY_SECTION_MESSAGE: "输入章节(标题)名称以创建",
//EmbeddedFileLoader.ts
INFINITE_LOOP_WARNING:
"EXCALIDRAW 警告\n停止加载嵌入的图像因为此文件中存在死循环\n",
@@ -624,7 +794,7 @@ FILENAME_HEAD: "文件名",
TOGGLE_FRAME_RENDERING: "开启或关闭框架渲染",
TOGGLE_FRAME_CLIPPING: "开启或关闭框架裁剪",
OPEN_LINK_CLICK: "打开所选的图形或文本元素里的链接",
OPEN_LINK_PROPS: "编辑所选 MD-Embed 的内部链接,或者打开所选的图形或文本元素里的链接",
OPEN_LINK_PROPS: "打开图像链接或 LaTeX 公式编辑器",
//IFrameActionsMenu.tsx
NARROW_TO_HEADING: "缩放至标题",
@@ -677,11 +847,31 @@ FILENAME_HEAD: "文件名",
PROMPT_BUTTON_INSERT_SPACE: "插入空格",
PROMPT_BUTTON_INSERT_LINK: "插入内部链接",
PROMPT_BUTTON_UPPERCASE: "大写",
PROMPT_SELECT_TEMPLATE: "选择一个模板",
//ModifierKeySettings
WEB_BROWSER_DRAG_ACTION: "从浏览器拖进来时",
LOCAL_FILE_DRAG_ACTION: "从本地文件系统拖进来时",
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: "启用框架裁剪",
//InsertPDFModal.ts
IPM_PAGES_TO_IMPORT_NAME: "要导入的页面",
IPM_SELECT_PAGES_TO_IMPORT: "请选择页面以进行导入",
IPM_ADD_BORDER_BOX_NAME: "添加带边框的盒子容器",
IPM_ADD_FRAME_NAME: "添加页面到框架",
IPM_ADD_FRAME_DESC: "为了更方便的操作,我建议将页面锁定在框架内。" +
"如果,你将锁定页面在框架内,则唯一的解锁方法是右键点击框架,选择‘从框架中移除元素’,然后解锁页面。",
IPM_GROUP_PAGES_NAME: "建立页面组",
IPM_GROUP_PAGES_DESC: "这将把所有页面建立为一个单独的组。如果您在导入后锁定页面,建议使用此方法,因为这样可以更方便地解锁整个组,而不是逐个解锁。",
IPM_SELECT_PDF: "请选择一个 PDF 文件",
};

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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}, storeAction: "update"});
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}, storeAction: "update"});
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>

View File

@@ -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: {
@@ -34,11 +34,12 @@ export const setPen = (pen: PenStyle, api: any) => {
currentItemRoughness: st.currentItemRoughness,
}}
: null,
}
},
storeAction: "update",
})
}
export const resetStrokeOptions = (resetCustomPen:any, api: ExcalidrawImperativeAPI, clearCurrentStrokeOptions: boolean) => {
export function resetStrokeOptions (resetCustomPen:any, api: ExcalidrawImperativeAPI, clearCurrentStrokeOptions: boolean) {
api.updateScene({
appState: {
...resetCustomPen ? {
@@ -50,13 +51,16 @@ export const resetStrokeOptions = (resetCustomPen:any, api: ExcalidrawImperative
}: null,
resetCustomPen: null,
...clearCurrentStrokeOptions ? {currentStrokeOptions: null} : null,
}
},
storeAction: "update",
});
}
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 +69,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 +168,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 +204,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 +223,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 +242,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 +257,7 @@ export class ObsidianMenu {
)
}
renderButton = (isMobile: boolean, appState: AppState) => {
public renderButton (isMobile: boolean, appState: AppState) {
return (
<>
<label
@@ -248,12 +268,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 +282,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 +293,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;
}
}

View File

@@ -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>

View File

@@ -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> {

View File

@@ -2,18 +2,20 @@ import {
App,
ButtonComponent,
DropdownComponent,
getIcon,
Modifier,
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 +38,9 @@ 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 { Rank } from "./menu/ActionIcons";
import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/constants/constSettingsTags";
import { HotkeyEditor } from "./dialogs/HotkeyEditor";
export interface ExcalidrawSettings {
folder: string;
@@ -47,8 +52,8 @@ 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;
autosaveInterval: number;
autosaveIntervalDesktop: number;
autosaveIntervalMobile: number;
drawingFilenamePrefix: string;
@@ -75,8 +80,10 @@ export interface ExcalidrawSettings {
matchThemeTrigger: boolean;
defaultMode: string;
defaultPenMode: "never" | "mobile" | "always";
penModeDoubleTapEraser: boolean;
penModeCrosshairVisible: boolean;
renderImageInMarkdownReadingMode: boolean,
renderImageInHoverPreviewForMDNotes: boolean,
renderImageInMarkdownToPDF: boolean,
allowPinchZoom: boolean;
allowWheelZoom: boolean;
@@ -120,8 +127,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;
@@ -161,6 +171,7 @@ export interface ExcalidrawSettings {
numberOfCustomPens: number;
pdfScale: number;
pdfBorderBox: boolean;
pdfFrame: boolean;
pdfGapSize: number;
pdfGroupPages: boolean;
pdfLockAfterImport: boolean;
@@ -174,6 +185,7 @@ export interface ExcalidrawSettings {
COLOR: string,
};
embeddableMarkdownDefaults: EmbeddableMDCustomProps;
markdownNodeOneClickEditing: boolean;
canvasImmersiveEmbed: boolean,
startupScriptPath: string,
openAIAPIToken: string,
@@ -193,6 +205,9 @@ export interface ExcalidrawSettings {
longPressDesktop: number;
longPressMobile: number;
isDebugMode: boolean;
rank: Rank;
modifierKeyOverrides: {modifiers: Modifier[], key: string}[];
showSplashscreen: boolean;
}
declare const PLUGIN_VERSION:string;
@@ -207,8 +222,8 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
compress: true,
decompressForMDView: false,
onceOffCompressFlagReset: false,
onceOffGPTVersionReset: false,
autosave: true,
autosaveInterval: 15000,
autosaveIntervalDesktop: 15000,
autosaveIntervalMobile: 10000,
drawingFilenamePrefix: "Drawing ",
@@ -235,8 +250,10 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
matchThemeTrigger: false,
defaultMode: "normal",
defaultPenMode: "never",
penModeDoubleTapEraser: true,
penModeCrosshairVisible: true,
renderImageInMarkdownReadingMode: false,
renderImageInHoverPreviewForMDNotes: false,
renderImageInMarkdownToPDF: false,
allowPinchZoom: false,
allowWheelZoom: false,
@@ -279,8 +296,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,
@@ -327,6 +347,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
numberOfCustomPens: 0,
pdfScale: 4,
pdfBorderBox: true,
pdfFrame: false,
pdfGapSize: 20,
pdfGroupPages: false,
pdfLockAfterImport: true,
@@ -350,11 +371,12 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
borderOpacity: 0,
filenameVisible: false,
},
markdownNodeOneClickEditing: false,
canvasImmersiveEmbed: true,
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",
@@ -445,6 +467,13 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
longPressDesktop: 500,
longPressMobile: 500,
isDebugMode: false,
rank: "Bronze",
modifierKeyOverrides: [
{modifiers: ["Mod"], key:"Enter"},
{modifiers: ["Mod"], key:"k"},
{modifiers: ["Mod"], key:"G"},
],
showSplashscreen: true,
};
export class ExcalidrawSettingTab extends PluginSettingTab {
@@ -453,6 +482,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
private requestReloadDrawings: boolean = false;
private requestUpdatePinnedPens: boolean = false;
private requestUpdateDynamicStyling: boolean = false;
private hotkeyEditor: HotkeyEditor;
//private reloadMathJax: boolean = false;
//private applyDebounceTimer: number = 0;
@@ -479,21 +509,25 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}
this.plugin.saveSettings();
if (this.requestUpdatePinnedPens) {
app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> {
this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> {
if (v.view instanceof ExcalidrawView) v.view.updatePinnedCustomPens()
})
}
if (this.requestUpdateDynamicStyling) {
app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> {
this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> {
if (v.view instanceof ExcalidrawView) {
setDynamicStyle(this.plugin.ea,v.view,v.view.previousBackgroundColor,this.plugin.settings.dynamicStyling);
}
})
}
this.hotkeyEditor.unload();
if (this.hotkeyEditor.isDirty) {
this.plugin.registerHotkeyOverrides();
}
if (this.requestReloadDrawings) {
const exs =
app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
for (const v of exs) {
if (v.view instanceof ExcalidrawView) {
await v.view.save(false);
@@ -533,6 +567,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
@@ -695,9 +769,6 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.autosaveIntervalDesktop.toString())
.onChange(async (value) => {
this.plugin.settings.autosaveIntervalDesktop = parseInt(value);
this.plugin.settings.autosaveInterval = DEVICE.isMobile
? this.plugin.settings.autosaveIntervalMobile
: this.plugin.settings.autosaveIntervalDesktop;
this.applySettingsUpdate();
}),
);
@@ -714,9 +785,6 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.autosaveIntervalMobile.toString())
.onChange(async (value) => {
this.plugin.settings.autosaveIntervalMobile = parseInt(value);
this.plugin.settings.autosaveInterval = DEVICE.isMobile
? this.plugin.settings.autosaveIntervalMobile
: this.plugin.settings.autosaveIntervalDesktop;
this.applySettingsUpdate();
}),
);
@@ -977,6 +1045,17 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}),
);
new Setting(detailsEl)
.setName(t("DISABLE_DOUBLE_TAP_ERASER_NAME"))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.penModeDoubleTapEraser)
.onChange(async (value) => {
this.plugin.settings.penModeDoubleTapEraser = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_NAME"))
.setDesc(fragWithHTML(t("SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_DESC")))
@@ -989,7 +1068,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}),
);
new Setting(detailsEl)
const readingModeEl = new Setting(detailsEl)
.setName(t("SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME"))
.setDesc(fragWithHTML(t("SHOW_DRAWING_OR_MD_IN_READING_MODE_DESC")))
.addToggle((toggle) =>
@@ -1000,7 +1079,20 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.applySettingsUpdate();
}),
);
readingModeEl.nameEl.setAttribute("id",TAG_MDREADINGMODE);
new Setting(detailsEl)
.setName(t("SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_NAME"))
.setDesc(fragWithHTML(t("SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.renderImageInHoverPreviewForMDNotes)
.onChange(async (value) => {
this.plugin.settings.renderImageInHoverPreviewForMDNotes = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("LEFTHANDED_MODE_NAME"))
.setDesc(fragWithHTML(t("LEFTHANDED_MODE_DESC")))
@@ -1018,6 +1110,29 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
);
addIframe(detailsEl, "H8Njp7ZXYag",999);
new Setting(detailsEl)
.setName(t("TOGGLE_SPLASHSCREEN"))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.showSplashscreen)
.onChange((value)=> {
this.plugin.settings.showSplashscreen = value;
this.applySettingsUpdate();
})
)
detailsEl = displayDetailsEl.createEl("details");
detailsEl.createEl("summary", {
text: t("HOTKEY_OVERRIDE_HEAD"),
cls: "excalidraw-setting-h3",
});
detailsEl.createEl("span", {}, (el) => {
el.innerHTML = t("HOTKEY_OVERRIDE_DESC");
});
this.hotkeyEditor = new HotkeyEditor(detailsEl, this.plugin.settings, this.applySettingsUpdate);
this.hotkeyEditor.onload();
detailsEl = displayDetailsEl.createEl("details");
detailsEl.createEl("summary", {
text: t("THEME_HEAD"),
@@ -1778,7 +1893,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
});
addIframe(detailsEl, "wTtaXmRJ7wg",171);
new Setting(detailsEl)
const pdfExportEl = new Setting(detailsEl)
.setName(t("SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME"))
.setDesc(fragWithHTML(t("SHOW_DRAWING_OR_MD_IN_EXPORTPDF_DESC")))
.addToggle((toggle) =>
@@ -1789,6 +1904,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.applySettingsUpdate();
}),
);
pdfExportEl.nameEl.setAttribute("id",TAG_PDFEXPORT);
new Setting(detailsEl)
.setName(t("EXPORT_EMBED_SCENE_NAME"))
@@ -1928,6 +2044,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
text: t("EXPORT_HEAD"),
cls: "excalidraw-setting-h4",
});
detailsEl.setAttribute("id",TAG_AUTOEXPORT);
new Setting(detailsEl)
.setName(t("EXPORT_SYNC_NAME"))
@@ -2048,7 +2165,26 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
text: t("MD_EMBED_CUSTOMDATA_HEAD_NAME"),
cls: "excalidraw-setting-h3",
});
detailsEl.createEl("span", {text: t("MD_EMBED_CUSTOMDATA_HEAD_DESC")});
new Setting(detailsEl)
.setName(t("MD_EMBED_SINGLECLICK_EDIT_NAME"))
.setDesc(fragWithHTML(t("MD_EMBED_SINGLECLICK_EDIT_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.markdownNodeOneClickEditing)
.onChange(async (value) => {
this.plugin.settings.markdownNodeOneClickEditing = value;
this.applySettingsUpdate();
})
);
detailsEl.createEl("hr", { cls: "excalidraw-setting-hr" });
detailsEl.createEl("span", {}, (el) => {
el.innerHTML = t("MD_EMBED_CUSTOMDATA_HEAD_DESC");
});
new EmbeddalbeMDFileCustomDataSettingsComponent(
detailsEl,
@@ -2125,7 +2261,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);
});
@@ -2211,7 +2347,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) => {
@@ -2369,6 +2505,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"),
@@ -2497,6 +2645,31 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
cls: "excalidraw-setting-h1",
});
new Setting(detailsEl)
.setName(t("DUMMY_TEXT_ELEMENT_LINT_SUPPORT_NAME"))
.setDesc(fragWithHTML(t("DUMMY_TEXT_ELEMENT_LINT_SUPPORT_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.addDummyTextElement)
.onChange((value) => {
this.plugin.settings.addDummyTextElement = value;
this.applySettingsUpdate();
}),
);
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"))

View File

@@ -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();
}

View File

@@ -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,

View File

@@ -1,82 +1,84 @@
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;
pdfjsLib: any;
}
}
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 }> };
}
}

4
src/types/worker.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module "web-worker:*" {
const WorkerFactory: new (options: any) => Worker;
export default WorkerFactory;
}

View File

@@ -249,4 +249,15 @@ export const extractCodeBlocks = (markdown: string): { data: string, type: strin
}
return result;
}
}
export const errorHTML = (message: string) => `<html>
<body style="margin: 0; text-align: center">
<div style="display: flex; align-items: center; justify-content: center; flex-direction: column; height: 100vh; padding: 0 60px">
<div style="color:red">There was an error during generation</div>
</br>
</br>
<div>${message}</div>
</div>
</body>
</html>`;

View File

@@ -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;
}
}

View File

@@ -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;
}

View 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');

View File

@@ -10,9 +10,33 @@ export let DEBUGGING = false;
export const log = console.log.bind(window.console);
export const debug = (fn: Function, fnName: string, ...messages: unknown[]) => {
console.log(fnName,fn,...messages);
//console.log(fnName,fn,...messages);
console.log(fnName, ...messages);
};
let timestamp: number[] = [];
let tsOrigin: number = 0;
export function tsInit(msg: string) {
tsOrigin = Date.now();
timestamp = [tsOrigin, tsOrigin, tsOrigin, tsOrigin, tsOrigin]; // Initialize timestamps for L0 to L4
console.log("0ms: " + msg);
}
export function ts(msg: string, level: number) {
if (level < 0 || level > 4) {
console.error("Invalid level. Please use level 0, 1, 2, 3, or 4.");
return;
}
const now = Date.now();
const diff = now - timestamp[level];
timestamp[level] = now;
const elapsedFromOrigin = now - tsOrigin;
console.log(`L${level} (${elapsedFromOrigin}ms) ${diff}ms: ${msg}`);
}
export class CustomMutationObserver {
private originalCallback: MutationCallback;
private observer: MutationObserver | null;

View File

@@ -1,8 +1,8 @@
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
import { ColorMaster } from "colormaster";
import { ColorMaster } from "@zsviczian/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";
@@ -16,18 +16,16 @@ export const setDynamicStyle = (
) => {
if(dynamicStyle === "none") {
view.excalidrawContainer?.removeAttribute("style");
setTimeout(()=>view.updateScene({appState:{dynamicStyle: ""}}));
setTimeout(()=>view.updateScene({appState:{dynamicStyle: ""}, storeAction: "update"}));
const toolspanel = view.toolsPanelRef?.current?.containerRef?.current;
if(toolspanel) {
let toolsStyle = toolspanel.getAttribute("style");
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";
@@ -76,6 +74,7 @@ export const setDynamicStyle = (
const str = (cm: ColorMaster) => cm.stringHEX({alpha:false});
const styleObject:{[x: string]: string;} = {
['backgroundColor']: str(cmBG()),
[`--color-primary`]: str(accent()),
[`--color-surface-low`]: str(gray1()),
[`--color-surface-mid`]: str(gray1()),
@@ -115,7 +114,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 +128,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)),
@@ -156,13 +168,12 @@ export const setDynamicStyle = (
appState:{
frameColor,
dynamicStyle: styleObject
}
},
storeAction: "update",
});
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);
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,45 @@
import { ExcalidrawArrowElement, ExcalidrawElement, ExcalidrawTextElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
export function updateElementIdsInScene(
{elements: sceneElements}: {elements: Mutable<ExcalidrawElement>[]},
elementToChange: Mutable<ExcalidrawElement>,
newID: string
) {
if(elementToChange.type === "text") {
const textElement = elementToChange as Mutable<ExcalidrawTextElement>;
if(textElement.containerId) {
const containerEl = sceneElements.find(el=>el.id === textElement.containerId) as unknown as Mutable<ExcalidrawElement>;
containerEl.boundElements?.filter(x=>x.id === textElement.id).forEach( x => {
(x.id as Mutable<string>) = newID;
});
}
}
if(elementToChange.boundElements?.length>0) {
elementToChange.boundElements.forEach( binding => {
const boundEl = sceneElements.find(el=>el.id === binding.id) as unknown as Mutable<ExcalidrawElement>;
boundEl.boundElements?.filter(x=>x.id === elementToChange.id).forEach( x => {
(x.id as Mutable<string>) = newID;
});
if(boundEl.type === "arrow") {
const arrow = boundEl as Mutable<ExcalidrawArrowElement>;
if(arrow.startBinding?.elementId === elementToChange.id) {
arrow.startBinding.elementId = newID;
}
if(arrow.endBinding?.elementId === elementToChange.id) {
arrow.endBinding.elementId = newID;
}
}
});
}
if(elementToChange.type === "frame") {
sceneElements.filter(el=>el.frameId === elementToChange.id).forEach(x => {
(x.frameId as Mutable<string>) = newID;
});
}
elementToChange.id = newID;
}

View File

@@ -1,19 +1,26 @@
import { MAX_IMAGE_SIZE, IMAGE_TYPES, ANIMATED_IMAGE_TYPES } from "src/constants/constants";
import { App, TFile } from "obsidian";
import { MAX_IMAGE_SIZE, IMAGE_TYPES, ANIMATED_IMAGE_TYPES, MD_EX_SECTIONS } from "src/constants/constants";
import { App, Notice, TFile, WorkspaceLeaf } from "obsidian";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "src/ExcalidrawData";
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK, getExcalidrawMarkdownHeaderSection, REGEX_TAGS } from "src/ExcalidrawData";
import ExcalidrawView from "src/ExcalidrawView";
import { ExcalidrawElement, ExcalidrawFrameElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { getLinkParts } from "./Utils";
import { getEmbeddedFilenameParts, getLinkParts, isImagePartRef } from "./Utils";
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> => {
ea.clear();
shouldInsertToView: boolean = true,
):Promise<string> {
if(shouldInsertToView) {ea.clear();}
ea.style.strokeColor = "transparent";
ea.style.backgroundColor = "transparent";
const api = ea.getExcalidrawAPI();
@@ -24,21 +31,22 @@ export const insertImageToView = async (
file,
scale,
);
await ea.addElementsToView(false, true, true);
if(shouldInsertToView) {await ea.addElementsToView(false, true, true);}
return id;
}
export const insertEmbeddableToView = async (
export async function insertEmbeddableToView (
ea: ExcalidrawAutomate,
position: { x: number, y: number },
file?: TFile,
link?: string,
):Promise<string> => {
ea.clear();
shouldInsertToView: boolean = true,
):Promise<string> {
if(shouldInsertToView) {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, undefined, shouldInsertToView);
} else {
const id = ea.addEmbeddable(
position.x,
@@ -48,12 +56,12 @@ export const insertEmbeddableToView = async (
link,
file,
);
await ea.addElementsToView(false, true, true);
if(shouldInsertToView) {await ea.addElementsToView(false, true, true);}
return id;
}
}
export const getLinkTextFromLink = (text: string): string => {
export function getLinkTextFromLink (text: string): string {
if (!text) return;
if (text.match(REG_LINKINDEX_HYPERLINK)) return;
@@ -66,42 +74,93 @@ export const getLinkTextFromLink = (text: string): string => {
return linktext;
}
export const openTagSearch = (link:string, app: App, view?: ExcalidrawView) => {
const tags = link
.matchAll(/#([\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+)/gu)
.next();
if (!tags.value || tags.value.length < 2) {
export function openTagSearch(link: string, app: App, view?: ExcalidrawView) {
const tags = REGEX_TAGS.getResList(link);
if (!tags.length || !tags[0].value || tags[0].value.length < 2) {
return;
}
const search = app.workspace.getLeavesOfType("search");
if (search.length == 0) {
if (search.length === 0) {
return;
}
//@ts-ignore
search[0].view.setQuery(`tag:${tags.value[1]}`);
search[0].view.setQuery(`tag:${tags[0].value[1]}`);
app.workspace.revealLeaf(search[0]);
if (view && view.isFullscreen()) {
view.exitFullscreen();
}
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) {
@@ -120,7 +179,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)=>{
@@ -129,4 +191,200 @@ export const getFrameBasedOnFrameNameOrId = (frameName: string, elements: Excali
.filter((item:any) => item.id === frameName || item.name === frameName)
.map((item:any)=>item.el as ExcalidrawFrameElement);
return frames.length === 1 ? frames[0] : null;
}
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];
const shouldAddHashtag = body && body.startsWith("%%");
const hastag = header.match(/#\n+$/m);
const shouldRemoveTrailingHashtag = Boolean(hastag);
view.data = data.replace(
header,
(shouldRemoveTrailingHashtag
? header.substring(0,header.length-hastag[0].length)
: header) +
`\n# ${title}\n\n${cardBody ? cardBody+"\n\n" : ""}${
shouldAddHashtag || shouldRemoveTrailingHashtag ? "#\n" : ""}`);
await view.forceSave(true);
let watchdog = 0;
await sleep(200);
let found:string;
while (watchdog++ < 10 && !(found=(await view.app.metadataCache.blockCache
.getForFile({ isCancelled: () => false },view.file))
.blocks.filter((b: any) => b.display && b.node?.type === "heading")
.filter((b: any) => !MD_EX_SECTIONS.includes(b.display))
.map((b: any) => cleanSectionHeading(b.display))
.find((b: any) => b === title))) {
await sleep(200);
}
const ea = getEA(view) as ExcalidrawAutomate;
const id = ea.addEmbeddable(
0,0,400,500,
`[[${view.file.path}#${title}]]`,
undefined,
embeddableCustomData
);
await ea.addElementsToView(true, false, true);
const api = view.excalidrawAPI as ExcalidrawImperativeAPI;
const el = ea.getViewElements().find(el=>el.id === id);
api.selectElements([el]);
if(activate) {
window.setTimeout(()=>{
api.updateScene({appState: {activeEmbeddable: {element: el, state: "active"}}, storeAction: "update"});
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) {
if(link?.split("#")[1] && !isImagePartRef(getEmbeddedFilenameParts(link))) {
return false;
}
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;
}

View File

@@ -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;
@@ -290,9 +291,7 @@ export const blobToBase64 = async (blob: Blob): Promise<string> => {
}
export const getPDFDoc = async (f: TFile): Promise<any> => {
//@ts-ignore
if(typeof window.pdfjsLib === "undefined") await loadPdfJs();
//@ts-ignore
return await window.pdfjsLib.getDocument(app.vault.getResourcePath(f)).promise;
}

View File

@@ -21,11 +21,13 @@ export type ImageKey = {
previewImageType: PreviewImageType;
scale: number;
isTransparent: boolean;
inlineFonts: boolean;
} & FILENAMEPARTS;
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.inlineFonts}#${
key.previewImageType === PreviewImageType.SVGIMG
? 1
: key.previewImageType === PreviewImageType.PNG
@@ -43,6 +45,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 +144,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 +174,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 < 12; // introduced hasGroupref, etc. in 1.9.28 // introduced hasClippedFrameref in 2.2.10 //introduced inlineFonts 2.2.11
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;
@@ -231,16 +253,16 @@ class ImageCache {
});
}
private async getObjectStore(mode: IDBTransactionMode, storeName: string): Promise<IDBObjectStore> {
private getObjectStore(mode: IDBTransactionMode, storeName: string): IDBObjectStore {
const transaction = this.db!.transaction(storeName, mode);
return transaction.objectStore(storeName);
}
private async getCacheData(key: string): Promise<FileCacheData | null> {
const store = await this.getObjectStore("readonly", this.cacheStoreName);
const store = this.getObjectStore("readonly", this.cacheStoreName);
const request = store.get(key);
return new Promise<FileCacheData | null>((resolve, reject) => {
return await new Promise<FileCacheData | null>((resolve, reject) => {
request.onsuccess = () => {
const result = request.result as FileCacheData;
resolve(result || null);
@@ -253,7 +275,7 @@ class ImageCache {
}
private async getBackupData(key: BackupKey): Promise<BackupData | null> {
const store = await this.getObjectStore("readonly", this.backupStoreName);
const store = this.getObjectStore("readonly", this.backupStoreName);
const request = store.get(key);
return new Promise<BackupData | null>((resolve, reject) => {
@@ -286,7 +308,9 @@ class ImageCache {
? await this.getCacheData(key)
: await Promise.race([
this.getCacheData(key),
new Promise<undefined>((_,reject) => setTimeout(() => reject(undefined), 100))
new Promise<undefined>((_,reject) => setTimeout(() => {
reject(undefined);
}, 100))
]);
this.fullyInitialized = true;
if(!cachedData) return undefined;
@@ -321,6 +345,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

View File

@@ -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");
}

View File

@@ -1,3 +1,4 @@
import { Modifier } from "obsidian";
import { DEVICE } from "src/constants/constants";
import { ExcalidrawSettings } from "src/settings";
export type ModifierKeys = {shiftKey:boolean, ctrlKey: boolean, metaKey: boolean, altKey: boolean};
@@ -177,4 +178,26 @@ export const emulateKeysForLinkClick = (action: PaneTarget): ModifierKeys => {
export const anyModifierKeysPressed = (e: ModifierKeys): boolean => {
return e.shiftKey || e.ctrlKey || e.metaKey || e.altKey;
}
export function modifierLabel(modifiers: Modifier[], platform?: "Mac" | "Other"): string {
const isMacPlatform = platform === "Mac" ||
(platform === undefined && (DEVICE.isIOS || DEVICE.isMacOS));
return modifiers.map(modifier => {
switch (modifier) {
case "Mod":
return isMacPlatform ? "CMD" : "CTRL";
case "Ctrl":
return "CTRL";
case "Meta":
return isMacPlatform ? "CMD" : "WIN";
case "Shift":
return "SHIFT";
case "Alt":
return isMacPlatform ? "OPTION" : "ALT";
default:
return modifier;
}
}).join("+");
}

View File

@@ -3,13 +3,14 @@ import {
Editor,
FrontMatterCache,
MarkdownView,
normalizePath, OpenViewState, parseFrontMatterEntry, TFile, View, Workspace, WorkspaceLeaf, WorkspaceSplit
normalizePath, OpenViewState, parseFrontMatterEntry, TFile, View, ViewState, Workspace, WorkspaceLeaf, WorkspaceSplit
} from "obsidian";
import ExcalidrawPlugin from "../main";
import { checkAndCreateFolder, splitFolderAndFilename } from "./FileUtils";
import { linkClickModifierType, ModifierKeys } from "./ModifierkeyHelper";
import { REG_BLOCK_REF_CLEAN, REG_SECTION_REF_CLEAN } from "src/constants/constants";
import { REG_BLOCK_REF_CLEAN, REG_SECTION_REF_CLEAN, VIEW_TYPE_EXCALIDRAW } from "src/constants/constants";
import yaml, { Mark } from "js-yaml";
import { debug, DEBUGGING } from "./DebugHelper";
export const getParentOfClass = (element: Element, cssClass: string):HTMLElement | null => {
let parent = element.parentElement;
@@ -300,7 +301,7 @@ export const openLeaf = ({
return {leaf, promise};
}
export const mergeMarkdownFiles = (template: string, target: string): string => {
export function mergeMarkdownFiles (template: string, target: string): string {
// Extract frontmatter from the template
const templateFrontmatterEnd = template.indexOf('---', 4); // Find end of frontmatter
const templateFrontmatter = template.substring(4, templateFrontmatterEnd).trim();
@@ -312,8 +313,8 @@ export const mergeMarkdownFiles = (template: string, target: string): string =>
// Extract frontmatter from the target if it exists
let targetFrontmatterObj: FrontMatterCache = {};
let targetContent = '';
if (target.includes('---')) {
const targetFrontmatterEnd = target.indexOf('---', 4); // Find end of frontmatter
if (target.startsWith('---\n') && target.indexOf('---\n', 4) > 0) {
const targetFrontmatterEnd = target.indexOf('---\n', 4); // Find end of frontmatter
const targetFrontmatter = target.substring(4, targetFrontmatterEnd).trim();
targetContent = target.substring(targetFrontmatterEnd + 3); // Skip frontmatter and ---
@@ -392,3 +393,20 @@ export const foldExcalidrawSection = (view: MarkdownView) => {
view.currentMode.applyFoldInfo({ folds: foldPositions, lines: lineCount });
}
};
export async function setExcalidrawView(leaf: WorkspaceLeaf) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(setExcalidrawView,`setExcalidrawView`, leaf);
await leaf.setViewState({
type: VIEW_TYPE_EXCALIDRAW,
state: leaf.view.getState(),
popstate: true,
} as ViewState);
}
export async function closeLeafView(leaf: WorkspaceLeaf) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(setExcalidrawView,`setExcalidrawView`, leaf);
await leaf.setViewState({
type: "empty",
state: {},
});
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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,8 @@ 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';
import { runCompressionWorker } from "src/workers/compression-worker";
declare const PLUGIN_VERSION:string;
declare var LZString: any;
@@ -50,8 +45,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 +57,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 +81,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 +156,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 +178,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 +226,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("&nbsp;", " "))),
)}`;
};
export const getBinaryFileFromDataURL = async (dataURL: string): Promise<ArrayBuffer> => {
export async function getBinaryFileFromDataURL (dataURL: string): Promise<ArrayBuffer> {
if (!dataURL) {
return null;
}
@@ -262,12 +265,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 +291,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 +337,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 +373,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 +400,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 +415,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 +425,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 +477,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 +490,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 +511,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 +529,43 @@ export const getLinkParts = (fname: string, file?: TFile): LinkParts => {
};
};
export const compress = (data: string): string => {
return LZString.compressToBase64(data).replace(/(.{256})/g, "$1\n\n");
export async function compressAsync (data: string): Promise<string> {
return await runCompressionWorker(data, "compress");
}
export function compress (data: string): string {
const compressed = LZString.compressToBase64(data);
let result = '';
const chunkSize = 256;
for (let i = 0; i < compressed.length; i += chunkSize) {
result += compressed.slice(i, i + chunkSize) + '\n\n';
}
return result.trim();
};
export const decompress = (data: string): string => {
return LZString.decompressFromBase64(data.replaceAll("\n", "").replaceAll("\r", ""));
export async function decompressAsync (data: string): Promise<string> {
return await runCompressionWorker(data, "decompress");
};
export const isMaskFile = (
export function decompress (data: string, isAsync:boolean = false): string {
let cleanedData = '';
const length = data.length;
for (let i = 0; i < length; i++) {
const char = data[i];
if (char !== '\n' && char !== '\r') {
cleanedData += char;
}
}
return LZString.decompressFromBase64(cleanedData);
};
export function isMaskFile (
plugin: ExcalidrawPlugin,
file: TFile,
): boolean => {
): boolean {
if (file) {
const fileCache = plugin.app.metadataCache.getFileCache(file);
if (
@@ -568,10 +579,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 +596,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 +616,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 +633,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 +650,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 +667,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 +704,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 +723,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 +738,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 +749,7 @@ export const getEmbeddedFilenameParts = (fname:string): FILENAMEPARTS => {
hasTaskbone: false,
hasArearef: false,
hasFrameref: false,
hasClippedFrameref: false,
blockref: "",
hasSectionref: false,
sectionref: "",
@@ -752,6 +764,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 +773,21 @@ export const getEmbeddedFilenameParts = (fname:string): FILENAMEPARTS => {
}
}
export const fragWithHTML = (html: string) =>
createFragment((frag) => (frag.createDiv().innerHTML = html));
export function isImagePartRef (parts: FILENAMEPARTS): boolean {
return (parts.hasGroupref || parts.hasArearef || parts.hasFrameref || parts.hasClippedFrameref);
}
export const errorlog = (data: {}) => {
export function fragWithHTML (html: string) {
return createFragment((frag) => (frag.createDiv().innerHTML = html));
}
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 +798,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 +818,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 +830,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 +841,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 +885,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 +895,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 +905,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 +921,51 @@ 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;
}
}
// Thanks https://stackoverflow.com/a/54555834
export function cropCanvas(
srcCanvas: HTMLCanvasElement,
crop: { left: number, top: number, width: number, height: number },
output: { width: number, height: number } = { width: crop.width, height: crop.height })
{
const dstCanvas = createEl('canvas');
dstCanvas.width = output.width;
dstCanvas.height = output.height;
dstCanvas.getContext('2d')!.drawImage(
srcCanvas,
crop.left, crop.top, crop.width, crop.height,
0, 0, output.width, output.height
);
return dstCanvas;
}

29
src/utils/WeakArray.ts Normal file
View 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 = [];
}
}

90
src/utils/matic.ts Normal file
View File

@@ -0,0 +1,90 @@
import { THEME } from "../constants/constants";
import type { Theme } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import type { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
import type { OpenAIInput, OpenAIOutput } from "@zsviczian/excalidraw/types/excalidraw/data/ai/types";
export type MagicCacheData =
| {
status: "pending";
}
| { status: "done"; html: string }
| {
status: "error";
message?: string;
code: "ERR_GENERATION_INTERRUPTED" | string;
};
const SYSTEM_PROMPT = `You are a skilled front-end developer who builds interactive prototypes from wireframes, and is an expert at CSS Grid and Flex design.
Your role is to transform low-fidelity wireframes into working front-end HTML code.
YOU MUST FOLLOW FOLLOWING RULES:
- Use HTML, CSS, JavaScript to build a responsive, accessible, polished prototype
- Leverage Tailwind for styling and layout (import as script <script src="https://cdn.tailwindcss.com"></script>)
- Inline JavaScript when needed
- Fetch dependencies from CDNs when needed (using unpkg or skypack)
- Source images from Unsplash or create applicable placeholders
- Interpret annotations as intended vs literal UI
- Fill gaps using your expertise in UX and business logic
- generate primarily for desktop UI, but make it responsive.
- Use grid and flexbox wherever applicable.
- Convert the wireframe in its entirety, don't omit elements if possible.
If the wireframes, diagrams, or text is unclear or unreadable, refer to provided text for clarification.
Your goal is a production-ready prototype that brings the wireframes to life.
Please output JUST THE HTML file containing your best attempt at implementing the provided wireframes.`;
export async function diagramToHTML({
image,
apiKey,
text,
theme = THEME.LIGHT,
}: {
image: DataURL;
apiKey: string;
text: string;
theme?: Theme;
}) {
const body: OpenAIInput.ChatCompletionCreateParamsBase = {
model: "gpt-4-vision-preview",
// 4096 are max output tokens allowed for `gpt-4-vision-preview` currently
max_tokens: 4096,
temperature: 0.1,
messages: [
{
role: "system",
content: SYSTEM_PROMPT,
},
{
role: "user",
content: [
{
type: "image_url",
image_url: {
url: image,
detail: "high",
},
},
{
type: "text",
text: `Above is the reference wireframe. Please make a new website based on these and return just the HTML file. Also, please make it for the ${theme} theme. What follows are the wireframe's text annotations (if any)...`,
},
{
type: "text",
text,
},
],
},
],
};
let result:
| ({ ok: true } & OpenAIOutput.ChatCompletion)
| ({ ok: false } & OpenAIOutput.APIError);
return await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify(body),
});
}

View File

@@ -0,0 +1,100 @@
function createWorkerBlob(jsCode:string) {
// Create a new Blob with the JavaScript code
const blob = new Blob([jsCode], { type: 'text/javascript' });
// Create a URL for the Blob
const url = URL.createObjectURL(blob);
return url;
}
const workerCode = `
var LZString=function(){var r=String.fromCharCode,o="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",e={};function t(r,o){if(!e[r]){e[r]={};for(var n=0;n<r.length;n++)e[r][r.charAt(n)]=n}return e[r][o]}var i={compressToBase64:function(r){if(null==r)return"";var n=i._compress(r,6,function(r){return o.charAt(r)});switch(n.length%4){default:case 0:return n;case 1:return n+"===";case 2:return n+"==";case 3:return n+"="}},decompressFromBase64:function(r){return null==r?"":""==r?null:i._decompress(r.length,32,function(n){return t(o,r.charAt(n))})},compressToUTF16:function(o){return null==o?"":i._compress(o,15,function(o){return r(o+32)})+" "},decompressFromUTF16:function(r){return null==r?"":""==r?null:i._decompress(r.length,16384,function(o){return r.charCodeAt(o)-32})},compressToUint8Array:function(r){for(var o=i.compress(r),n=new Uint8Array(2*o.length),e=0,t=o.length;e<t;e++){var s=o.charCodeAt(e);n[2*e]=s>>>8,n[2*e+1]=s%256}return n},decompressFromUint8Array:function(o){if(null==o)return i.decompress(o);for(var n=new Array(o.length/2),e=0,t=n.length;e<t;e++)n[e]=256*o[2*e]+o[2*e+1];var s=[];return n.forEach(function(o){s.push(r(o))}),i.decompress(s.join(""))},compressToEncodedURIComponent:function(r){return null==r?"":i._compress(r,6,function(r){return n.charAt(r)})},decompressFromEncodedURIComponent:function(r){return null==r?"":""==r?null:(r=r.replace(/ /g,"+"),i._decompress(r.length,32,function(o){return t(n,r.charAt(o))}))},compress:function(o){return i._compress(o,16,function(o){return r(o)})},_compress:function(r,o,n){if(null==r)return"";var e,t,i,s={},u={},a="",p="",c="",l=2,f=3,h=2,d=[],m=0,v=0;for(i=0;i<r.length;i+=1)if(a=r.charAt(i),Object.prototype.hasOwnProperty.call(s,a)||(s[a]=f++,u[a]=!0),p=c+a,Object.prototype.hasOwnProperty.call(s,p))c=p;else{if(Object.prototype.hasOwnProperty.call(u,c)){if(c.charCodeAt(0)<256){for(e=0;e<h;e++)m<<=1,v==o-1?(v=0,d.push(n(m)),m=0):v++;for(t=c.charCodeAt(0),e=0;e<8;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;e<h;e++)m=m<<1|t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=c.charCodeAt(0),e=0;e<16;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;0==--l&&(l=Math.pow(2,h),h++),s[p]=f++,c=String(a)}if(""!==c){if(Object.prototype.hasOwnProperty.call(u,c)){if(c.charCodeAt(0)<256){for(e=0;e<h;e++)m<<=1,v==o-1?(v=0,d.push(n(m)),m=0):v++;for(t=c.charCodeAt(0),e=0;e<8;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;e<h;e++)m=m<<1|t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=c.charCodeAt(0),e=0;e<16;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;0==--l&&(l=Math.pow(2,h),h++)}for(t=2,e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;for(;;){if(m<<=1,v==o-1){d.push(n(m));break}v++}return d.join("")},decompress:function(r){return null==r?"":""==r?null:i._decompress(r.length,32768,function(o){return r.charCodeAt(o)})},_decompress:function(o,n,e){var t,i,s,u,a,p,c,l=[],f=4,h=4,d=3,m="",v=[],g={val:e(0),position:n,index:1};for(t=0;t<3;t+=1)l[t]=t;for(s=0,a=Math.pow(2,2),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;switch(s){case 0:for(s=0,a=Math.pow(2,8),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;c=r(s);break;case 1:for(s=0,a=Math.pow(2,16),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;c=r(s);break;case 2:return""}for(l[3]=c,i=c,v.push(c);;){if(g.index>o)return"";for(s=0,a=Math.pow(2,d),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;switch(c=s){case 0:for(s=0,a=Math.pow(2,8),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;l[h++]=r(s),c=h-1,f--;break;case 1:for(s=0,a=Math.pow(2,16),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;l[h++]=r(s),c=h-1,f--;break;case 2:return v.join("")}if(0==f&&(f=Math.pow(2,d),d++),l[c])m=l[c];else{if(c!==h)return null;m=i+i.charAt(0)}v.push(m),l[h++]=i+m.charAt(0),i=m,0==--f&&(f=Math.pow(2,d),d++)}}};return i}();"function"==typeof define&&define.amd?define(function(){return LZString}):"undefined"!=typeof module&&null!=module?module.exports=LZString:"undefined"!=typeof angular&&null!=angular&&angular.module("LZString",[]).factory("LZString",function(){return LZString});
self.onmessage = function(e) {
const { data, action } = e.data;
try {
switch (action) {
case 'compress':
if (!data) throw new Error("No input string provided for compression.");
const compressed = LZString.compressToBase64(data);
let result = '';
const chunkSize = 256;
for (let i = 0; i < compressed.length; i += chunkSize) {
result += compressed.slice(i, i + chunkSize) + '\\n\\n';
}
self.postMessage({ compressed: result.trim() });
break;
case 'decompress':
if (!data) throw new Error("No input string provided for decompression.");
let cleanedData = '';
const length = data.length;
for (let i = 0; i < length; i++) {
const char = data[i];
if (char !== '\\n' && char !== '\\r') {
cleanedData += char;
}
}
const decompressed = LZString.decompressFromBase64(cleanedData);
self.postMessage({ decompressed });
break;
default:
throw new Error("Unknown action.");
}
} catch (error) {
// Post the error message back to the main thread
self.postMessage({ error: error.message });
}
};
`;
let worker:Worker | null = null;
export function initCompressionWorker() {
if(!worker) {
worker = new Worker(createWorkerBlob(workerCode));
}
}
export async function runCompressionWorker(data:string, action: 'compress' | 'decompress'): Promise<string> {
return new Promise((resolve, reject) => {
worker.onmessage = function(e) {
const { compressed, decompressed, error } = e.data;
if (error) {
reject(new Error(error));
} else if (compressed || decompressed) {
resolve(compressed || decompressed);
} else {
reject(new Error('Unexpected response from worker'));
}
};
// Set up the worker's error handler
worker.onerror = function(error) {
reject(new Error(error.message));
};
// Post the message to the worker
worker.postMessage({ data, action });
});
}
export function terminateCompressionWorker() {
worker.terminate();
worker = null;
}
export let IS_WORKER_SUPPORTED = false;
function canCreateWorkerFromBlob() {
try {
const blob = new Blob(["self.onmessage = function() {}"]);
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
worker.terminate();
URL.revokeObjectURL(url);
IS_WORKER_SUPPORTED = true;
} catch (e) {
IS_WORKER_SUPPORTED = false;
}
}
canCreateWorkerFromBlob();

View File

@@ -594,4 +594,39 @@ 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;
}
.excalidraw .color-picker-content input[type="color"] {
filter: var(--theme-filter);
}

View File

@@ -2,9 +2,9 @@
"compilerOptions": {
"baseUrl": ".",
"sourceMap": false,
"module": "ES2015",
"target": "es2017", //es2017 because script engine requires for async execution
"allowJs": true,
"module": "es2020",
"target": "es2022", //min es2017 because script engine requires for async execution and min es2018 for named capture groups
"allowJs": false,
"noImplicitAny": true,
"moduleResolution": "node",
"esModuleInterop": true,
@@ -13,16 +13,15 @@
"lib": [
"dom",
"scripthost",
"es2015",
"esnext",
"es2022",
"DOM.Iterable"
],
"jsx": "react",
"inlineSourceMap": true
"inlineSourceMap": true,
},
"include": [
"**/*.ts",
"**/*.tsx", "src/Dialogs/OpenDrawing.ts",
"src/types.d.ts"
"src/types/types.d.ts",
]
}

View File

@@ -2,9 +2,9 @@
"compilerOptions": {
"baseUrl": ".",
"sourceMap": false,
"module": "es2015",
"target": "es2017", //es2017 because script engine requires for async execution
"allowJs": true,
"module": "es2020",
"target": "es2022", //min es2017 because script engine requires for async execution and min es2018 for named capture groups
"allowJs": false,
"noImplicitAny": true,
"moduleResolution": "node",
"esModuleInterop": true,
@@ -13,15 +13,14 @@
"lib": [
"dom",
"scripthost",
"es2015",
"esnext",
"es2022",
"DOM.Iterable"
],
"jsx": "react"
"jsx": "react",
},
"include": [
"**/*.ts",
"**/*.tsx", "src/Dialogs/OpenDrawing.ts",
"src/types.d.ts"
"src/types/types.d.ts",
]
}