mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
121 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
681321a595 | ||
|
|
fcd50d4bc2 | ||
|
|
7e214e5aaa | ||
|
|
de39053857 | ||
|
|
f543e3218e | ||
|
|
e668aea214 | ||
|
|
d810daa735 | ||
|
|
b94b3118eb | ||
|
|
06cbd0c92d | ||
|
|
96ebbbf11d | ||
|
|
fc1467b05b | ||
|
|
53c27f2a59 | ||
|
|
db80f5c715 | ||
|
|
405c98ca50 | ||
|
|
4892aed9e6 | ||
|
|
1b04b94db2 | ||
|
|
4a430f5fe7 | ||
|
|
325bfd825f | ||
|
|
471553f913 | ||
|
|
a43d0689d8 | ||
|
|
afacb8a94b | ||
|
|
afe5300e00 | ||
|
|
ef85d0e323 | ||
|
|
7718f69269 | ||
|
|
af50b0c5c6 | ||
|
|
b576faad82 | ||
|
|
4583e603e9 | ||
|
|
7ee316a605 | ||
|
|
7dca225691 | ||
|
|
8a27d0240a | ||
|
|
939bd6fd91 | ||
|
|
dfbd385de7 | ||
|
|
0d791070dd | ||
|
|
94fbac38bf | ||
|
|
73cf8e75d3 | ||
|
|
395cbc104c | ||
|
|
9a24db8379 | ||
|
|
934a5f4838 | ||
|
|
9022500087 | ||
|
|
795807b6cf | ||
|
|
9eff79733c | ||
|
|
c4e95d9207 | ||
|
|
32cd3a62b6 | ||
|
|
d05ccc0055 | ||
|
|
ff1d7b44b4 | ||
|
|
2b86ba2128 | ||
|
|
44a3b30e3b | ||
|
|
bb9389c7dd | ||
|
|
ba4bfe9de7 | ||
|
|
3a73b14ebb | ||
|
|
31f54db433 | ||
|
|
26812dd297 | ||
|
|
ae4f4b4f08 | ||
|
|
4ac0a4c565 | ||
|
|
69c9f824a0 | ||
|
|
6a2220c960 | ||
|
|
aa501c2843 | ||
|
|
efce44f0a7 | ||
|
|
491eb83d35 | ||
|
|
35cf0802d1 | ||
|
|
f768548f60 | ||
|
|
37789f9907 | ||
|
|
131294464e | ||
|
|
b7652a41f8 | ||
|
|
91c5f85ec6 | ||
|
|
985983b31d | ||
|
|
9a27e38ce2 | ||
|
|
0017ed7c92 | ||
|
|
d5cf4ace21 | ||
|
|
1c899746fd | ||
|
|
9ea09c0fdd | ||
|
|
cda674c289 | ||
|
|
ef7d7ccc91 | ||
|
|
21aa5eb2d6 | ||
|
|
e725fb9b65 | ||
|
|
fbd634bfce | ||
|
|
9a686f3827 | ||
|
|
999219a0c9 | ||
|
|
65fd370cc5 | ||
|
|
232f0c38fa | ||
|
|
fa03968508 | ||
|
|
78dace32d4 | ||
|
|
a21e7aa0c5 | ||
|
|
9b7d828209 | ||
|
|
a1071426c0 | ||
|
|
23baf21ef5 | ||
|
|
d280b33073 | ||
|
|
d1ab6bb7a1 | ||
|
|
146d04ea64 | ||
|
|
fd46a3f8ac | ||
|
|
9974ca1e2e | ||
|
|
2c8a733359 | ||
|
|
4c9eeb9a61 | ||
|
|
de23be8c2d | ||
|
|
bb25a1fde9 | ||
|
|
9267c27bc5 | ||
|
|
591f073413 | ||
|
|
9c28c1ee00 | ||
|
|
120d41ea2f | ||
|
|
80b24a45ad | ||
|
|
e8db9cbff6 | ||
|
|
5db4f8dd95 | ||
|
|
820b2ff6c4 | ||
|
|
596102fb68 | ||
|
|
fa5c63b224 | ||
|
|
aa66b8f716 | ||
|
|
c06cdfcfa0 | ||
|
|
e6943cf7f5 | ||
|
|
b3a2f067ab | ||
|
|
35407cf13d | ||
|
|
d671faf712 | ||
|
|
815ad559ac | ||
|
|
d2cf594756 | ||
|
|
6709f6cf87 | ||
|
|
f463f79222 | ||
|
|
51cf3a9219 | ||
|
|
8700405af8 | ||
|
|
d1d082b4f9 | ||
|
|
6dd9d1a056 | ||
|
|
46b03725e9 | ||
|
|
65d6577b28 |
12
.github/ISSUE_TEMPLATE/bug_report.md
vendored
12
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,12 +1,22 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help me improve Excalidraw
|
||||
about: When something is clearly broken. Everything else is a feature request.
|
||||
title: 'BUG: '
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Help me help you. I am a one man show doing this plugin as a part time hobby. There is no point in flooding me with issues, if there are too many, and they are poorly documented, I will just ignore them. Sorry...
|
||||
|
||||
Before creating a bug report, please
|
||||
1. review recent release notes - maybe there is already an answer,
|
||||
2. search issues (including closed ones) to see if there is anything similar.
|
||||
|
||||
⚠️ I will have to close all recorded bugs that do not provide this background information. Sorry, I need to control my workload/time. ⚠️
|
||||
|
||||
--------
|
||||
|
||||
**Your environment**
|
||||
Please run `Command Palette/Show Debug info` in Obsidian and paste the result here.
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ This is relevant when setting a fix height using the `addText()` function.
|
||||
### startArrowHead, endArrowHead
|
||||
String. Valid values are "arrow", "bar", "dot", and "none". Specifies the beginning and ending of an arrow.
|
||||
|
||||
This is relavant when using the `addArrow()` and the `connectObjects()` functions.
|
||||
This is relevant when using the `addArrow()` and the `connectObjects()` functions.
|
||||
|
||||
## canvas
|
||||
Sets the properties of the canvas.
|
||||
|
||||
@@ -3,10 +3,13 @@
|
||||
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
|
||||
|
||||
<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>
|
||||
|
||||
### Here's my complete catalog of videos:
|
||||
<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 Part (slightly outdated) Video Walkthrough</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;"/> 1 Getting Started</a><br>
|
||||
@@ -240,11 +243,11 @@ Drag the desired file from the Obsidian file explorer and hold down <kbd>SHIFT</
|
||||
- In plugin settings, you can add a custom fourth font. For more details, see this [video](https://youtu.be/eKFmrSQhFA4)
|
||||
- The plugin includes OCR support using Taskbone OCR. For more details, see this [video](https://youtu.be/7gu4ETx7zro)
|
||||
- You can convert SVG files into Excalidraw drawings (with some limitation). For more details, see this [video](https://youtu.be/vlC1-iBvIfo)
|
||||
- You can define custom freedraw pens. See documentation [here].(https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Alternative%20Pens.md), [video](https://youtu.be/uZz5MgzWXiM)
|
||||
- You can define custom pens and higlighters and pin them to the sidebar. For more details, see this [video](https://youtu.be/OjNhjaH2KjI). Using ExcalidrawAutomate, you can add support for [auto-toggling](<ea-scripts/Auto Draw for Pen.md>) pen & support for [hardware eraser buttons](<ea-scripts/Hardware Eraser Support.md>).
|
||||
|
||||
### Script Engine
|
||||
|
||||
- Since 1.5.0, you can easily execute ExcalidrawAutomate macros and assign command palette shortcuts to them, using the ScriptEngine. You will find an intro video and a growing library of ready to install scripts [here](https://github.com/zsviczian/obsidian-excalidraw-plugin/tree/master/ea-scripts).
|
||||
- Since 1.5.0, you can easily execute ExcalidrawAutomate macros and assign command palette shortcuts to them, using the ScriptEngine. You will find an intro video and a growing library of ready to install scripts [here](ea-scripts/README.md).
|
||||
- You can organize scripts into groups on the Obsidian Tools Panel in Excalidraw by moving scripts and accompanying SVG icon files to folders. See the demo [video](https://youtu.be/wTtaXmRJ7wg?t=16).
|
||||
|
||||
### Other
|
||||
|
||||
13
docs/API/ExcalidrawAutomate.d.ts
vendored
13
docs/API/ExcalidrawAutomate.d.ts
vendored
@@ -35,7 +35,7 @@ export declare class ExcalidrawAutomate {
|
||||
*/
|
||||
newFilePrompt(newFileNameOrPath: string, shouldOpenNewFile: boolean, targetPane?: PaneTarget, parentFile?: TFile): Promise<TFile | null>;
|
||||
/**
|
||||
* Generates a new Obsidian Leaf following Excalidraw plugin settings such as open in Main Workspace or not, open in adjacent pane if avaialble, etc.
|
||||
* Generates a new Obsidian Leaf following Excalidraw plugin settings such as open in Main Workspace or not, open in adjacent pane if available, etc.
|
||||
* @param origo // the currently active leaf, the origin of the new leaf
|
||||
* @param targetPane //type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";
|
||||
* @returns
|
||||
@@ -148,7 +148,7 @@ export declare class ExcalidrawAutomate {
|
||||
}>;
|
||||
/**
|
||||
* get all elements from ExcalidrawAutomate elementsDict
|
||||
* @returns elements from elemenetsDict
|
||||
* @returns elements from elementsDict
|
||||
*/
|
||||
getElements(): ExcalidrawElement[];
|
||||
/**
|
||||
@@ -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
|
||||
@@ -698,25 +699,25 @@ export declare class ExcalidrawAutomate {
|
||||
*/
|
||||
moveViewElementToZIndex(elementId: number, newZIndex: number): void;
|
||||
/**
|
||||
* Depricated. Use getCM / ColorMaster instead
|
||||
* Deprecated. Use getCM / ColorMaster instead
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
hexStringToRgb(color: string): number[];
|
||||
/**
|
||||
* Depricated. Use getCM / ColorMaster instead
|
||||
* Deprecated. Use getCM / ColorMaster instead
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
rgbToHexString(color: number[]): string;
|
||||
/**
|
||||
* Depricated. Use getCM / ColorMaster instead
|
||||
* Deprecated. Use getCM / ColorMaster instead
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
hslToRgb(color: number[]): number[];
|
||||
/**
|
||||
* Depricated. Use getCM / ColorMaster instead
|
||||
* Deprecated. Use getCM / ColorMaster instead
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
|
||||
@@ -42,7 +42,7 @@ export declare class ExcalidrawAutomate {
|
||||
*/
|
||||
newFilePrompt(newFileNameOrPath: string, shouldOpenNewFile: boolean, targetPane?: PaneTarget, parentFile?: TFile): Promise<TFile | null>;
|
||||
/**
|
||||
* Generates a new Obsidian Leaf following Excalidraw plugin settings such as open in Main Workspace or not, open in adjacent pane if avaialble, etc.
|
||||
* Generates a new Obsidian Leaf following Excalidraw plugin settings such as open in Main Workspace or not, open in adjacent pane if available, etc.
|
||||
* @param origo // the currently active leaf, the origin of the new leaf
|
||||
* @param targetPane //type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";
|
||||
* @returns
|
||||
@@ -155,7 +155,7 @@ export declare class ExcalidrawAutomate {
|
||||
}>;
|
||||
/**
|
||||
* get all elements from ExcalidrawAutomate elementsDict
|
||||
* @returns elements from elemenetsDict
|
||||
* @returns elements from elementsDict
|
||||
*/
|
||||
getElements(): ExcalidrawElement[];
|
||||
/**
|
||||
@@ -705,25 +705,25 @@ export declare class ExcalidrawAutomate {
|
||||
*/
|
||||
moveViewElementToZIndex(elementId: number, newZIndex: number): void;
|
||||
/**
|
||||
* Depricated. Use getCM / ColorMaster instead
|
||||
* Deprecated. Use getCM / ColorMaster instead
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
hexStringToRgb(color: string): number[];
|
||||
/**
|
||||
* Depricated. Use getCM / ColorMaster instead
|
||||
* Deprecated. Use getCM / ColorMaster instead
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
rgbToHexString(color: number[]): string;
|
||||
/**
|
||||
* Depricated. Use getCM / ColorMaster instead
|
||||
* Deprecated. Use getCM / ColorMaster instead
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
hslToRgb(color: number[]): number[];
|
||||
/**
|
||||
* Depricated. Use getCM / ColorMaster instead
|
||||
* Deprecated. Use getCM / ColorMaster instead
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
|
||||
@@ -88,4 +88,4 @@ This is relevant when setting a fix height using the `addText()` function.
|
||||
### startArrowHead, endArrowHead
|
||||
String. Valid values are "arrow", "bar", "dot", and "none". Specifies the beginning and ending of an arrow.
|
||||
|
||||
This is relavant when using the `addArrow()` and the `connectObjects()` functions.
|
||||
This is relevant when using the `addArrow()` and the `connectObjects()` functions.
|
||||
@@ -22,7 +22,7 @@ Places the generated drawing to the clipboard. Useful when you don't want to cre
|
||||
```typescript
|
||||
getElements():ExcalidrawElement[];
|
||||
```
|
||||
Returns the elements in ExcalidrawAutomate as an array of ExcalidrawElements. This format is usefull when working with ExcalidrawRef.
|
||||
Returns the elements in ExcalidrawAutomate as an array of ExcalidrawElements. This format is useful when working with ExcalidrawRef.
|
||||
|
||||
### getElement()
|
||||
```typescript
|
||||
@@ -156,7 +156,7 @@ You first need to set the view calling `setView()`.
|
||||
|
||||
Gets the array of selected elements in the scene. Returns [] if no elements are selected.
|
||||
|
||||
Note: you can call `getExcalidrawAPI().getSceneElements()` to retreive all the elements in the scene.
|
||||
Note: you can call `getExcalidrawAPI().getSceneElements()` to retrieve all the elements in the scene.
|
||||
|
||||
#### viewToggleFullScreen()
|
||||
```typescript
|
||||
@@ -178,7 +178,7 @@ Same as `connectObjects()`, but ObjectB is the currently selected element in the
|
||||
async addElementsToView(repositionToCursor:boolean=false, save:boolean=false):Promise<boolean>
|
||||
```
|
||||
Adds elements created with ExcalidrawAutomate to the target ExcalidrawView.
|
||||
`repositionToCursor` dafault is false
|
||||
`repositionToCursor` default is false
|
||||
- true: the elements will be moved such that the center point of the elements will be aligned with the current position of the pointer on ExcalidrawView. You can point and place elements to a desired location in your drawing using this switch.
|
||||
- false: elements will be positioned as defined by the x&y coordinates of each element.
|
||||
|
||||
@@ -204,7 +204,7 @@ onDropHook (data: {
|
||||
```
|
||||
|
||||
Callback function triggered when an draggable item is dropped on Excalidraw.
|
||||
The function should return a boolean value. True if the drop was handled by the hook and futher native processing should be stopped, and false if Excalidraw should continue with the processing of the drop.
|
||||
The function should return a boolean value. True if the drop was handled by the hook and further native processing should be stopped, and false if Excalidraw should continue with the processing of the drop.
|
||||
type of drop can be one of:
|
||||
- "file" if a file from Obsidian file explorer is dropped onto Excalidraw. In this case payload.files will contain the list of files dropped.
|
||||
- "text" if a link (e.g. url, or wiki link) or other text is dropped. In this case payload.text will contain the received string
|
||||
|
||||
@@ -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
|
||||

|
||||
|
||||
@@ -21,7 +21,7 @@ This will allow you to assign hotkeys to your favorite scripts just like to any
|
||||
|
||||
## Script development
|
||||
An Excalidraw script will automatically receive two objects:
|
||||
- `ea`: The Script Enginge will initialize the `ea` object including setting the active view to the View from which the script was called.
|
||||
- `ea`: The Script Engine will initialize the `ea` object including setting the active view to the View from which the script was called.
|
||||
- `utils`: I have borrowed functions exposed on utils from [QuickAdd](https://github.com/chhoumann/quickadd/blob/master/docs/QuickAddAPI.md), though currently not all QuickAdd utility functions are implemented in Excalidraw. As of now, these are the available functions. See the example below for details.
|
||||
- `inputPrompt: (header: string, placeholder?: string, value?: string, buttons?: [{caption:string, action:Function}])`
|
||||
- Opens a prompt that asks for an input. Returns a string with the input.
|
||||
|
||||
@@ -8,7 +8,7 @@ With a little work, using ExcalidrawAutomate you can generate simple mindmaps, b
|
||||
## API documentation
|
||||
- **start here** [Introduction to the API](API/introduction.md)
|
||||
- [Overview of Attributes and Functions](API/attributes_functions_overview.md)
|
||||
- [Element Sytle](API/element_style.md)
|
||||
- [Element Style](API/element_style.md)
|
||||
- [Canvas Style](API/canvas_style.md)
|
||||
- [Adding Objects](API/objects.md)
|
||||
- [Utility Functions](API/utility.md)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
THIS SCRIPT REQUIRES EXCALIDRAW 1.5.15
|
||||
|
||||
The script will
|
||||
1) send the selected image file to [taskbone.com](https://taskbone.com) to exctract the text from the image, and
|
||||
1) send the selected image file to [taskbone.com](https://taskbone.com) to extract the text from the image, and
|
||||
2) will add the text to your drawing as a text element
|
||||
|
||||
I recommend also installing the [Transfer TextElements to Excalidraw markdown metadata](Transfer%20TextElements%20to%20Excalidraw%20markdown%20metadata.md) script as well.
|
||||
|
||||
@@ -15,7 +15,7 @@ In the `Command Palette` installed scripts are prefixed with `Downloaded/`, thus
|
||||
|
||||
## Attention developers and hobby hackers
|
||||
<img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/hobby-programmer.svg' align='left' style='background-color:whitesmoke; width:80px; margin-right:15px; margin-bottom:10px;'/>
|
||||
If you want to modify scripts, I recommend moving them to the `Excalidraw Automate script folder` or a different subfolder under the script folder. Scripts in the `Downloaded` folder will be overwritten when you click the `Update this script` button. Note also, that at this time, I do not check if the script file has been updated on GitHub, thus the `Update this script` button is always visible once you have installed a script, not only when an update is availble (hope to build this feature in the future).
|
||||
If you want to modify scripts, I recommend moving them to the `Excalidraw Automate script folder` or a different subfolder under the script folder. Scripts in the `Downloaded` folder will be overwritten when you click the `Update this script` button. Note also, that at this time, I do not check if the script file has been updated on GitHub, thus the `Update this script` button is always visible once you have installed a script, not only when an update is available (hope to build this feature in the future).
|
||||
|
||||
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.
|
||||
@@ -243,7 +243,7 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/OCR%20-%20Optical%20Character%20Recognition.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/OCR%20-%20Optical%20Character%20Recognition.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">REQUIRES EXCALIDRAW 1.5.15<br>The script will 1) send the selected image file to [taskbone.com](https://taskbone.com) to exctract the text from the image, and 2) will add the text to your drawing as a text element.<br><mark>⚠ Note that you will need to manually paste your token into the script after the first run! ⚠</mark><br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-ocr.jpg'><br><iframe width="560" height="315" src="https://www.youtube.com/embed/W2NMzR8s4eE" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td></tr></table>
|
||||
<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/OCR%20-%20Optical%20Character%20Recognition.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">REQUIRES EXCALIDRAW 1.5.15<br>The script will 1) send the selected image file to [taskbone.com](https://taskbone.com) to extract the text from the image, and 2) will add the text to your drawing as a text element.<br><mark>⚠ Note that you will need to manually paste your token into the script after the first run! ⚠</mark><br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-ocr.jpg'><br><iframe width="560" height="315" src="https://www.youtube.com/embed/W2NMzR8s4eE" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td></tr></table>
|
||||
|
||||
## Organic Line
|
||||
```excalidraw-script-install
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
With This Script it is possible to make boolean Operations on Shapes.
|
||||
The style of the resulting shape will be the style of the highest ranking Element that was used.
|
||||
The ranking of the elemtns is based on their background. The "denser" the background, the higher the ranking (the order of backgroundstyles is shown below). If they have the same background the opacity will decide. If thats also the same its decided by the order they were created.
|
||||
The ranking is also important for the diffrence operation, so a tranparent object for example will cut a hole into a solid object.
|
||||
The ranking of the elements is based on their background. The "denser" the background, the higher the ranking (the order of backgroundstyles is shown below). If they have the same background the opacity will decide. If thats also the same its decided by the order they were created.
|
||||
The ranking is also important for the difference operation, so a transparent object for example will cut a hole into a solid object.
|
||||

|
||||

|
||||
|
||||
@@ -29,7 +29,7 @@ if(elements.length === 0) {
|
||||
}
|
||||
|
||||
const PolyBool = ea.getPolyBool();
|
||||
const polyboolAction = await utils.suggester(["union (a + b)", "intersect (a && b)", "diffrence (a - b)", "reversed diffrence (b - a)", "xor"], [
|
||||
const polyboolAction = await utils.suggester(["union (a + b)", "intersect (a && b)", "difference (a - b)", "reversed difference (b - a)", "xor"], [
|
||||
PolyBool.union, PolyBool.intersect, PolyBool.difference, PolyBool.differenceRev, PolyBool.xor
|
||||
], "What would you like todo with the object");
|
||||
|
||||
|
||||
75
ea-scripts/Crop Vintage Mask.md
Normal file
75
ea-scripts/Crop Vintage Mask.md
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
Adds a rounded mask to the image by adding a full cover black mask and a rounded rectangle white mask. The script is also useful for adding just a black mask. In this case, run the script, then delete the white mask and add your custom white mask.
|
||||

|
||||
```js*/
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.0.18")) {
|
||||
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!ea.isExcalidrawMaskFile()) {
|
||||
new Notice("This script only works with Mask Files");
|
||||
return;
|
||||
}
|
||||
|
||||
const frames = ea.getViewElements().filter(el=>el.type==="frame")
|
||||
if(frames.length !== 1) {
|
||||
new Notice("Multiple frames found");
|
||||
return;
|
||||
}
|
||||
const frame = frames[0];
|
||||
ea.copyViewElementsToEAforEditing(ea.getViewElements().filter(el=>el.frameId === frame.id));
|
||||
const frameId = ea.generateElementId();
|
||||
ea.style.fillStyle = "solid";
|
||||
ea.style.roughness = 0;
|
||||
ea.style.strokeColor = "transparent";
|
||||
ea.style.strokeWidth = 0.1;
|
||||
ea.style.opacity = 50;
|
||||
|
||||
let blackEl = ea.getViewElements().find(el=>el.id === "allblack");
|
||||
let whiteEl = ea.getViewElements().find(el=>el.id === "whiteovr");
|
||||
|
||||
if(blackEl && whiteEl) {
|
||||
ea.copyViewElementsToEAforEditing([blackEl, whiteEl]);
|
||||
} else
|
||||
if (blackEl && !whiteEl) {
|
||||
ea.copyViewElementsToEAforEditing([blackEl]);
|
||||
ea.style.backgroundColor = "white";
|
||||
ea.addRect(frame.x,frame.y,frame.width,frame.height, "whiteovr");
|
||||
} else
|
||||
if (!blackEl && whiteEl) {
|
||||
ea.style.backgroundColor = "black";
|
||||
ea.addRect(frame.x-2,frame.y-2,frame.width+4,frame.height+4, "allblack");
|
||||
ea.copyViewElementsToEAforEditing([whiteEl]);
|
||||
} else {
|
||||
ea.style.backgroundColor = "black";
|
||||
ea.addRect(frame.x-2,frame.y-2,frame.width+4,frame.height+4, "allblack");
|
||||
ea.style.backgroundColor = "white";
|
||||
ea.addRect(frame.x,frame.y,frame.width,frame.height, "whiteovr");
|
||||
}
|
||||
blackEl = ea.getElement("allblack");
|
||||
whiteEl = ea.getElement("whiteovr");
|
||||
|
||||
//this "magic" is required to ensure the frame element is above in sequence of the new rectangle elements
|
||||
ea.getElements().forEach(el=>{el.frameId = frameId});
|
||||
ea.copyViewElementsToEAforEditing(ea.getViewElements().filter(el=>el.id === frame.id));
|
||||
const newFrame = ea.getElement(frame.id);
|
||||
newFrame.id = frameId;
|
||||
ea.elementsDict[frameId] = newFrame;
|
||||
ea.copyViewElementsToEAforEditing(ea.getViewElements().filter(el=>el.id === frame.id));
|
||||
ea.getElement(frame.id).isDeleted = true;
|
||||
|
||||
let curve = await utils.inputPrompt(
|
||||
"Set roundess",
|
||||
"Positive whole number",
|
||||
`${whiteEl.roundness?.value ?? "500"}`
|
||||
);
|
||||
|
||||
if(!curve) return;
|
||||
curve = parseInt(curve);
|
||||
if(isNaN(curve) || curve < 0) {
|
||||
new Notice ("Roudness is not a valid positive whole number");
|
||||
return;
|
||||
}
|
||||
whiteEl.roundness = {type: 3, value: curve};
|
||||
ea.addElementsToView(false,false,true);
|
||||
1
ea-scripts/Crop Vintage Mask.svg
Normal file
1
ea-scripts/Crop Vintage Mask.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-squircle"><path fill="none" d="M12 3c7.2 0 9 1.8 9 9s-1.8 9-9 9-9-1.8-9-9 1.8-9 9-9"/></svg>
|
||||
|
After Width: | Height: | Size: 294 B |
15
ea-scripts/Custom Zoom.md
Normal file
15
ea-scripts/Custom Zoom.md
Normal file
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
You can set a custom zoom level with this script. This allows you to set a zoom level below 10% or set the zoom level to a specific value. Note however, that Excalidraw has a bug under 10% zoom, and a phantom copy of your image may appear on screen. If this happens, increase the zoom and the phantom should disappear, if it doesn't then close and open the drawing.
|
||||
|
||||
```js*/
|
||||
const api = ea.getExcalidrawAPI();
|
||||
const appState = api.getAppState();
|
||||
const zoomStr = await utils.inputPrompt("Zoom [%]",null,`${appState.zoom.value*100}%`);
|
||||
if(!zoomStr) return;
|
||||
const zoomNum = parseFloat(zoomStr.match(/^\d*/)[0]);
|
||||
if(isNaN(zoomNum)) {
|
||||
new Notice("You must provide a number");
|
||||
return;
|
||||
}
|
||||
|
||||
ea.getExcalidrawAPI().updateScene({appState:{zoom:{value: zoomNum/100 }}});
|
||||
1
ea-scripts/Custom Zoom.svg
Normal file
1
ea-scripts/Custom Zoom.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scan-search"><g stroke-width="2"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><circle cx="12" cy="12" r="3"/><path d="m16 16-1.9-1.9"/></g></svg>
|
||||
|
After Width: | Height: | Size: 444 B |
@@ -9,7 +9,7 @@ Select some elements in the scene. The script will take these elements and move
|
||||
|
||||
```javascript
|
||||
*/
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.9.19")) {
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.0.25")) {
|
||||
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
|
||||
return;
|
||||
}
|
||||
@@ -17,11 +17,11 @@ if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.9.19")) {
|
||||
// -------------------------------
|
||||
// Utility variables and functions
|
||||
// -------------------------------
|
||||
const excalidrawTemplate = app.metadataCache.getFirstLinkpathDest(ea.plugin.settings.templateFilePath,"");
|
||||
const excalidrawTemplates = ea.getListOfTemplateFiles();
|
||||
if(typeof window.ExcalidrawDeconstructElements === "undefined") {
|
||||
window.ExcalidrawDeconstructElements = {
|
||||
openDeconstructedImage: true,
|
||||
templatePath: excalidrawTemplate?.path??""
|
||||
templatePath: excalidrawTemplates?.[0].path??""
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,11 +45,21 @@ if(!settings["Templates"]) {
|
||||
await ea.setScriptSettings(settings);
|
||||
}
|
||||
|
||||
if(!settings["Default file name"]) {
|
||||
settings["Default file name"] = {
|
||||
value: "deconstructed",
|
||||
description: "The default filename to use when deconstructing elements."
|
||||
};
|
||||
await ea.setScriptSettings(settings);
|
||||
}
|
||||
|
||||
const DEFAULT_FILENAME = settings["Default file name"].value;
|
||||
|
||||
const templates = settings["Templates"]
|
||||
.value
|
||||
.split(",")
|
||||
.map(p=>app.metadataCache.getFirstLinkpathDest(p.trim(),""))
|
||||
.concat(excalidrawTemplate)
|
||||
.concat(excalidrawTemplates)
|
||||
.filter(f=>Boolean(f))
|
||||
.sort((a,b) => a.basename.localeCompare(b.basename));
|
||||
|
||||
@@ -144,7 +154,7 @@ const customControls = (container) => {
|
||||
const path = await utils.inputPrompt(
|
||||
"Filename for new file",
|
||||
"Filename",
|
||||
await ea.getAttachmentFilepath("deconstructed"),
|
||||
await ea.getAttachmentFilepath(DEFAULT_FILENAME),
|
||||
actionButtons,
|
||||
2,
|
||||
false,
|
||||
@@ -166,15 +176,27 @@ const newPath = await ea.create ({
|
||||
silent: !window.ExcalidrawDeconstructElements.openDeconstructedImage
|
||||
});
|
||||
|
||||
setTimeout(async ()=>{
|
||||
const file = app.metadataCache.getFirstLinkpathDest(newPath,"");
|
||||
ea.deleteViewElements(els);
|
||||
ea.clear();
|
||||
await ea.addImage(bb.topX,bb.topY,file,false, shouldAnchor);
|
||||
await ea.addElementsToView(false, true, true);
|
||||
ea.getExcalidrawAPI().history.clear(); //to avoid undo/redo messing up the decomposition
|
||||
},1000);
|
||||
let f = app.vault.getAbstractFileByPath(newPath);
|
||||
let counter = 0;
|
||||
while((!f || !ea.isExcalidrawFile(f)) && counter++<100) {
|
||||
await sleep(50);
|
||||
f = app.vault.getAbstractFileByPath(newPath);
|
||||
}
|
||||
|
||||
if(!f || !ea.isExcalidrawFile(f)) {
|
||||
new Notice("Something went wrong");
|
||||
return;
|
||||
}
|
||||
|
||||
let padding = parseFloat(app.metadataCache.getCache(f.path)?.frontmatter["excalidraw-export-padding"]);
|
||||
if(isNaN(padding)) {
|
||||
padding = ea.plugin.settings.exportPaddingSVG;
|
||||
}
|
||||
|
||||
ea.getElements().forEach(el=>el.isDeleted = true);
|
||||
await ea.addImage(bb.topX-padding,bb.topY-padding,f,false, shouldAnchor);
|
||||
await ea.addElementsToView(false, true, true);
|
||||
ea.getExcalidrawAPI().history.clear();
|
||||
if(!window.ExcalidrawDeconstructElements.openDeconstructedImage) {
|
||||
new Notice("Deconstruction ready");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@ const outputTypes = {
|
||||
"image-gen": {
|
||||
instruction: "Return a single message with the generated image prompt in a codeblock",
|
||||
blocktype: "image"
|
||||
},
|
||||
"image-gen-silent": {
|
||||
instruction: "Return a single message with the generated image prompt in a codeblock",
|
||||
blocktype: "image-silent"
|
||||
},
|
||||
"image-edit": {
|
||||
instruction: "",
|
||||
@@ -49,6 +53,12 @@ const systemPrompts = {
|
||||
type: "svg",
|
||||
help: "Convert text prompts into simple icons inserted as Excalidraw elements. Expect only a text prompt. Experimental and may not produce good drawings."
|
||||
},
|
||||
|
||||
"Create a stick figure": {
|
||||
prompt: "You will receive a prompt from the user. Your task involves drawing a simple stick figure or a scene involving a few stick figures based on the user's prompt. Create the stickfigure based on the following style description. DO NOT add any detail, just use it AS-IS: Create a simple stick figure character with a large round head and a face in the style of sketchy caricatures. The stick figure should have a rudimentary body composed of straight lines representing the arms and legs. Hands and toes should be should be represented with round shapes, do not add details such as fingers or toes. Use fine lines, smooth curves, rounded shapes. The stick figure should retain a playful and childlike simplicity, reminiscent of a doodle someone might draw on the corner of a notebook page. Create a black and white drawing, a hand-drawn figure on white background.",
|
||||
type: "image-gen",
|
||||
help: "Send only the text prompt to OpenAI. Provide a detailed description; OpenAI will enrich your prompt automatically. To avoid it, start your prompt like this 'DO NOT add any detail, just use it AS-IS:'"
|
||||
},
|
||||
"Edit an image": {
|
||||
prompt: null,
|
||||
type: "image-edit",
|
||||
@@ -69,6 +79,11 @@ const systemPrompts = {
|
||||
type: "image-gen",
|
||||
help: "ExcaliAI will create an image prompt to illustrate your text input - a quote - with GPT, then generate an image using Dall-e. In case you include the Author's name, GPT will try to generate an image that in some way references the Author."
|
||||
},
|
||||
"Generate 4 icon-variants based on input image": {
|
||||
prompt: "Given a simple sketch and an optional text prompt from the user, your task is to generate a descriptive narrative tailored for effective image generation, capturing the style of the sketch. Utilize the text prompt to guide the description. Your objective is to instruct DALL-E to create a collage of four minimalist black and white hand-drawn pencil sketches in a 2x2 matrix format. Each sketch should convert the user's sketch into simple artistic SVG icons with transparent backgrounds. Ensure the resulting images remain text-free, maintaining a minimalist, easy-to-understand style, and omit framing borders. Only include a pencil in the drawing if it is explicitely metioned in the user prompt or included in the sketch.",
|
||||
type: "image-gen-silent",
|
||||
help: "Generate a collage of 4 icons based on the drawing using ChatGPT-Vision and Dall-e. You may provide a contextual text-prompt to improve accuracy of interpretation."
|
||||
},
|
||||
"Visual brainstorm": {
|
||||
prompt: "Your objective is to interpret a screenshot of a whiteboard, creating an image aimed at sparking further thoughts on the subject. The whiteboard will present diverse ideas about a specific topic. Your generated image should achieve one of two purposes: highlighting concepts that challenge, dispute, or contradict the whiteboard content, or introducing ideas that expand, complement, or enrich the user's thinking. You have the option to include multiple tiles in the resulting image, resembling a sequence akin to a comic strip. Ensure that the image remains devoid of text.",
|
||||
type: "image-gen",
|
||||
@@ -319,7 +334,7 @@ const setMermaidDataToStorage = (mermaidDefinition) => {
|
||||
// --------------------------------------
|
||||
// Submit Prompt
|
||||
// --------------------------------------
|
||||
const generateImage = async(text, spinnerID, bb) => {
|
||||
const generateImage = async(text, spinnerID, bb, silent=false) => {
|
||||
const requestObject = {
|
||||
text,
|
||||
imageGenerationProperties: {
|
||||
@@ -342,7 +357,7 @@ const generateImage = async(text, spinnerID, bb) => {
|
||||
const imageID = await ea.addImage(spinner.x, spinner.y, result.json.data[0].url);
|
||||
const imageEl = ea.getElement(imageID);
|
||||
const revisedPrompt = result.json.data[0].revised_prompt;
|
||||
if(revisedPrompt) {
|
||||
if(revisedPrompt && !silent) {
|
||||
ea.style.fontSize = 16;
|
||||
const rectID = ea.addText(imageEl.x+15, imageEl.y + imageEl.height + 50, revisedPrompt, {
|
||||
width: imageEl.width-30,
|
||||
@@ -356,6 +371,7 @@ const generateImage = async(text, spinnerID, bb) => {
|
||||
}
|
||||
|
||||
await ea.addElementsToView(false, true, true);
|
||||
if(silent) return;
|
||||
ea.getExcalidrawAPI().setToast({
|
||||
message: IMAGE_WARNING,
|
||||
duration: 15000,
|
||||
@@ -371,7 +387,7 @@ const run = async (text) => {
|
||||
|
||||
const systemPrompt = systemPrompts[agentTask];
|
||||
const outputType = outputTypes[systemPrompt.type];
|
||||
const isImageGenRequest = outputType.blocktype === "image";
|
||||
const isImageGenRequest = outputType.blocktype === "image" || outputType.blocktype === "image-silent";
|
||||
const isImageEditRequest = systemPrompt.type === "image-edit";
|
||||
|
||||
if(isImageEditRequest) {
|
||||
@@ -473,7 +489,7 @@ const run = async (text) => {
|
||||
}
|
||||
|
||||
if(isImageGenRequest) {
|
||||
generateImage(content,spinnerID,bb);
|
||||
generateImage(content,spinnerID,bb,outputType.blocktype === "image-silent");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -519,7 +535,7 @@ const run = async (text) => {
|
||||
// --------------------------------------
|
||||
let previewDiv;
|
||||
const fragWithHTML = (html) => createFragment((frag) => (frag.createDiv().innerHTML = html));
|
||||
const isImageGenerationTask = () => systemPrompts[agentTask].type === "image-gen" || systemPrompts[agentTask].type === "image-edit";
|
||||
const isImageGenerationTask = () => systemPrompts[agentTask].type === "image-gen" || systemPrompts[agentTask].type === "image-gen-silent" || systemPrompts[agentTask].type === "image-edit";
|
||||
const addPreviewImage = () => {
|
||||
if(!previewDiv) return;
|
||||
previewDiv.empty();
|
||||
|
||||
@@ -77,7 +77,7 @@ const systemPrompts = {
|
||||
"Wireframe to code": {
|
||||
prompt: `You are an expert tailwind developer. A user will provide you with a low-fidelity wireframe of an application and you will return a single html file that uses tailwind to create the website. Use creative license to make the application more fleshed out. Write the necessary javascript code. If you need to insert an image, use placehold.co to create a placeholder image.`,
|
||||
type: "html",
|
||||
help: "Use GPT Visions to interpret the wireframe and generate a web application. YOu may copy the resulting code from the active embeddable's top left menu."
|
||||
help: "Use GPT Visions to interpret the wireframe and generate a web application. You may copy the resulting code from the active embeddable's top left menu."
|
||||
},
|
||||
}
|
||||
|
||||
@@ -464,7 +464,7 @@ const run = async (text) => {
|
||||
return;
|
||||
}
|
||||
|
||||
//exctract codeblock and display result
|
||||
//extract codeblock and display result
|
||||
let content = ea.extractCodeBlocks(result.json.choices[0]?.message?.content)[0]?.data;
|
||||
|
||||
if(!content) {
|
||||
|
||||
@@ -631,7 +631,7 @@ modal.onOpen = async () => {
|
||||
.addDropdown(dropdown=>dropdown
|
||||
.addOption("none","None")
|
||||
.addOption("top-down","Top down")
|
||||
.addOption("bottom-up","Bootom up")
|
||||
.addOption("bottom-up","Bottom up")
|
||||
.addOption("center-out","Center out")
|
||||
.addOption("center-in","Center in")
|
||||
.setValue(vDirection)
|
||||
|
||||
@@ -33,8 +33,20 @@ const invertColor = (color) => {
|
||||
}
|
||||
}
|
||||
|
||||
const invertPaletteColors = (palette) => Object.keys(palette).forEach(key => palette[key] = invertColor(palette[key]));
|
||||
Object.keys(colorPalette).forEach(key => invertPaletteColors(colorPalette[key]));
|
||||
function invertColorsRecursively(obj) {
|
||||
if (typeof obj === 'string') {
|
||||
return invertColor(obj);
|
||||
} else if (Array.isArray(obj)) {
|
||||
return obj.map(item => invertColorsRecursively(item));
|
||||
} else if (typeof obj === 'object' && obj !== null) {
|
||||
const result = {};
|
||||
Object.keys(obj).forEach(key => result[key] = invertColorsRecursively(obj[key]));
|
||||
return result;
|
||||
} else {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
colorPalette = invertColorsRecursively(colorPalette);
|
||||
|
||||
ea.copyViewElementsToEAforEditing(ea.getViewElements());
|
||||
ea.getElements().forEach(el=>{
|
||||
|
||||
@@ -58,7 +58,7 @@ Open the script you are interested in and save it to your Obsidian Vault includi
|
||||
|[Mindmap connector](Mindmap%20connector.md)|This script creates mindmap like lines (only right side and down available currently) for selected elements. The line will start according to the creation time of the elements. So you should create the header element first.||[@xllowl](https://github.com/xllowl)|
|
||||
|[Modify background color opacity](Modify%20background%20color%20opacity.md)|This script changes the opacity of the background color of the selected boxes. The default background color in Excalidraw is so dark that the text is hard to read. You can lighten the color a bit by setting transparency. And you can tweak the transparency over and over again until you're happy with it. Although excalidraw has the opacity option in its native property Settings, it also changes the transparency of the border. Use this script to change only the opacity of the background color without affecting the border.||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[Normalize Selected Arrows](Normalize%20Selected%20Arrows.md)|This script will reset the start and end positions of the selected arrows. The arrow will point to the center of the connected box and will have a gap of 8px from the box.||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[OCR - Optical Character Recognition](OCR%20-%20Optical%20Character%20Recognition.md)|The script will 1) send the selected image file to [taskbone.com](https://taskbone.com) to exctract the text from the image, and 2) will add the text to your drawing as a text element.||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[OCR - Optical Character Recognition](OCR%20-%20Optical%20Character%20Recognition.md)|The script will 1) send the selected image file to [taskbone.com](https://taskbone.com) to extract the text from the image, and 2) will add the text to your drawing as a text element.||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[Organic Line](Organic%20Line.md)|Converts selected freedraw lines such that pencil pressure will decrease from maximum to minimum from the beginning of the line to its end. The resulting line is placed at the back of the layers, under all other items. Helpful when drawing organic mindmaps.||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[Repeat Elements](Repeat%20Elements.md)|This script will detect the difference between 2 selected elements, including position, size, angle, stroke and background color, and create several elements that repeat these differences based on the number of repetitions entered by the user.||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[Reverse arrows](Reverse%20arrows.md)|Reverse the direction of **arrows** within the scope of selected elements.||[@zsviczian](https://github.com/zsviczian)|
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||

|
||||
|
||||
This script allows users to streamline their Obsidian-Excalidraw workflows by enabling the selection of elements based on similar properties. Users can precisely define which attributes such as stroke color, fill style, font family, and more, should match for selection. It's perfect for large canvases where manual selection would be cumbersome. Users can either run the script to find and select matching elements across the entire scene, or define a specific group of elements to apply the selection criteria within a defined timeframe. This script enhances control and efficiency in your Excalidraw experience.
|
||||
This script enables the selection of elements based on matching properties. Select the attributes (such as stroke color, fill style, font family, etc) that should match for selection. It's perfect for large scenes where manual selection of elements would be cumbersome. You can either run the script to select matching elements across the entire scene, or define a specific group of elements to apply the selection criteria to.
|
||||
|
||||
```js */
|
||||
|
||||
let config = window.ExcalidrawSelectConfig;
|
||||
config = config && (Date.now() - config.timestamp < 60000) ? config : null;
|
||||
config = Boolean(config) && (Date.now() - config.timestamp < 60000) ? config : null;
|
||||
|
||||
let elements = ea.getViewSelectedElements();
|
||||
if(!config && (elements.length !==1)) {
|
||||
@@ -19,7 +19,7 @@ if(!config && (elements.length !==1)) {
|
||||
}
|
||||
}
|
||||
|
||||
const {angle, backgroundColor, fillStyle, fontFamily, fontSize, height, width, opacity, roughness, roundness, strokeColor, strokeStyle, strokeWidth, type, startArrowhead, endArrowhead} = ea.getViewSelectedElement();
|
||||
const {angle, backgroundColor, fillStyle, fontFamily, fontSize, height, width, opacity, roughness, roundness, strokeColor, strokeStyle, strokeWidth, type, startArrowhead, endArrowhead, fileId} = ea.getViewSelectedElement();
|
||||
|
||||
const fragWithHTML = (html) => createFragment((frag) => (frag.createDiv().innerHTML = html));
|
||||
|
||||
@@ -27,14 +27,14 @@ const fragWithHTML = (html) => createFragment((frag) => (frag.createDiv().innerH
|
||||
// RUN
|
||||
//--------------------------
|
||||
const run = () => {
|
||||
selectedElements = ea.getViewElements().filter(el=>
|
||||
selectedElements = elements.filter(el=>
|
||||
((typeof config.angle === "undefined") || (el.angle === config.angle)) &&
|
||||
((typeof config.backgroundColor === "undefined") || (el.backgroundColor === config.backgroundColor)) &&
|
||||
((typeof config.fillStyle === "undefined") || (el.fillStyle === config.fillStyle)) &&
|
||||
((typeof config.fontFamily === "undefined") || (el.fontFamily === config.fontFamily)) &&
|
||||
((typeof config.fontSize === "undefined") || (el.fontSize === config.fontSize)) &&
|
||||
((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)) &&
|
||||
@@ -43,7 +43,8 @@ const run = () => {
|
||||
((typeof config.strokeWidth === "undefined") || (el.strokeWidth === config.strokeWidth)) &&
|
||||
((typeof config.type === "undefined") || (el.type === config.type)) &&
|
||||
((typeof config.startArrowhead === "undefined") || (el.startArrowhead === config.startArrowhead)) &&
|
||||
((typeof config.endArrowhead === "undefined") || (el.endArrowhead === config.endArrowhead))
|
||||
((typeof config.endArrowhead === "undefined") || (el.endArrowhead === config.endArrowhead)) &&
|
||||
((typeof config.fileId === "undefined") || (el.fileId === config.fileId))
|
||||
)
|
||||
ea.selectElementsInView(selectedElements);
|
||||
delete window.ExcalidrawSelectConfig;
|
||||
@@ -55,12 +56,12 @@ 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 => {
|
||||
el.createEl("li", {text: "Click 'RUN' to find matching elements throughout the entire scene."});
|
||||
el.createEl("li", {text: "Click 'SELECT' to first choose a specific group of elements. Then run the 'Select Similar Elements' script once more on that group within 1 minute."});
|
||||
el.createEl("li", {text: "Click 'SELECT' to 1) first choose a specific group of elements in the scene, then 2) run the 'Select Similar Elements' once more within 1 minute to apply the filter criteria only to that group of elements."});
|
||||
});
|
||||
instructionsModal.contentEl.createEl("p", {text: "Note: If you choose 'SELECT', make sure to click the 'Select Similar Elements' script again within 1 minute to apply your selection criteria to the group of elements you chose."});
|
||||
};
|
||||
@@ -70,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
|
||||
@@ -97,11 +98,12 @@ const selectAttributesToCopy = () => {
|
||||
{name: "End arrowhead", key: "endArrowhead"},
|
||||
{name: "Height", key: "height"},
|
||||
{name: "Width", key: "width"},
|
||||
{name: "ImageID", key: "fileId"},
|
||||
];
|
||||
|
||||
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) {
|
||||
@@ -142,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;
|
||||
}
|
||||
@@ -165,7 +165,7 @@ const selectAttributesToCopy = () => {
|
||||
});
|
||||
|
||||
|
||||
//Add Toggle for the rest of the attirbutes. Organize attributes into a logical sequence or groups by adding
|
||||
//Add Toggle for the rest of the attributes. Organize attributes into a logical sequence or groups by adding
|
||||
//configModal.contentEl.createEl("h") or similar to the code
|
||||
|
||||
new ea.obsidian.Setting(configModal.contentEl)
|
||||
@@ -190,7 +190,9 @@ const selectAttributesToCopy = () => {
|
||||
|
||||
|
||||
configModal.onClose = () => {
|
||||
setTimeout(()=>delete configModal);
|
||||
setTimeout(()=>{
|
||||
delete configModal
|
||||
});
|
||||
}
|
||||
|
||||
configModal.open();
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
/*
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/JwgtCrIVeEU" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
# About the slideshow script
|
||||
The script will convert your drawing into a slideshow presentation.
|
||||

|
||||
|
||||

|
||||
The script will convert your drawing into a slideshow presentation.
|
||||
If you select an arrow or line element, the script will use that as the presentation path.
|
||||
If you select nothing, but the file has a hidden presentation path, the script will use that for determining the slide sequence.
|
||||
If there are frames, the script will use the frames for the presentation. Frames are played in alphabetical order of their titles.
|
||||
## Presentation options
|
||||
- If you select an arrow or line element, the script will use that as the presentation path.
|
||||
- If you select nothing, but the file has a hidden presentation path, the script will use that for determining the slide sequence.
|
||||
- If there are frames, the script will use the frames for the presentation. Frames are played in alphabetical order of their titles.
|
||||
# Keyboard shortcuts and modifier keys
|
||||
**Forward**: Arrow Down, Arrow Right, or SPACE
|
||||
**Backward**: Arrow Up, Arrow Left
|
||||
**Finish presentation**: Backspace, ESC (I had issues with ESC not working in full screen presentation mode on Mac)
|
||||
|
||||
**Run presentation in a window**: Hold down the ALT/OPT modifier key when clicking the presentation script button
|
||||
**Continue presentation**: Hold down SHIFT when clicking the presentation script button. (The feature also works in combination with the ALT/OPT modifier to start the presentation in a window). The feature will only resume while you are within the same Obsidian session (i.e. if you restart Obsidian, slideshow will no longer remember where you were). I have two use cases in mind for this feature:
|
||||
1) When you are designing your presentation you may want to test how a slide looks. Using this feature you can get back to where you left off by starting the presentation with SHIFT.
|
||||
2) During presentation you may want to exit presentation mode to show something additional to your audience. You stop the presentation, show the additional thing you wanted, now you want to continue from where you left off. Hold down SHIFT when clicking the slideshow button.
|
||||
|
||||
```javascript
|
||||
*/
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.9.23")) {
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.1.7")) {
|
||||
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
|
||||
return;
|
||||
}
|
||||
@@ -20,7 +31,9 @@ const hostView = hostLeaf.view;
|
||||
const statusBarElement = document.querySelector("div.status-bar");
|
||||
const ctrlKey = ea.targetView.modifierKeyDown.ctrlKey || ea.targetView.modifierKeyDown.metaKey;
|
||||
const altKey = ea.targetView.modifierKeyDown.altKey || ctrlKey;
|
||||
|
||||
const shiftKey = ea.targetView.modifierKeyDown.shiftKey;
|
||||
const shouldStartWithLastSlide = shiftKey && window.ExcalidrawSlideshow &&
|
||||
(window.ExcalidrawSlideshow.script === utils.scriptFile.path) && (typeof window.ExcalidrawSlideshow.slide === "number")
|
||||
//-------------------------------
|
||||
//constants
|
||||
//-------------------------------
|
||||
@@ -28,7 +41,7 @@ const TRANSITION_STEP_COUNT = 100;
|
||||
const TRANSITION_DELAY = 1000; //maximum time for transition between slides in milliseconds
|
||||
const FRAME_SLEEP = 1; //milliseconds
|
||||
const EDIT_ZOOMOUT = 0.7; //70% of original slide zoom, set to a value between 1 and 0
|
||||
const FADE_LEVEL = 0.15; //opacity of the slideshow controls after fade delay (value between 0 and 1)
|
||||
const FADE_LEVEL = 0.1; //opacity of the slideshow controls after fade delay (value between 0 and 1)
|
||||
//using outerHTML because the SVG object returned by Obsidin is in the main workspace window
|
||||
//but excalidraw might be open in a popout window which has a different document object
|
||||
const SVG_COG = ea.obsidian.getIcon("lucide-settings").outerHTML;
|
||||
@@ -45,10 +58,11 @@ const SVG_LASER_OFF = ea.obsidian.getIcon("lucide-wand").outerHTML;
|
||||
//utility & convenience functions
|
||||
//-------------------------------
|
||||
let isLaserOn = false;
|
||||
let slide = 0;
|
||||
let slide = shouldStartWithLastSlide ? window.ExcalidrawSlideshow.slide : 0;
|
||||
let isFullscreen = false;
|
||||
const ownerDocument = ea.targetView.ownerDocument;
|
||||
const startFullscreen = !altKey;
|
||||
|
||||
//The plugin and Obsidian App run in the window object
|
||||
//When Excalidraw is open in a popout window, the Excalidraw component will run in the ownerWindow
|
||||
//and in this case ownerWindow !== window
|
||||
@@ -176,7 +190,7 @@ let preventFullscreenExit = true;
|
||||
const gotoFullscreen = async () => {
|
||||
if(isFullscreen) return;
|
||||
preventFullscreenExit = true;
|
||||
if(app.isMobile) {
|
||||
if(ea.DEVICE.isMobile) {
|
||||
ea.viewToggleFullScreen();
|
||||
} else {
|
||||
await contentEl.webkitRequestFullscreen();
|
||||
@@ -192,8 +206,8 @@ const gotoFullscreen = async () => {
|
||||
const exitFullscreen = async () => {
|
||||
if(!isFullscreen) return;
|
||||
preventFullscreenExit = true;
|
||||
if(!app.isMobile && ownerDocument?.fullscreenElement) await ownerDocument.exitFullscreen();
|
||||
if(app.isMobile) ea.viewToggleFullScreen();
|
||||
if(!ea.DEVICE.isMobile && ownerDocument?.fullscreenElement) await ownerDocument.exitFullscreen();
|
||||
if(ea.DEVICE.isMobile) ea.viewToggleFullScreen();
|
||||
if(toggleFullscreenButton) toggleFullscreenButton.innerHTML = SVG_MAXIMIZE;
|
||||
await waitForExcalidrawResize();
|
||||
resetControlPanelElPosition();
|
||||
@@ -256,7 +270,7 @@ const getNavigationRect = ({ x1, y1, x2, y2 }) => {
|
||||
const { width, height } = excalidrawAPI.getAppState();
|
||||
const ratioX = width / Math.abs(x1 - x2);
|
||||
const ratioY = height / Math.abs(y1 - y2);
|
||||
let ratio = Math.min(Math.max(ratioX, ratioY), 10);
|
||||
let ratio = Math.min(Math.max(ratioX, ratioY), 30);
|
||||
|
||||
const scaledWidth = Math.abs(x1 - x2) * ratio;
|
||||
const scaledHeight = Math.abs(y1 - y2) * ratio;
|
||||
@@ -336,6 +350,9 @@ const navigate = async (dir) => {
|
||||
}
|
||||
if(selectSlideDropdown) selectSlideDropdown.value = slide+1;
|
||||
await scrollToNextRect(nextRect);
|
||||
if(window.ExcalidrawSlideshow && (typeof window.ExcalidrawSlideshow.slide === "number")) {
|
||||
window.ExcalidrawSlideshow.slide = slide;
|
||||
}
|
||||
}
|
||||
|
||||
const navigateToSlide = (slideNumber) => {
|
||||
@@ -532,9 +549,11 @@ const keydownListener = (e) => {
|
||||
if(hostLeaf.width === 0 && hostLeaf.height === 0) return;
|
||||
e.preventDefault();
|
||||
switch(e.key) {
|
||||
case "Backspace":
|
||||
case "Escape":
|
||||
exitPresentation();
|
||||
break;
|
||||
case "Space":
|
||||
case "ArrowRight":
|
||||
case "ArrowDown":
|
||||
navigate("fwd");
|
||||
@@ -630,7 +649,7 @@ const initializeEventListners = () => {
|
||||
controlPanelEl.removeEventListener('mouseenter', onMouseEnter, false);
|
||||
controlPanelEl.removeEventListener('mouseleave', onMouseLeave, false);
|
||||
controlPanelEl.parentElement?.removeChild(controlPanelEl);
|
||||
if(!app.isMobile) {
|
||||
if(!ea.DEVICE.isMobile) {
|
||||
contentEl.removeEventListener('webkitfullscreenchange', fullscreenListener);
|
||||
contentEl.removeEventListener('fullscreenchange', fullscreenListener);
|
||||
}
|
||||
@@ -645,7 +664,7 @@ const initializeEventListners = () => {
|
||||
return true;
|
||||
};
|
||||
|
||||
if(!app.isMobile) {
|
||||
if(!ea.DEVICE.isMobile) {
|
||||
contentEl.addEventListener('webkitfullscreenchange', fullscreenListener);
|
||||
contentEl.addEventListener('fullscreenchange', fullscreenListener);
|
||||
}
|
||||
@@ -708,7 +727,7 @@ const exitPresentation = async (openForEdit = false) => {
|
||||
//Resets pointer offsets. Ugly solution.
|
||||
//During testing offsets were wrong after presentation, but don't know why.
|
||||
//This should solve it even if they are wrong.
|
||||
hostView.refresh();
|
||||
hostView.refreshCanvasOffset();
|
||||
excalidrawAPI.setActiveTool({type: "selection"});
|
||||
})
|
||||
}
|
||||
@@ -719,6 +738,15 @@ const exitPresentation = async (openForEdit = false) => {
|
||||
const start = async () => {
|
||||
statusBarElement.style.display = "none";
|
||||
ea.setViewModeEnabled(true);
|
||||
const helpButton = ea.targetView.excalidrawContainer?.querySelector(".ToolIcon__icon.help-icon");
|
||||
if(helpButton) {
|
||||
helpButton.style.display = "none";
|
||||
}
|
||||
const zoomButton = ea.targetView.excalidrawContainer?.querySelector(".Stack.Stack_vertical.zoom-actions");
|
||||
if(zoomButton) {
|
||||
zoomButton.style.display = "none";
|
||||
}
|
||||
|
||||
createPresentationNavigationPanel();
|
||||
initializeEventListners();
|
||||
if(startFullscreen) {
|
||||
@@ -743,7 +771,8 @@ if(window.ExcalidrawSlideshow && (window.ExcalidrawSlideshow.script === utils.sc
|
||||
}
|
||||
window.ExcalidrawSlideshow = {
|
||||
script: utils.scriptFile.path,
|
||||
timestamp
|
||||
timestamp,
|
||||
slide: 0
|
||||
};
|
||||
window.ExcalidrawSlideshowStartTimer = window.setTimeout(start,500);
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,10 @@ elements.forEach((el)=>{
|
||||
ea.style.strokeColor = el.strokeColor;
|
||||
ea.style.fontFamily = el.fontFamily;
|
||||
ea.style.fontSize = el.fontSize;
|
||||
const text = el.text.split("\n");
|
||||
const text = el.rawText.split("\n");
|
||||
for(i=0;i<text.length;i++) {
|
||||
ea.addText(el.x,el.y+i*el.height/text.length,text[i]);
|
||||
ea.addText(el.x,el.y+i*el.height/text.length,text[i].trim());
|
||||
}
|
||||
});
|
||||
ea.addElementsToView(false,false);
|
||||
ea.addElementsToView(false,false,true);
|
||||
ea.deleteViewElements(elements);
|
||||
@@ -46,4 +46,4 @@ for(i=0;i<el.text.length;i++) {
|
||||
objectIDs.push(ea.addText(x,y,character));
|
||||
}
|
||||
ea.addToGroup(objectIDs);
|
||||
ea.addElementsToView(true);
|
||||
ea.addElementsToView(true, false, true);
|
||||
File diff suppressed because one or more lines are too long
@@ -15,12 +15,12 @@ In the `Command Palette` installed scripts are prefixed with `Downloaded/`, thus
|
||||
|
||||
## Attention developers and hobby hackers
|
||||
<img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/hobby-programmer.svg' align='left' style='background-color:whitesmoke; width:80px; margin-right:15px; margin-bottom:10px;'/>
|
||||
If you want to modify scripts, I recommend moving them to the `Excalidraw Automate script folder` or a different subfolder under the script folder. Scripts in the `Downloaded` folder will be overwritten when you click the `Update this script` button. Note also, that at this time, I do not check if the script file has been updated on GitHub, thus the `Update this script` button is always visible once you have installed a script, not only when an update is availble (hope to build this feature in the future).
|
||||
If you want to modify scripts, I recommend moving them to the `Excalidraw Automate script folder` or a different subfolder under the script folder. Scripts in the `Downloaded` folder will be overwritten when you click the `Update this script` button. Note also, that at this time, I do not check if the script file has been updated on GitHub, thus the `Update this script` button is always visible once you have installed a script, not only when an update is available (hope to build this feature in the future).
|
||||
|
||||
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)
|
||||
|
||||
---
|
||||
|
||||
@@ -116,6 +116,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/Auto%20Draw%20for%20Pen.svg"/></div>|[[#Auto Draw for Pen]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Boolean%20Operations.svg"/></div>|[[#Boolean Operations]]|
|
||||
|<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/GPT-Draw-a-UI.svg"/></div>|[[#GPT Draw-a-UI]]|
|
||||
@@ -146,6 +147,13 @@ 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/Convert%20freedraw%20to%20line.svg"/></div>|[[#Convert freedraw to line]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Deconstruct%20selected%20elements%20into%20new%20drawing.svg"/></div>|[[#Deconstruct selected elements into new drawing]]|
|
||||
|
||||
## Masking and cropping
|
||||
**Keywords**: Crop, Mask, Transform images
|
||||
|
||||
| | |
|
||||
|----|-----|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Crop%20Vintage%20Mask.svg"/></div>|[[#Crop Vintage Mask]]|
|
||||
|
||||
---
|
||||
|
||||
# Description and Installation
|
||||
@@ -190,7 +198,7 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Boolean%20Operations.md
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/GColoy'>@GColoy</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/Boolean%20Operations.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">With This Script it is possible to make boolean Operations on Shapes.<br>The style of the resulting shape will be the style of the highest ranking Element that was used.<br>The ranking of the elemtns is based on their background. The "denser" the background, the higher the ranking (the order of backgroundstyles is shown below). If they have the same background the opacity will decide. If thats also the same its decided by the order they were created.<br>The ranking is also important for the diffrence operation, so a tranparent object for example will cut a hole into a solid object.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-boolean-operations-showcase.png'><br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-boolean-operations-element-ranking.png'></td></tr></table>
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/GColoy'>@GColoy</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/Boolean%20Operations.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">With This Script it is possible to make boolean Operations on Shapes.<br>The style of the resulting shape will be the style of the highest ranking Element that was used.<br>The ranking of the elements is based on their background. The "denser" the background, the higher the ranking (the order of backgroundstyles is shown below). If they have the same background the opacity will decide. If thats also the same its decided by the order they were created.<br>The ranking is also important for the difference operation, so a transparent object for example will cut a hole into a solid object.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-boolean-operations-showcase.png'><br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-boolean-operations-element-ranking.png'></td></tr></table>
|
||||
|
||||
|
||||
## Box Each Selected Groups
|
||||
@@ -259,6 +267,18 @@ 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/Create%20new%20markdown%20file%20and%20embed%20into%20active%20drawing.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">The script will prompt you for a filename, then create a new markdown document with the file name provided, open the new markdown document in an adjacent pane, and embed the markdown document into the active Excalidraw drawing.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-create-and-embed-new-markdown-file.jpg'></td></tr></table>
|
||||
|
||||
## Crop Vintage Mask
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Crop%20Vintage%20Mask.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/Crop%20Vintage%20Mask.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Adds a rounded mask to the image by adding a full cover black mask and a rounded rectangle white mask. The script is also useful for adding just a black mask. In this case, run the script, then delete the white mask and add your custom white mask.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-crop-vintage.jpg'></td></tr></table>
|
||||
|
||||
## Custom Zoom
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Custom%20Zoom.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/Custom%20Zoom.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">You can set a custom zoom level with this script. This allows you to set a zoom level below 10% or set the zoom level to a specific value. Note however, that Excalidraw has a bug under 10% zoom... a phantom copy of your image may appear on screen. If this happens, increase the zoom and the phantom should disappear, if it doesn't, then close and open the drawing.</td></tr></table>
|
||||
|
||||
## Darken background color
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Darken%20background%20color.md
|
||||
@@ -488,7 +508,7 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Select%20Similar%20Elements.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/Select%20Similar%20Elements.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script allows you to streamline your Obsidian-Excalidraw workflows by enabling the selection of elements based on similar properties. you can precisely define which attributes such as stroke color, fill style, font family, and more, should match for selection. It's perfect for large canvases where manual selection would be cumbersome. You can either run the script to find and select matching elements across the entire scene, or define a specific group of elements to apply the selection criteria within a defined timeframe. This script enhances control and efficiency in your Excalidraw experience.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-select-similar-elements.png'></td></tr></table>
|
||||
<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/Select%20Similar%20Elements.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script allows you to streamline your Obsidian-Excalidraw workflows by enabling the selection of elements based on similar properties. you can precisely define which attributes such as stroke color, fill style, font family, and more, should match for selection. It's perfect for large canvases where manual selection would be cumbersome. You can either run the script to find and select matching elements across the entire scene, or define a specific group of elements to apply the selection criteria within a defined timeframe.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-select-similar-elements.png'></td></tr></table>
|
||||
|
||||
## Set background color of unclosed line object by adding a shadow clone
|
||||
```excalidraw-script-install
|
||||
|
||||
BIN
images/scripts-crop-vintage.jpg
Normal file
BIN
images/scripts-crop-vintage.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
BIN
images/vintage-mask.png
Normal file
BIN
images/vintage-mask.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "2.0.1-beta-2",
|
||||
"version": "2.2.10-1",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "2.0.14",
|
||||
"version": "2.2.10",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
27
package.json
27
package.json
@@ -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",
|
||||
@@ -18,23 +18,24 @@
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@zsviczian/excalidraw": "0.17.1-obsidian-9",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@zsviczian/excalidraw": "0.17.1-obsidian-32",
|
||||
"chroma-js": "^2.4.2",
|
||||
"clsx": "^2.0.0",
|
||||
"colormaster": "^1.2.1",
|
||||
"gl-matrix": "^3.4.3",
|
||||
"lucide-react": "^0.263.1",
|
||||
"mathjax-full": "^3.2.2",
|
||||
"monkey-around": "^2.3.0",
|
||||
"nanoid": "^4.0.2",
|
||||
"polybooljs": "^1.2.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"roughjs": "^4.5.2",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"nanoid": "^4.0.2",
|
||||
"lucide-react": "^0.263.1",
|
||||
"mathjax-full": "^3.2.2"
|
||||
"js-yaml": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"lz-string": "^1.5.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"@babel/core": "^7.22.9",
|
||||
"@babel/preset-env": "^7.22.10",
|
||||
"@babel/preset-react": "^7.22.5",
|
||||
@@ -50,11 +51,14 @@
|
||||
"@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",
|
||||
"obsidian": "^1.4.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",
|
||||
@@ -65,8 +69,11 @@
|
||||
"tslib": "^2.6.1",
|
||||
"ttypescript": "^1.5.15",
|
||||
"typescript": "^5.2.2",
|
||||
"cssnano": "^6.0.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"
|
||||
},
|
||||
"resolutions": {
|
||||
"@typescript-eslint/typescript-estree": "5.3.0"
|
||||
|
||||
102
rollup.config.js
102
rollup.config.js
@@ -1,19 +1,22 @@
|
||||
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import { env } from "process";
|
||||
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 fs from 'fs';
|
||||
import LZString from 'lz-string';
|
||||
import postprocess from 'rollup-plugin-postprocess';
|
||||
import cssnano from 'cssnano';
|
||||
|
||||
// Load environment variables
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
const DIST_FOLDER = 'dist';
|
||||
const isProd = (process.env.NODE_ENV === "production")
|
||||
const isProd = (process.env.NODE_ENV === "production");
|
||||
const isLib = (process.env.NODE_ENV === "lib");
|
||||
console.log(`Running: ${process.env.NODE_ENV}`);
|
||||
|
||||
@@ -26,14 +29,15 @@ const react_pkg = isLib ? "" : isProd
|
||||
const reactdom_pkg = isLib ? "" : isProd
|
||||
? fs.readFileSync("./node_modules/react-dom/umd/react-dom.production.min.js", "utf8")
|
||||
: fs.readFileSync("./node_modules/react-dom/umd/react-dom.development.js", "utf8");
|
||||
|
||||
const lzstring_pkg = isLib ? "" : fs.readFileSync("./node_modules/lz-string/libs/lz-string.min.js", "utf8");
|
||||
if(!isLib) {
|
||||
if (!isLib) {
|
||||
const excalidraw_styles = isProd
|
||||
? fs.readFileSync("./node_modules/@zsviczian/excalidraw/dist/styles.production.css", "utf8")
|
||||
: fs.readFileSync("./node_modules/@zsviczian/excalidraw/dist/styles.development.css", "utf8");
|
||||
const plugin_styles = fs.readFileSync("./styles.css", "utf8")
|
||||
const plugin_styles = fs.readFileSync("./styles.css", "utf8");
|
||||
const styles = plugin_styles + excalidraw_styles;
|
||||
cssnano()
|
||||
cssnano()
|
||||
.process(styles) // Process the CSS
|
||||
.then(result => {
|
||||
fs.writeFileSync(`./${DIST_FOLDER}/styles.css`, result.css);
|
||||
@@ -45,47 +49,60 @@ if(!isLib) {
|
||||
|
||||
const manifestStr = isLib ? "" : fs.readFileSync("manifest.json", "utf-8");
|
||||
const manifest = isLib ? {} : JSON.parse(manifestStr);
|
||||
!isLib && console.log(manifest.version);
|
||||
if (!isLib) console.log(manifest.version);
|
||||
|
||||
const packageString = isLib
|
||||
? ""
|
||||
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)};' +
|
||||
'return {react:React, reactDOM:ReactDOM, excalidrawLib: ExcalidrawLib};})();`);\n' +
|
||||
'const PLUGIN_VERSION="'+manifest.version+'";';
|
||||
'\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' +
|
||||
'let PLUGIN_VERSION="' + manifest.version + '";';
|
||||
|
||||
const BASE_CONFIG = {
|
||||
input: 'src/main.ts',
|
||||
external: ['obsidian', '@zsviczian/excalidraw', 'react', 'react-dom'],
|
||||
}
|
||||
external: [
|
||||
'@codemirror/autocomplete',
|
||||
'@codemirror/collab',
|
||||
'@codemirror/commands',
|
||||
'@codemirror/language',
|
||||
'@codemirror/lint',
|
||||
'@codemirror/search',
|
||||
'@codemirror/state',
|
||||
'@codemirror/view',
|
||||
'@lezer/common',
|
||||
'@lezer/highlight',
|
||||
'@lezer/lr',
|
||||
'obsidian',
|
||||
'@zsviczian/excalidraw',
|
||||
'react',
|
||||
'react-dom'
|
||||
],
|
||||
};
|
||||
|
||||
const getRollupPlugins = (tsconfig, ...plugins) =>
|
||||
[
|
||||
typescript2(tsconfig),
|
||||
nodeResolve({ browser: true }),
|
||||
commonjs(),
|
||||
webWorker({ inline: true, forceInline: true, targetPlatform: "browser" }),
|
||||
].concat(plugins);
|
||||
const getRollupPlugins = (tsconfig, ...plugins) => [
|
||||
typescript2(tsconfig),
|
||||
nodeResolve({ browser: true }),
|
||||
commonjs(),
|
||||
webWorker({ inline: true, forceInline: true, targetPlatform: "browser" }),
|
||||
].concat(plugins);
|
||||
|
||||
const BUILD_CONFIG = {
|
||||
...BASE_CONFIG,
|
||||
output: {
|
||||
dir: DIST_FOLDER,
|
||||
entryFileNames: 'main.js',
|
||||
//sourcemap: isProd?false:'inline',
|
||||
format: 'cjs',
|
||||
exports: 'default',
|
||||
},
|
||||
plugins: [
|
||||
typescript2({
|
||||
tsconfig: isProd ? "tsconfig.json" : "tsconfig.dev.json",
|
||||
//inlineSources: !isProd
|
||||
}),
|
||||
replace({
|
||||
preventAssignment: true,
|
||||
"process.env.NODE_ENV": JSON.stringify(env.NODE_ENV),
|
||||
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
|
||||
}),
|
||||
babel({
|
||||
presets: [['@babel/preset-env', {
|
||||
@@ -93,28 +110,27 @@ const BUILD_CONFIG = {
|
||||
esmodules: true,
|
||||
},
|
||||
}]],
|
||||
exclude: "node_modules/**"
|
||||
exclude: "node_modules/**",
|
||||
}),
|
||||
commonjs(),
|
||||
nodeResolve({ browser: true, preferBuiltins: false }),
|
||||
...isProd
|
||||
? [
|
||||
...(isProd ? [
|
||||
terser({
|
||||
toplevel: false,
|
||||
compress: {passes: 2}
|
||||
compress: { passes: 2 },
|
||||
}),
|
||||
//!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"\);/, packageString],
|
||||
])
|
||||
]
|
||||
: [
|
||||
[/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],
|
||||
])
|
||||
],
|
||||
]),
|
||||
]),
|
||||
copy({
|
||||
targets: [
|
||||
{ src: 'manifest.json', dest: DIST_FOLDER },
|
||||
@@ -122,7 +138,7 @@ const BUILD_CONFIG = {
|
||||
verbose: true, // Optional: To display copied files in the console
|
||||
}),
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
const LIB_CONFIG = {
|
||||
...BASE_CONFIG,
|
||||
@@ -134,16 +150,16 @@ const LIB_CONFIG = {
|
||||
name: "Excalidraw (Library)",
|
||||
},
|
||||
plugins: getRollupPlugins(
|
||||
{ tsconfig: "tsconfig-lib.json"},
|
||||
copy({ targets: [{ src: "src/*.d.ts", dest: "lib/typings" }] })
|
||||
),
|
||||
}
|
||||
{ tsconfig: "tsconfig-lib.json" },
|
||||
copy({ targets: [{ src: "src/*.d.ts", dest: "lib/typings" }] })
|
||||
),
|
||||
};
|
||||
|
||||
let config = [];
|
||||
if(process.env.NODE_ENV === "lib") {
|
||||
if (process.env.NODE_ENV === "lib") {
|
||||
config.push(LIB_CONFIG);
|
||||
} else {
|
||||
config.push(BUILD_CONFIG);
|
||||
}
|
||||
|
||||
export default config;
|
||||
export default config;
|
||||
|
||||
49
src/CodeMirrorExtension/EditorHandler.ts
Normal file
49
src/CodeMirrorExtension/EditorHandler.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Extension } from "@codemirror/state";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
import { HideTextBetweenCommentsExtension } from "./Fadeout";
|
||||
export const EDITOR_FADEOUT = "fadeOutExcalidrawMarkup";
|
||||
|
||||
const editorExtensions: {[key:string]:Extension}= {
|
||||
[EDITOR_FADEOUT]: HideTextBetweenCommentsExtension,
|
||||
}
|
||||
|
||||
export class EditorHandler {
|
||||
private activeEditorExtensions: Extension[] = [];
|
||||
|
||||
constructor(private plugin: ExcalidrawPlugin) {}
|
||||
|
||||
destroy(): void {
|
||||
this.plugin = null;
|
||||
}
|
||||
|
||||
setup(): void {
|
||||
this.plugin.registerEditorExtension(this.activeEditorExtensions);
|
||||
this.updateCMExtensionState(EDITOR_FADEOUT, this.plugin.settings.fadeOutExcalidrawMarkup);
|
||||
}
|
||||
|
||||
updateCMExtensionState(
|
||||
extensionIdentifier: string,
|
||||
extensionState: boolean,
|
||||
) {
|
||||
const extension = editorExtensions[extensionIdentifier];
|
||||
if(!extension) return;
|
||||
if (extensionState == true) {
|
||||
this.activeEditorExtensions.push(extension);
|
||||
// @ts-ignore
|
||||
this.activeEditorExtensions[this.activeEditorExtensions.length - 1].exID = extensionIdentifier;
|
||||
} else {
|
||||
for (let i = 0; i < this.activeEditorExtensions.length; i++) {
|
||||
const ext = this.activeEditorExtensions[i];
|
||||
// @ts-ignore
|
||||
if (ext.exID === extensionIdentifier) {
|
||||
this.activeEditorExtensions.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.plugin.app.workspace.updateOptions();
|
||||
}
|
||||
update(): void {
|
||||
this.plugin.app.workspace.updateOptions();
|
||||
}
|
||||
}
|
||||
66
src/CodeMirrorExtension/Fadeout.ts
Normal file
66
src/CodeMirrorExtension/Fadeout.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { RangeSetBuilder } from "@codemirror/state";
|
||||
import { Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view";
|
||||
|
||||
const o30 = Decoration.line({ attributes: {class: "ex-opacity-30"} });
|
||||
const o15 = Decoration.line({ attributes: {class: "ex-opacity-15"} });
|
||||
const o8 = Decoration.line({ attributes: {class: "ex-opacity-8"} });
|
||||
const o5 = Decoration.line({ attributes: {class: "ex-opacity-5"} });
|
||||
const o0 = Decoration.line({ attributes: {class: "ex-opacity-0"} });
|
||||
|
||||
export const HideTextBetweenCommentsExtension = ViewPlugin.fromClass(
|
||||
class {
|
||||
view: EditorView;
|
||||
decorations: DecorationSet;
|
||||
reExcalidrawData = /^%%(?:\r\n|\r|\n)# Excalidraw Data$/gm;
|
||||
reTextElements = /^%%(?:\r\n|\r|\n)# Text Elements$/gm;
|
||||
reDrawing = /^%%(?:\r\n|\r|\n)##? Drawing$/gm;
|
||||
linecount = 0;
|
||||
isExcalidraw = false;
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.view = view;
|
||||
this.isExcalidraw = view.state.doc.toString().search(/^excalidraw-plugin: /m) > 0;
|
||||
if(!this.isExcalidraw) {
|
||||
this.decorations = Decoration.none;
|
||||
return;
|
||||
}
|
||||
this.decorations = this.updateDecorations(view);
|
||||
}
|
||||
|
||||
updateDecorations (view: EditorView) {
|
||||
const { state } = view;
|
||||
const { doc } = state;
|
||||
|
||||
const text = doc.toString();
|
||||
|
||||
let start = text.search(this.reExcalidrawData);
|
||||
if(start == -1) {
|
||||
start = text.search(this.reTextElements);
|
||||
}
|
||||
if(start == -1) {
|
||||
start = text.search(this.reDrawing);
|
||||
}
|
||||
if(start == -1) return Decoration.none;
|
||||
|
||||
const startLine = doc.lineAt(start).number;
|
||||
const endLine = doc.lines;
|
||||
let builder = new RangeSetBuilder<Decoration>()
|
||||
for (let l = startLine; l <= endLine; l++) {
|
||||
const line = doc.line(l);
|
||||
const pos = l-startLine;
|
||||
builder.add(line.from, line.from,
|
||||
pos == 0 ? o30 : (pos == 1) ? o15 : (pos < 6) ? o8 : (pos < 12) ? o5 : o0);
|
||||
}
|
||||
return builder.finish()
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
if (this.isExcalidraw && update.docChanged) {
|
||||
this.decorations = this.updateDecorations(update.view)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: (x) => x.decorations,
|
||||
}
|
||||
);
|
||||
@@ -1,7 +1,7 @@
|
||||
//https://stackoverflow.com/questions/2068344/how-do-i-get-a-youtube-video-thumbnail-from-the-youtube-api
|
||||
//https://img.youtube.com/vi/uZz5MgzWXiM/maxresdefault.jpg
|
||||
|
||||
import { ExcalidrawElement, ExcalidrawImageElement, FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
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 {
|
||||
@@ -12,13 +12,10 @@ import {
|
||||
import {
|
||||
DEFAULT_MD_EMBED_CSS,
|
||||
fileid,
|
||||
FRONTMATTER_KEY_BORDERCOLOR,
|
||||
FRONTMATTER_KEY_FONT,
|
||||
FRONTMATTER_KEY_FONTCOLOR,
|
||||
FRONTMATTER_KEY_MD_STYLE,
|
||||
IMAGE_TYPES,
|
||||
nanoid,
|
||||
THEME_FILTER,
|
||||
FRONTMATTER_KEYS,
|
||||
} from "./constants/constants";
|
||||
import { createSVG } from "./ExcalidrawAutomate";
|
||||
import { ExcalidrawData, getTransclusion } from "./ExcalidrawData";
|
||||
@@ -26,7 +23,7 @@ import { ExportSettings } from "./ExcalidrawView";
|
||||
import { t } from "./lang/helpers";
|
||||
import { tex2dataURL } from "./LaTeX";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import { blobToBase64, getDataURLFromURL, getMimeType, getPDFDoc, getURLImageExtension, readLocalFileBinary } from "./utils/FileUtils";
|
||||
import { blobToBase64, getDataURLFromURL, getMimeType, getPDFDoc, getURLImageExtension, hasExcalidrawEmbeddedImagesTreeChanged, readLocalFileBinary } from "./utils/FileUtils";
|
||||
import {
|
||||
errorlog,
|
||||
getDataURL,
|
||||
@@ -40,10 +37,15 @@ import {
|
||||
hasExportTheme,
|
||||
LinkParts,
|
||||
svgToBase64,
|
||||
isMaskFile,
|
||||
embedFontsInSVG,
|
||||
getEmbeddedFilenameParts,
|
||||
} from "./utils/Utils";
|
||||
import { ValueOf } from "./types";
|
||||
import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
|
||||
import { mermaidToExcalidraw } from "src/constants/constants";
|
||||
import { ImageKey, imageCache } from "./utils/ImageCache";
|
||||
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 +146,6 @@ const replaceSVGColors = (svg: SVGSVGElement | string, colorMap: ColorMap | null
|
||||
return svg;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class EmbeddedFile {
|
||||
public file: TFile = null;
|
||||
public isSVGwithBitmap: boolean = false;
|
||||
@@ -156,6 +156,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;
|
||||
@@ -166,7 +167,7 @@ export class EmbeddedFile {
|
||||
constructor(plugin: ExcalidrawPlugin, hostPath: string, imgPath: string, colorMapJSON?: string) {
|
||||
this.plugin = plugin;
|
||||
this.resetImage(hostPath, imgPath);
|
||||
if(this.file && (this.plugin.ea.isExcalidrawFile(this.file) || this.file.extension.toLowerCase() === "svg")) {
|
||||
if(this.file && (this.plugin.isExcalidrawFile(this.file) || this.file.extension.toLowerCase() === "svg")) {
|
||||
try {
|
||||
this.colorMap = colorMapJSON ? JSON.parse(colorMapJSON.toLocaleLowerCase()) : null;
|
||||
} catch (error) {
|
||||
@@ -203,7 +204,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 +215,9 @@ export class EmbeddedFile {
|
||||
5000,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.filenameparts = getEmbeddedFilenameParts(imgPath);
|
||||
this.filenameparts.filepath = this.file.path;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +235,7 @@ export class EmbeddedFile {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return this.mtime != this.file.stat.mtime;
|
||||
return this.mtime !== this.file.stat.mtime;
|
||||
}
|
||||
|
||||
public setImage(
|
||||
@@ -349,6 +353,7 @@ export class EmbeddedFilesLoader {
|
||||
elements?: ExcalidrawElement[];
|
||||
}) : Promise<{dataURL: DataURL, hasSVGwithBitmap:boolean}> {
|
||||
//debug({where:"EmbeddedFileLoader.getExcalidrawSVG",uid:this.uid,file:file.name});
|
||||
const isMask = isMaskFile(this.plugin, file);
|
||||
const forceTheme = hasExportTheme(this.plugin, file)
|
||||
? getExportTheme(this.plugin, file, "light")
|
||||
: undefined;
|
||||
@@ -357,23 +362,64 @@ export class EmbeddedFilesLoader {
|
||||
? getWithBackground(this.plugin, file)
|
||||
: false,
|
||||
withTheme: !!forceTheme,
|
||||
isMask,
|
||||
};
|
||||
const svg = replaceSVGColors(
|
||||
await createSVG(
|
||||
file?.path,
|
||||
true,
|
||||
exportSettings,
|
||||
this,
|
||||
forceTheme,
|
||||
null,
|
||||
null,
|
||||
elements,
|
||||
this.plugin,
|
||||
depth+1,
|
||||
getExportPadding(this.plugin, file),
|
||||
),
|
||||
inFile instanceof EmbeddedFile ? inFile.colorMap : null
|
||||
) as SVGSVGElement;
|
||||
|
||||
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 = {
|
||||
...hasFilenameParts? filenameParts : {
|
||||
filepath: file.path,
|
||||
hasBlockref: false,
|
||||
hasGroupref: false,
|
||||
hasTaskbone: false,
|
||||
hasArearef: false,
|
||||
hasFrameref: false,
|
||||
hasClippedFrameref: false,
|
||||
hasSectionref: false,
|
||||
blockref: null,
|
||||
sectionref: null,
|
||||
linkpartReference: null,
|
||||
linkpartAlias: null,
|
||||
},
|
||||
isDark,
|
||||
previewImageType: PreviewImageType.SVG,
|
||||
scale: 1,
|
||||
isTransparent: !exportSettings.withBackground,
|
||||
}
|
||||
|
||||
const maybeSVG = shouldUseCache
|
||||
? await imageCache.getImageFromCache(cacheKey)
|
||||
: undefined;
|
||||
|
||||
const svg = (maybeSVG && (maybeSVG instanceof SVGSVGElement))
|
||||
? maybeSVG
|
||||
: replaceSVGColors(
|
||||
await createSVG(
|
||||
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,
|
||||
null,
|
||||
elements,
|
||||
this.plugin,
|
||||
depth+1,
|
||||
getExportPadding(this.plugin, file),
|
||||
),
|
||||
inFile instanceof EmbeddedFile ? inFile.colorMap : null
|
||||
) as SVGSVGElement;
|
||||
|
||||
//https://stackoverflow.com/questions/51154171/remove-css-filter-on-child-elements
|
||||
const imageList = svg.querySelectorAll(
|
||||
@@ -382,7 +428,8 @@ export class EmbeddedFilesLoader {
|
||||
if (imageList.length > 0) {
|
||||
hasSVGwithBitmap = true;
|
||||
}
|
||||
if (hasSVGwithBitmap && isDark) {
|
||||
|
||||
if (hasSVGwithBitmap && isDark && !Boolean(maybeSVG)) {
|
||||
imageList.forEach((i) => {
|
||||
const id = i.parentElement?.id;
|
||||
svg.querySelectorAll(`use[href='#${id}']`).forEach((u) => {
|
||||
@@ -393,7 +440,21 @@ export class EmbeddedFilesLoader {
|
||||
if (!hasSVGwithBitmap && svg.getAttribute("hasbitmap")) {
|
||||
hasSVGwithBitmap = true;
|
||||
}
|
||||
const dURL = svgToBase64(svg.outerHTML) as DataURL;
|
||||
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);
|
||||
}
|
||||
const svgWithFont = embedFontsInSVG(svg, this.plugin);
|
||||
if(!svgWithFont.hasAttribute("width") && svgWithFont.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 = svgWithFont.getAttr("viewBox").split(" ");
|
||||
Boolean(vb[2]) && svgWithFont.setAttribute("width", vb[2]);
|
||||
Boolean(vb[3]) && svgWithFont.setAttribute("height", vb[3]);
|
||||
}
|
||||
const dURL = svgToBase64(svgWithFont.outerHTML) as DataURL;
|
||||
return {dataURL: dURL as DataURL, hasSVGwithBitmap};
|
||||
};
|
||||
|
||||
@@ -511,7 +572,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,
|
||||
@@ -526,9 +588,11 @@ export class EmbeddedFilesLoader {
|
||||
public async loadSceneFiles(
|
||||
excalidrawData: ExcalidrawData,
|
||||
addFiles: (files: FileData[], isDark: boolean, final?: boolean) => void,
|
||||
depth:number
|
||||
depth:number,
|
||||
isThemeChange:boolean = false,
|
||||
) {
|
||||
if(depth > 4) {
|
||||
|
||||
if(depth > 7) {
|
||||
new Notice(t("INFINITE_LOOP_WARNING")+depth.toString(), 6000);
|
||||
return;
|
||||
}
|
||||
@@ -562,7 +626,8 @@ export class EmbeddedFilesLoader {
|
||||
}
|
||||
//files.push(fileData);
|
||||
}
|
||||
} else if (embeddedFile.isSVGwithBitmap) {
|
||||
} else if (embeddedFile.isSVGwithBitmap && (depth !== 0 || isThemeChange)) {
|
||||
//this will reload the image in light/dark mode when switching themes
|
||||
const fileData = {
|
||||
mimeType: embeddedFile.mimeType,
|
||||
id: entry.value[0],
|
||||
@@ -745,9 +810,9 @@ export class EmbeddedFilesLoader {
|
||||
let fontName = plugin.settings.mdFont;
|
||||
if (
|
||||
fileCache?.frontmatter &&
|
||||
Boolean(fileCache.frontmatter[FRONTMATTER_KEY_FONT])
|
||||
Boolean(fileCache.frontmatter[FRONTMATTER_KEYS["font"].name])
|
||||
) {
|
||||
fontName = fileCache.frontmatter[FRONTMATTER_KEY_FONT];
|
||||
fontName = fileCache.frontmatter[FRONTMATTER_KEYS["font"].name];
|
||||
}
|
||||
switch (fontName) {
|
||||
case "Virgil":
|
||||
@@ -776,15 +841,15 @@ export class EmbeddedFilesLoader {
|
||||
}
|
||||
|
||||
const fontColor = fileCache?.frontmatter
|
||||
? fileCache.frontmatter[FRONTMATTER_KEY_FONTCOLOR] ??
|
||||
? fileCache.frontmatter[FRONTMATTER_KEYS["font-color"].name] ??
|
||||
plugin.settings.mdFontColor
|
||||
: plugin.settings.mdFontColor;
|
||||
|
||||
let style = fileCache?.frontmatter
|
||||
? fileCache.frontmatter[FRONTMATTER_KEY_MD_STYLE] ?? ""
|
||||
? fileCache.frontmatter[FRONTMATTER_KEYS["md-css"].name] ?? ""
|
||||
: "";
|
||||
let frontmatterCSSisAfile = false;
|
||||
if (style && style != "") {
|
||||
if (style && style !== "") {
|
||||
const f = plugin.app.metadataCache.getFirstLinkpathDest(style, file.path);
|
||||
if (f) {
|
||||
style = await plugin.app.vault.read(f);
|
||||
@@ -804,7 +869,7 @@ export class EmbeddedFilesLoader {
|
||||
}
|
||||
|
||||
const borderColor = fileCache?.frontmatter
|
||||
? fileCache.frontmatter[FRONTMATTER_KEY_BORDERCOLOR] ??
|
||||
? fileCache.frontmatter[FRONTMATTER_KEYS["border-color"].name] ??
|
||||
plugin.settings.mdBorderColor
|
||||
: plugin.settings.mdBorderColor;
|
||||
|
||||
@@ -957,7 +1022,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);
|
||||
@@ -972,4 +1037,37 @@ export const generateIdFromFile = async (file: ArrayBuffer): Promise<FileId> =>
|
||||
id = fileid() as FileId;
|
||||
}
|
||||
return id;
|
||||
};*/
|
||||
|
||||
export const generateIdFromFile = async (file: ArrayBuffer, key?: string): Promise<FileId> => {
|
||||
let id: FileId;
|
||||
try {
|
||||
// Convert the file ArrayBuffer to a Uint8Array
|
||||
const fileArray = new Uint8Array(file);
|
||||
|
||||
// If a key is provided, concatenate it to the file data
|
||||
let dataToHash: Uint8Array;
|
||||
if (key) {
|
||||
const encoder = new TextEncoder();
|
||||
const keyArray = encoder.encode(key);
|
||||
dataToHash = new Uint8Array(fileArray.length + keyArray.length);
|
||||
dataToHash.set(fileArray);
|
||||
dataToHash.set(keyArray, fileArray.length);
|
||||
} else {
|
||||
dataToHash = fileArray;
|
||||
}
|
||||
|
||||
// Hash the combined data (file and key, if provided)
|
||||
const hashBuffer = await window.crypto.subtle.digest("SHA-1", dataToHash);
|
||||
id =
|
||||
// Convert buffer to byte array
|
||||
Array.from(new Uint8Array(hashBuffer))
|
||||
// Convert to hex string
|
||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||
.join("") as FileId;
|
||||
} catch (error) {
|
||||
errorlog({ where: "EmbeddedFileLoader.generateIdFromFile", error });
|
||||
id = fileid() as FileId;
|
||||
}
|
||||
return id;
|
||||
};
|
||||
|
||||
@@ -10,10 +10,12 @@ import {
|
||||
ExcalidrawTextElement,
|
||||
StrokeRoundness,
|
||||
RoundnessType,
|
||||
ExcalidrawFrameElement,
|
||||
ExcalidrawTextContainer,
|
||||
} from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { Editor, normalizePath, Notice, OpenViewState, RequestUrlResponse, TFile, TFolder, WorkspaceLeaf } from "obsidian";
|
||||
import * as obsidian_module from "obsidian";
|
||||
import ExcalidrawView, { ExportSettings, TextMode } from "src/ExcalidrawView";
|
||||
import ExcalidrawView, { ExportSettings, TextMode, getTextMode } from "src/ExcalidrawView";
|
||||
import { ExcalidrawData, getMarkdownDrawingSection, REGEX_LINK } from "src/ExcalidrawData";
|
||||
import {
|
||||
FRONTMATTER,
|
||||
@@ -34,8 +36,9 @@ import {
|
||||
REG_LINKINDEX_INVALIDCHARS,
|
||||
THEME_FILTER,
|
||||
mermaidToExcalidraw,
|
||||
refreshTextDimensions,
|
||||
} from "src/constants/constants";
|
||||
import { blobToBase64, checkAndCreateFolder, getDrawingFilename, getNewUniqueFilepath, } from "src/utils/FileUtils";
|
||||
import { blobToBase64, checkAndCreateFolder, getDrawingFilename, getExcalidrawEmbeddedFilesFiletree, getListOfTemplateFiles, getNewUniqueFilepath, hasExcalidrawEmbeddedImagesTreeChanged, } from "src/utils/FileUtils";
|
||||
import {
|
||||
//debug,
|
||||
embedFontsInSVG,
|
||||
@@ -45,16 +48,17 @@ import {
|
||||
getLinkParts,
|
||||
getPNG,
|
||||
getSVG,
|
||||
isMaskFile,
|
||||
isVersionNewerThanOther,
|
||||
log,
|
||||
scaleLoadedImage,
|
||||
wrapTextAtCharLength,
|
||||
arrayToMap,
|
||||
} from "src/utils/Utils";
|
||||
import { getAttachmentsFolderAndFilePath, getLeaf, getNewOrAdjacentLeaf, isObsidianThemeDark } from "src/utils/ObsidianUtils";
|
||||
import { getAttachmentsFolderAndFilePath, getLeaf, getNewOrAdjacentLeaf, isObsidianThemeDark, mergeMarkdownFiles, openLeaf } from "src/utils/ObsidianUtils";
|
||||
import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI, Point } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { EmbeddedFile, EmbeddedFilesLoader, FileData } from "src/EmbeddedFileLoader";
|
||||
import { tex2dataURL } from "src/LaTeX";
|
||||
import { GenericInputPrompt, NewFileActions, Prompt } from "src/dialogs/Prompt";
|
||||
import { GenericInputPrompt, NewFileActions } from "src/dialogs/Prompt";
|
||||
import { t } from "src/lang/helpers";
|
||||
import { ScriptEngine } from "src/Scripts";
|
||||
import { ConnectionPoint, DeviceType } from "src/types";
|
||||
@@ -76,7 +80,7 @@ import { TInput } from "colormaster/types";
|
||||
import {ConversionResult, svgToExcalidraw} from "src/svgToExcalidraw/parser"
|
||||
import { ROUNDNESS } from "src/constants/constants";
|
||||
import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard";
|
||||
import { emulateKeysForLinkClick, KeyEvent, PaneTarget } from "src/utils/ModifierkeyHelper";
|
||||
import { emulateKeysForLinkClick, PaneTarget } from "src/utils/ModifierkeyHelper";
|
||||
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
|
||||
import PolyBool from "polybooljs";
|
||||
import { EmbeddableMDCustomProps } from "./dialogs/EmbeddableSettings";
|
||||
@@ -85,7 +89,9 @@ import {
|
||||
postOpenAI as _postOpenAI,
|
||||
extractCodeBlocks as _extractCodeBlocks,
|
||||
} from "./utils/AIUtils";
|
||||
import { EXCALIDRAW_AUTOMATE_INFO } from "./dialogs/SuggesterInfo";
|
||||
import { EXCALIDRAW_AUTOMATE_INFO, EXCALIDRAW_SCRIPTENGINE_INFO } from "./dialogs/SuggesterInfo";
|
||||
import { addBackOfTheNoteCard, getFrameBasedOnFrameNameOrId } from "./utils/ExcalidrawViewUtils";
|
||||
import { log } from "./utils/DebugHelper";
|
||||
|
||||
extendPlugins([
|
||||
HarmonyPlugin,
|
||||
@@ -126,7 +132,7 @@ export class ExcalidrawAutomate {
|
||||
|
||||
public help(target: Function | string) {
|
||||
if (!target) {
|
||||
console.log("Usage: ea.help(ea.functionName) or ea.help('propertyName')");
|
||||
log("Usage: ea.help(ea.functionName) or ea.help('propertyName') or ea.help('utils.functionName') - notice property name and utils function name is in quotes");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -135,15 +141,27 @@ export class ExcalidrawAutomate {
|
||||
if (typeof target === 'function') {
|
||||
funcInfo = EXCALIDRAW_AUTOMATE_INFO.find((info) => info.field === target.name);
|
||||
} else if (typeof target === 'string') {
|
||||
funcInfo = EXCALIDRAW_AUTOMATE_INFO.find((info) => info.field === target);
|
||||
let stringTarget:string = target;
|
||||
stringTarget = stringTarget.startsWith("utils.") ? stringTarget.substring(6) : stringTarget;
|
||||
stringTarget = stringTarget.startsWith("ea.") ? stringTarget.substring(3) : stringTarget;
|
||||
funcInfo = EXCALIDRAW_AUTOMATE_INFO.find((info) => info.field === stringTarget);
|
||||
if(!funcInfo) {
|
||||
funcInfo = EXCALIDRAW_SCRIPTENGINE_INFO.find((info) => info.field === stringTarget);
|
||||
}
|
||||
}
|
||||
|
||||
if(!funcInfo) {
|
||||
console.log("Usage: ea.help(ea.functionName) or\nea.help('propertyName') - notice property name is in quotes");
|
||||
log("Usage: ea.help(ea.functionName) or ea.help('propertyName') or ea.help('utils.functionName') - notice property name and utils function name is in quotes");
|
||||
return;
|
||||
}
|
||||
|
||||
let isMissing = true;
|
||||
if (funcInfo.code) {
|
||||
isMissing = false;
|
||||
log(`Declaration: ${funcInfo.code}`);
|
||||
}
|
||||
if (funcInfo.desc) {
|
||||
isMissing = false;
|
||||
const formattedDesc = funcInfo.desc
|
||||
.replaceAll("<br>", "\n")
|
||||
.replace(/<code>(.*?)<\/code>/g, '%c\u200b$1%c') // Zero-width space
|
||||
@@ -151,11 +169,10 @@ export class ExcalidrawAutomate {
|
||||
.replace(/<a onclick='window\.open\("(.*?)"\)'>(.*?)<\/a>/g, (_, href, text) => `%c\u200b${text}%c\u200b (link: ${href})`); // Zero-width non-joiner
|
||||
|
||||
const styles = Array.from({ length: (formattedDesc.match(/%c/g) || []).length }, (_, i) => i % 2 === 0 ? 'color: #007bff;' : '');
|
||||
|
||||
console.log(`Declaration: ${funcInfo.code}`);
|
||||
console.log(`Description: ${formattedDesc}`, ...styles);
|
||||
} else {
|
||||
console.log("Description not available for this function.");
|
||||
log(`Description: ${formattedDesc}`, ...styles);
|
||||
}
|
||||
if (isMissing) {
|
||||
log("Description not available for this function.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,6 +242,30 @@ export class ExcalidrawAutomate {
|
||||
return getNewUniqueFilepath(app.vault, filename, folderpath);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns the Excalidraw Template files or null.
|
||||
*/
|
||||
public getListOfTemplateFiles(): TFile[] | null {
|
||||
return getListOfTemplateFiles(this.plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retruns the embedded images in the scene recursively. If excalidrawFile is not provided,
|
||||
* the function will use ea.targetView.file
|
||||
* @param excalidrawFile
|
||||
* @returns TFile[] of all nested images and Excalidraw drawings recursively
|
||||
*/
|
||||
public getEmbeddedImagesFiletree(excalidrawFile?: TFile): TFile[] {
|
||||
if(!excalidrawFile && this.targetView && this.targetView.file) {
|
||||
excalidrawFile = this.targetView.file;
|
||||
}
|
||||
if(!excalidrawFile) {
|
||||
return [];
|
||||
}
|
||||
return getExcalidrawEmbeddedFilesFiletree(excalidrawFile, this.plugin);
|
||||
}
|
||||
|
||||
public async getAttachmentFilepath(filename: string): Promise<string> {
|
||||
if (!this.targetView || !this.targetView?.file) {
|
||||
errorMessage("targetView not set", "getAttachmentFolderAndFilePath()");
|
||||
@@ -281,7 +322,7 @@ export class ExcalidrawAutomate {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new Obsidian Leaf following Excalidraw plugin settings such as open in Main Workspace or not, open in adjacent pane if avaialble, etc.
|
||||
* Generates a new Obsidian Leaf following Excalidraw plugin settings such as open in Main Workspace or not, open in adjacent pane if available, etc.
|
||||
* @param origo // the currently active leaf, the origin of the new leaf
|
||||
* @param targetPane //type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";
|
||||
* @returns
|
||||
@@ -319,6 +360,17 @@ export class ExcalidrawAutomate {
|
||||
return null;
|
||||
}
|
||||
|
||||
public isExcalidrawMaskFile(file?:TFile): boolean {
|
||||
if(file) {
|
||||
return this.isExcalidrawFile(file) && isMaskFile(this.plugin, file);
|
||||
}
|
||||
if (!this.targetView || !this.targetView?.file) {
|
||||
errorMessage("targetView not set", "isMaskFile()");
|
||||
return null;
|
||||
}
|
||||
return isMaskFile(this.plugin, this.targetView.file);
|
||||
}
|
||||
|
||||
plugin: ExcalidrawPlugin;
|
||||
elementsDict: {[key:string]:any}; //contains the ExcalidrawElements currently edited in Automate indexed by el.id
|
||||
imagesDict: {[key: FileId]: any}; //the images files including DataURL, indexed by fileId
|
||||
@@ -521,7 +573,7 @@ export class ExcalidrawAutomate {
|
||||
|
||||
/**
|
||||
* get all elements from ExcalidrawAutomate elementsDict
|
||||
* @returns elements from elemenetsDict
|
||||
* @returns elements from elementsDict
|
||||
*/
|
||||
getElements(): Mutable<ExcalidrawElement>[] {
|
||||
const elements = [];
|
||||
@@ -563,10 +615,14 @@ export class ExcalidrawAutomate {
|
||||
"excalidraw-export-dark"?: boolean;
|
||||
"excalidraw-export-padding"?: number;
|
||||
"excalidraw-export-pngscale"?: number;
|
||||
"excalidraw-export-embed-scene"?: boolean;
|
||||
"excalidraw-default-mode"?: "view" | "zen";
|
||||
"excalidraw-onload-script"?: string;
|
||||
"excalidraw-linkbutton-opacity"?: number;
|
||||
"excalidraw-autoexport"?: boolean;
|
||||
"excalidraw-mask"?: boolean;
|
||||
"excalidraw-open-md"?: boolean;
|
||||
"cssclasses"?: string;
|
||||
};
|
||||
plaintext?: string; //text to insert above the `# Text Elements` section
|
||||
}): Promise<string> {
|
||||
@@ -605,6 +661,12 @@ export class ExcalidrawAutomate {
|
||||
: FRONTMATTER;
|
||||
}
|
||||
|
||||
frontmatter += params.plaintext ? params.plaintext + "\n\n" : "";
|
||||
if(template?.frontmatter && params?.frontmatterKeys) {
|
||||
//the frontmatter tags supplyed to create take priority
|
||||
frontmatter = mergeMarkdownFiles(template.frontmatter,frontmatter);
|
||||
}
|
||||
|
||||
const scene = {
|
||||
type: "excalidraw",
|
||||
version: 2,
|
||||
@@ -657,9 +719,8 @@ export class ExcalidrawAutomate {
|
||||
};
|
||||
|
||||
const generateMD = ():string => {
|
||||
let outString = params.plaintext ? params.plaintext + "\n\n" : "";
|
||||
const textElements = this.getElements().filter(el => el.type === "text") as ExcalidrawTextElement[];
|
||||
outString += "# Text Elements\n";
|
||||
let outString = `# Excalidraw Data\n## Text Elements\n`;
|
||||
textElements.forEach(te=> {
|
||||
outString += `${te.rawText ?? (te.originalText ?? te.text)} ^${te.id}\n\n`;
|
||||
});
|
||||
@@ -670,7 +731,7 @@ export class ExcalidrawAutomate {
|
||||
})
|
||||
|
||||
outString += Object.keys(this.imagesDict).length > 0
|
||||
? "\n# Embedded files\n"
|
||||
? `\n## Embedded Files\n`
|
||||
: "";
|
||||
|
||||
Object.keys(this.imagesDict).forEach((key: FileId)=> {
|
||||
@@ -678,10 +739,27 @@ export class ExcalidrawAutomate {
|
||||
if(item.latex) {
|
||||
outString += `${key}: $$${item.latex}$$\n`;
|
||||
} else {
|
||||
outString += `${key}: [[${item.file}]]\n`;
|
||||
if(item.file) {
|
||||
if(item.file instanceof TFile) {
|
||||
outString += `${key}: [[${item.file.path}]]\n`;
|
||||
} else {
|
||||
outString += `${key}: [[${item.file}]]\n`;
|
||||
}
|
||||
} else {
|
||||
const hyperlinkSplit = item.hyperlink.split("#");
|
||||
const file = this.plugin.app.vault.getAbstractFileByPath(hyperlinkSplit[0]);
|
||||
if(file && file instanceof TFile) {
|
||||
const hasFileRef = hyperlinkSplit.length === 2
|
||||
outString += hasFileRef
|
||||
? `${key}: [[${file.path}#${hyperlinkSplit[1]}]]\n`
|
||||
: `${key}: [[${file.path}]]\n`;
|
||||
} else {
|
||||
outString += `${key}: ${item.hyperlink}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return outString;
|
||||
return outString + "\n%%\n";
|
||||
}
|
||||
|
||||
const filename = params?.filename
|
||||
@@ -735,6 +813,7 @@ export class ExcalidrawAutomate {
|
||||
exportSettings = {
|
||||
withBackground: this.plugin.settings.exportWithBackground,
|
||||
withTheme: true,
|
||||
isMask: false,
|
||||
};
|
||||
}
|
||||
if (!loader) {
|
||||
@@ -791,6 +870,7 @@ export class ExcalidrawAutomate {
|
||||
exportSettings = {
|
||||
withBackground: this.plugin.settings.exportWithBackground,
|
||||
withTheme: true,
|
||||
isMask: false,
|
||||
};
|
||||
}
|
||||
if (!loader) {
|
||||
@@ -950,10 +1030,33 @@ export class ExcalidrawAutomate {
|
||||
* @param topY
|
||||
* @param width
|
||||
* @param height
|
||||
* @param name: the display name of the frame
|
||||
* @returns
|
||||
*/
|
||||
addRect(topX: number, topY: number, width: number, height: number): string {
|
||||
const id = nanoid();
|
||||
addFrame(topX: number, topY: number, width: number, height: number, name?: string): string {
|
||||
const id = this.addRect(topX, topY, width, height);
|
||||
const frame = this.getElement(id) as Mutable<ExcalidrawFrameElement>;
|
||||
frame.type = "frame";
|
||||
frame.backgroundColor = "transparent";
|
||||
frame.strokeColor = "#000";
|
||||
frame.strokeStyle = "solid";
|
||||
frame.strokeWidth = 2;
|
||||
frame.roughness = 0;
|
||||
frame.roundness = null;
|
||||
if(name) frame.name = name;
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param topX
|
||||
* @param topY
|
||||
* @param width
|
||||
* @param height
|
||||
* @returns
|
||||
*/
|
||||
addRect(topX: number, topY: number, width: number, height: number, id?: string): string {
|
||||
if(!id) id = nanoid();
|
||||
this.elementsDict[id] = this.boxedElement(
|
||||
id,
|
||||
"rectangle",
|
||||
@@ -978,8 +1081,9 @@ export class ExcalidrawAutomate {
|
||||
topY: number,
|
||||
width: number,
|
||||
height: number,
|
||||
id?: string,
|
||||
): string {
|
||||
const id = nanoid();
|
||||
if(!id) id = nanoid();
|
||||
this.elementsDict[id] = this.boxedElement(
|
||||
id,
|
||||
"diamond",
|
||||
@@ -1004,8 +1108,9 @@ export class ExcalidrawAutomate {
|
||||
topY: number,
|
||||
width: number,
|
||||
height: number,
|
||||
id?: string,
|
||||
): string {
|
||||
const id = nanoid();
|
||||
if(!id) id = nanoid();
|
||||
this.elementsDict[id] = this.boxedElement(
|
||||
id,
|
||||
"ellipse",
|
||||
@@ -1025,7 +1130,7 @@ export class ExcalidrawAutomate {
|
||||
* @param height
|
||||
* @returns
|
||||
*/
|
||||
addBlob(topX: number, topY: number, width: number, height: number): string {
|
||||
addBlob(topX: number, topY: number, width: number, height: number, id?: string): string {
|
||||
const b = height * 0.5; //minor axis of the ellipsis
|
||||
const a = width * 0.5; //major axis of the ellipsis
|
||||
const sx = a / 9;
|
||||
@@ -1064,7 +1169,7 @@ export class ExcalidrawAutomate {
|
||||
}
|
||||
return p;
|
||||
};
|
||||
const id = this.addLine(scale(p));
|
||||
id = this.addLine(scale(p), id);
|
||||
this.elementsDict[id] = repositionElementsToCursor(
|
||||
[this.getElement(id)],
|
||||
{ x: topX, y: topY },
|
||||
@@ -1082,14 +1187,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)
|
||||
);
|
||||
// @ts-ignore
|
||||
element.width = w; element.height = h; element.baseline = baseline;
|
||||
element.width = w;
|
||||
element.height = h;
|
||||
}
|
||||
|
||||
|
||||
@@ -1108,7 +1213,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";
|
||||
@@ -1121,8 +1227,12 @@ 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,
|
||||
@@ -1132,9 +1242,9 @@ export class ExcalidrawAutomate {
|
||||
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":
|
||||
@@ -1180,12 +1290,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),
|
||||
autoResize: formatting?.box ? true : (formatting?.autoResize ?? true),
|
||||
};
|
||||
if (boxId && formatting?.box === "blob") {
|
||||
this.addToGroup([id, boxId]);
|
||||
@@ -1197,6 +1307,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;
|
||||
};
|
||||
|
||||
@@ -1205,9 +1334,9 @@ export class ExcalidrawAutomate {
|
||||
* @param points
|
||||
* @returns
|
||||
*/
|
||||
addLine(points: [[x: number, y: number]]): string {
|
||||
addLine(points: [[x: number, y: number]], id?: string): string {
|
||||
const box = getLineBox(points);
|
||||
const id = nanoid();
|
||||
id = id ?? nanoid();
|
||||
this.elementsDict[id] = {
|
||||
points: normalizeLinePoints(points),
|
||||
lastCommittedPoint: null,
|
||||
@@ -1234,9 +1363,10 @@ export class ExcalidrawAutomate {
|
||||
startObjectId?: string;
|
||||
endObjectId?: string;
|
||||
},
|
||||
id?: string,
|
||||
): string {
|
||||
const box = getLineBox(points);
|
||||
const id = nanoid();
|
||||
id = id ?? nanoid();
|
||||
const startPoint = points[0];
|
||||
const endPoint = points[points.length - 1];
|
||||
this.elementsDict[id] = {
|
||||
@@ -1348,7 +1478,7 @@ export class ExcalidrawAutomate {
|
||||
async addImage(
|
||||
topX: number,
|
||||
topY: number,
|
||||
imageFile: TFile | string,
|
||||
imageFile: TFile | string, //string may also be an Obsidian filepath with a reference such as folder/path/my.pdf#page=2
|
||||
scale: boolean = true, //default is true which will scale the image to MAX_IMAGE_SIZE, false will insert image at 100% of its size
|
||||
anchor: boolean = true, //only has effect if scale is false. If anchor is true the image path will include |100%, if false the image will be inserted at 100%, but if resized by the user it won't pop back to 100% the next time Excalidraw is opened.
|
||||
): Promise<string> {
|
||||
@@ -1729,12 +1859,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
|
||||
@@ -1795,10 +1943,32 @@ export class ExcalidrawAutomate {
|
||||
elements.forEach((el) => {
|
||||
this.elementsDict[el.id] = cloneElement(el);
|
||||
if(el.type === "image") {
|
||||
this.imagesDict[el.fileId] = sceneFiles?.[el.fileId];
|
||||
const ef = this.targetView.excalidrawData.getFile(el.fileId);
|
||||
const imageWithRef = ef && ef.file && ef.linkParts && ef.linkParts.ref;
|
||||
const equation = this.targetView.excalidrawData.getEquation(el.fileId);
|
||||
const sceneFile = sceneFiles?.[el.fileId];
|
||||
this.imagesDict[el.fileId] = {
|
||||
mimeType: sceneFile.mimeType,
|
||||
id: el.fileId,
|
||||
dataURL: sceneFile.dataURL,
|
||||
created: sceneFile.created,
|
||||
...ef ? {
|
||||
isHyperLink: ef.isHyperLink || imageWithRef,
|
||||
hyperlink: imageWithRef ? `${ef.file.path}#${ef.linkParts.ref}` : ef.hyperlink,
|
||||
file: imageWithRef ? null : ef.file,
|
||||
hasSVGwithBitmap: ef.isSVGwithBitmap,
|
||||
latex: null,
|
||||
} : {},
|
||||
...equation ? {
|
||||
file: null,
|
||||
isHyperLink: false,
|
||||
hyperlink: null,
|
||||
hasSVGwithBitmap: false,
|
||||
latex: equation.latex,
|
||||
} : {},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
elements.forEach((el) => {
|
||||
this.elementsDict[el.id] = cloneElement(el);
|
||||
@@ -1825,7 +1995,7 @@ export class ExcalidrawAutomate {
|
||||
appState: {
|
||||
viewModeEnabled: !isFullscreen,
|
||||
},
|
||||
commitToHistory: false,
|
||||
storeAction: "none",
|
||||
});
|
||||
this.targetView.toolsPanelRef?.current?.setExcalidrawViewMode(!isFullscreen);
|
||||
}
|
||||
@@ -1860,6 +2030,7 @@ export class ExcalidrawAutomate {
|
||||
appState?: AppState,
|
||||
files?: BinaryFileData,
|
||||
commitToHistory?: boolean,
|
||||
storeAction?: "capture" | "none" | "update",
|
||||
},
|
||||
restore: boolean = false,
|
||||
):void {
|
||||
@@ -1868,7 +2039,16 @@ export class ExcalidrawAutomate {
|
||||
errorMessage("targetView not set", "viewToggleFullScreen()");
|
||||
return;
|
||||
}
|
||||
this.targetView.updateScene(scene,restore);
|
||||
if (!Boolean(scene.storeAction)) {
|
||||
scene.storeAction = scene.commitToHistory ? "capture" : "none";
|
||||
}
|
||||
|
||||
this.targetView.updateScene({
|
||||
elements: scene.elements,
|
||||
appState: scene.appState,
|
||||
files: scene.files,
|
||||
storeAction: scene.storeAction,
|
||||
},restore);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2091,6 +2271,19 @@ export class ExcalidrawAutomate {
|
||||
color: string,
|
||||
) => void = null;
|
||||
|
||||
/**
|
||||
* If set, this callback is triggered whenever a drawing is exported to SVG.
|
||||
* The string returned will replace the link in the exported SVG.
|
||||
* The hook is only executed if the link is to a file internal to Obsidian
|
||||
* see: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1605
|
||||
*/
|
||||
onUpdateElementLinkForExportHook: (data: {
|
||||
originalLink: string,
|
||||
obsidianLink: string,
|
||||
linkedFile: TFile | null,
|
||||
hostFile: TFile,
|
||||
}) => string = null;
|
||||
|
||||
/**
|
||||
* utility function to generate EmbeddedFilesLoader object
|
||||
* @param isDark
|
||||
@@ -2109,8 +2302,9 @@ export class ExcalidrawAutomate {
|
||||
getExportSettings(
|
||||
withBackground: boolean,
|
||||
withTheme: boolean,
|
||||
isMask: boolean = false,
|
||||
): ExportSettings {
|
||||
return { withBackground, withTheme };
|
||||
return { withBackground, withTheme, isMask };
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -2140,7 +2334,7 @@ export class ExcalidrawAutomate {
|
||||
* @returns
|
||||
*/
|
||||
getMaximumGroups(elements: ExcalidrawElement[]): ExcalidrawElement[][] {
|
||||
return getMaximumGroups(elements);
|
||||
return getMaximumGroups(elements, arrayToMap(elements));
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -2222,13 +2416,19 @@ export class ExcalidrawAutomate {
|
||||
|
||||
/**
|
||||
* Gets all the elements from elements[] that are contained in the frame.
|
||||
* @param element
|
||||
* @param elements - typically all the non-deleted elements in the scene
|
||||
* @param frameElement - the frame element for which to get the elements
|
||||
* @param elements - typically all the non-deleted elements in the scene
|
||||
* @param shouldIncludeFrame - if true, the frame element will be included in the returned array
|
||||
* this is useful when generating an image in which you want the frame to be clipped
|
||||
* @returns
|
||||
*/
|
||||
getElementsInFrame(frameElement: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[] {
|
||||
getElementsInFrame(
|
||||
frameElement: ExcalidrawElement,
|
||||
elements: ExcalidrawElement[],
|
||||
shouldIncludeFrame: boolean = false,
|
||||
): ExcalidrawElement[] {
|
||||
if(!frameElement || !elements || frameElement.type !== "frame") return [];
|
||||
return elements.filter(el=>el.frameId === frameElement.id);
|
||||
return elements.filter(el=>(el.frameId === frameElement.id) || (shouldIncludeFrame && el.id === frameElement.id));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2274,8 +2474,13 @@ export class ExcalidrawAutomate {
|
||||
if (!this.targetView) {
|
||||
return null;
|
||||
}
|
||||
const leaf = getNewOrAdjacentLeaf(this.plugin, this.targetView.leaf);
|
||||
leaf.openFile(file, openState ?? {active: true});
|
||||
|
||||
const {leaf, promise} = openLeaf({
|
||||
plugin: this.plugin,
|
||||
fnGetLeaf: () => getNewOrAdjacentLeaf(this.plugin, this.targetView.leaf),
|
||||
file,
|
||||
openState: openState ?? {active: true}
|
||||
});
|
||||
return leaf;
|
||||
};
|
||||
|
||||
@@ -2410,12 +2615,12 @@ export class ExcalidrawAutomate {
|
||||
elements.splice(newZIndex, 0, elements.splice(oldZIndex, 1)[0]);
|
||||
this.targetView.updateScene({
|
||||
elements,
|
||||
commitToHistory: true,
|
||||
storeAction: "capture",
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Depricated. Use getCM / ColorMaster instead
|
||||
* Deprecated. Use getCM / ColorMaster instead
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
@@ -2425,7 +2630,7 @@ export class ExcalidrawAutomate {
|
||||
};
|
||||
|
||||
/**
|
||||
* Depricated. Use getCM / ColorMaster instead
|
||||
* Deprecated. Use getCM / ColorMaster instead
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
@@ -2435,7 +2640,7 @@ export class ExcalidrawAutomate {
|
||||
};
|
||||
|
||||
/**
|
||||
* Depricated. Use getCM / ColorMaster instead
|
||||
* Deprecated. Use getCM / ColorMaster instead
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
@@ -2445,7 +2650,7 @@ export class ExcalidrawAutomate {
|
||||
};
|
||||
|
||||
/**
|
||||
* Depricated. Use getCM / ColorMaster instead
|
||||
* Deprecated. Use getCM / ColorMaster instead
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
@@ -2496,12 +2701,26 @@ export class ExcalidrawAutomate {
|
||||
importSVG(svgString:string):boolean {
|
||||
const res:ConversionResult = svgToExcalidraw(svgString);
|
||||
if(res.hasErrors) {
|
||||
new Notice (`There were errors while parsing the given SVG:\n${[...res.errors].map((el) => el.innerHTML)}`);
|
||||
new Notice (`There were errors while parsing the given SVG:\n${res.errors}`);
|
||||
return false;
|
||||
}
|
||||
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(
|
||||
@@ -2514,10 +2733,6 @@ export async function initExcalidrawAutomate(
|
||||
return ea;
|
||||
}
|
||||
|
||||
export function destroyExcalidrawAutomate() {
|
||||
delete window.ExcalidrawAutomate;
|
||||
}
|
||||
|
||||
function normalizeLinePoints(
|
||||
points: [x: number, y: number][],
|
||||
//box: { x: number; y: number; w: number; h: number },
|
||||
@@ -2553,10 +2768,14 @@ function getFontFamily(id: number) {
|
||||
}
|
||||
}
|
||||
|
||||
async function initFonts() {
|
||||
export async function initFonts(doc: Document = document) {
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
await (document as any).fonts.load(`20px ${getFontFamily(i)}`);
|
||||
await (doc as any).fonts.load(`20px ${getFontFamily(i)}`);
|
||||
}
|
||||
await (doc as any).fonts.load("400 20px Assistant");
|
||||
await (doc as any).fonts.load("500 20px Assistant");
|
||||
await (doc as any).fonts.load("600 20px Assistant");
|
||||
await (doc as any).fonts.load("700 20px Assistant");
|
||||
}
|
||||
|
||||
export function _measureText(
|
||||
@@ -2578,7 +2797,7 @@ export function _measureText(
|
||||
`${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(
|
||||
@@ -2622,18 +2841,16 @@ async function getTemplate(
|
||||
};
|
||||
}
|
||||
|
||||
const parsed =
|
||||
data.search("excalidraw-plugin: parsed\n") > -1 ||
|
||||
data.search("excalidraw-plugin: locked\n") > -1; //locked for backward compatibility
|
||||
const textMode = getTextMode(data);
|
||||
await excalidrawData.loadData(
|
||||
data,
|
||||
file,
|
||||
parsed ? TextMode.parsed : TextMode.raw,
|
||||
textMode,
|
||||
);
|
||||
|
||||
let trimLocation = data.search("# Text Elements\n");
|
||||
let trimLocation = data.search(/^##? Text Elements$/m);
|
||||
if (trimLocation == -1) {
|
||||
trimLocation = data.search("# Drawing\n");
|
||||
trimLocation = data.search(/##? Drawing\n/);
|
||||
}
|
||||
|
||||
let scene = excalidrawData.scene;
|
||||
@@ -2668,14 +2885,14 @@ async function getTemplate(
|
||||
groupElements = plugin.ea.getElementsInTheSameGroupWithElement(el[0],scene.elements)
|
||||
}
|
||||
}
|
||||
if(filenameParts.hasFrameref) {
|
||||
const el = scene.elements.filter((el: ExcalidrawElement)=>el.id===filenameParts.blockref)
|
||||
if(el.length === 1) {
|
||||
groupElements = plugin.ea.getElementsInFrame(el[0],scene.elements)
|
||||
if(filenameParts.hasFrameref || filenameParts.hasClippedFrameref) {
|
||||
const el = getFrameBasedOnFrameNameOrId(filenameParts.blockref,scene.elements);
|
||||
|
||||
if(el) {
|
||||
groupElements = plugin.ea.getElementsInFrame(el,scene.elements, filenameParts.hasClippedFrameref);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(filenameParts.hasTaskbone) {
|
||||
groupElements = groupElements.filter( el =>
|
||||
el.type==="freedraw" ||
|
||||
@@ -2684,6 +2901,7 @@ async function getTemplate(
|
||||
));
|
||||
}
|
||||
|
||||
excalidrawData.destroy();
|
||||
return {
|
||||
elements: convertMarkdownLinksToObsidianURLs
|
||||
? updateElementLinksToObsidianLinks({
|
||||
@@ -2752,6 +2970,7 @@ export async function createPNG(
|
||||
theme: forceTheme ?? template?.appState?.theme ?? canvasTheme,
|
||||
viewBackgroundColor:
|
||||
template?.appState?.viewBackgroundColor ?? canvasBackgroundColor,
|
||||
...template?.appState?.frameRendering ? {frameRendering: template.appState.frameRendering} : {},
|
||||
},
|
||||
files,
|
||||
},
|
||||
@@ -2759,6 +2978,7 @@ export async function createPNG(
|
||||
withBackground:
|
||||
exportSettings?.withBackground ?? plugin.settings.exportWithBackground,
|
||||
withTheme: exportSettings?.withTheme ?? plugin.settings.exportWithTheme,
|
||||
isMask: exportSettings?.isMask ?? false,
|
||||
},
|
||||
padding,
|
||||
scale,
|
||||
@@ -2788,7 +3008,15 @@ export const updateElementLinksToObsidianLinks = ({elements, hostFile}:{
|
||||
if(!file) {
|
||||
return el;
|
||||
}
|
||||
const link = app.getObsidianUrl(file);
|
||||
let link = app.getObsidianUrl(file);
|
||||
if(window.ExcalidrawAutomate?.onUpdateElementLinkForExportHook) {
|
||||
link = window.ExcalidrawAutomate.onUpdateElementLinkForExportHook({
|
||||
originalLink: el.link,
|
||||
obsidianLink: link,
|
||||
linkedFile: file,
|
||||
hostFile: hostFile
|
||||
});
|
||||
}
|
||||
const newElement: Mutable<ExcalidrawElement> = cloneElement(el);
|
||||
newElement.link = link;
|
||||
return newElement;
|
||||
@@ -2838,6 +3066,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
|
||||
@@ -2849,6 +3078,7 @@ export async function createSVG(
|
||||
theme,
|
||||
viewBackgroundColor:
|
||||
template?.appState?.viewBackgroundColor ?? canvasBackgroundColor,
|
||||
...template?.appState?.frameRendering ? {frameRendering: template.appState.frameRendering} : {},
|
||||
},
|
||||
files,
|
||||
},
|
||||
@@ -2856,6 +3086,10 @@ export async function createSVG(
|
||||
withBackground:
|
||||
exportSettings?.withBackground ?? plugin.settings.exportWithBackground,
|
||||
withTheme,
|
||||
isMask: exportSettings?.isMask ?? false,
|
||||
...filenameParts?.hasClippedFrameref
|
||||
? {frameRendering: {enabled: true, name: false, outline: false, clip: true}}
|
||||
: {},
|
||||
},
|
||||
padding,
|
||||
null,
|
||||
@@ -2863,9 +3097,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
|
||||
@@ -2952,7 +3185,7 @@ function errorMessage(message: string, source: string) {
|
||||
errorlog({
|
||||
where: "ExcalidrawAutomate",
|
||||
source,
|
||||
message: "this function is not avalable on Obsidian Mobile",
|
||||
message: "this function is not available on Obsidian Mobile",
|
||||
});
|
||||
break;
|
||||
default:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
34
src/ExcalidrawLib.d.ts
vendored
34
src/ExcalidrawLib.d.ts
vendored
@@ -1,8 +1,8 @@
|
||||
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 { ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameElement, 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, ExcalidrawTextContainer, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, NonDeletedExcalidrawElement, Theme } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { AppState, BinaryFiles, Point, Zoom } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
|
||||
|
||||
type EmbeddedLink =
|
||||
@@ -87,15 +87,34 @@ declare namespace ExcalidrawLib {
|
||||
elements: ExcalidrawElement[] | readonly NonDeleted<ExcalidrawElement>[],
|
||||
): BoundingBox;
|
||||
|
||||
function getContainerElement(
|
||||
element: ExcalidrawTextElement | null,
|
||||
elementsMap: ElementsMap,
|
||||
): ExcalidrawTextContainer | null;
|
||||
|
||||
function refreshTextDimensions(
|
||||
textElement: ExcalidrawTextElement,
|
||||
container: ExcalidrawTextContainer | null,
|
||||
elementsMap: ElementsMap,
|
||||
text: string,
|
||||
): {
|
||||
text: string,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
};
|
||||
|
||||
function getMaximumGroups(
|
||||
elements: ExcalidrawElement[],
|
||||
elementsMap: ElementsMap,
|
||||
): ExcalidrawElement[][];
|
||||
|
||||
function measureText(
|
||||
text: string,
|
||||
font: FontString,
|
||||
lineHeight: number,
|
||||
): { width: number; height: number; baseline: number };
|
||||
): { width: number; height: number; };
|
||||
|
||||
function getDefaultLineHeight(fontFamily: FontFamilyValues): number;
|
||||
|
||||
@@ -136,4 +155,13 @@ declare namespace ExcalidrawLib {
|
||||
files?: any;
|
||||
error?: string;
|
||||
} | undefined>;
|
||||
|
||||
var getSceneVersion: any;
|
||||
var Excalidraw: any;
|
||||
var MainMenu: any;
|
||||
var WelcomeScreen: any;
|
||||
var TTDDialogTrigger: any;
|
||||
var TTDDialog: any;
|
||||
|
||||
function destroyObsidianUtils(): void;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
14
src/LaTeX.ts
14
src/LaTeX.ts
@@ -7,21 +7,18 @@ import {RegisterHTMLHandler} from 'mathjax-full/js/handlers/html.js';
|
||||
import {AllPackages} from 'mathjax-full/js/input/tex/AllPackages.js';
|
||||
|
||||
import ExcalidrawView from "./ExcalidrawView";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import { FileData, MimeType } from "./EmbeddedFileLoader";
|
||||
import { FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { getImageSize, svgToBase64 } from "./utils/Utils";
|
||||
import { fileid } from "./constants/constants";
|
||||
import { TFile } from "obsidian";
|
||||
import { MathDocument } from "mathjax-full/js/core/MathDocument";
|
||||
import { stripVTControlCharacters } from "util";
|
||||
|
||||
export const updateEquation = async (
|
||||
equation: string,
|
||||
fileId: string,
|
||||
view: ExcalidrawView,
|
||||
addFiles: Function,
|
||||
plugin: ExcalidrawPlugin,
|
||||
) => {
|
||||
const data = await tex2dataURL(equation);
|
||||
if (data) {
|
||||
@@ -40,11 +37,15 @@ export const updateEquation = async (
|
||||
};
|
||||
|
||||
let adaptor: LiteAdaptor;
|
||||
let input: TeX<unknown, unknown, unknown>;
|
||||
let output: SVG<unknown, unknown, unknown>;
|
||||
let html: MathDocument<any, any, any>;
|
||||
let preamble: string;
|
||||
|
||||
export const clearMathJaxVariables = () => {
|
||||
adaptor = null;
|
||||
html = null;
|
||||
preamble = null;
|
||||
};
|
||||
|
||||
//https://github.com/xldenis/obsidian-latex/blob/master/main.ts
|
||||
const loadPreamble = async () => {
|
||||
const file = app.vault.getAbstractFileByPath("preamble.sty");
|
||||
@@ -63,6 +64,9 @@ export async function tex2dataURL(
|
||||
created: number;
|
||||
size: { height: number; width: number };
|
||||
}> {
|
||||
let input: TeX<unknown, unknown, unknown>;
|
||||
let output: SVG<unknown, unknown, unknown>;
|
||||
|
||||
if(!adaptor) {
|
||||
await loadPreamble();
|
||||
adaptor = liteAdaptor();
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import {
|
||||
MarkdownPostProcessorContext,
|
||||
MetadataCache,
|
||||
PaneType,
|
||||
TFile,
|
||||
Vault,
|
||||
} from "obsidian";
|
||||
import { RERENDER_EVENT } from "./constants/constants";
|
||||
import { DEVICE, RERENDER_EVENT } from "./constants/constants";
|
||||
import { EmbeddedFilesLoader } from "./EmbeddedFileLoader";
|
||||
import { createPNG, createSVG } from "./ExcalidrawAutomate";
|
||||
import { ExportSettings } from "./ExcalidrawView";
|
||||
@@ -19,12 +20,15 @@ import {
|
||||
getWithBackground,
|
||||
hasExportTheme,
|
||||
convertSVGStringToElement,
|
||||
isMaskFile,
|
||||
} from "./utils/Utils";
|
||||
import { getParentOfClass, isObsidianThemeDark, getFileCSSClasses } from "./utils/ObsidianUtils";
|
||||
import { linkClickModifierType } from "./utils/ModifierkeyHelper";
|
||||
import { ImageKey, imageCache } from "./utils/ImageCache";
|
||||
import { FILENAMEPARTS, PreviewImageType } from "./utils/UtilTypes";
|
||||
import { CustomMutationObserver, isDebugMode } from "./utils/DebugHelper";
|
||||
import { CustomMutationObserver, DEBUGGING } from "./utils/DebugHelper";
|
||||
import { getExcalidrawFileForwardLinks } from "./utils/ExcalidrawViewUtils";
|
||||
import { linkPrompt } from "./dialogs/Prompt";
|
||||
|
||||
interface imgElementAttributes {
|
||||
file?: TFile;
|
||||
@@ -41,11 +45,20 @@ let metadataCache: MetadataCache;
|
||||
const getDefaultWidth = (plugin: ExcalidrawPlugin): string => {
|
||||
const width = parseInt(plugin.settings.width);
|
||||
if (isNaN(width) || width === 0 || width === null) {
|
||||
if(getDefaultHeight(plugin)!=="") return "";
|
||||
return "400";
|
||||
}
|
||||
return plugin.settings.width;
|
||||
};
|
||||
|
||||
const getDefaultHeight = (plugin: ExcalidrawPlugin): string => {
|
||||
const height = parseInt(plugin.settings.height);
|
||||
if (isNaN(height) || height === 0 || height === null) {
|
||||
return "";
|
||||
}
|
||||
return plugin.settings.height;
|
||||
};
|
||||
|
||||
export const initializeMarkdownPostProcessor = (p: ExcalidrawPlugin) => {
|
||||
plugin = p;
|
||||
vault = p.app.vault;
|
||||
@@ -73,7 +86,7 @@ const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,ex
|
||||
? 2
|
||||
: 1;
|
||||
|
||||
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.PNG, scale};
|
||||
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.PNG, scale, isTransparent: !exportSettings.withBackground};
|
||||
|
||||
if(cacheReady) {
|
||||
const src = await imageCache.getImageFromCache(cacheKey);
|
||||
@@ -92,11 +105,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,
|
||||
@@ -119,9 +134,14 @@ const setStyle = ({element,imgAttributes,onCanvas}:{
|
||||
onCanvas: boolean,
|
||||
}
|
||||
) => {
|
||||
let style = `max-width:${imgAttributes.fwidth}${imgAttributes.fwidth.match(/\d$/) ? "px":""}; `; //width:100%;`; //removed !important https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/886
|
||||
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
|
||||
} else {
|
||||
style = "width: fit-content;"
|
||||
}
|
||||
if (imgAttributes.fheight) {
|
||||
style += `height:${imgAttributes.fheight}px;`;
|
||||
style += `${imgAttributes.fwidth?"min-":"max-"}height:${imgAttributes.fheight}px;`;
|
||||
}
|
||||
if(!onCanvas) element.setAttribute("style", style);
|
||||
element.classList.add(...Array.from(imgAttributes.style))
|
||||
@@ -145,7 +165,7 @@ 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};
|
||||
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.SVGIMG, scale:1, isTransparent: !exportSettings.withBackground};
|
||||
if(cacheReady) {
|
||||
const src = await imageCache.getImageFromCache(cacheKey);
|
||||
if(src && typeof src === "string") {
|
||||
@@ -166,11 +186,13 @@ const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSetting
|
||||
|
||||
let 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,
|
||||
@@ -202,20 +224,22 @@ 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};
|
||||
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.SVG, scale:1, isTransparent: !exportSettings.withBackground};
|
||||
let maybeSVG;
|
||||
if(cacheReady) {
|
||||
maybeSVG = await imageCache.getImageFromCache(cacheKey);
|
||||
}
|
||||
|
||||
let svg = maybeSVG && (maybeSVG instanceof SVGSVGElement)
|
||||
let 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,
|
||||
@@ -232,11 +256,14 @@ const _getSVGNative = async ({filenameParts,theme,cacheReady,containerElement,fi
|
||||
return null;
|
||||
}
|
||||
|
||||
//cache SVG should have the width and height parameters and not the embedded font
|
||||
if(!Boolean(maybeSVG)) {
|
||||
cacheReady && imageCache.addImageToCache(cacheKey,"", svg);
|
||||
}
|
||||
svg = embedFontsInSVG(svg, plugin, true);
|
||||
svg.removeAttribute("width");
|
||||
svg.removeAttribute("height");
|
||||
containerElement.append(svg);
|
||||
cacheReady && imageCache.addImageToCache(cacheKey,"", svg);
|
||||
return containerElement;
|
||||
}
|
||||
|
||||
@@ -272,6 +299,7 @@ const getIMG = async (
|
||||
const exportSettings: ExportSettings = {
|
||||
withBackground: getWithBackground(plugin, file),
|
||||
withTheme: forceTheme ? true : plugin.settings.exportWithTheme,
|
||||
isMask: isMaskFile(plugin, file),
|
||||
};
|
||||
|
||||
const theme =
|
||||
@@ -339,7 +367,7 @@ const createImgElement = async (
|
||||
imgOrDiv.setAttribute("draggable","false");
|
||||
imgOrDiv.setAttribute("onCanvas",onCanvas?"true":"false");
|
||||
|
||||
let timer:NodeJS.Timeout;
|
||||
let timer:number;
|
||||
const clickEvent = (ev:PointerEvent) => {
|
||||
if(!(ev.target instanceof Element)) {
|
||||
return;
|
||||
@@ -354,12 +382,31 @@ const createImgElement = async (
|
||||
if (src) {
|
||||
const srcParts = src.match(/([^#]*)(.*)/);
|
||||
if(!srcParts) return;
|
||||
plugin.openDrawing(
|
||||
vault.getAbstractFileByPath(srcParts[1]) as TFile,
|
||||
linkClickModifierType(ev),
|
||||
true,
|
||||
srcParts[2],
|
||||
);
|
||||
const f = vault.getAbstractFileByPath(srcParts[1]) as TFile;
|
||||
const linkModifier = linkClickModifierType(ev);
|
||||
if (plugin.isExcalidrawFile(f) && isMaskFile(plugin, f)) {
|
||||
(async () => {
|
||||
const linkString = `[[${f.path}${srcParts[2]?"#"+srcParts[2]:""}]] ${getExcalidrawFileForwardLinks(plugin.app, f, new Set<string>())}`;
|
||||
const result = await linkPrompt(linkString, plugin.app);
|
||||
if(!result) return;
|
||||
const [file, linkText, subpath] = result;
|
||||
if(plugin.isExcalidrawFile(file)) {
|
||||
plugin.openDrawing(file,linkModifier, true, subpath);
|
||||
return;
|
||||
}
|
||||
let paneType: boolean | PaneType = false;
|
||||
switch(linkModifier) {
|
||||
case "active-pane": paneType = false; break;
|
||||
case "new-pane": paneType = "split"; break;
|
||||
case "popout-window": paneType = "window"; break;
|
||||
case "new-tab": paneType = "tab"; break;
|
||||
case "md-properties": paneType = "tab"; break;
|
||||
}
|
||||
plugin.app.workspace.openLinkText(linkText,"",paneType,subpath ? {eState: {subpath}} : {});
|
||||
})()
|
||||
return;
|
||||
}
|
||||
plugin.openDrawing(f,linkModifier,true,srcParts[2]);
|
||||
} //.ctrlKey||ev.metaKey);
|
||||
};
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1003
|
||||
@@ -373,17 +420,20 @@ 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;
|
||||
}
|
||||
});
|
||||
eventElement.addEventListener("pointerdown",(ev)=>{
|
||||
if(imgOrDiv?.parentElement?.hasClass("canvas-node-content")) return;
|
||||
timer = setTimeout(()=>clickEvent(ev),500);
|
||||
//@ts-ignore
|
||||
const PLUGIN = app.plugins.plugins["obsidian-excalidraw-plugin"] as ExcalidrawPlugin;
|
||||
const timeoutValue = DEVICE.isDesktop ? PLUGIN.settings.longPressDesktop : PLUGIN.settings.longPressMobile;
|
||||
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);
|
||||
@@ -486,9 +536,11 @@ const processInternalEmbed = async (internalEmbedEl: Element, file: TFile ):Prom
|
||||
internalEmbedEl.addClass("image-embed");
|
||||
|
||||
attr.fwidth = internalEmbedEl.getAttribute("width")
|
||||
? internalEmbedEl.getAttribute("width")
|
||||
: getDefaultWidth(plugin);
|
||||
attr.fheight = internalEmbedEl.getAttribute("height");
|
||||
? internalEmbedEl.getAttribute("width")
|
||||
: getDefaultWidth(plugin);
|
||||
attr.fheight = internalEmbedEl.getAttribute("height")
|
||||
? internalEmbedEl.getAttribute("height")
|
||||
: getDefaultHeight(plugin);
|
||||
let alt = internalEmbedEl.getAttribute("alt");
|
||||
attr.style = ["excalidraw-svg"];
|
||||
processAltText(src.split("#")[0],alt,attr);
|
||||
@@ -525,7 +577,7 @@ const isTextOnlyEmbed = (internalEmbedEl: Element):boolean => {
|
||||
const src = internalEmbedEl.getAttribute("src");
|
||||
if(!src) return true; //technically this does not mean this is a text only embed, but still should abort further processing
|
||||
const fnameParts = getEmbeddedFilenameParts(src);
|
||||
return !(fnameParts.hasArearef || fnameParts.hasGroupref || fnameParts.hasFrameref) &&
|
||||
return !(fnameParts.hasArearef || fnameParts.hasGroupref || fnameParts.hasFrameref || fnameParts.hasClippedFrameref) &&
|
||||
(fnameParts.hasBlockref || fnameParts.hasSectionref)
|
||||
}
|
||||
|
||||
@@ -549,8 +601,18 @@ const tmpObsidianWYSIWYG = async (
|
||||
|
||||
//@ts-ignore
|
||||
const containerEl = ctx.containerEl;
|
||||
|
||||
if(!plugin.settings.renderImageInMarkdownReadingMode && containerEl.parentElement?.parentElement?.hasClass("markdown-reading-view")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!plugin.settings.renderImageInMarkdownToPDF && containerEl.parentElement?.hasClass("print")) {
|
||||
return;
|
||||
}
|
||||
|
||||
let internalEmbedDiv: HTMLElement = containerEl;
|
||||
while (
|
||||
!internalEmbedDiv.hasClass("print") &&
|
||||
!internalEmbedDiv.hasClass("dataview") &&
|
||||
!internalEmbedDiv.hasClass("cm-preview-code-block") &&
|
||||
!internalEmbedDiv.hasClass("cm-embed-block") &&
|
||||
@@ -561,7 +623,7 @@ const tmpObsidianWYSIWYG = async (
|
||||
) {
|
||||
internalEmbedDiv = internalEmbedDiv.parentElement;
|
||||
}
|
||||
|
||||
|
||||
if(
|
||||
internalEmbedDiv.hasClass("dataview") ||
|
||||
internalEmbedDiv.hasClass("cm-preview-code-block") ||
|
||||
@@ -570,23 +632,50 @@ const tmpObsidianWYSIWYG = async (
|
||||
return; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/835
|
||||
}
|
||||
|
||||
|
||||
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,
|
||||
fheight: "",
|
||||
fwidth: getDefaultWidth(plugin),
|
||||
fheight: isPrinting ? "100%" : getDefaultHeight(plugin),
|
||||
fwidth: isPrinting ? "100%" : getDefaultWidth(plugin),
|
||||
style: ["excalidraw-svg"],
|
||||
};
|
||||
|
||||
attr.file = file;
|
||||
|
||||
const markdownEmbed = internalEmbedDiv.hasClass("markdown-embed");
|
||||
const markdownReadingView = internalEmbedDiv.hasClass("markdown-reading-view");
|
||||
const markdownReadingView = internalEmbedDiv.hasClass("markdown-reading-view") || isPrinting;
|
||||
if (!internalEmbedDiv.hasClass("internal-embed") && (markdownEmbed || markdownReadingView)) {
|
||||
if(isPrinting) {
|
||||
internalEmbedDiv = containerEl;
|
||||
}
|
||||
//We are processing the markdown preview of an actual Excalidraw file
|
||||
//the excalidraw file in markdown preview mode
|
||||
const isFrontmatterDiv = Boolean(el.querySelector(".frontmatter"));
|
||||
el.empty();
|
||||
if(!isFrontmatterDiv) {
|
||||
let areaPreview = false;
|
||||
if(Boolean(ctx.frontmatter)) {
|
||||
el.empty();
|
||||
} 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=|clippedframe=)[^ ]*)/)?.[1];
|
||||
if(ref) {
|
||||
attr.fname = file.path + ref;
|
||||
areaPreview = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if(!isFrontmatterDiv && !areaPreview) {
|
||||
if(el.parentElement === containerEl) containerEl.removeChild(el);
|
||||
return;
|
||||
}
|
||||
@@ -628,22 +717,22 @@ 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);
|
||||
internalEmbedDiv.appendChild(imgDiv);
|
||||
}, 500);
|
||||
}
|
||||
const observer = isDebugMode
|
||||
const observer = DEBUGGING
|
||||
? new CustomMutationObserver(markdownObserverFn, "markdowPostProcessorObserverFn")
|
||||
: new MutationObserver(markdownObserverFn);
|
||||
observer.observe(internalEmbedDiv, {
|
||||
@@ -674,7 +763,8 @@ 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"));
|
||||
if (excalidrawFile) {
|
||||
const isPrinting = Boolean(document.body.querySelectorAll("body > .print"));
|
||||
if (excalidrawFile && !isPrinting) {
|
||||
el.style.display = "none";
|
||||
return;
|
||||
}
|
||||
@@ -736,10 +826,10 @@ const legacyExcalidrawPopoverObserverFn: MutationCallback = async (m) => {
|
||||
if (!plugin.hover.linkText) {
|
||||
return;
|
||||
}
|
||||
if (m.length != 1) {
|
||||
if (m.length !== 1) {
|
||||
return;
|
||||
}
|
||||
if (m[0].addedNodes.length != 1) {
|
||||
if (m[0].addedNodes.length !== 1) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
@@ -751,7 +841,7 @@ const legacyExcalidrawPopoverObserverFn: MutationCallback = async (m) => {
|
||||
node.empty();
|
||||
|
||||
//this div will be on top of original DIV. By stopping the propagation of the click
|
||||
//I prevent the default Obsidian feature of openning the link in the native app
|
||||
//I prevent the default Obsidian feature of opening the link in the native app
|
||||
const img = await getIMG({
|
||||
file,
|
||||
fname: file.path,
|
||||
@@ -776,7 +866,7 @@ const legacyExcalidrawPopoverObserverFn: MutationCallback = async (m) => {
|
||||
node.appendChild(div);
|
||||
};
|
||||
|
||||
export const legacyExcalidrawPopoverObserver = isDebugMode
|
||||
export const legacyExcalidrawPopoverObserver = DEBUGGING
|
||||
? new CustomMutationObserver(legacyExcalidrawPopoverObserverFn, "legacyExcalidrawPopoverObserverFn")
|
||||
: new MutationObserver(legacyExcalidrawPopoverObserverFn);
|
||||
|
||||
|
||||
147
src/Scripts.ts
147
src/Scripts.ts
@@ -12,6 +12,8 @@ import { ButtonDefinition, GenericInputPrompt, GenericSuggester } from "./dialog
|
||||
import { getIMGFilename } from "./utils/FileUtils";
|
||||
import { splitFolderAndFilename } from "./utils/FileUtils";
|
||||
import { getEA } from "src";
|
||||
import { ExcalidrawAutomate } from "./ExcalidrawAutomate";
|
||||
import { WeakArray } from "./utils/WeakArray";
|
||||
|
||||
export type ScriptIconMap = {
|
||||
[key: string]: { name: string; group: string; svgString: string };
|
||||
@@ -22,6 +24,7 @@ export class ScriptEngine {
|
||||
private scriptPath: string;
|
||||
//https://stackoverflow.com/questions/60218638/how-to-force-re-render-if-map-value-changes
|
||||
public scriptIconMap: ScriptIconMap;
|
||||
eaInstances = new WeakArray<ExcalidrawAutomate>();
|
||||
|
||||
constructor(plugin: ExcalidrawPlugin) {
|
||||
this.plugin = plugin;
|
||||
@@ -30,65 +33,95 @@ export class ScriptEngine {
|
||||
this.registerEventHandlers();
|
||||
}
|
||||
|
||||
registerEventHandlers() {
|
||||
const handleSvgFileChange = (path: string) => {
|
||||
if (!path.endsWith(".svg")) {
|
||||
return;
|
||||
public removeViewEAs(view: ExcalidrawView) {
|
||||
const eas = new Set<ExcalidrawAutomate>();
|
||||
this.eaInstances.forEach((ea) => {
|
||||
if (ea.targetView === view) {
|
||||
eas.add(ea);
|
||||
ea.destroy();
|
||||
}
|
||||
const scriptFile = app.vault.getAbstractFileByPath(
|
||||
getIMGFilename(path, "md"),
|
||||
);
|
||||
if (scriptFile && scriptFile instanceof TFile) {
|
||||
this.unloadScript(this.getScriptName(scriptFile), scriptFile.path);
|
||||
this.loadScript(scriptFile);
|
||||
}
|
||||
};
|
||||
});
|
||||
this.eaInstances.removeObjects(eas);
|
||||
}
|
||||
|
||||
const deleteEventHandler = async (file: TFile) => {
|
||||
if (!(file instanceof TFile)) {
|
||||
return;
|
||||
}
|
||||
if (!file.path.startsWith(this.scriptPath)) {
|
||||
return;
|
||||
}
|
||||
this.unloadScript(this.getScriptName(file), file.path);
|
||||
handleSvgFileChange(file.path);
|
||||
};
|
||||
this.plugin.registerEvent(
|
||||
app.vault.on("delete", deleteEventHandler),
|
||||
public destroy() {
|
||||
this.eaInstances.forEach((ea) => ea.destroy());
|
||||
this.eaInstances.clear();
|
||||
this.eaInstances = null;
|
||||
this.scriptIconMap = null;
|
||||
this.plugin = null;
|
||||
this.scriptPath = null;
|
||||
}
|
||||
|
||||
private handleSvgFileChange (path: string) {
|
||||
if (!path.endsWith(".svg")) {
|
||||
return;
|
||||
}
|
||||
const scriptFile = app.vault.getAbstractFileByPath(
|
||||
getIMGFilename(path, "md"),
|
||||
);
|
||||
if (scriptFile && scriptFile instanceof TFile) {
|
||||
this.unloadScript(this.getScriptName(scriptFile), scriptFile.path);
|
||||
this.loadScript(scriptFile);
|
||||
}
|
||||
}
|
||||
|
||||
const createEventHandler = async (file: TFile) => {
|
||||
if (!(file instanceof TFile)) {
|
||||
return;
|
||||
}
|
||||
if (!file.path.startsWith(this.scriptPath)) {
|
||||
return;
|
||||
}
|
||||
private async deleteEventHandler (file: TFile) {
|
||||
if (!(file instanceof TFile)) {
|
||||
return;
|
||||
}
|
||||
if (!file.path.startsWith(this.scriptPath)) {
|
||||
return;
|
||||
}
|
||||
this.unloadScript(this.getScriptName(file), file.path);
|
||||
this.handleSvgFileChange(file.path);
|
||||
};
|
||||
|
||||
private async createEventHandler (file: TFile) {
|
||||
if (!(file instanceof TFile)) {
|
||||
return;
|
||||
}
|
||||
if (!file.path.startsWith(this.scriptPath)) {
|
||||
return;
|
||||
}
|
||||
this.loadScript(file);
|
||||
this.handleSvgFileChange(file.path);
|
||||
};
|
||||
|
||||
private async renameEventHandler (file: TAbstractFile, oldPath: string) {
|
||||
if (!(file instanceof TFile)) {
|
||||
return;
|
||||
}
|
||||
const oldFileIsScript = oldPath.startsWith(this.scriptPath);
|
||||
const newFileIsScript = file.path.startsWith(this.scriptPath);
|
||||
if (oldFileIsScript) {
|
||||
this.unloadScript(this.getScriptName(oldPath), oldPath);
|
||||
this.handleSvgFileChange(oldPath);
|
||||
}
|
||||
if (newFileIsScript) {
|
||||
this.loadScript(file);
|
||||
handleSvgFileChange(file.path);
|
||||
};
|
||||
this.plugin.registerEvent(
|
||||
app.vault.on("create", createEventHandler),
|
||||
);
|
||||
this.handleSvgFileChange(file.path);
|
||||
}
|
||||
}
|
||||
|
||||
const renameEventHandler = async (file: TAbstractFile, oldPath: string) => {
|
||||
if (!(file instanceof TFile)) {
|
||||
return;
|
||||
}
|
||||
const oldFileIsScript = oldPath.startsWith(this.scriptPath);
|
||||
const newFileIsScript = file.path.startsWith(this.scriptPath);
|
||||
if (oldFileIsScript) {
|
||||
this.unloadScript(this.getScriptName(oldPath), oldPath);
|
||||
handleSvgFileChange(oldPath);
|
||||
}
|
||||
if (newFileIsScript) {
|
||||
this.loadScript(file);
|
||||
handleSvgFileChange(file.path);
|
||||
}
|
||||
};
|
||||
registerEventHandlers() {
|
||||
this.plugin.registerEvent(
|
||||
app.vault.on("rename", renameEventHandler),
|
||||
this.plugin.app.vault.on(
|
||||
"delete",
|
||||
(file: TFile)=>this.deleteEventHandler(file)
|
||||
),
|
||||
);
|
||||
this.plugin.registerEvent(
|
||||
this.plugin.app.vault.on(
|
||||
"create",
|
||||
(file: TFile)=>this.createEventHandler(file)
|
||||
),
|
||||
);
|
||||
this.plugin.registerEvent(
|
||||
this.plugin.app.vault.on(
|
||||
"rename",
|
||||
(file: TAbstractFile, oldPath: string)=>this.renameEventHandler(file, oldPath)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -172,6 +205,7 @@ export class ScriptEngine {
|
||||
(async()=>{
|
||||
const script = await app.vault.read(f);
|
||||
if(script) {
|
||||
//remove YAML frontmatter if present
|
||||
this.executeScript(view, script, scriptName,f);
|
||||
}
|
||||
})()
|
||||
@@ -212,7 +246,9 @@ export class ScriptEngine {
|
||||
if (!view || !script || !title) {
|
||||
return;
|
||||
}
|
||||
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
|
||||
@@ -251,7 +287,7 @@ export class ScriptEngine {
|
||||
instructions?: Instruction[],
|
||||
) =>
|
||||
ScriptEngine.suggester(
|
||||
app,
|
||||
this.plugin.app,
|
||||
displayItems,
|
||||
items,
|
||||
hint,
|
||||
@@ -263,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
@@ -2,8 +2,20 @@ import { customAlphabet } from "nanoid";
|
||||
import { DeviceType } from "../types";
|
||||
import { ExcalidrawLib } from "../ExcalidrawLib";
|
||||
import { moment } from "obsidian";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
//This is only for backward compatibility because an early version of obsidian included an encoding to avoid fantom links from littering Obsidian graph view
|
||||
declare const PLUGIN_VERSION:string;
|
||||
export let EXCALIDRAW_PLUGIN: ExcalidrawPlugin = null;
|
||||
export const setExcalidrawPlugin = (plugin: ExcalidrawPlugin) => {
|
||||
EXCALIDRAW_PLUGIN = plugin;
|
||||
};
|
||||
const MD_EXCALIDRAW = "# Excalidraw Data";
|
||||
const MD_TEXTELEMENTS = "## Text Elements";
|
||||
const MD_ELEMENTLINKS = "## Element Links";
|
||||
const MD_EMBEDFILES = "## Embedded Files";
|
||||
const MD_DRAWING = "## Drawing";
|
||||
|
||||
export const MD_EX_SECTIONS = [MD_EXCALIDRAW, MD_TEXTELEMENTS, MD_ELEMENTLINKS, MD_EMBEDFILES, MD_DRAWING];
|
||||
|
||||
export const ERROR_IFRAME_CONVERSION_CANCELED = "iframe conversion canceled";
|
||||
|
||||
@@ -82,6 +94,8 @@ export const {
|
||||
mutateElement,
|
||||
restore,
|
||||
mermaidToExcalidraw,
|
||||
getContainerElement,
|
||||
refreshTextDimensions,
|
||||
} = excalidrawLib;
|
||||
|
||||
export function JSON_parse(x: string): any {
|
||||
@@ -143,25 +157,32 @@ export const IMAGE_TYPES = ["jpeg", "jpg", "png", "gif", "svg", "webp", "bmp", "
|
||||
export const ANIMATED_IMAGE_TYPES = ["gif", "webp", "apng", "svg"];
|
||||
export const EXPORT_TYPES = ["svg", "dark.svg", "light.svg", "png", "dark.png", "light.png"];
|
||||
export const MAX_IMAGE_SIZE = 500;
|
||||
export const FRONTMATTER_KEY = "excalidraw-plugin";
|
||||
export const FRONTMATTER_KEY_EXPORT_TRANSPARENT =
|
||||
"excalidraw-export-transparent";
|
||||
export const FRONTMATTER_KEY_EXPORT_DARK = "excalidraw-export-dark";
|
||||
export const FRONTMATTER_KEY_EXPORT_SVGPADDING = "excalidraw-export-svgpadding"; //depricated
|
||||
export const FRONTMATTER_KEY_EXPORT_PADDING = "excalidraw-export-padding";
|
||||
export const FRONTMATTER_KEY_EXPORT_PNGSCALE = "excalidraw-export-pngscale";
|
||||
export const FRONTMATTER_KEY_CUSTOM_PREFIX = "excalidraw-link-prefix";
|
||||
export const FRONTMATTER_KEY_CUSTOM_URL_PREFIX = "excalidraw-url-prefix";
|
||||
export const FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS = "excalidraw-link-brackets";
|
||||
export const FRONTMATTER_KEY_ONLOAD_SCRIPT = "excalidraw-onload-script";
|
||||
export const FRONTMATTER_KEY_LINKBUTTON_OPACITY = "excalidraw-linkbutton-opacity";
|
||||
export const FRONTMATTER_KEY_DEFAULT_MODE = "excalidraw-default-mode";
|
||||
export const FRONTMATTER_KEY_FONT = "excalidraw-font";
|
||||
export const FRONTMATTER_KEY_FONTCOLOR = "excalidraw-font-color";
|
||||
export const FRONTMATTER_KEY_BORDERCOLOR = "excalidraw-border-color";
|
||||
export const FRONTMATTER_KEY_MD_STYLE = "excalidraw-css";
|
||||
export const FRONTMATTER_KEY_AUTOEXPORT = "excalidraw-autoexport"
|
||||
export const FRONTMATTER_KEY_EMBEDDABLE_THEME = "excalidraw-iframe-theme";
|
||||
|
||||
export const FRONTMATTER_KEYS:{[key:string]: {name: string, type: string, depricated?:boolean}} = {
|
||||
"plugin": {name: "excalidraw-plugin", type: "text"},
|
||||
"export-transparent": {name: "excalidraw-export-transparent", type: "checkbox"},
|
||||
"mask": {name: "excalidraw-mask", type: "checkbox"},
|
||||
"export-dark": {name: "excalidraw-export-dark", type: "checkbox"},
|
||||
"export-svgpadding": {name: "excalidraw-export-svgpadding", type: "number", depricated: true},
|
||||
"export-padding": {name: "excalidraw-export-padding", type: "number"},
|
||||
"export-pngscale": {name: "excalidraw-export-pngscale", type: "number"},
|
||||
"export-embed-scene": {name: "excalidraw-export-embed-scene", type: "checkbox"},
|
||||
"link-prefix": {name: "excalidraw-link-prefix", type: "text"},
|
||||
"url-prefix": {name: "excalidraw-url-prefix", type: "text"},
|
||||
"link-brackets": {name: "excalidraw-link-brackets", type: "checkbox"},
|
||||
"onload-script": {name: "excalidraw-onload-script", type: "text"},
|
||||
"linkbutton-opacity": {name: "excalidraw-linkbutton-opacity", type: "number"},
|
||||
"default-mode": {name: "excalidraw-default-mode", type: "text"},
|
||||
"font": {name: "excalidraw-font", type: "text"},
|
||||
"font-color": {name: "excalidraw-font-color", type: "text"},
|
||||
"border-color": {name: "excalidraw-border-color", type: "text"},
|
||||
"md-css": {name: "excalidraw-css", type: "text"},
|
||||
"autoexport": {name: "excalidraw-autoexport", type: "text"},
|
||||
"iframe-theme": {name: "excalidraw-iframe-theme", type: "text", depricated: true},
|
||||
"embeddable-theme": {name: "excalidraw-embeddable-theme", type: "text"},
|
||||
"open-as-markdown": {name: "excalidraw-open-md", type: "checkbox"},
|
||||
};
|
||||
|
||||
export const EMBEDDABLE_THEME_FRONTMATTER_VALUES = ["light", "dark", "auto", "dafault"];
|
||||
export const VIEW_TYPE_EXCALIDRAW = "excalidraw";
|
||||
export const ICON_NAME = "excalidraw-icon";
|
||||
@@ -175,11 +196,11 @@ export const DARK_BLANK_DRAWING =
|
||||
export const FRONTMATTER = [
|
||||
"---",
|
||||
"",
|
||||
`${FRONTMATTER_KEY}: parsed`,
|
||||
`${FRONTMATTER_KEYS["plugin"].name}: parsed`,
|
||||
"tags: [excalidraw]",
|
||||
"",
|
||||
"---",
|
||||
"==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠==",
|
||||
"==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠== You can decompress Drawing data with the command palette: 'Decompress current Excalidraw file'. For more info check in plugin settings under 'Saving'",
|
||||
"",
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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} | 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;
|
||||
@@ -154,9 +156,9 @@ function RenderObsidianView(
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
//mount the workspace leaf or the canvas node depending on subpath
|
||||
//Mount the workspace leaf or the canvas node depending on subpath
|
||||
//--------------------------------------------------------------------------------
|
||||
react.useEffect(() => {
|
||||
React.useEffect(() => {
|
||||
if(!containerRef?.current) {
|
||||
return;
|
||||
}
|
||||
@@ -175,8 +177,9 @@ function RenderObsidianView(
|
||||
rootSplit.containerEl.style.height = '100%';
|
||||
rootSplit.containerEl.style.borderRadius = "var(--embeddable-radius)";
|
||||
leafRef.current = {
|
||||
leaf: app.workspace.createLeafInParent(rootSplit, 0),
|
||||
node: null
|
||||
leaf: view.app.workspace.createLeafInParent(rootSplit, 0),
|
||||
node: null,
|
||||
editNode: null,
|
||||
};
|
||||
|
||||
const setKeepOnTop = () => {
|
||||
@@ -221,16 +224,27 @@ function RenderObsidianView(
|
||||
const workspaceLeaf:HTMLDivElement = rootSplit.containerEl.querySelector("div.workspace-leaf");
|
||||
if(workspaceLeaf) workspaceLeaf.style.borderRadius = "var(--embeddable-radius)";
|
||||
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]);
|
||||
|
||||
const setColors = (canvasNode: HTMLDivElement, element: NonDeletedExcalidrawElement, mdProps: EmbeddableMDCustomProps, canvasColor: string) => {
|
||||
//--------------------------------------------------------------------------------
|
||||
//Set colors of the canvas node
|
||||
//--------------------------------------------------------------------------------
|
||||
function setColors (canvasNode: HTMLDivElement, element: NonDeletedExcalidrawElement, mdProps: EmbeddableMDCustomProps, canvasColor: string) {
|
||||
if(!mdProps) return;
|
||||
if (!leafRef.current?.hasOwnProperty("node")) return;
|
||||
|
||||
@@ -280,16 +294,19 @@ function RenderObsidianView(
|
||||
: "transparent";
|
||||
canvasNode?.style.setProperty("--canvas-border", color);
|
||||
canvasNode?.style.setProperty("--canvas-color", color);
|
||||
canvasNodeContainer?.style.setProperty("border-color", color);
|
||||
//canvasNodeContainer?.style.setProperty("border-color", color);
|
||||
} else if(!(mdProps?.borderMatchElement ?? true)) {
|
||||
const color = ea.getCM(mdProps.borderColor).alphaTo((mdProps.borderOpacity??100)/100).stringHEX();
|
||||
canvasNode?.style.setProperty("--canvas-border", color);
|
||||
canvasNode?.style.setProperty("--canvas-color", color);
|
||||
canvasNodeContainer?.style.setProperty("border-color", color);
|
||||
//canvasNodeContainer?.style.setProperty("border-color", color);
|
||||
}
|
||||
}
|
||||
|
||||
react.useEffect(() => {
|
||||
//--------------------------------------------------------------------------------
|
||||
//Set colors of the canvas node
|
||||
//--------------------------------------------------------------------------------
|
||||
React.useEffect(() => {
|
||||
if(!containerRef.current) {
|
||||
return;
|
||||
}
|
||||
@@ -304,7 +321,10 @@ function RenderObsidianView(
|
||||
canvasColor,
|
||||
])
|
||||
|
||||
react.useEffect(() => {
|
||||
//--------------------------------------------------------------------------------
|
||||
//Switch to preview mode when the iframe is not active
|
||||
//--------------------------------------------------------------------------------
|
||||
React.useEffect(() => {
|
||||
if(isEditingRef.current) {
|
||||
if(leafRef.current?.node) {
|
||||
containerRef.current?.addClasses(["is-editing", "is-focused"]);
|
||||
@@ -314,13 +334,12 @@ 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();
|
||||
event?.stopPropagation();
|
||||
}
|
||||
|
||||
if (isActiveRef.current && !isEditingRef.current && leafRef.current?.leaf) {
|
||||
@@ -349,10 +368,26 @@ function RenderObsidianView(
|
||||
}
|
||||
}, [leafRef.current?.leaf, element.id, view, themeRef.current]);
|
||||
|
||||
if(leafRef.current) leafRef.current.editNode = handleClick;
|
||||
// Event listener for key press
|
||||
React.useEffect(() => {
|
||||
const handleKeyPress = (event: KeyboardEvent) => {
|
||||
if (event.key === "Enter") {
|
||||
handleClick(event); // Call handleClick function when Enter key is pressed
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyPress); // Add event listener for key press
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyPress); // Remove event listener when component unmounts
|
||||
};
|
||||
}, [handleClick]);
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
// Set isActiveRef and switch to preview mode when the iframe is not active
|
||||
//--------------------------------------------------------------------------------
|
||||
react.useEffect(() => {
|
||||
React.useEffect(() => {
|
||||
if(!containerRef?.current || !leafRef?.current) {
|
||||
return;
|
||||
}
|
||||
@@ -400,11 +435,10 @@ 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 (
|
||||
<div
|
||||
ref={containerRef}
|
||||
|
||||
@@ -49,7 +49,7 @@ export class EmbeddableSettings extends Modal {
|
||||
this.zoomValue = element.scale[0];
|
||||
this.isYouTube = isYouTube(this.element.link);
|
||||
this.notExcalidrawIsInternal = this.file && !this.view.plugin.isExcalidrawFile(this.file)
|
||||
this.isMDFile = this.file && this.file.extension === "md" && !this.view.plugin.isExcalidrawFile(this.file);
|
||||
this.isMDFile = this.file && this.file.extension === "md"; // && !this.view.plugin.isExcalidrawFile(this.file);
|
||||
this.isLocalURI = this.element.link.startsWith("file://");
|
||||
if(isYouTube) this.youtubeStart = getYouTubeStartAt(this.element.link);
|
||||
|
||||
@@ -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.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;
|
||||
} catch(e) {
|
||||
new Notice("File rename failed. "+e,10000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(this.isYouTube && this.youtubeStart !== getYouTubeStartAt(this.element.link)) {
|
||||
@@ -212,9 +223,15 @@ export class EmbeddableSettings extends Modal {
|
||||
el.scale = [this.zoomValue,this.zoomValue];
|
||||
}
|
||||
if(dirty) {
|
||||
this.ea.addElementsToView();
|
||||
(async() => {
|
||||
await this.ea.addElementsToView();
|
||||
//@ts-ignore
|
||||
this.ea.viewUpdateScene({appState: {}});
|
||||
this.close(); //close should only run once update scene is done
|
||||
})();
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
this.close();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { DEVICE } from "src/constants/constants";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
import ExcalidrawView from "src/ExcalidrawView";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
import { fragWithHTML, getExportPadding, getExportTheme, getPNGScale, getWithBackground } from "src/utils/Utils";
|
||||
import { fragWithHTML, getExportPadding, getExportTheme, getPNGScale, getWithBackground, shouldEmbedScene } from "src/utils/Utils";
|
||||
|
||||
export class ExportDialog extends Modal {
|
||||
private ea: ExcalidrawAutomate;
|
||||
@@ -33,20 +33,33 @@ 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);
|
||||
this.scale = getPNGScale(this.plugin,this.file)
|
||||
this.theme = getExportTheme(this.plugin, this.file, (this.api).getAppState().theme)
|
||||
this.boundingBox = this.ea.getBoundingBox(this.ea.getViewElements());
|
||||
this.embedScene = false;
|
||||
this.embedScene = shouldEmbedScene(this.plugin, this.file);
|
||||
this.exportSelectedOnly = false;
|
||||
this.saveToVault = true;
|
||||
this.transparent = !getWithBackground(this.plugin, this.file);
|
||||
this.saveSettings = false;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.app = null;
|
||||
this.plugin = null;
|
||||
this.ea.destroy();
|
||||
this.ea = null;
|
||||
this.view = null;
|
||||
this.file = null;
|
||||
this.api = null;
|
||||
this.theme = null;
|
||||
this.selectedOnlySetting = null;
|
||||
this.containerEl.remove();
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.containerEl.classList.add("excalidraw-release");
|
||||
this.titleEl.setText(`Export Image`);
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
SuggestModal,
|
||||
Scope,
|
||||
} from "obsidian";
|
||||
import { t } from "../lang/helpers";
|
||||
import { createPopper, Instance as PopperInstance } from "@popperjs/core";
|
||||
|
||||
class Suggester<T> {
|
||||
@@ -132,7 +131,7 @@ class Suggester<T> {
|
||||
export abstract class SuggestionModal<T> extends FuzzySuggestModal<T> {
|
||||
items: T[] = [];
|
||||
suggestions: HTMLDivElement[];
|
||||
popper: PopperInstance;
|
||||
popper: WeakRef<PopperInstance>;
|
||||
//@ts-ignore
|
||||
scope: Scope = new Scope(this.app.scope);
|
||||
suggester: Suggester<FuzzyMatch<T>>;
|
||||
@@ -197,7 +196,7 @@ export abstract class SuggestionModal<T> extends FuzzySuggestModal<T> {
|
||||
this.app.keymap.pushScope(this.scope);
|
||||
|
||||
this.inputEl.ownerDocument.body.appendChild(this.suggestEl);
|
||||
this.popper = createPopper(this.inputEl, this.suggestEl, {
|
||||
this.popper = new WeakRef(createPopper(this.inputEl, this.suggestEl, {
|
||||
placement: "bottom-start",
|
||||
modifiers: [
|
||||
{
|
||||
@@ -213,7 +212,7 @@ export abstract class SuggestionModal<T> extends FuzzySuggestModal<T> {
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
onEscape(): void {
|
||||
@@ -225,11 +224,15 @@ export abstract class SuggestionModal<T> extends FuzzySuggestModal<T> {
|
||||
this.app.keymap.popScope(this.scope);
|
||||
|
||||
this.suggester.setSuggestions([]);
|
||||
if (this.popper) {
|
||||
this.popper.destroy();
|
||||
if (this.popper?.deref()) {
|
||||
this.popper.deref().destroy();
|
||||
}
|
||||
|
||||
this.suggestEl.detach();
|
||||
this.inputEl.removeEventListener("input", this.onInputChanged.bind(this));
|
||||
this.inputEl.removeEventListener("focus", this.onFocus.bind(this));
|
||||
this.inputEl.removeEventListener("blur", this.close.bind(this));
|
||||
|
||||
this.suggestEl.detach();
|
||||
}
|
||||
createPrompt(prompts: HTMLSpanElement[]) {
|
||||
if (!this.promptEl) {
|
||||
|
||||
82
src/dialogs/FrameSettings.ts
Normal file
82
src/dialogs/FrameSettings.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
import { t } from "src/lang/helpers";
|
||||
|
||||
export const showFrameSettings = (ea: ExcalidrawAutomate) => {
|
||||
const {enabled, clip, name, outline} = ea.getExcalidrawAPI().getAppState().frameRendering;
|
||||
|
||||
// Create modal dialog
|
||||
const frameSettingsModal = new ea.obsidian.Modal(app);
|
||||
|
||||
frameSettingsModal.onOpen = () => {
|
||||
const {contentEl} = frameSettingsModal;
|
||||
|
||||
contentEl.createEl("h1", {text: t("FRAME_SETTINGS_TITLE")});
|
||||
|
||||
const settings = { enabled, clip, name, outline };
|
||||
|
||||
// Add toggles
|
||||
const enableFramesSetting = new ea.obsidian.Setting(contentEl)
|
||||
.setName(t("FRAME_SETTINGS_ENABLE"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(settings.enabled)
|
||||
.onChange(value => {
|
||||
settings.enabled = value;
|
||||
hideComponent(displayFrameNameSetting, !value);
|
||||
hideComponent(displayFrameOutlineSetting, !value);
|
||||
hideComponent(enableFrameClippingSetting, !value);
|
||||
})
|
||||
);
|
||||
|
||||
const displayFrameNameSetting = new ea.obsidian.Setting(contentEl)
|
||||
.setName(t("FRAME_SETTIGNS_NAME"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(settings.name)
|
||||
.onChange(value => settings.name = value)
|
||||
);
|
||||
|
||||
const displayFrameOutlineSetting = new ea.obsidian.Setting(contentEl)
|
||||
.setName(t("FRAME_SETTINGS_OUTLINE"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(settings.outline)
|
||||
.onChange(value => settings.outline = value)
|
||||
);
|
||||
|
||||
const enableFrameClippingSetting = new ea.obsidian.Setting(contentEl)
|
||||
.setName(t("FRAME_SETTINGS_CLIP"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(settings.clip)
|
||||
.onChange(value => settings.clip = value)
|
||||
);
|
||||
|
||||
// Hide or show components based on initial state
|
||||
hideComponent(displayFrameNameSetting, !settings.enabled);
|
||||
hideComponent(displayFrameOutlineSetting, !settings.enabled);
|
||||
hideComponent(enableFrameClippingSetting, !settings.enabled);
|
||||
|
||||
// Add OK button
|
||||
new ea.obsidian.Setting(contentEl)
|
||||
.addButton(button => button
|
||||
.setButtonText("OK")
|
||||
.onClick(() => {
|
||||
// Update appState with new settings
|
||||
ea.viewUpdateScene({
|
||||
// @ts-ignore
|
||||
appState: {
|
||||
frameRendering: settings
|
||||
}
|
||||
});
|
||||
frameSettingsModal.close();
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
frameSettingsModal.onClose = () => {
|
||||
ea.destroy();
|
||||
}
|
||||
frameSettingsModal.open();
|
||||
};
|
||||
|
||||
// Function to hide or show a component
|
||||
function hideComponent(comp:any, value:any) {
|
||||
comp.settingEl.style.display = value ? "none" : "";
|
||||
}
|
||||
@@ -3,12 +3,20 @@ import { REG_LINKINDEX_INVALIDCHARS } from "../constants/constants";
|
||||
import ExcalidrawView from "../ExcalidrawView";
|
||||
import { t } from "../lang/helpers";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
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;
|
||||
@@ -36,15 +44,23 @@ export class ImportSVGDialog extends FuzzySuggestModal<TFile> {
|
||||
return item.path;
|
||||
}
|
||||
|
||||
async onChooseItem(item: TFile, event: KeyboardEvent): Promise<void> {
|
||||
async onChooseItem(item: TFile, _: KeyboardEvent): Promise<void> {
|
||||
if(!item) return;
|
||||
const ea = this.plugin.ea;
|
||||
ea.reset();
|
||||
ea.setView(this.view);
|
||||
const svg = await app.vault.read(item);
|
||||
const ea = getEA(this.view) as ExcalidrawAutomate;
|
||||
const svg = await this.app.vault.read(item);
|
||||
if(!svg || svg === "") return;
|
||||
ea.importSVG(svg);
|
||||
ea.addElementsToView(true, true, true,true);
|
||||
ea.addToGroup(ea.getElements().map(el=>el.id));
|
||||
await ea.addElementsToView(true, true, true,true);
|
||||
ea.destroy();
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
//deley this.view destruction until onChooseItem is called
|
||||
window.setTimeout(() => {
|
||||
this.view = null;
|
||||
});
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
public start(view: ExcalidrawView) {
|
||||
|
||||
@@ -6,6 +6,11 @@ export class InsertCommandDialog extends FuzzySuggestModal<TFile> {
|
||||
public app: App;
|
||||
private addText: Function;
|
||||
|
||||
destroy() {
|
||||
this.app = null;
|
||||
this.addText = null;
|
||||
}
|
||||
|
||||
constructor(app: App) {
|
||||
super(app);
|
||||
this.app = app;
|
||||
@@ -32,10 +37,18 @@ export class InsertCommandDialog extends FuzzySuggestModal<TFile> {
|
||||
onChooseItem(item: any): void {
|
||||
const cmdId = item?.id;
|
||||
this.addText(`⚙️[${item.name}](cmd://${item.id})`);
|
||||
this.addText = null;
|
||||
}
|
||||
|
||||
public start(addText: Function) {
|
||||
this.addText = addText;
|
||||
this.open();
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
window.setTimeout(()=>{
|
||||
this.addText = null;
|
||||
}) //onChooseItem must run first
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,28 @@ import { DEVICE, IMAGE_TYPES, REG_LINKINDEX_INVALIDCHARS } from "../constants/co
|
||||
import ExcalidrawView from "../ExcalidrawView";
|
||||
import { t } from "../lang/helpers";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { getEA } from "src";
|
||||
|
||||
export class InsertImageDialog extends FuzzySuggestModal<TFile> {
|
||||
public app: App;
|
||||
public plugin: ExcalidrawPlugin;
|
||||
private view: ExcalidrawView;
|
||||
|
||||
destroy() {
|
||||
this.app = null;
|
||||
this.plugin = null;
|
||||
this.view = null;
|
||||
this.inputEl.onkeyup = null;
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
//deley this.view destruction until onChooseItem is called
|
||||
window.setTimeout(() => {
|
||||
this.view = null;
|
||||
});
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
constructor(plugin: ExcalidrawPlugin) {
|
||||
super(plugin.app);
|
||||
this.plugin = plugin;
|
||||
@@ -55,13 +71,14 @@ export class InsertImageDialog extends FuzzySuggestModal<TFile> {
|
||||
}
|
||||
|
||||
onChooseItem(item: TFile, event: KeyboardEvent): void {
|
||||
const ea = this.plugin.ea.getAPI(this.view);
|
||||
const ea = getEA(this.view);
|
||||
ea.canvas.theme = this.view.excalidrawAPI.getAppState().theme;
|
||||
const scaleToFullsize = scaleToFullsizeModifier(event);
|
||||
(async () => {
|
||||
//this.view.currentPosition = this.position;
|
||||
await ea.addImage(0, 0, item, !scaleToFullsize);
|
||||
ea.addElementsToView(true, true, true);
|
||||
await ea.addElementsToView(true, true, true);
|
||||
ea.destroy();
|
||||
})();
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,12 @@ export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
|
||||
private addText: Function;
|
||||
private drawingPath: string;
|
||||
|
||||
destroy() {
|
||||
this.app = null;
|
||||
this.addText = null;
|
||||
this.drawingPath = null;
|
||||
}
|
||||
|
||||
constructor(private plugin: ExcalidrawPlugin) {
|
||||
super(plugin.app);
|
||||
this.app = plugin.app;
|
||||
@@ -51,6 +57,13 @@ export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
|
||||
this.addText(getLink(this.plugin,{embed: false, path: filepath, alias: item.alias}), filepath, item.alias);
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
window.setTimeout(()=>{
|
||||
this.addText = null
|
||||
}); //make sure this happens after onChooseItem runs
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
public start(drawingPath: string, addText: Function) {
|
||||
this.addText = addText;
|
||||
this.drawingPath = drawingPath;
|
||||
|
||||
@@ -2,12 +2,19 @@ import { App, FuzzySuggestModal, TFile } from "obsidian";
|
||||
import ExcalidrawView from "../ExcalidrawView";
|
||||
import { t } from "../lang/helpers";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { getEA } from "src";
|
||||
|
||||
export class InsertMDDialog extends FuzzySuggestModal<TFile> {
|
||||
public app: App;
|
||||
public plugin: ExcalidrawPlugin;
|
||||
private view: ExcalidrawView;
|
||||
|
||||
destroy() {
|
||||
this.app = null;
|
||||
this.plugin = null;
|
||||
this.view = null;
|
||||
}
|
||||
|
||||
constructor(plugin: ExcalidrawPlugin) {
|
||||
super(plugin.app);
|
||||
this.plugin = plugin;
|
||||
@@ -34,12 +41,11 @@ export class InsertMDDialog extends FuzzySuggestModal<TFile> {
|
||||
}
|
||||
|
||||
onChooseItem(item: TFile): void {
|
||||
const ea = this.plugin.ea;
|
||||
ea.reset();
|
||||
ea.setView(this.view);
|
||||
const ea = getEA(this.view);
|
||||
(async () => {
|
||||
await ea.addImage(0, 0, item);
|
||||
ea.addElementsToView(true, false, true);
|
||||
await ea.addElementsToView(true, false, true);
|
||||
ea.destroy();
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -47,4 +53,12 @@ export class InsertMDDialog extends FuzzySuggestModal<TFile> {
|
||||
this.view = view;
|
||||
this.open();
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
//deley this.view destruction until onChooseItem is called
|
||||
window.setTimeout(() => {
|
||||
this.view = null;
|
||||
});
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export class InsertPDFModal extends Modal {
|
||||
private plugin: ExcalidrawPlugin,
|
||||
private view: ExcalidrawView,
|
||||
) {
|
||||
super(app);
|
||||
super(plugin.app);
|
||||
}
|
||||
|
||||
open (file?: TFile) {
|
||||
@@ -54,12 +54,17 @@ export class InsertPDFModal extends Modal {
|
||||
this.plugin.settings.pdfNumRows = this.numRows;
|
||||
this.plugin.settings.pdfDirection = this.direction;
|
||||
this.plugin.settings.pdfLockAfterImport = this.lockAfterImport;
|
||||
this.plugin.saveSettings();
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
if(this.pdfDoc) {
|
||||
this.pdfDoc.destroy();
|
||||
this.pdfDoc = null;
|
||||
}
|
||||
this.plugin = null;
|
||||
this.view = null;
|
||||
this.app = null;
|
||||
this.imageSizeMessage.remove();
|
||||
this.setImageSizeMessage = null;
|
||||
}
|
||||
|
||||
private async getPageDimensions (pdfDoc: any) {
|
||||
@@ -162,6 +167,19 @@ export class InsertPDFModal extends Modal {
|
||||
numPagesMessage.innerHTML = `There are <b>${numPages}</b> pages in the selected document.`;
|
||||
}
|
||||
|
||||
let pageRangesTextComponent: TextComponent
|
||||
let importPagesMessage: HTMLParagraphElement;
|
||||
|
||||
const rangeOnChange = (value:string) => {
|
||||
const pages = this.createPageListFromString(value);
|
||||
if(pages.length > 15) {
|
||||
importPagesMessage.innerHTML = `You are importing <b>${pages.length}</b> pages. ⚠️ This may take a while. ⚠️`;
|
||||
} else {
|
||||
importPagesMessage.innerHTML = `You are importing <b>${pages.length}</b> pages.`;
|
||||
}
|
||||
importButtonMessages();
|
||||
}
|
||||
|
||||
const setFile = async (file: TFile) => {
|
||||
if(this.pdfDoc) await this.pdfDoc.destroy();
|
||||
this.pdfDoc = null;
|
||||
@@ -171,6 +189,8 @@ export class InsertPDFModal extends Modal {
|
||||
this.pdfFile = file;
|
||||
if(this.pdfDoc) {
|
||||
numPages = this.pdfDoc.numPages;
|
||||
pageRangesTextComponent.setValue(`1-${numPages}`);
|
||||
rangeOnChange(`1-${numPages}`);
|
||||
importButtonMessages();
|
||||
numPagesMessages();
|
||||
this.getPageDimensions(this.pdfDoc);
|
||||
@@ -190,23 +210,14 @@ export class InsertPDFModal extends Modal {
|
||||
|
||||
numPagesMessage = ce.createEl("p", {text: ""});
|
||||
numPagesMessages();
|
||||
let importPagesMessage: HTMLParagraphElement;
|
||||
let pageRangesTextComponent: TextComponent
|
||||
new Setting(ce)
|
||||
.setName("Pages to import")
|
||||
.setDesc("e.g.: 1,3-5,7,9-10")
|
||||
.addText(text => {
|
||||
pageRangesTextComponent = text;
|
||||
text
|
||||
.setPlaceholder("e.g.: 1,3-5,7,9-10")
|
||||
.onChange((value) => {
|
||||
const pages = this.createPageListFromString(value);
|
||||
if(pages.length > 15) {
|
||||
importPagesMessage.innerHTML = `You are importing <b>${pages.length}</b> pages. ⚠️ This may take a while. ⚠️`;
|
||||
} else {
|
||||
importPagesMessage.innerHTML = `You are importing <b>${pages.length}</b> pages.`;
|
||||
}
|
||||
importButtonMessages();
|
||||
})
|
||||
.setValue("")
|
||||
.onChange((value) => rangeOnChange(value))
|
||||
text.inputEl.style.width = "100%";
|
||||
})
|
||||
importPagesMessage = ce.createEl("p", {text: ""});
|
||||
@@ -402,6 +413,7 @@ export class InsertPDFModal extends Modal {
|
||||
const viewElements = ea.getViewElements().filter(el => ids.includes(el.id));
|
||||
api.selectElements(viewElements);
|
||||
api.zoomToFit(viewElements);
|
||||
ea.destroy();
|
||||
this.close();
|
||||
})
|
||||
importButton = button;
|
||||
|
||||
@@ -6,7 +6,7 @@ If you'd like to learn more, please subscribe to my YouTube channel: [Visual PKM
|
||||
Thank you & Enjoy!
|
||||
|
||||
<div class="excalidraw-videoWrapper"><div>
|
||||
<iframe src="https://www.youtube.com/embed/o0exK-xFP3k" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
<iframe src="https://www.youtube.com/embed/P_Q6avJGoWI" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div></div>
|
||||
`;
|
||||
|
||||
@@ -17,6 +17,515 @@ I develop this plugin as a hobby, spending my free time doing this. If you find
|
||||
|
||||
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://cdn.ko-fi.com/cdn/kofi3.png?v=3" height=45></a></div>
|
||||
`,
|
||||
"2.2.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
|
||||
|
||||

|
||||
`,
|
||||
"2.2.1":`
|
||||
## Fixed
|
||||
- Text height becomes unreadable after 2.2.0 update [#1784](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1784)
|
||||
- Images are loaded with a rounded border when loading old Excalidraw files
|
||||
- Embedded Excalidraw images cache gets stuck with old version of the image
|
||||
- Extremely long loading times with legacy (3+ years old) Excalidraw files
|
||||
`,
|
||||
"2.2.0":`
|
||||
<div class="excalidraw-videoWrapper"><div>
|
||||
<iframe src="https://www.youtube.com/embed/dV0NEOwn5NM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div></div>
|
||||
|
||||
⚠️⚠️⚠️ BREAKING CHANGE ⚠️⚠️⚠️
|
||||
Files you save with 2.2.0 are not backward compatible with earlier plugin versions!
|
||||
|
||||
## New from excalidraw.com
|
||||
- Wrapable text elements (without the need for transparent sticky notes!)
|
||||
|
||||
## New
|
||||
- File format. I nested all Excalidraw markup under ${String.fromCharCode(96)}# Excalidraw Data${String.fromCharCode(96)}. Here's the new structure.
|
||||
${String.fromCharCode(96,96,96)}markdown
|
||||
---
|
||||
excalidraw-plugin: parsed
|
||||
other-frontmatter-properties: values
|
||||
---
|
||||
back of the note bla bla bla
|
||||
|
||||
# Excalidraw Data
|
||||
## Text Element
|
||||
## Element Links
|
||||
## Embedded Files
|
||||
%%
|
||||
## Drawing
|
||||
%%
|
||||
${String.fromCharCode(96,96,96)}
|
||||
- When opening Excalidraw in Markdown ${String.fromCharCode(96)}# Excalidraw Data${String.fromCharCode(96)} will be folded.
|
||||
- New command palette action: ${String.fromCharCode(96)}Open the back-of-the-note of the selected Excalidraw image${String.fromCharCode(96)}. The action is only visible when selecting an embedded Excalidraw drawing in the Scene. On a desktop, the command will open the back of the selected card in a popout window, and on a mobile, in a new tab.
|
||||
|
||||
## Fixed
|
||||
- Drag and drop from Finder/Explorer (OS external). Images will retain their filenames. PDFs will be imported to the Vault. [#1779](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1779)
|
||||
`,
|
||||
"2.1.8":`
|
||||
## Fixed
|
||||
- Fixing issues that surfaced after upgrading to Obsidian 1.6.0
|
||||
- Fixed Excalidraw hover previews
|
||||
- Cursor for editing links, text elements, and frame names became virtually invisible if Obsidian was in dark mode and Excalidraw in light mode and vica versa.
|
||||
- Rendering Excalidraw drawings in Markdown views, right after Obsidian loaded did not work.
|
||||
- I implemented more graceful fail if you submitted a malformed SVG for SVG to Excalidraw conversation. [#1768](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1768)
|
||||
|
||||
## New
|
||||
- New setting under "Save" in plugin settings to NOT decompress JSON when switching to Markdown view mode. For details see description under "Save" settings. The benefit is smaller file size and fewer results in the Obsidian search. If you want to edit the JSON, you can always manually decompress JSON in markdown view mode with the command palette action "Excalidraw: Decompress JSON".
|
||||
`,
|
||||
"2.1.7:":`
|
||||
<div class="excalidraw-videoWrapper"><div>
|
||||
<iframe src="https://www.youtube.com/embed/P_Q6avJGoWI" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div></div>
|
||||
|
||||
## Updates from Excalidraw.com
|
||||
- Improved undo management.
|
||||
- Improved handle to scale images from the side.
|
||||
- Changed arrow binding behavior.
|
||||
- Many other minor fixes and improvements.
|
||||
|
||||
## New
|
||||
- Introduced image caching for nested (embedded) Excalidraw drawings on the scene. This enhancement should lead to improved scene loading times, especially when dealing with numerous embedded Excalidraw drawings.
|
||||
- Added new OCR Command Palette actions. Users can now re-run OCR and run OCR for selected elements.
|
||||
|
||||
## Fixed
|
||||
- Fixed an issue where cropping an embeddable PDF frame in the Excalidraw Scene caused distortion based on the embeddable element's aspect ratio. ([#1756](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1756))
|
||||
- Removed the listing of ${String.fromCharCode(96)}# Embedded files${String.fromCharCode(96)} section when adding a "Back of the note card". ([#1754](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1754))
|
||||
- Resolved the issue where closing the on-screen keyboard with the keyboard hide button of your phone, instead of tapping somewhere else on the Excalidraw scene, did not resize the scene correctly. ([#1729](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1729))
|
||||
- Fixed the problem where pasting a text element as text into markdown incorrectly pasted the text to the end of the MD note, with line breaks as rendered on screen in Excalidraw. Also addressed the issue where pasting an image element as an image resulted in it being pasted to the end of the document. ([#1749](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1749))
|
||||
- Corrected the color inversion of embedded images when changing the theme from light to dark, then back from dark to light, and again from light to dark on the third change.
|
||||
- Addressed the problem where cropping an image while unlocking and rotating it in the cropper did not reflect the rotation. Note that rotating the image in Cropper required switching to markdown view mode, changing the "locked": true property to false, then switching back to Excalidraw mode. This issue likely impacted only a very few power users. ([#1745](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1745))
|
||||
|
||||
## New in ExcalidrawAutomate
|
||||
${String.fromCharCode(96,96,96)}ts
|
||||
/**
|
||||
* Retruns the embedded images in the scene recursively. If excalidrawFile is not provided,
|
||||
* the function will use ea.targetView.file
|
||||
* @param excalidrawFile
|
||||
* @returns TFile[] of all nested images and Excalidraw drawings recursively
|
||||
*/
|
||||
public getEmbeddedImagesFiletree(excalidrawFile?: TFile): TFile[];
|
||||
${String.fromCharCode(96,96,96)}
|
||||
|
||||
`,
|
||||
"2.1.6":`
|
||||
## Two minor fixes
|
||||
- Scaling of LaTeX formulas when the formula is changed
|
||||
- If the back of the note card only contains a block embed ${String.fromCharCode(96)}![[embed]]${String.fromCharCode(96)} this got removed when saving the Excalidraw file. This issue has been present since November, 2021 (v1.4.9).
|
||||
`,
|
||||
"2.1.5":`
|
||||
## New
|
||||
- Save "Snap to objects" with the scene state. If this is the only change you make to the scene, force save it using CTRL+S (note, use CTRL on Mac as well).
|
||||
- Added "Copy markdown link" to the context menu.
|
||||
|
||||
## Fixed
|
||||
- Paste operation occasionally duplicated text elements.
|
||||
- Pasting multiple instances of the same image from excalidraw.com or another instance of Obsidian, or pasting an image from anywhere and making copies with ALT/OPT + drag immediately after pasting (before autosave triggered) led to broken images when reopening the drawing.
|
||||
- CTRL/CMD+Click on a Text Element with an element link did not work (previously, you had to click the top right link indicator). Now, you can click anywhere on the element.
|
||||
- Hover preview for elements with a link only worked when hovering over the element link. Now, you can hover anywhere. If there are multiple elements with links, the top-level element will take precedence.
|
||||
- Link navigation within drawing when the "Focus on Existing Tab" feature is enabled under "Links, transclusion and TODOs" in settings works again.
|
||||
- If a link points to a back-of-the-card section or block the drawing will automatically switch to markdown view mode and navigate to the block or section.
|
||||
- DynamicSytle, dark mode when canvas background is set to transparent.
|
||||
- Scale to maintain the aspect ratio of a markdown notes embedded as images.
|
||||
- You can now borrow interactive markdown embeds to tables, blockquotes, list elements and callouts - not just paragraphs.
|
||||
- Back of the drawing cards:
|
||||
- Leaving the Section Name empty when creating the first back of the card note resulted in an error.
|
||||
- If you add the markdown comment (${String.fromCharCode(96)}%%${String.fromCharCode(96)}) directly before ${String.fromCharCode(96)}# Text Elements${String.fromCharCode(96)}, a trailing ${String.fromCharCode(96)}#${String.fromCharCode(96)} will be added to your document, when adding a back of the card note. This is to hide the markdown comment from the card. The trailing (empty) ${String.fromCharCode(96)}#${String.fromCharCode(96)} will not be visible in reading mode, pdf exports, and when publishing with Obsidian Publish.
|
||||
Here's a sample markdown structure of your document:
|
||||
|
||||
${String.fromCharCode(96,96,96)}markdown
|
||||
---
|
||||
excalidraw-plugin: parsed
|
||||
---
|
||||
# Your back of the card section
|
||||
bla bla bla
|
||||
|
||||
#
|
||||
%%
|
||||
# Text Elements
|
||||
... the rest of the Excalidraw file
|
||||
${String.fromCharCode(96,96,96)}
|
||||
`,
|
||||
"2.1.4":`
|
||||
## Fixed
|
||||
- Fixed the **aspect ratio** of an Excalidraw embedded within another Excalidraw **not updating**. [#1707](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1707)
|
||||
- Some plugins automatically add document properties to all files in the Vault. Users with this configuration were **unable to run Excalidraw scripts**. Excalidraw now removes document properties from the script before execution.
|
||||
- The very last markdown edit sometimes **wasn't saved when immediately switching from Markdown to Excalidraw View**. I now force a save before switching views.
|
||||
- The setting to disable/enable ${String.fromCharCode(96)}CTRL/CMD + CLICK on text with [[links]] or [](links) to open them${String.fromCharCode(96)} works again. [#1704](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1704)
|
||||
- **Annotation and cropping** of images in Markdown notes now also work **with Markdown links that have encoded characters** e.g.: ${String.fromCharCode(96)}${String.fromCharCode(96)}.
|
||||
- Solved compatibility issue of **Taskbone OCR on Android**.
|
||||
|
||||
## New
|
||||
- New settings:
|
||||
- Under "Appearance and Behavior": Option to **render Excalidraw file as an image in Markdown reading mode**. This setting is disabled by default. [#1706](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1706), [#1705](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1705)
|
||||
- Under "Embedding Excalidraw ... and Exporting"/"Export Settings": Option to **render Excalidraw file as an image when exporting to PDF** in Markdown mode. This option is disabled by default. When enabled, exporting an Excalidraw drawing in markdown view mode to PDF will render the image on the page.
|
||||
- **Enhanced annotation and cropping** of images in Markdown documents:
|
||||
- Newly embedded **links will now follow the style of the original link**. If the original format was a ${String.fromCharCode(96)}${String.fromCharCode(96)}, the annotated file will follow this format. For ${String.fromCharCode(96)}[[wiki links]]${String.fromCharCode(96)}, it will follow that style. Additionally, if an alias was specified like ${String.fromCharCode(96)}[[link|alias]]${String.fromCharCode(96)}, the annotated or cropped image will retain the alias.
|
||||
- Introduced a new setting under "Saving" titled **"Preserve image size when annotating"**. This setting is disabled by default. When enabled, the embed link replacing the annotated image will maintain the size of the original image.
|
||||
- Option to **automatically embed the scene in exported PNG and SVG image files**. Including the scene will allow users to open the picture on Excalidraw.com or in another Obsidian Vault as an editable Excalidraw file.New setting is under the Export category. The new frontmatter tag is: ${String.fromCharCode(96)}excalidraw-export-embed-scene: true/false${String.fromCharCode(96)}.
|
||||
`,
|
||||
"2.1.3":`
|
||||
This is a republish of 2.1.2 with a minor change. Sorry about the frequent releases. I will hold back for a few weeks now.
|
||||
`,
|
||||
"2.1.2":`
|
||||
## Quality of Life Improvements
|
||||
- The "Insert Any File" option that disappeared from the Command Palette is now restored. [#1690](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1690)
|
||||
- Improved two-finger pan speed.
|
||||
- Fixed text wrapping issue that caused text to jump around when editing text in a sticky note when the Obsidian zoom level was not set to 100%.
|
||||
- Mask Generation in [ExcaliAI](https://youtu.be/3G8hsV-V-gQ) Edit Image now works properly again. [#1684](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1684)
|
||||
- Fixed aspect ratio change for .jpg, .png, .bmp, .webp, .SVG (non-Excalidraw) images. Previously, if the image was distorted (i.e. you held SHIFT while resizing it), it would revert to the original aspect ratio upon saving the drawing. Resetting the aspect ratio is the desired behavior for nested Excalidraw drawings since you might have changed the source image and want it to still display with the correct aspect ratio, however for other image files, the behavior is not desired. [#1698](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1698)
|
||||
- The command palette action "Set selected image element size to 100% of original" now works even on freshly pasted images, not just after saving the drawing. ([#1695](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1698))
|
||||
- If a text element has an element link (CTRL/CMD+K), but the link was not reflected in the Element Text, then CTRL/CMD+clicking the text element did not navigate to the link, only clicking the link indicator did. Now you can also CTRL/CMD click anywhere on the text element and it will navigate. Note, however, that links in the text element text take precedence over element links.
|
||||
`,
|
||||
"2.1.1":`
|
||||
## Fixed
|
||||
- Printing a markdown page that has an Excalidraw drawing on the back side, resulted in an empty PDF. This is now resolved.
|
||||
|
||||
## New
|
||||
- Reduce the visual clutter by fading out the Excalidraw markup in markdown view mode. This feature needs to be enabled in plugin settings. You'll find the setting under ${String.fromCharCode(96)}Miscellaneous features${String.fromCharCode(96)}. Look for ${String.fromCharCode(96)}Fade out Excalidraw markup${String.fromCharCode(96)}. Depending on the location of the markdown comment ${String.fromCharCode(96)}%%${String.fromCharCode(96)}, if the comment starts before ${String.fromCharCode(96)}# Text Elements${String.fromCharCode(96)} then the fading will start from ${String.fromCharCode(96)}# Text Elements${String.fromCharCode(96)}, if the comment is before ${String.fromCharCode(96)}# Drawing${String.fromCharCode(96)} then the fading will only start with "drawing". If you delete the opening ${String.fromCharCode(96)}%%${String.fromCharCode(96)} the markup will be visible. Note, that if you place the comment before ${String.fromCharCode(96)}#Text Elements${String.fromCharCode(96)}, you will not be able to reference blocks in the ${String.fromCharCode(96)}# Text Elements${String.fromCharCode(96)} section, because Obsidian does not index blocks within comment blocks. Image references are not effective, they will work.
|
||||
|
||||
<img src="https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/bb96cdb4-8c5f-4dc5-ad39-7fccee6d5cac" referrerpolicy="no-referrer" style="width: 150px; margin: 0 auto;">
|
||||
<img src="https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/e627fdb7-6820-4d7d-97f9-a030016be9aa" referrerpolicy="no-referrer" style="width: 100%; margin: 0 auto;">
|
||||
`,
|
||||
"2.1.0":`
|
||||
Bumping the version to 2.1.0 due to minor file format changes that aren't backward compatible. Essentially, 2.0.26 is already not backward compatible, but I forgot to update the version number.
|
||||
|
||||
If you haven't watched the [walkthrough video](https://youtu.be/tHUcD4rWIuY) for 2.0.26, I recommend you do so.
|
||||
|
||||
## New
|
||||
- Settings under ${String.fromCharCode(96)}Excalidraw Appearance and Behavior${String.fromCharCode(96)}
|
||||
- Configure visibility of the crosshair cursor when using the pen tool. [#1673](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1673)
|
||||
- Set the time delay for long press to open drawings from markdown under "Link Click and Modifier Keys".
|
||||
|
||||
##
|
||||
`,
|
||||
"2.0.26":`
|
||||
<div class="excalidraw-videoWrapper"><div>
|
||||
<iframe src="https://www.youtube.com/embed/tHUcD4rWIuY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div></div>
|
||||
|
||||
## New
|
||||
- Minor updates from [Excalidraw.com](https://excalidraw.com). The key change is text measurements that should result in consistent text sizing between desktop and mobile devices.
|
||||
- Now you can embed the markdown section of an Excalidraw note to your drawing. Simply select ${String.fromCharCode(96)}Insert ANY file${String.fromCharCode(96)}, choose the drawing, and select the relevant heading section to embed.
|
||||
- This also works with "back-of-the-drawing" markdown sections. Use the context menu ${String.fromCharCode(96)}Add back-of-note Card${String.fromCharCode(96)}. The action is also available on the Command Palette and in the Excalidraw-Obsidian Tools Panel.
|
||||
- Editing an embedded markdown note is now easier. Just press Enter when the element is selected. [#1650](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1650)
|
||||
- The crosshair cursor is now hidden when the freedraw tool is active and using a pen. [#1659](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1659)
|
||||
- ${String.fromCharCode(96)}Convert markdown note to Excalidraw Drawing${String.fromCharCode(96)} now converts an existing markdown note (not just empty notes) into a drawing. The original markdown note will be on the "back side of the drawing".
|
||||
- Introducing ${String.fromCharCode(96)}Annotate image in Excalidraw${String.fromCharCode(96)}, which works very similarly to ${String.fromCharCode(96)}Crop and mask image${String.fromCharCode(96)}. You can replace an image in a markdown note or on the Obsidian Canvas with an Excalidraw drawing containing that image. You will be able to annotate the image in Excalidraw.
|
||||
- Now you can reference frames in images embedded in markdown and canvas with frame names e.g. ${String.fromCharCode(96)}![[drawing#^frame=Frame 01]]${String.fromCharCode(96)}
|
||||
- Excalidraw file format change:
|
||||
- New frontmatter switch ${String.fromCharCode(96)}excalidraw-open-md${String.fromCharCode(96)}: If set to true, the file by default will open as a markdown file. You can switch to Excalidraw View Mode via the command palette action or by right-clicking the tab.
|
||||
- Easter Egg: If you add a comment in front of ${String.fromCharCode(96)}# Text Elements${String.fromCharCode(96)}, then the entire Excalidraw data: markdown and JSON will be commented out, thus invisible when exporting to the web. If you remove the comment from before ${String.fromCharCode(96)}# Text Elements${String.fromCharCode(96)}, then only the JSON will be commented out.
|
||||
|
||||
Before:
|
||||
${String.fromCharCode(96,96,96)}markdown
|
||||
#1657
|
||||
%%
|
||||
# Text Elements
|
||||
...
|
||||
# Drawing
|
||||
${String.fromCharCode(96,96,96)}
|
||||
|
||||
After:
|
||||
${String.fromCharCode(96,96,96)}markdown
|
||||
# Text Elements
|
||||
....
|
||||
%%
|
||||
# Drawing
|
||||
${String.fromCharCode(96,96,96)}
|
||||
`,
|
||||
"2.0.25":`
|
||||
# New - a small change that opens big opportunities
|
||||
- You can now set a folder as the Excalidraw Template in settings (See under Basic). If a folder is provided, Excalidraw will treat drawings in that folder as templates and will prompt you to select the template to use for new drawings.
|
||||
- I updated the <a href="https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Deconstruct%20selected%20elements%20into%20new%20drawing.md">Deconstruct Selected Elements into new Drawing</a> script to accommodate the new template setting.
|
||||
`,
|
||||
"2.0.24":`
|
||||
Quality of Life Fixes!
|
||||
|
||||
## Fixed
|
||||
- Text editing issue on mobile devices with an on-screen keyboard is now fixed 🥳. Previously, Excalidraw's UI fell apart when the keyboard was activated, and often even after you stopped editing, the canvas positioning was off. I hope to have solved the issue (we'll see after your testing and feedback!). This is one of those cases that seems insignificant but took enormous effort. It took me 2.5 full days of net time to figure out the root cause and the solution (this is not an exaggeration).
|
||||
- Tool buttons did not get selected on the first click.
|
||||
- Images flicker on Forced Save.
|
||||
- Hover preview fixes:
|
||||
- ${String.fromCharCode(96)}area=${String.fromCharCode(96)}, ${String.fromCharCode(96)}group=${String.fromCharCode(96)}, ${String.fromCharCode(96)}frame=${String.fromCharCode(96)} references now display the part of the image as expected in hover preview (showed an empty preview until now).
|
||||
- Block and section references to notes on the "back side of the drawing" now correctly show up in hover preview (showed an empty preview until now).
|
||||
|
||||
## New
|
||||
- Default height setting in Plugin Settings. Thanks @leoccyao! [#1612](https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/1612)
|
||||
`,
|
||||
"2.0.23":`
|
||||
## New
|
||||
- Additional arrowheads (Circle, Circle Outline, Diamond, Diamond Outline, Triangle Outline) are now available via element properties.
|
||||
- Setting under "Links and Transclusions" to show/hide second-order links
|
||||
|
||||
## Fixed
|
||||
- some styling issues with dynamic styles (e.g.: text color of context menu)
|
||||
|
||||
## New in ExcalidrawAutomate
|
||||
- Excalidraw Publish Support: New hook to modify the link in the exported SVGs. This is useful when you want to export SVGs to your website. If set, this callback is triggered whenever a drawing is exported to SVG. The string returned by the hook will replace the link in the exported SVG. The hook is only executed if the link is to a file internal to Obsidian. [1605](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1605)
|
||||
${String.fromCharCode(96,96,96)}js
|
||||
onUpdateElementLinkForExportHook: (data: {
|
||||
* originalLink: string,
|
||||
* obsidianLink: string,
|
||||
* linkedFile: TFile | null,
|
||||
* hostFile: TFile,
|
||||
* }) => string = null;
|
||||
${String.fromCharCode(96,96,96)}
|
||||
`,
|
||||
"2.0.22":`
|
||||
## Fixed
|
||||
- BUG: Unable to load obsidian excalidraw plugin on ipad 15.x or older [#1525](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1525)
|
||||
- BUG: ea.help does not display help if only function signature is available [#1601](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1601)
|
||||
`,
|
||||
"2.0.21":`
|
||||
## New/changed
|
||||
**"Focus on Existing Tab"**
|
||||
- New Setting: Disabled by default.
|
||||
- Prevents multiple instances of the same drawing from opening when clicking on links within Excalidraw.
|
||||
- Overrides the "Reuse Adjacent Pane" option when the file is already open.
|
||||
- Accessible under "Links, Transclusions, and TODOs" in plugin settings.
|
||||
|
||||
**Enhanced Context Menu Functions for Text Containers**
|
||||
- Two new context menu functions added for containers with a text element:
|
||||
- Right-click to select the text element only, allowing independent color changes from the container.
|
||||
- Remove orphaned element links when the text element has a link but no longer includes a link in the text.
|
||||
|
||||
**Improved Laser Pointer Activation**
|
||||
- Laser pointer activation on double tap in view mode removed due to interference with link navigation and other features.
|
||||
- When the drawing is in "view" mode, laser pointer activation now available via long-press/right-click context menu.
|
||||
- Alternatively, activate the laser pointer with "k" if you have a keyboard.
|
||||
|
||||
## Fixed
|
||||
- **Older iOS and Android webview support**: Rebuilt all packages and dependencies with Node 18, hoping to address (sorry I can't reproduce/test these issues myself) compatibility issues with older iPad OS versions, up to 15.7. [#1525](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1525), and Android [1598](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1598)
|
||||
- **Double-click navigation**: Fixed the issue where double-clicking an embedded image did not navigate to the link in view mode.
|
||||
- **ExcaliBrain new file creation**: Resolved the issue with new file creation from ExcaliBrain. [#201](https://github.com/zsviczian/excalibrain/issues/201)
|
||||
- **Canvas immersive style**: Removed Canvas immersive embedding style support from the Excalidraw stylesheet to address performance issues experienced by some users with various Obsidian themes. If you require this feature, you can add a CSS snippet with the provided code.
|
||||
|
||||
${String.fromCharCode(96,96,96)}css
|
||||
.canvas-node:not(.is-editing):has(.excalidraw-canvas-immersive) {
|
||||
::-webkit-scrollbar,
|
||||
::-webkit-scrollbar-horizontal {
|
||||
display: none;
|
||||
}
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.canvas-node:not(.is-editing) .canvas-node-container:has(.excalidraw-canvas-immersive) {
|
||||
border: unset;
|
||||
box-shadow: unset;
|
||||
}
|
||||
${String.fromCharCode(96,96,96)}
|
||||
`,
|
||||
"2.0.20":`
|
||||
## Fixed in ExcalidrawAutomate
|
||||
- Regression: ${String.fromCharCode(96)}ea.getMaximumGroups(elements)${String.fromCharCode(96)} stopped working after the 2.0.19 update. [#1576](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1576)
|
||||
`,
|
||||
"2.0.19":`
|
||||
<div class="excalidraw-videoWrapper"><div>
|
||||
<iframe src="https://www.youtube.com/embed/4wp6vLiIdGM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div></div>
|
||||
|
||||
## Fixed
|
||||
- When updating Excalidraw, some open drawings weren't automatically reopening. I hope I got this fixed (note this change will only have an effect when you receive the update after this).
|
||||
- In dark mode, the frame header is challenging to see when modified [#1568](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1568).
|
||||
|
||||
## New
|
||||
- Crop PDF pages:
|
||||
- Available in Excalidraw, Markdown Notes, and on the Canvas.
|
||||
- Crop the active page from the embedded PDF viewer and insert the cropped image into the current view, both in Excalidraw and on Canvas.
|
||||
- New Command Palette Action: "Insert active PDF page as image." This action is functional in Excalidraw. If an embedded Obsidian-PDF-viewer is present, executing this command will insert the active page as an image into the Excalidraw scene.
|
||||
- Two new settings introduced:
|
||||
- "Basic" section allows setting the folder for crop files.
|
||||
- "Saving/filename" section enables setting the prefix for crop files.
|
||||
- PDF import now defaults to importing all pages.
|
||||
- Rounded corners now available for images.
|
||||
- Second-order links now encompass forward links from embedded Excalidraw Files.
|
||||
- Clicking a cropped file in a markdown note or on Canvas will prompt to open the original file, not just the cropper.
|
||||
`,
|
||||
"2.0.18":`
|
||||
## New
|
||||
|
||||
<div style="text-align: center;">
|
||||
<a data-tooltip-position="top" aria-label="https://youtube.com/shorts/ST6h4uaXmnY" rel="noopener" class="external-link" href="https://youtube.com/shorts/ST6h4uaXmnY" target="_blank">
|
||||
<img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/vintage-mask.png" referrerpolicy="no-referrer" style="width: 150px; margin: 0 auto;">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
- [Crop Vintage Mask Script](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Crop%20Vintage%20Mask.md) adds rounded corner mask to cropped images. Install it from the script library.
|
||||
- Advanced Setting: Modify image zoom memory limit for sharper zoom. See under "Non-Excalidraw.com Supported Features" in settings.
|
||||
- Laser Pointer will not activate on double-click in ExcaliBrain
|
||||
|
||||
## Fixed
|
||||
- Resolved cropping issue with rotated images.
|
||||
|
||||
## New in ExcalidrawAutomate
|
||||
- You can now specify elementId to add functions: addLine, addArrow, addRect, etc.
|
||||
- ea.help() now provides help on Script Engine utils functions as well
|
||||
- ea.isExcalidrawMask(file?:TFile) will return true if the currently open view or the supplied file is an Excalidraw Mask file.
|
||||
`,
|
||||
"2.0.17":`
|
||||
## Fixed
|
||||
- Image cropping now supports dark mode
|
||||
- Image cropping/carve out was not working reliably in some cases [#1546](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1546)
|
||||
- Masking a mirrored image resulted in an off-positioned mask
|
||||
|
||||
## New
|
||||
- Context menu action to convert SVG to Excalidraw strokes
|
||||
- Updated Chinese translation (Thank you @tswwe)
|
||||
`,
|
||||
"2.0.16":`
|
||||
## Fixed
|
||||
- Image cropping did not work consistently with large image files on lower-powered devices [#1538](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1538).
|
||||
- Mermaid editor was not working when Excalidraw was open in an Obsidian popout window [#1503](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1503)
|
||||
`,
|
||||
"2.0.15":`
|
||||
<div class="excalidraw-videoWrapper"><div>
|
||||
<iframe src="https://www.youtube.com/embed/uHFd0XoHRxE" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div></div>
|
||||
|
||||
## New
|
||||
- Crop and Mask Images in Excalidraw, Canvas, and Markdown. (Inspired by @bonecast [#4566](https://github.com/excalidraw/excalidraw/issues/4566))
|
||||
- Draw metadata around images but hide it on the export.
|
||||
|
||||
## Fixed
|
||||
- Freedraw closed circles (2nd attempt)
|
||||
- Interactive Markdown embeddable border-color (setting did not have an effect)
|
||||
`,
|
||||
"2.0.14":`
|
||||
## New
|
||||
- Stylus button now activates the eraser function. Note: This feature is supported for styluses that comply with industry-standard button events. Unfortunately, Samsung SPEN and Apple Pencil do not support this functionality.
|
||||
@@ -781,7 +1290,7 @@ ${String.fromCharCode(96,96,96)}`,
|
||||
## New
|
||||
- New scripts by @threethan:
|
||||
- [Auto Draw for Pen](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Auto%20Draw%20for%20Pen.md): Automatically switches between the select and draw tools, based on whether a pen is being used. Supports most pens including Apple Pencil.
|
||||
- [Hardware Eraser Support](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Hardware%20Eraser%20Support.md): Adds support for pen inversion, a.k.a. the hardware eraser on the back of your pen. Supports Windows based styluses. Does not suppoprt Apple Pencil or S-Pen.
|
||||
- [Hardware Eraser Support](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Hardware%20Eraser%20Support.md): Adds support for pen inversion, a.k.a. the hardware eraser on the back of your pen. Supports Windows based styluses. Does not support Apple Pencil or S-Pen.
|
||||
- Added separate buttons to support copying link, area or group references to objects on the drawing. [#1063](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1063). See [this video](https://youtu.be/yZQoJg2RCKI) for more details on how this works.
|
||||
- Hover preview will no longer trigger for image files (.png, .svg, .jpg, .gif, .webp, .bmp, .ico, .excalidraw)
|
||||
- Minor updates to the [Slideshow](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Slideshow.md) script. You can download the updated script from the Excalidraw script library. The slideshow will now correctly run also when initiated in a popout window. When the drawing is in a popout window, the slideshow will not be full screen, but will only occupy the popout window. If you run the slideshow from the main Obsidian workspace, it will be displayed in full-screen mode.
|
||||
|
||||
@@ -14,6 +14,13 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
|
||||
private action: openDialogAction;
|
||||
private onNewPane: boolean;
|
||||
|
||||
destroy() {
|
||||
this.app = null;
|
||||
this.plugin = null;
|
||||
this.action = null;
|
||||
this.inputEl.onkeyup = null;
|
||||
}
|
||||
|
||||
constructor(app: App, plugin: ExcalidrawPlugin) {
|
||||
super(app);
|
||||
this.app = app;
|
||||
|
||||
@@ -393,7 +393,7 @@ export class PenSettingsModal extends Modal {
|
||||
let spSetting: Setting;
|
||||
|
||||
new Setting(ce)
|
||||
.setName("Pressure sensitve pen?")
|
||||
.setName("Pressure sensitive pen?")
|
||||
.setDesc(fragWithHTML(`<b>toggle on</b>: pressure sensitive<br><b>toggle off</b>: constant pressure`))
|
||||
.addToggle(toggle =>
|
||||
toggle
|
||||
|
||||
@@ -8,17 +8,21 @@ import {
|
||||
TFile,
|
||||
Notice,
|
||||
TextAreaComponent,
|
||||
TFolder,
|
||||
} from "obsidian";
|
||||
import ExcalidrawView from "../ExcalidrawView";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { escapeRegExp, sleep } from "../utils/Utils";
|
||||
import { getLeaf } from "../utils/ObsidianUtils";
|
||||
import { escapeRegExp, getLinkParts, sleep } from "../utils/Utils";
|
||||
import { getLeaf, openLeaf } from "../utils/ObsidianUtils";
|
||||
import { checkAndCreateFolder, splitFolderAndFilename } from "src/utils/FileUtils";
|
||||
import { KeyEvent, isWinCTRLorMacCMD } from "src/utils/ModifierkeyHelper";
|
||||
import { t } from "src/lang/helpers";
|
||||
import { ExcalidrawElement, getEA } from "src";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
import { MAX_IMAGE_SIZE } from "src/constants/constants";
|
||||
import { MAX_IMAGE_SIZE, REG_LINKINDEX_INVALIDCHARS } from "src/constants/constants";
|
||||
import { REGEX_LINK } from "src/ExcalidrawData";
|
||||
import { ScriptEngine } from "src/Scripts";
|
||||
import { openExternalLink, openTagSearch, parseObsidianLink } from "src/utils/ExcalidrawViewUtils";
|
||||
|
||||
export type ButtonDefinition = { caption: string; tooltip?:string; action: Function };
|
||||
|
||||
@@ -496,7 +500,7 @@ export class NewFileActions extends Modal {
|
||||
this.view = view;
|
||||
this.openNewFile = openNewFile;
|
||||
this.sourceElement = sourceElement;
|
||||
if(!parentFile) this.parentFile = view.file;
|
||||
this.parentFile = parentFile ?? view.file;
|
||||
this.waitForClose = new Promise<TFile|null>((resolve, reject) => {
|
||||
this.resolvePromise = resolve;
|
||||
this.rejectPromise = reject;
|
||||
@@ -512,13 +516,22 @@ export class NewFileActions extends Modal {
|
||||
if (!file || !this.openNewFile) {
|
||||
return;
|
||||
}
|
||||
const leaf = getLeaf(this.plugin,this.view.leaf,this.keys)
|
||||
leaf.openFile(file, {active:true});
|
||||
openLeaf({
|
||||
plugin: this.plugin,
|
||||
fnGetLeaf: () => getLeaf(this.plugin,this.view.leaf,this.keys),
|
||||
file,
|
||||
openState: { active: true },
|
||||
});
|
||||
}
|
||||
|
||||
onClose() {
|
||||
super.onClose();
|
||||
this.resolvePromise(this.newFile);
|
||||
this.app = null;
|
||||
this.plugin = null;
|
||||
this.view = null;
|
||||
this.parentFile = null;
|
||||
this.sourceElement = null;
|
||||
}
|
||||
|
||||
createForm(): void {
|
||||
@@ -580,7 +593,8 @@ export class NewFileActions extends Modal {
|
||||
ea.copyViewElementsToEAforEditing([this.sourceElement]);
|
||||
ea.getElement(this.sourceElement.id).isDeleted = true;
|
||||
ea.addEmbeddable(this.sourceElement.x, this.sourceElement.y,MAX_IMAGE_SIZE, MAX_IMAGE_SIZE, undefined,f);
|
||||
ea.addElementsToView();
|
||||
await ea.addElementsToView();
|
||||
ea.destroy();
|
||||
}
|
||||
this.close();
|
||||
};
|
||||
@@ -693,3 +707,67 @@ export class ConfirmationPrompt extends Modal {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function linkPrompt (
|
||||
linkText:string,
|
||||
app: App,
|
||||
view?: ExcalidrawView,
|
||||
message: string = "Select link to open",
|
||||
):Promise<[file:TFile, linkText:string, subpath: string]> {
|
||||
const partsArray = REGEX_LINK.getResList(linkText);
|
||||
let subpath: string = null;
|
||||
let file: TFile = null;
|
||||
let parts = partsArray[0];
|
||||
if (partsArray.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)),
|
||||
message,
|
||||
);
|
||||
if(!parts) return;
|
||||
}
|
||||
|
||||
if(!parts) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!parts.value) {
|
||||
openTagSearch(linkText, 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);
|
||||
subpath = `#${linkParts.isBlockRef ? "^" : ""}${linkParts.ref}`;
|
||||
linkText = linkParts.path;
|
||||
}
|
||||
if (linkText.match(REG_LINKINDEX_INVALIDCHARS)) {
|
||||
new Notice(t("FILENAME_INVALID_CHARS"), 4000);
|
||||
return;
|
||||
}
|
||||
file = app.metadataCache.getFirstLinkpathDest(
|
||||
linkText,
|
||||
view ? view.file.path : "",
|
||||
);
|
||||
return [file, linkText, subpath];
|
||||
}
|
||||
|
||||
export const templatePromt = async (files: TFile[], app: App): Promise<TFile> => {
|
||||
if(files.length === 1) return files[0];
|
||||
return ((await linkPrompt(
|
||||
files.map(f=>`[[${f.path}|${f.name}]]`).join(" "),
|
||||
app,
|
||||
undefined,
|
||||
t("PROMPT_SELECT_TEMPLATE")
|
||||
))??[null, null, null])[0];
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { MarkdownRenderer, Modal, Notice, request } from "obsidian";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { errorlog, escapeRegExp, log } from "../utils/Utils";
|
||||
import { errorlog, escapeRegExp } from "../utils/Utils";
|
||||
import { log } from "src/utils/DebugHelper";
|
||||
|
||||
const URL =
|
||||
"https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/index-new.md";
|
||||
|
||||
67
src/dialogs/SelectCard.ts
Normal file
67
src/dialogs/SelectCard.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { App, FuzzySuggestModal, Notice, TFile } from "obsidian";
|
||||
import { t } from "../lang/helpers";
|
||||
import ExcalidrawView from "src/ExcalidrawView";
|
||||
import { getEA } from "src";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
import { MD_EX_SECTIONS } from "src/constants/constants";
|
||||
import { addBackOfTheNoteCard } from "src/utils/ExcalidrawViewUtils";
|
||||
|
||||
export class SelectCard extends FuzzySuggestModal<string> {
|
||||
|
||||
constructor(
|
||||
public app: App,
|
||||
private view: ExcalidrawView,
|
||||
private sections: string[]
|
||||
) {
|
||||
super(app);
|
||||
this.limit = 20;
|
||||
this.setInstructions([
|
||||
{
|
||||
command: t("TYPE_SECTION"),
|
||||
purpose: "",
|
||||
},
|
||||
]);
|
||||
|
||||
this.inputEl.onkeyup = (e) => {
|
||||
if (e.key == "Enter") {
|
||||
if (this.containerEl.innerText.includes(t("EMPTY_SECTION_MESSAGE"))) {
|
||||
const item = this.inputEl.value;
|
||||
if(item === "" || MD_EX_SECTIONS.includes(item)) {
|
||||
new Notice(t("INVALID_SECTION_NAME"));
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
addBackOfTheNoteCard(this.view, item);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getItems(): string[] {
|
||||
return this.sections;
|
||||
}
|
||||
|
||||
getItemText(item: string): string {
|
||||
return item;
|
||||
}
|
||||
|
||||
onChooseItem(item: string): void {
|
||||
const ea = getEA(this.view) as ExcalidrawAutomate;
|
||||
const id = ea.addEmbeddable(
|
||||
0,0,400,500,
|
||||
`[[${this.view.file.path}#${item}]]`
|
||||
);
|
||||
(async () => {
|
||||
await ea.addElementsToView(true, false, true);
|
||||
ea.selectElementsInView([id]);
|
||||
ea.destroy();
|
||||
})();
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
this.emptyStateText = t("EMPTY_SECTION_MESSAGE");
|
||||
this.setPlaceholder(t("SELECT_SECTION_OR_TYPE_NEW"));
|
||||
this.open();
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
@@ -16,6 +19,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
desc: "Utility function that provides help about ExcalidrawAutomate functions and properties. I recommend calling this function from Developer Console to print out help to the console.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field:"isExcalidrawMaskFile",
|
||||
code:"isExcalidrawMaskFile(file?:TFile): boolean;",
|
||||
desc:"Returns true if the file is an Excalidraw Mask file. If file is not provided, the function will use ea.targetView.file",
|
||||
after:"",
|
||||
},
|
||||
{
|
||||
field: "plugin",
|
||||
code: null,
|
||||
@@ -180,8 +189,24 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
},
|
||||
{
|
||||
field: "create",
|
||||
code: 'async create(params?: {filename?: string, foldername?: string, templatePath?: string, onNewPane?: boolean, silent?: boolean, frontmatterKeys?: { "excalidraw-plugin"?: "raw" | "parsed", "excalidraw-link-prefix"?: string, "excalidraw-link-brackets"?: boolean, "excalidraw-url-prefix"?: string,},}): Promise<string>;',
|
||||
desc: "Create a drawing and save it to filename.\nIf filename is null: default filename as defined in Excalidraw settings.\nIf folder is null: default folder as defined in Excalidraw settings\nReturns the path to the created file",
|
||||
code: 'async create(params?: {filename?: string, foldername?: string, templatePath?: string, onNewPane?: boolean, silent?: boolean, frontmatterKeys?: {},}): Promise<string>;',
|
||||
desc: "Create a drawing and save it to filename.\nIf filename is null: default filename as defined in Excalidraw settings.\nIf folder is null: default folder as defined in Excalidraw settings\nReturns the path to the created file.\n" +
|
||||
'frontmatterKeys: {\n' +
|
||||
' "excalidraw-plugin"?: "raw" | "parsed";\n' +
|
||||
' "excalidraw-link-prefix"?: string;\n' +
|
||||
' "excalidraw-link-brackets"?: boolean;\n' +
|
||||
' "excalidraw-url-prefix"?: string;\n' +
|
||||
' "excalidraw-export-transparent"?: boolean;\n' +
|
||||
' "excalidraw-export-dark"?: boolean;\n' +
|
||||
' "excalidraw-export-padding"?: number;\n' +
|
||||
' "excalidraw-export-pngscale"?: number;\n' +
|
||||
' "excalidraw-export-embed-scene"?: boolean;\n' +
|
||||
' "excalidraw-default-mode"?: "view" | "zen";\n' +
|
||||
' "excalidraw-onload-script"?: string;\n' +
|
||||
' "excalidraw-linkbutton-opacity"?: number;\n' +
|
||||
' "excalidraw-autoexport"?: boolean;\n' +
|
||||
' "excalidraw-mask"?: boolean;\n' +
|
||||
' "cssclasses"?: string;\n}',
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
@@ -210,25 +235,25 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
},
|
||||
{
|
||||
field: "addRect",
|
||||
code: "addRect(topX: number, topY: number, width: number, height: number): string;",
|
||||
code: "addRect(topX: number, topY: number, width: number, height: number, id?:string): string;",
|
||||
desc: null,
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "addDiamond",
|
||||
code: "addDiamond(topX: number, topY: number, width: number, height: number): string;",
|
||||
code: "addDiamond(topX: number, topY: number, width: number, height: number, id?:string): string;",
|
||||
desc: null,
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "addEllipse",
|
||||
code: "addEllipse(topX: number, topY: number, width: number, height: number): string;",
|
||||
code: "addEllipse(topX: number, topY: number, width: number, height: number, id?:string): string;",
|
||||
desc: null,
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "addBlob",
|
||||
code: "addBlob(topX: number, topY: number, width: number, height: number): string;",
|
||||
code: "addBlob(topX: number, topY: number, width: number, height: number, id?: string): string;",
|
||||
desc: null,
|
||||
after: "",
|
||||
},
|
||||
@@ -240,32 +265,35 @@ 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: "",
|
||||
},
|
||||
{
|
||||
field: "addLine",
|
||||
code: "addLine(points: [[x: number, y: number]]): string;",
|
||||
code: "addLine(points: [[x: number, y: number]], id?:string): string;",
|
||||
desc: null,
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "addArrow",
|
||||
code: "addArrow(points: [[x: number, y: number]], formatting?: { startArrowHead?: string; endArrowHead?: string; startObjectId?: string; endObjectId?: string;},): string;",
|
||||
code: "addArrow(points: [[x: number, y: number]], formatting?: { startArrowHead?: string; endArrowHead?: string; startObjectId?: string; endObjectId?: string;}, id?:string): string;",
|
||||
desc: `valid values for startArrowHead and endArrowHead are: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null`,
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "addImage",
|
||||
code: "async addImage(topX: number, topY: number, imageFile: TFile, scale?: boolean, anchor?: boolean): Promise<string>;",
|
||||
desc: "set scale to false if you want to embed the image at 100% of its original size. Default is true which will insert a scaled image. anchor will only be evaluated if scale is false. anchor true will add |100% to the end of the filename, resulting in an image that will always pop back to 100% when the source file is updated or when the Excalidraw file is reopened. ",
|
||||
code: "async addImage(topX: number, topY: number, imageFile: TFile|string, scale?: boolean, anchor?: boolean): Promise<string>;",
|
||||
desc: "imageFile may be a TFile or a string that contains a hyperlink. imageFile may also be an obsidian filepath including a reference eg.: 'path/my.pdf#page=3'\nSet scale to false if you want to embed the image at 100% of its original size. Default is true which will insert a scaled image.\nanchor will only be evaluated if scale is false. anchor true will add |100% to the end of the filename, resulting in an image that will always pop back to 100% when the source file is updated or when the Excalidraw file is reopened.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
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: "",
|
||||
},
|
||||
{
|
||||
@@ -343,6 +371,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;",
|
||||
@@ -445,6 +481,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
desc: "Gets all the elements from elements[] that share one or more groupIds with element.",
|
||||
after: ""
|
||||
},
|
||||
{
|
||||
field: "getElementsInFrame",
|
||||
code: " getElementsInFrame(frameElement: ExcalidrawElement,elements: ExcalidrawElement[],shouldIncludeFrame: boolean = false,): ExcalidrawElement[];",
|
||||
desc: "Gets all the elements from elements[] that are inside the frameElement. If shouldIncludeFrame is true, the frameElement will also be included in the result.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "activeScript",
|
||||
code: "activeScript: string;",
|
||||
@@ -475,6 +517,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
desc: "Measures text size based on current style settings",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "getOriginalImageSize",
|
||||
code: "async getOriginalImageSize(imageElement: ExcalidrawImageElement): Promise<{width: number; height: number}>",
|
||||
desc: "Returns the size of the image element at 100% (i.e. the original size). This is an async function, you need to await the result.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "verifyMinimumPluginVersion",
|
||||
code: "verifyMinimumPluginVersion(requiredVersion: string): boolean;",
|
||||
@@ -535,12 +583,35 @@ 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",
|
||||
desc: `Access functions and objects available on the ${hyperlink("https://github.com/obsidianmd/obsidian-api/blob/master/obsidian.d.ts","Obsidian Module")}`,
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "getListOfTemplateFiles",
|
||||
code: "getListOfTemplateFiles(): TFile[] | null",
|
||||
desc: "Returns a list of files in the template folder. " +
|
||||
"If the Excalidraw Template is set as a single file, it returns a single element in the list. " +
|
||||
"If no template is set, it returns null.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "getEmbeddedImagesFiletree",
|
||||
code: "getEmbeddedImagesFiletree(excalidrawFile?: TFile): TFile[]",
|
||||
desc: "Retruns the embedded images in the scene recursively. If excalidrawFile is not provided, " +
|
||||
"the function will use ea.targetView.file",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "getAttachmentFilepath",
|
||||
code: "async getAttachmentFilepath(filename: string): Promise<string>",
|
||||
@@ -574,7 +645,7 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
{
|
||||
field: "getleaf",
|
||||
code: "getLeaf(origo: WorkspaceLeaf, targetPane?: PaneTarget): WorkspaceLeaf;",
|
||||
desc: "Generates a new Obsidian Leaf following Excalidraw plugin settings such as open in Main Workspace or not, open in adjacent pane if avaialble, etc.<br>" +
|
||||
desc: "Generates a new Obsidian Leaf following Excalidraw plugin settings such as open in Main Workspace or not, open in adjacent pane if available, etc.<br>" +
|
||||
"@param origo: the currently active leaf, the origin of the new leaf<br>" +
|
||||
'@param targetPane: <code>type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";',
|
||||
after: "",
|
||||
@@ -622,7 +693,7 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
},
|
||||
{
|
||||
field: "postOpenAI",
|
||||
code: "async postOpenAI(requst: AIRequest): Promise<RequestUrlResponse>",
|
||||
code: "async postOpenAI(request: AIRequest): Promise<RequestUrlResponse>",
|
||||
desc:
|
||||
"This asynchronous function should be awaited. It posts the supplied request to the OpenAI API and returns the response.<br>" +
|
||||
"The response is a dictionary with the following keys:<br><code>{image, text, instruction, systemPrompt, responseType}</code><br>"+
|
||||
@@ -649,8 +720,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: "",
|
||||
},
|
||||
{
|
||||
@@ -792,6 +864,18 @@ export const FRONTMATTER_KEYS_INFO: SuggesterInfo[] = [
|
||||
desc: "If this key is present it will override the default excalidraw embed and export setting. This only affects export to PNG. Specify the export scale for the image. The typical range is between 0.5 and 5, but you can experiment with other values as well.",
|
||||
after: ": 1",
|
||||
},
|
||||
{
|
||||
field: "excalidraw-export-embed-scene",
|
||||
code: null,
|
||||
desc: "If this key is present it will override the default excalidraw embed and export setting.",
|
||||
after: ": false",
|
||||
},
|
||||
{
|
||||
field: "open-md",
|
||||
code: null,
|
||||
desc: "If this key is present the file will be opened as a markdown file in the editor",
|
||||
after: ": true",
|
||||
},
|
||||
{
|
||||
field: "autoexport",
|
||||
code: null,
|
||||
@@ -799,11 +883,15 @@ export const FRONTMATTER_KEYS_INFO: SuggesterInfo[] = [
|
||||
after: ": png",
|
||||
},
|
||||
{
|
||||
field: "iframe-theme",
|
||||
field: "embeddable-theme",
|
||||
code: null,
|
||||
desc: "Override iFrame theme plugin-settings for this file. 'match' will match the Excalidraw theme, 'default' will match the obsidian theme. Valid values are\ndark\nlight\nauto\ndefault",
|
||||
desc: "Override embeddable's theme plugin-settings for this file. 'auto' will match the Excalidraw theme, 'default' will match the Obsidian theme. Valid values are\ndark\nlight\nauto\ndefault",
|
||||
after: ": auto",
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
field: "mask",
|
||||
code: null,
|
||||
desc: "If this key is present the drawing will be handled as a mask to crop an image.",
|
||||
after: ": true",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -3,7 +3,7 @@ import ExcalidrawView from "../ExcalidrawView";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { Modal, Setting, TextComponent } from "obsidian";
|
||||
import { FileSuggestionModal } from "./FolderSuggester";
|
||||
import { IMAGE_TYPES, sceneCoordsToViewportCoords, viewportCoordsToSceneCoords, MAX_IMAGE_SIZE, ANIMATED_IMAGE_TYPES } from "src/constants/constants";
|
||||
import { IMAGE_TYPES, sceneCoordsToViewportCoords, viewportCoordsToSceneCoords, MAX_IMAGE_SIZE, ANIMATED_IMAGE_TYPES, MD_EX_SECTIONS } from "src/constants/constants";
|
||||
import { insertEmbeddableToView, insertImageToView } from "src/utils/ExcalidrawViewUtils";
|
||||
import { getEA } from "src";
|
||||
import { InsertPDFModal } from "./InsertPDFModal";
|
||||
@@ -19,7 +19,7 @@ export class UniversalInsertFileModal extends Modal {
|
||||
private plugin: ExcalidrawPlugin,
|
||||
private view: ExcalidrawView,
|
||||
) {
|
||||
super(app);
|
||||
super(plugin.app);
|
||||
const appState = (view.excalidrawAPI as ExcalidrawImperativeAPI).getAppState();
|
||||
const containerRect = view.containerEl.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
|
||||
@@ -78,6 +78,7 @@ export class UniversalInsertFileModal extends Modal {
|
||||
|
||||
const updateForm = async () => {
|
||||
const ea = this.plugin.ea;
|
||||
const isSelf = file === this.view.file;
|
||||
const isMarkdown = file && file.extension === "md" && !ea.isExcalidrawFile(file);
|
||||
const isImage = file && (IMAGE_TYPES.contains(file.extension) || ea.isExcalidrawFile(file));
|
||||
const isAnimatedImage = file && ANIMATED_IMAGE_TYPES.contains(file.extension);
|
||||
@@ -85,39 +86,43 @@ export class UniversalInsertFileModal extends Modal {
|
||||
const isPDF = file && file.extension === "pdf";
|
||||
const isExcalidraw = file && ea.isExcalidrawFile(file);
|
||||
|
||||
if (isMarkdown) {
|
||||
const sections = (file && file.extension === "md")
|
||||
? (await this.plugin.app.metadataCache.blockCache
|
||||
.getForFile({ isCancelled: () => false },file))
|
||||
.blocks.filter((b: any) => b.display && b.node?.type === "heading")
|
||||
.filter((b: any) => !isExcalidraw || !MD_EX_SECTIONS.includes(b.display))
|
||||
: null;
|
||||
|
||||
if (isMarkdown || (isExcalidraw && sections?.length > 0)) {
|
||||
sectionPickerSetting.settingEl.style.display = "";
|
||||
sectionPicker.selectEl.style.display = "block";
|
||||
while(sectionPicker.selectEl.options.length > 0) {
|
||||
sectionPicker.selectEl.remove(0);
|
||||
}
|
||||
sectionPicker.addOption("","");
|
||||
(await app.metadataCache.blockCache
|
||||
.getForFile({ isCancelled: () => false },file))
|
||||
.blocks.filter((b: any) => b.display && b.node?.type === "heading")
|
||||
.forEach((b: any) => {
|
||||
sectionPicker.addOption(
|
||||
`#${cleanSectionHeading(b.display)}`,
|
||||
b.display)
|
||||
});
|
||||
if(!isExcalidraw) sectionPicker.addOption("","");
|
||||
sections.forEach((b: any) => {
|
||||
sectionPicker.addOption(
|
||||
`#${cleanSectionHeading(b.display)}`,
|
||||
b.display)
|
||||
});
|
||||
} else {
|
||||
sectionPickerSetting.settingEl.style.display = "none";
|
||||
sectionPicker.selectEl.style.display = "none";
|
||||
}
|
||||
|
||||
if (isExcalidraw) {
|
||||
if (isExcalidraw && !isSelf) {
|
||||
sizeToggleSetting.settingEl.style.display = "";
|
||||
} else {
|
||||
sizeToggleSetting.settingEl.style.display = "none";
|
||||
}
|
||||
|
||||
if (isImage || (file?.extension === "md")) {
|
||||
if (!isSelf && (isImage || (file?.extension === "md"))) {
|
||||
actionImage.buttonEl.style.display = "block";
|
||||
} else {
|
||||
actionImage.buttonEl.style.display = "none";
|
||||
}
|
||||
|
||||
if (isIFrame || isAnimatedImage) {
|
||||
if (isIFrame || isAnimatedImage || (isExcalidraw && sections?.length > 0)) {
|
||||
actionIFrame.buttonEl.style.display = "block";
|
||||
} else {
|
||||
actionIFrame.buttonEl.style.display = "none";
|
||||
@@ -131,9 +136,17 @@ export class UniversalInsertFileModal extends Modal {
|
||||
|
||||
}
|
||||
|
||||
const sections = (await this.plugin.app.metadataCache.blockCache
|
||||
.getForFile({ isCancelled: () => false },this.view.file))
|
||||
.blocks.filter((b: any) => b.display && b.node?.type === "heading")
|
||||
.filter((b: any) => !MD_EX_SECTIONS.includes(b.display));
|
||||
|
||||
const search = new TextComponent(ce);
|
||||
search.inputEl.style.width = "100%";
|
||||
const suggester = new FileSuggestionModal(this.app, search,app.vault.getFiles().filter((f: TFile) => f!==this.view.file));
|
||||
const suggester = new FileSuggestionModal(
|
||||
this.app,
|
||||
search,
|
||||
this.app.vault.getFiles().filter((f: TFile) => sections?.length > 0 || f!==this.view.file));
|
||||
search.onChange(() => {
|
||||
file = suggester.getSelectedItem();
|
||||
updateForm();
|
||||
@@ -176,6 +189,7 @@ export class UniversalInsertFileModal extends Modal {
|
||||
`[[${path}${sectionPicker.selectEl.value}]]`,
|
||||
)]
|
||||
);
|
||||
ea.destroy();
|
||||
this.close();
|
||||
})
|
||||
actionIFrame = button;
|
||||
@@ -207,6 +221,7 @@ export class UniversalInsertFileModal extends Modal {
|
||||
ea.isExcalidrawFile(file) ? !anchorTo100 : undefined,
|
||||
)]
|
||||
);
|
||||
ea.destroy();
|
||||
this.close();
|
||||
})
|
||||
actionImage = button;
|
||||
@@ -258,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import {
|
||||
DEVICE,
|
||||
FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS,
|
||||
FRONTMATTER_KEY_CUSTOM_PREFIX,
|
||||
FRONTMATTER_KEY_CUSTOM_URL_PREFIX,
|
||||
FRONTMATTER_KEYS,
|
||||
} from "src/constants/constants";
|
||||
import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/utils/ModifierkeyHelper";
|
||||
|
||||
@@ -11,6 +9,7 @@ export default {
|
||||
// main.ts
|
||||
CONVERT_URL_TO_FILE: "Save image from URL to local file",
|
||||
UNZIP_CURRENT_FILE: "Decompress current Excalidraw file",
|
||||
ZIP_CURRENT_FILE: "Compress current Excalidraw file",
|
||||
PUBLISH_SVG_CHECK: "Obsidian Publish: Find SVG and PNG exports that are out of date",
|
||||
EMBEDDABLE_PROPERTIES: "Embeddable Properties",
|
||||
EMBEDDABLE_RELATIVE_ZOOM: "Scale selected embeddable elements to 100% relative to the current canvas zoom",
|
||||
@@ -25,7 +24,7 @@ export default {
|
||||
"Script is up to date - Click to reinstall",
|
||||
OPEN_AS_EXCALIDRAW: "Open as Excalidraw Drawing",
|
||||
TOGGLE_MODE: "Toggle between Excalidraw and Markdown mode",
|
||||
CONVERT_NOTE_TO_EXCALIDRAW: "Convert empty note to Excalidraw Drawing",
|
||||
CONVERT_NOTE_TO_EXCALIDRAW: "Convert markdown note to Excalidraw Drawing",
|
||||
CONVERT_EXCALIDRAW: "Convert *.excalidraw to *.md files",
|
||||
CREATE_NEW: "Create new drawing",
|
||||
CONVERT_FILE_KEEP_EXT: "*.excalidraw => *.excalidraw.md",
|
||||
@@ -37,6 +36,7 @@ export default {
|
||||
TRANSCLUDE: "Embed a drawing",
|
||||
TRANSCLUDE_MOST_RECENT: "Embed the most recently edited drawing",
|
||||
TOGGLE_LEFTHANDED_MODE: "Toggle left-handed mode",
|
||||
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",
|
||||
NEW_IN_ACTIVE_PANE: "Create new drawing - IN THE CURRENT ACTIVE WINDOW",
|
||||
@@ -50,37 +50,70 @@ 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 [[link]] for selected element(s)",
|
||||
COPY_DRAWING_LINK: "Copy ![[embed link]] for this drawing",
|
||||
INSERT_LINK_TO_ELEMENT:
|
||||
`Copy markdown link for selected element to clipboard. ${labelCTRL()}+CLICK to copy 'group=' link. ${labelSHIFT()}+CLICK to copy an 'area=' link. ${labelALT()}+CLICK to watch a help video.`,
|
||||
`Copy [[link]] for selected element to clipboard. ${labelCTRL()}+CLICK to copy 'group=' link. ${labelSHIFT()}+CLICK to copy an 'area=' link. ${labelALT()}+CLICK to watch a help video.`,
|
||||
INSERT_LINK_TO_ELEMENT_GROUP:
|
||||
"Copy 'group=' markdown link for selected element to clipboard.",
|
||||
"Copy 'group=' ![[link]] for selected element to clipboard.",
|
||||
INSERT_LINK_TO_ELEMENT_AREA:
|
||||
"Copy 'area=' markdown link for selected element to clipboard.",
|
||||
"Copy 'area=' ![[link]] for selected element to clipboard.",
|
||||
INSERT_LINK_TO_ELEMENT_FRAME:
|
||||
"Copy 'frame=' markdown link for selected element to clipboard.",
|
||||
"Copy 'frame=' ![[link]] for selected element to clipboard.",
|
||||
INSERT_LINK_TO_ELEMENT_FRAME_CLIPPED:
|
||||
"Copy 'clippedframe=' ![[link]] for selected element to clipboard.",
|
||||
INSERT_LINK_TO_ELEMENT_NORMAL:
|
||||
"Copy markdown link for selected element to clipboard.",
|
||||
"Copy [[link]] for selected element to clipboard.",
|
||||
INSERT_LINK_TO_ELEMENT_ERROR: "Select a single element in the scene",
|
||||
INSERT_LINK_TO_ELEMENT_READY: "Link is READY and available on the clipboard",
|
||||
INSERT_LINK: "Insert link to file",
|
||||
INSERT_COMMAND: "Insert Obsidian Command as a link",
|
||||
INSERT_IMAGE: "Insert image or Excalidraw drawing from your vault",
|
||||
IMPORT_SVG: "Import an SVG file as Excalidraw strokes (limited SVG support, TEXT currently not supported)",
|
||||
IMPORT_SVG_CONTEXTMENU: "Convert SVG to strokes - with limitations",
|
||||
INSERT_MD: "Insert markdown file from vault",
|
||||
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.`,
|
||||
ENTER_LATEX: "Enter a valid LaTeX expression",
|
||||
READ_RELEASE_NOTES: "Read latest release notes",
|
||||
RUN_OCR: "OCR: Grab text from freedraw scribble and pictures to clipboard",
|
||||
RUN_OCR: "OCR full drawing: Grab text from freedraw + images to clipboard and doc.props",
|
||||
RERUN_OCR: "OCR full drawing re-run: Grab text from freedraw + images to clipboard and doc.props",
|
||||
RUN_OCR_ELEMENTS: "OCR selected elements: Grab text from freedraw + images to clipboard",
|
||||
TRAY_MODE: "Toggle property-panel tray-mode",
|
||||
SEARCH: "Search for text in drawing",
|
||||
CROP_PAGE: "Crop and mask selected page",
|
||||
CROP_IMAGE: "Crop and mask image",
|
||||
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",
|
||||
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
|
||||
FORCE_SAVE_ABORTED: "Force Save aborted because saving is in progress",
|
||||
LINKLIST_SECOND_ORDER_LINK: "Second Order Link",
|
||||
MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT_TITLE: "Customize the link",
|
||||
MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT: "Do not add [[square brackets]] around the filename!<br>Follow this format when editing your link:<br><mark>filename#^blockref|WIDTHxMAXHEIGHT</mark>",
|
||||
FRAME_CLIPPING_ENABLED: "Frame Rendering: Enabled",
|
||||
FRAME_CLIPPING_DISABLED: "Frame Rendering: Disabled",
|
||||
ARROW_BINDING_INVERSE_MODE: "Inverted Mode: Default arrow binding is now disabled. Use CTRL/CMD to temporarily enable binding when needed.",
|
||||
ARROW_BINDING_NORMAL_MODE: "Normal Mode: Arrow binding is now enabled. Use CTRL/CMD to temporarily disable binding when needed.",
|
||||
EXPORT_FILENAME_PROMPT: "Please provide filename",
|
||||
EXPORT_FILENAME_PROMPT_PLACEHOLDER: "filename, leave blank to cancel action",
|
||||
WARNING_SERIOUS_ERROR: "WARNING: Excalidraw ran into an unknown problem!\n\n" +
|
||||
"There is a risk that your most recent changes cannot be saved.\n\n" +
|
||||
"To be on the safe side...\n" +
|
||||
"1) Please select your drawing using CTRL/CMD+A and make a copy with CTRL/CMD+C.\n" +
|
||||
"2) Then create an empty drawing in a new pane by CTRL/CMD+clicking the Excalidraw ribbon button,\n" +
|
||||
"3) and paste your work to the new document with CTRL/CMD+V.",
|
||||
ARIA_LABEL_TRAY_MODE: "Tray-mode offers an alternative, more spacious canvas",
|
||||
MASK_FILE_NOTICE: "This is a mask file. It is used to crop images and mask out parts of the image. Press and hold notice to open the help video.",
|
||||
INSTALL_SCRIPT_BUTTON: "Install or update Excalidraw Scripts",
|
||||
OPEN_AS_MD: "Open as Markdown",
|
||||
EXPORT_IMAGE: `Export Image`,
|
||||
@@ -103,10 +136,15 @@ export default {
|
||||
BACKUP_RESTORED: "Backup restored",
|
||||
CACHE_NOT_READY: "I apologize for the inconvenience, but an error occurred while loading your file.<br><br><mark>Having a little patience can save you a lot of time...</mark><br><br>The plugin has a backup cache, but it appears that you have just started Obsidian. Initializing the Backup Cache may take some time, usually up to a minute or more depending on your device's performance. You will receive a notification in the top right corner when the cache initialization is complete.<br><br>Please press OK to attempt loading the file again and check if the cache has finished initializing. If you see a completely empty file behind this message, I recommend waiting until the backup cache is ready before proceeding. Alternatively, you can choose Cancel to manually correct your file.<br>",
|
||||
OBSIDIAN_TOOLS_PANEL: "Obsidian Tools Panel",
|
||||
ERROR_SAVING_IMAGE: "Unknown error occured while fetching the image. It could be that for some reason the image is not available or rejected the fetch request from Obsidian",
|
||||
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",
|
||||
LASER_ON: "Enable laser pointer",
|
||||
LASER_OFF: "Disable laser pointer",
|
||||
|
||||
//settings.ts
|
||||
RELEASE_NOTES_NAME: "Display Release Notes after update",
|
||||
@@ -123,19 +161,39 @@ export default {
|
||||
FOLDER_NAME: "Excalidraw folder",
|
||||
FOLDER_DESC:
|
||||
"Default location for new drawings. If empty, drawings will be created in the Vault root.",
|
||||
CROP_PREFIX_NAME: "Crop file prefix",
|
||||
CROP_PREFIX_DESC:
|
||||
"The first part of the filename for new drawings created when cropping an image. " +
|
||||
"If empty the default 'cropped_' will be used.",
|
||||
ANNOTATE_PREFIX_NAME: "Annotation file prefix",
|
||||
ANNOTATE_PREFIX_DESC:
|
||||
"The first part of the filename for new drawings created when annotating an image. " +
|
||||
"If empty the default 'annotated_' will be used.",
|
||||
ANNOTATE_PRESERVE_SIZE_NAME: "Preserve image size when annotating",
|
||||
ANNOTATE_PRESERVE_SIZE_DESC:
|
||||
"When annotating an image in markdown the replacment image link will include the width of the original image.",
|
||||
CROP_FOLDER_NAME: "Crop file folder",
|
||||
CROP_FOLDER_DESC:
|
||||
"Default location for new drawings created when cropping an image. If empty, drawings will be created following the Vault attachments settings.",
|
||||
ANNOTATE_FOLDER_NAME: "Image annotation file folder",
|
||||
ANNOTATE_FOLDER_DESC:
|
||||
"Default location for new drawings created when annotating an image. If empty, drawings will be created following the Vault attachments settings.",
|
||||
FOLDER_EMBED_NAME:
|
||||
"Use Excalidraw folder when embedding a drawing into the active document",
|
||||
FOLDER_EMBED_DESC:
|
||||
"Define which folder to place the newly inserted drawing into " +
|
||||
"when using the command palette action: 'Create a new drawing and embed into active document'.<br>" +
|
||||
"<b><u>Toggle ON:</u></b> Use Excalidraw folder<br><b><u>Toggle OFF:</u></b> Use the attachments folder defined in Obsidian settings.",
|
||||
TEMPLATE_NAME: "Excalidraw template file",
|
||||
TEMPLATE_NAME: "Excalidraw template file or folder",
|
||||
TEMPLATE_DESC:
|
||||
"Full filepath to the Excalidraw template. " +
|
||||
"E.g.: If your template is in the default Excalidraw folder and its name is " +
|
||||
"Full filepath or folderpath to the Excalidraw template.<br>" +
|
||||
"<b>Template File:</b>E.g.: If your template is in the default Excalidraw folder and its name is " +
|
||||
"Template.md, the setting would be: Excalidraw/Template.md (or just Excalidraw/Template - you may omit the .md file extension). " +
|
||||
"If you are using Excalidraw in compatibility mode, then your template must be a legacy Excalidraw file as well " +
|
||||
"such as Excalidraw/Template.excalidraw.",
|
||||
"such as Excalidraw/Template.excalidraw. <br><b>Template Folder:</b> You can also set a folder as your template. " +
|
||||
"In this case you will be prompted which tempalte to use when creating a new drawing.<br>" +
|
||||
"<b>Pro Tip:</b> If you are using the Obsidian Templater plugin, you can add Templater code to your different Excalidraw " +
|
||||
"templates to automate configuration of your drawings.",
|
||||
SCRIPT_FOLDER_NAME: "Excalidraw Automate script folder (CASE SeNSitiVE!)",
|
||||
SCRIPT_FOLDER_DESC:
|
||||
"The files you place in this folder will be treated as Excalidraw Automate scripts. " +
|
||||
@@ -186,6 +244,14 @@ export default {
|
||||
"once you switch back to Excalidraw view. " +
|
||||
"The setting only has effect 'point forward', meaning, existing drawings will not be affected by the setting " +
|
||||
"until you open them and save them.<br><b><u>Toggle ON:</u></b> Compress drawing JSON<br><b><u>Toggle OFF:</u></b> Leave drawing JSON uncompressed",
|
||||
DECOMPRESS_FOR_MD_NAME: "Decompress Excalidraw JSON in Markdown View",
|
||||
DECOMPRESS_FOR_MD_DESC:
|
||||
"By enabling this feature Excalidraw will automatically decompress the drawing JSON when you switch to Markdown view. " +
|
||||
"This will allow you to easily read and edit the JSON string. The drawing will be compressed again " +
|
||||
"once you switch back to Excalidraw view and save the drawing (CTRL+S).<br>" +
|
||||
"I recommend switching this feature off as it will result in smaller file sizes and avoiding unnecessary results in Obsidian search. " +
|
||||
"You can always use the 'Excalidraw: Decompress current Excalidraw file' command from the command palette "+
|
||||
"to manually decompress the drawing JSON when you need to read or edit it.",
|
||||
AUTOSAVE_INTERVAL_DESKTOP_NAME: "Interval for autosave on Desktop",
|
||||
AUTOSAVE_INTERVAL_DESKTOP_DESC:
|
||||
"The time interval between saves. Autosave will skip if there are no changes in the drawing. " +
|
||||
@@ -256,6 +322,24 @@ FILENAME_HEAD: "Filename",
|
||||
DEFAULT_PEN_MODE_NAME: "Pen mode",
|
||||
DEFAULT_PEN_MODE_DESC:
|
||||
"Should pen mode be automatically enabled when opening Excalidraw?",
|
||||
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_HOVER_PREVIEW_NAME: "Render image in hover preview for MD files",
|
||||
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_DESC:
|
||||
"This setting effects files that have the <b>excalidraw-open-md: true</b> frontmatter key.",
|
||||
SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME: "Render image when in markdown reading mode",
|
||||
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",
|
||||
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>",
|
||||
THEME_HEAD: "Theme and styling",
|
||||
ZOOM_HEAD: "Zoom",
|
||||
DEFAULT_PINCHZOOM_NAME: "Allow pinch zoom in pen mode",
|
||||
@@ -297,6 +381,18 @@ FILENAME_HEAD: "Filename",
|
||||
"These settings are different for Apple and non-Apple. If you use Obsidian on multiple platforms, you'll need to make the settings separately. "+
|
||||
"The toggles follow the order of " +
|
||||
(DEVICE.isIOS || DEVICE.isMacOS ? "SHIFT, CMD, OPT, CONTROL." : "SHIFT, CTRL, ALT, META (Windows key)."),
|
||||
LONG_PRESS_DESKTOP_NAME: "Long press to open desktop",
|
||||
LONG_PRESS_DESKTOP_DESC: "Long press delay in milliseconds to open an Excalidraw Drawing embedded in a Markdown file. ",
|
||||
LONG_PRESS_MOBILE_NAME: "Long press to open mobile",
|
||||
LONG_PRESS_MOBILE_DESC: "Long press delay in milliseconds to open an Excalidraw Drawing embedded in a Markdown file. ",
|
||||
|
||||
FOCUS_ON_EXISTING_TAB_NAME: "Focus on Existing Tab",
|
||||
FOCUS_ON_EXISTING_TAB_DESC: "When opening a link, Excalidraw will focus on the existing tab if the file is already open. " +
|
||||
"Enabling this setting overrides 'Reuse Adjacent Pane' when the file is already open.",
|
||||
SECOND_ORDER_LINKS_NAME: "Show second-order links",
|
||||
SECOND_ORDER_LINKS_DESC: "Show links when clicking on a link in Excalidraw. Second-order link are backlinks pointing to the link being clicked. " +
|
||||
"When using image icons to connect similar notes, second order links allow you to get to related notes in one click instead of two. " +
|
||||
"See <a href='https://youtube.com/shorts/O_1ls9c6wBY?feature=share'>YT Short</a> to understand.",
|
||||
ADJACENT_PANE_NAME: "Reuse adjacent pane",
|
||||
ADJACENT_PANE_DESC:
|
||||
`When ${labelCTRL()}+${labelALT()} clicking a link in Excalidraw, by default the plugin will open the link in a new pane. ` +
|
||||
@@ -311,17 +407,17 @@ FILENAME_HEAD: "Filename",
|
||||
LINK_BRACKETS_DESC: `${
|
||||
"In PREVIEW mode, when parsing Text Elements, place brackets around links. " +
|
||||
"You can override this setting for a specific drawing by adding <code>"
|
||||
}${FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS}: true/false</code> to the file's frontmatter.`,
|
||||
}${FRONTMATTER_KEYS["link-brackets"].name}: true/false</code> to the file's frontmatter.`,
|
||||
LINK_PREFIX_NAME: "Link prefix",
|
||||
LINK_PREFIX_DESC: `${
|
||||
"In PREVIEW mode, if the Text Element contains a link, precede the text with these characters. " +
|
||||
"You can override this setting for a specific drawing by adding <code>"
|
||||
}${FRONTMATTER_KEY_CUSTOM_PREFIX}: "📍 "</code> to the file's frontmatter.`,
|
||||
}${FRONTMATTER_KEYS["link-prefix"].name}: "📍 "</code> to the file's frontmatter.`,
|
||||
URL_PREFIX_NAME: "URL prefix",
|
||||
URL_PREFIX_DESC: `${
|
||||
"In PREVIEW mode, if the Text Element contains a URL link, precede the text with these characters. " +
|
||||
"You can override this setting for a specific drawing by adding <code>"
|
||||
}${FRONTMATTER_KEY_CUSTOM_URL_PREFIX}: "🌐 "</code> to the file's frontmatter.`,
|
||||
}${FRONTMATTER_KEYS["url-prefix"].name}: "🌐 "</code> to the file's frontmatter.`,
|
||||
PARSE_TODO_NAME: "Parse todo",
|
||||
PARSE_TODO_DESC: "Convert '- [ ] ' and '- [x] ' to checkbox and tick in the box.",
|
||||
TODO_NAME: "Open TODO icon",
|
||||
@@ -340,7 +436,7 @@ FILENAME_HEAD: "Filename",
|
||||
`${labelCTRL()}+CLICK on text with [[links]] or [](links) to open them`,
|
||||
LINK_CTRL_CLICK_DESC:
|
||||
"You can turn this feature off if it interferes with default Excalidraw features you want to use. If " +
|
||||
"this is turned off, only the link button in the title bar of the drawing pane will open links.",
|
||||
`this is turned off, you can either use ${labelCTRL()} + ${labelMETA()} or the link indicator in the top right of the element to open links.`,
|
||||
TRANSCLUSION_WRAP_NAME: "Overflow wrap behavior of transcluded text",
|
||||
TRANSCLUSION_WRAP_DESC:
|
||||
"Number specifies the character count where the text should be wrapped. " +
|
||||
@@ -420,6 +516,10 @@ FILENAME_HEAD: "Filename",
|
||||
EMBED_IMAGE_CACHE_NAME: "Cache images for embedding in markdown",
|
||||
EMBED_IMAGE_CACHE_DESC: "Cache images for embedding in markdown. This will speed up the embedding process, but in case you compose images of several sub-component drawings, " +
|
||||
"the embedded image in Markdown won't update until you open the drawing and save it to trigger an update of the cache.",
|
||||
SCENE_IMAGE_CACHE_NAME: "Cache nested Excalidraws in Scene",
|
||||
SCENE_IMAGE_CACHE_DESC: "Cache nested Excalidraws in the Scene for faster scene rendering. This will speed up the rendering process, especially if you have deeply nested Excalidraws in your scene. " +
|
||||
"Excalidraw will try to intelligently identify if any children of a nested Excalidraw have changed and will update the cache accordingly. " +
|
||||
"You may want to turn this off, in case you are suspecting that the cache is not updating properly.",
|
||||
EMBED_IMAGE_CACHE_CLEAR: "Purge Cache",
|
||||
BACKUP_CACHE_CLEAR: "Purge Backups",
|
||||
BACKUP_CACHE_CLEAR_CONFIRMATION: "This action will delete all Excalidraw drawing backups. Backups are used as a safety measure in case your drawing file gets damaged. Each time you open Obsidian the plugin automatically deletes backups for files that no longer exist in your Vault. Are you sure you want to clear all backups?",
|
||||
@@ -449,6 +549,11 @@ FILENAME_HEAD: "Filename",
|
||||
"The default width of an embedded drawing. This applies to live preview edit and reading mode, as well as to hover previews. You can specify a custom " +
|
||||
"width when embedding an image using the <code>![[drawing.excalidraw|100]]</code> or " +
|
||||
"<code>[[drawing.excalidraw|100x100]]</code> format.",
|
||||
EMBED_HEIGHT_NAME: "Default height of embedded (transcluded) image",
|
||||
EMBED_HEIGHT_DESC:
|
||||
"The default height of an embedded drawing. This applies to live preview edit and reading mode, as well as to hover previews. You can specify a custom " +
|
||||
"height when embedding an image using the <code>![[drawing.excalidraw|100]]</code> or " +
|
||||
"<code>[[drawing.excalidraw|100x100]]</code> format.",
|
||||
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 " +
|
||||
@@ -470,13 +575,17 @@ 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",
|
||||
EXPORT_THEME_DESC:
|
||||
"Export the image matching the dark/light theme of your drawing. If turned off, " +
|
||||
"drawings created in dark mode will appear as they would in light mode.",
|
||||
EXPORT_EMBED_SCENE_NAME: "Embed scene in exported image",
|
||||
EXPORT_EMBED_SCENE_DESC:
|
||||
"Embed Excalidraw scene in exported image. Can be overridden at a file level by adding the <code>excalidraw-export-embed-scene: true/false<code> frontmatter key. " +
|
||||
"The setting only takes effect the next time you (re)open drawings.",
|
||||
EXPORT_HEAD: "Auto-export Settings",
|
||||
EXPORT_SYNC_NAME:
|
||||
"Keep the .SVG and/or .PNG filenames in sync with the drawing file",
|
||||
@@ -498,6 +607,18 @@ 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>",
|
||||
SLIDING_PANES_NAME: "Sliding panes plugin support",
|
||||
SLIDING_PANES_DESC:
|
||||
"Need to restart Obsidian for this change to take effect.<br>" +
|
||||
@@ -527,6 +648,11 @@ FILENAME_HEAD: "Filename",
|
||||
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. `,
|
||||
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. " +
|
||||
"This means you are multiplying the limit set by default in Excalidraw, the larger the multiplier the better the image zoom in resolution will be, and the more memory it will consume. " +
|
||||
"I recommend playing with multiple values for this setting. You know you've hit the wall, when zooming in to a larger PNG image suddenly the image disappears from view. The default value is 1. The setting has no effect on iOS.",
|
||||
CUSTOM_PEN_HEAD: "Custom pens",
|
||||
CUSTOM_PEN_NAME: "Number of custom pens",
|
||||
CUSTOM_PEN_DESC: "You will see these pens next to the Obsidian Menu on the canvas. You can customize the pens on the canvas by long-pressing the pen button.",
|
||||
@@ -561,6 +687,16 @@ FILENAME_HEAD: "Filename",
|
||||
"Turn this on to support image embedding styles such as ![[drawing|width|style]] in live preview editing mode. " +
|
||||
"The setting will not affect the currently open documents. You need close the open documents and re-open them for the change " +
|
||||
"to take effect.",
|
||||
FADE_OUT_EXCALIDRAW_MARKUP_NAME: "Fade out Excalidraw markup",
|
||||
FADE_OUT_EXCALIDRAW_MARKUP_DESC: "In Markdown view mode, the section after the markdown comment %% " +
|
||||
"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.",
|
||||
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: "Fourth font",
|
||||
ENABLE_FOURTH_FONT_NAME: "Enable fourth font option",
|
||||
ENABLE_FOURTH_FONT_DESC:
|
||||
@@ -605,6 +741,13 @@ FILENAME_HEAD: "Filename",
|
||||
PDF_PAGES_HEADER: "Pages to load?",
|
||||
PDF_PAGES_DESC: "Format: 1, 3-5, 7, 9-11",
|
||||
|
||||
//SelectCard.ts
|
||||
TYPE_SECTION: "Type section name to select.",
|
||||
SELECT_SECTION_OR_TYPE_NEW:
|
||||
"Select existing section or type name of a new section then press Enter.",
|
||||
INVALID_SECTION_NAME: "Invalid section name.",
|
||||
EMPTY_SECTION_MESSAGE: "Type the Section Name and hit enter to create a new Section",
|
||||
|
||||
//EmbeddedFileLoader.ts
|
||||
INFINITE_LOOP_WARNING:
|
||||
"EXCALIDRAW WARNING\nAborted loading embedded images due to infinite loop in file:\n",
|
||||
@@ -677,6 +820,7 @@ FILENAME_HEAD: "Filename",
|
||||
PROMPT_BUTTON_INSERT_SPACE: "Insert space",
|
||||
PROMPT_BUTTON_INSERT_LINK: "Insert markdown link to file",
|
||||
PROMPT_BUTTON_UPPERCASE: "Uppercase",
|
||||
PROMPT_SELECT_TEMPLATE: "Select a template",
|
||||
|
||||
//ModifierKeySettings
|
||||
WEB_BROWSER_DRAG_ACTION: "Web Browser Drag Action",
|
||||
@@ -684,4 +828,11 @@ FILENAME_HEAD: "Filename",
|
||||
INTERNAL_DRAG_ACTION: "Obsidian Internal Drag Action",
|
||||
PANE_TARGET: "Link click behavior",
|
||||
DEFAULT_ACTION_DESC: "In case none of the combinations apply the default action for this group is: ",
|
||||
|
||||
//FrameSettings.ts
|
||||
FRAME_SETTINGS_TITLE: "Frame Settings",
|
||||
FRAME_SETTINGS_ENABLE: "Enable Frames",
|
||||
FRAME_SETTIGNS_NAME: "Display Frame Name",
|
||||
FRAME_SETTINGS_OUTLINE: "Display Frame Outline",
|
||||
FRAME_SETTINGS_CLIP: "Enable Frame Clipping",
|
||||
};
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import {
|
||||
DEVICE,
|
||||
FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS,
|
||||
FRONTMATTER_KEY_CUSTOM_PREFIX,
|
||||
FRONTMATTER_KEY_CUSTOM_URL_PREFIX,
|
||||
FRONTMATTER_KEYS,
|
||||
} from "src/constants/constants";
|
||||
import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/utils/ModifierkeyHelper";
|
||||
|
||||
// 简体中文
|
||||
export default {
|
||||
// main.ts
|
||||
CONVERT_URL_TO_FILE: "从 URL 下载图像到本地",
|
||||
UNZIP_CURRENT_FILE: "解压当前 Excalidraw 文件",
|
||||
ZIP_CURRENT_FILE: "压缩当前 Excalidraw 文件",
|
||||
PUBLISH_SVG_CHECK: "Obsidian Publish:搜索过期的 SVG 和 PNG 导出文件",
|
||||
EMBEDDABLE_PROPERTIES: "Embeddable 元素设置",
|
||||
EMBEDDABLE_RELATIVE_ZOOM: "使元素的缩放等级等于当前画布的缩放等级",
|
||||
OPEN_IMAGE_SOURCE: "打开 Excalidraw 绘图文件",
|
||||
INSTALL_SCRIPT: "安装此脚本",
|
||||
UPDATE_SCRIPT: "有可用更新 - 点击安装",
|
||||
CHECKING_SCRIPT:
|
||||
@@ -31,6 +36,7 @@ export default {
|
||||
TRANSCLUDE: "嵌入绘图(形如 ![[drawing]])到当前 Markdown 文档中",
|
||||
TRANSCLUDE_MOST_RECENT: "嵌入最近编辑过的绘图(形如 ![[drawing]])到当前 Markdown 文档中",
|
||||
TOGGLE_LEFTHANDED_MODE: "切换为左手模式",
|
||||
FLIP_IMAGE: "打开当前所选 excalidraw 图像的“背景笔记”",
|
||||
NEW_IN_NEW_PANE: "新建绘图 - 于新面板",
|
||||
NEW_IN_NEW_TAB: "新建绘图 - 于新页签",
|
||||
NEW_IN_ACTIVE_PANE: "新建绘图 - 于当前面板",
|
||||
@@ -43,41 +49,75 @@ export default {
|
||||
"新建绘图 - 于当前面板 - 并将其嵌入(形如 ![[drawing]])到当前 Markdown 文档中",
|
||||
NEW_IN_POPOUT_WINDOW_EMBED: "新建绘图 - 于新窗口 - 并将其嵌入(形如 ![[drawing]])到当前 Markdown 文档中",
|
||||
TOGGLE_LOCK: "文本元素:原文模式(RAW)⟺ 预览模式(PREVIEW)",
|
||||
DELETE_FILE: "从库中删除所选图像或 MD-Embed 的源文件",
|
||||
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()} 可观看视频演示。`,
|
||||
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: "未选择画布里的单个元素",
|
||||
INSERT_LINK_TO_ELEMENT_READY: "链接已生成并复制到剪贴板",
|
||||
INSERT_LINK: "插入任意文件(以内部链接形式嵌入,形如 [[drawing]] )到当前绘图中",
|
||||
INSERT_COMMAND: "插入 Obsidian 命令(以内部链接形式嵌入)到当前绘图中",
|
||||
INSERT_IMAGE: "插入图像或 Excalidraw 绘图(以图像形式嵌入)到当前绘图中",
|
||||
IMPORT_SVG: "从 SVG 文件导入图形元素到当前绘图中(暂不支持文本元素)",
|
||||
IMPORT_SVG_CONTEXTMENU: "转换 SVG 到线条 - 有限制",
|
||||
INSERT_MD: "插入 Markdown 文档(以图像形式嵌入)到当前绘图中",
|
||||
INSERT_PDF: "插入 PDF 文档(以图像形式嵌入)到当前绘图中",
|
||||
UNIVERSAL_ADD_FILE: "插入任意文件(以 Embeddable 形式嵌入)到当前绘图中",
|
||||
UNIVERSAL_ADD_FILE: "插入任意文件(以交互形式嵌入,或者以图像形式嵌入)到当前绘图中",
|
||||
INSERT_CARD: "插入“背景笔记”卡片",
|
||||
CONVERT_CARD_TO_FILE: "将“背景笔记”卡片保存到文件",
|
||||
ERROR_TRY_AGAIN: "请重试。",
|
||||
PASTE_CODEBLOCK: "粘贴代码块",
|
||||
INSERT_LATEX:
|
||||
`插入 LaTeX 公式到当前绘图。按住 ${labelALT()} 可观看视频演示。`,
|
||||
`插入 LaTeX 公式到当前绘图`,
|
||||
ENTER_LATEX: "输入 LaTeX 表达式",
|
||||
READ_RELEASE_NOTES: "阅读本插件的更新说明",
|
||||
RUN_OCR: "OCR:识别涂鸦和图片里的文本并复制到剪贴板",
|
||||
RUN_OCR: "OCR 完整画布:识别涂鸦和图片里的文本并复制到剪贴板和文档属性中",
|
||||
RERUN_OCR: "重新运行 OCR 完整画笔:识别涂鸦和图片里的文本并复制到剪贴板和文档属性中",
|
||||
RUN_OCR_ELEMENTS: "OCR 选中的元素:识别涂鸦和图片里的文本并复制到剪贴板",
|
||||
TRAY_MODE: "绘图工具属性页:面板模式 ⟺ 托盘模式",
|
||||
SEARCH: "搜索文本",
|
||||
CROP_PAGE: "对所选页面裁剪并添加蒙版",
|
||||
CROP_IMAGE: "对图片裁剪并添加蒙版",
|
||||
ANNOTATE_IMAGE : "在 Excalidraw 中标注图像",
|
||||
INSERT_ACTIVE_PDF_PAGE_AS_IMAGE: "将当前激活的的 PDF 页面作为图片插入",
|
||||
RESET_IMG_TO_100: "重设图像元素的尺寸为 100%",
|
||||
TEMPORARY_DISABLE_AUTOSAVE: "临时禁用自动保存功能,直到本次 Obsidian 退出(小白慎用!)",
|
||||
TEMPORARY_ENABLE_AUTOSAVE: "启用自动保存功能",
|
||||
|
||||
//ExcalidrawView.ts
|
||||
FORCE_SAVE_ABORTED: "自动保存被中止,因为文件正在保存中",
|
||||
LINKLIST_SECOND_ORDER_LINK: "二级链接",
|
||||
MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT_TITLE: "自定义链接",
|
||||
MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT: "不要在文件名周围添加[[方括号(Wiki 格式链接)]]!<br>编辑链接时请遵循以下格式:<br><mark>文件名#^块引用|宽度x最大高度</mark>",
|
||||
FRAME_CLIPPING_ENABLED: "渲染框架:已启用",
|
||||
FRAME_CLIPPING_DISABLED: "渲染框架:已禁用",
|
||||
ARROW_BINDING_INVERSE_MODE: "反转模式:默认方向按键已禁用。需要时请使用 Ctrl/CMD 临时启用。",
|
||||
ARROW_BINDING_NORMAL_MODE: "正常模式:方向键已启用。需要时请使用 Ctrl/CMD 临时禁用。",
|
||||
EXPORT_FILENAME_PROMPT: "请提供文件名",
|
||||
EXPORT_FILENAME_PROMPT_PLACEHOLDER: "请输入文件名,留空以取消操作",
|
||||
WARNING_SERIOUS_ERROR: "警告:Excalidraw 遇到了未知的问题!\n\n" +
|
||||
"您最近的更改可能无法保存。\n\n" +
|
||||
"为了安全起见,请按以下步骤操作:\n" +
|
||||
"1) 使用 Ctrl/CMD+A 选择您的绘图,然后使用 Ctrl/CMD+C 进行复制。\n" +
|
||||
"2) 然后在新窗格中,通过 Ctrl/CMD 点击 Excalidraw 功能区按钮创建一个空白绘图。\n" +
|
||||
"3) 最后,使用 Ctrl/CMD+V 将您的作品粘贴到新文档中。",
|
||||
ARIA_LABEL_TRAY_MODE: "托盘模式提供了一个更宽敞的画布空间",
|
||||
MASK_FILE_NOTICE: "这是一个蒙版图像。长按本提示来观看视频讲解。",
|
||||
INSTALL_SCRIPT_BUTTON: "安装或更新 Excalidraw 脚本",
|
||||
OPEN_AS_MD: "打开为 Markdown 文档",
|
||||
EXPORT_IMAGE: `导出为图像`,
|
||||
OPEN_LINK: "打开所选元素里的链接 \n(按住 SHIFT 在新面板打开)",
|
||||
OPEN_LINK: "打开所选元素里的链接 \n(按住 Shift 在新面板打开)",
|
||||
EXPORT_EXCALIDRAW: "导出为 .excalidraw 文件(旧版绘图文件格式)",
|
||||
LINK_BUTTON_CLICK_NO_TEXT:
|
||||
"请选择一个含有链接的图形或文本元素。",
|
||||
@@ -96,9 +136,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: "使用“插入任意文件(以 iFrame 形式嵌入)”功能来嵌入 Markdown 文档",
|
||||
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: "显示更新说明",
|
||||
@@ -110,29 +156,83 @@ export default {
|
||||
"<b>开启:</b>当本插件存在可用更新时,显示通知。<br>" +
|
||||
"<b>关闭:</b>您需要手动检查本插件的更新(设置 - 第三方插件 - 检查更新)。",
|
||||
|
||||
BASIC_HEAD: "基本",
|
||||
BASIC_DESC: `包括:更新说明,更新提示,新绘图文件、模板文件、脚本文件的存储路径等的设置。`,
|
||||
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:
|
||||
"在命令面板中执行“新建绘图”系列命令时," +
|
||||
"新建的绘图文件的存储路径。<br>" +
|
||||
"<b>开启:</b>使用 Excalidraw 文件夹。 <br><b>关闭:</b>使用 Obsidian 设置的新附件默认位置。",
|
||||
"<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 自动化脚本。" +
|
||||
"您可以在 Obsidian 命令面板中执行这些脚本," +
|
||||
"还可以为喜欢的脚本分配快捷键,就像为其他 Obsidian 命令分配快捷键一样。<br>" +
|
||||
"该项不能设为库的根目录。",
|
||||
AI_HEAD: "AI(实验性)",
|
||||
AI_DESC: `OpenAI GPT API 的设置。 ` +
|
||||
`目前 OpenAI API 还处于测试中,您需要在自己的。` +
|
||||
`OpenAI 账户中充值至少 5 美元后才能生成 API key,` +
|
||||
`然后就可以在 Excalidraw 中配置并使用 AI。`,
|
||||
AI_OPENAI_TOKEN_NAME: "OpenAI API key",
|
||||
AI_OPENAI_TOKEN_DESC:
|
||||
"您可以访问您的<a href='https://platform.openai.com/api-keys'> OpenAI 账户</a>来获取自己的 OpenAI API key。",
|
||||
AI_OPENAI_TOKEN_PLACEHOLDER: "OpenAI API key",
|
||||
AI_OPENAI_DEFAULT_MODEL_NAME: "默认的文本 AI 模型",
|
||||
AI_OPENAI_DEFAULT_MODEL_DESC:
|
||||
"使用哪个 AI 模型来生成文本。请填写有效的 OpenAI 模型名称。" +
|
||||
"您可访问<a href='https://platform.openai.com/docs/models'> OpenAI 网站</a>了解更多模型信息。",
|
||||
AI_OPENAI_DEFAULT_MODEL_PLACEHOLDER: "gpt-3.5-turbo-1106",
|
||||
AI_OPENAI_DEFAULT_IMAGE_MODEL_NAME: "默认的图像 AI 模型",
|
||||
AI_OPENAI_DEFAULT_IMAGE_MODEL_DESC:
|
||||
"使用哪个 AI 模型来生成图像(在编辑和调整图像时会强制使用 dall-e-2 模型," +
|
||||
"因为目前只有该模型支持编辑和调整图像)。" +
|
||||
"请填写有效的 OpenAI 模型名称。" +
|
||||
"您可访问<a href='https://platform.openai.com/docs/models'>OpenAI 网站</a>了解更多模型信息。",
|
||||
AI_OPENAI_DEFAULT_IMAGE_MODEL_PLACEHOLDER: "dall-e-3",
|
||||
AI_OPENAI_DEFAULT_VISION_MODEL_NAME: "默认的 AI 视觉模型",
|
||||
AI_OPENAI_DEFAULT_VISION_MODEL_DESC:
|
||||
"根据文本生成图像时,使用哪个 AI 视觉模型。请填写有效的 OpenAI 模型名称。" +
|
||||
"您可访问<a href='https://platform.openai.com/docs/models'> OpenAI 网站</a>了解更多模型信息。",
|
||||
AI_OPENAI_DEFAULT_API_URL_NAME: "OpenAI API URL",
|
||||
AI_OPENAI_DEFAULT_API_URL_DESC:
|
||||
"默认的 OpenAI API URL。请填写有效的 OpenAI API URL。" +
|
||||
"Excalidraw 会通过该 URL 发送 API 请求给 OpenAI。我没有对此选项做任何错误处理,请谨慎修改。",
|
||||
AI_OPENAI_DEFAULT_IMAGE_API_URL_NAME: "OpenAI 图像生成 API URL",
|
||||
AI_OPENAI_DEFAULT_VISION_MODEL_PLACEHOLDER: "gpt-4-vision-preview",
|
||||
SAVING_HEAD: "保存",
|
||||
SAVING_DESC: "包括:压缩,自动保存的时间间隔,文件的命名格式和扩展名等的设置。",
|
||||
COMPRESS_NAME: "压缩 Excalidraw JSON",
|
||||
COMPRESS_DESC:
|
||||
"Excalidraw 绘图文件默认将元素记录为 JSON 格式。开启此项,可将元素的 JSON 数据以 BASE64 编码" +
|
||||
@@ -144,6 +244,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:
|
||||
"每隔多长时间自动保存一次(如果绘图文件没有发生改变,将不会保存)。" +
|
||||
@@ -180,19 +288,20 @@ FILENAME_HEAD: "文件名",
|
||||
FILENAME_EXCALIDRAW_EXTENSION_DESC:
|
||||
"该选项在兼容模式(即非 Excalidraw 专用 Markdown 文件)下不会生效。<br>" +
|
||||
"<b>开启:</b>使用 .excalidraw.md 作为扩展名。<br><b>关闭:</b>使用 .md 作为扩展名。",
|
||||
DISPLAY_HEAD: "显示",
|
||||
DISPLAY_HEAD: "界面 & 行为",
|
||||
DISPLAY_DESC: "包括:左手模式,主题匹配,缩放,激光笔工具,修饰键等的设置。",
|
||||
DYNAMICSTYLE_NAME: "动态样式",
|
||||
DYNAMICSTYLE_DESC:
|
||||
"根据画布颜色调节 Excalidraw 界面颜色",
|
||||
"根据画布颜色自动调节 Excalidraw 界面颜色",
|
||||
LEFTHANDED_MODE_NAME: "左手模式",
|
||||
LEFTHANDED_MODE_DESC:
|
||||
"目前只在托盘模式下生效。若开启此项,则托盘(绘图工具属性页)将位于右侧。" +
|
||||
"<br><b>开启:</b>左手模式。<br><b>关闭:</b>右手模式。",
|
||||
IFRAME_MATCH_THEME_NAME: "使 MD-Embed 匹配 Excalidraw 主题",
|
||||
IFRAME_MATCH_THEME_NAME: "使 Embeddable 匹配 Excalidraw 主题",
|
||||
IFRAME_MATCH_THEME_DESC:
|
||||
"<b>开启:</b>当你的 Obsidian 和 Excalidraw 一个使用黑暗主题、一个使用明亮主题时," +
|
||||
"开启此项,MD-Embed 将会匹配 Excalidraw 主题。<br>" +
|
||||
"<b>关闭:</b>如果你想要 MD-Embed 匹配 Obsidian 主题,请关闭此项。",
|
||||
"<b>开启:</b>当 Obsidian 和 Excalidraw 一个使用黑暗主题、一个使用明亮主题时," +
|
||||
"开启此项后,以交互形式嵌入到绘图中的元素(Embeddable) 将会匹配 Excalidraw 主题。<br>" +
|
||||
"<b>关闭:</b>如果您想要 Embeddable 匹配 Obsidian 主题,请关闭此项。",
|
||||
MATCH_THEME_NAME: "使新建的绘图匹配 Obsidian 主题",
|
||||
MATCH_THEME_DESC:
|
||||
"如果 Obsidian 使用黑暗主题,新建的绘图文件也将使用黑暗主题。<br>" +
|
||||
@@ -213,11 +322,30 @@ FILENAME_HEAD: "文件名",
|
||||
DEFAULT_PEN_MODE_NAME: "触控笔模式(Pen mode)",
|
||||
DEFAULT_PEN_MODE_DESC:
|
||||
"打开绘图时,是否自动开启触控笔模式?",
|
||||
|
||||
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: "在 Markdown 文件的悬停预览中渲染为图片",
|
||||
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_DESC:
|
||||
"这个设置影响 frontmatter 中具有 <b>excalidraw-open-md: true</b> 的文件。",
|
||||
SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME: "在 Markdown 文件阅读模式下渲染为图片",
|
||||
SHOW_DRAWING_OR_MD_IN_READING_MODE_DESC:
|
||||
"必须关闭活动的 Excalidraw/Markdown 文件,然后重新打开才能使此更改生效。<br>当您处于 Markdown 阅读模式(即阅读 Excalidraw 的背景笔记)时 Excalidraw 绘图是否应该呈现为图像? " +
|
||||
"此设置不会影响您处于 Excalidraw 模式时的绘图显示,也不会影响将绘图嵌入到 Markdown 文档中或在渲染悬停预览时的显示。<br><ul>" +
|
||||
"<li>看下面的“嵌入和导出”中的 <b>PDF 导出</b>的其他相关设置。</li>" +
|
||||
"<li>请务必查看“其他功能”部分中的<b>淡化设置</b>。</li></ul>",
|
||||
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME: "在 Markdown 模式下导出为 PDF 时渲染为图片",
|
||||
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_DESC:
|
||||
"必须关闭活动的 Excalidraw/Markdown 文件,然后重新打开才能使此更改生效。<br>当您处于 Markdown 阅读模式(即阅读 Excalidraw 的背景笔记)时将笔记导出为 PDF,Excalidraw 绘图是否应该呈现为图像? <br><ul>" +
|
||||
"<li>查看上面“外观和行为”下的 <b>Markdown 阅读模式</b>的其他相关设置。</li>" +
|
||||
"<li>请务必查看“其他功能”部分中的<b>淡化设置</b>。</li></ul>",
|
||||
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:
|
||||
@@ -232,14 +360,39 @@ FILENAME_HEAD: "文件名",
|
||||
ZOOM_TO_FIT_MAX_LEVEL_NAME: "自动缩放的最高级别",
|
||||
ZOOM_TO_FIT_MAX_LEVEL_DESC:
|
||||
"自动缩放画布时,允许放大的最高级别。该值不能低于 0.5(50%)且不能超过 10(1000%)。",
|
||||
LINKS_HEAD: "链接(Links) & 以内部链接形式嵌入到绘图中的 Markdown 文档(Transclusion)",
|
||||
LASER_HEAD: "激光笔工具(More Tools > Laser pointer)",
|
||||
LASER_COLOR: "激光笔颜色",
|
||||
LASER_DECAY_TIME_NAME: "激光笔消失时间",
|
||||
LASER_DECAY_TIME_DESC: "单位是毫秒,默认是 1000(即 1 秒)。",
|
||||
LASER_DECAY_LENGTH_NAME: "激光笔轨迹长度",
|
||||
LASER_DECAY_LENGTH_DESC: "默认是 50。",
|
||||
LINKS_HEAD: "链接 & 以内部链接形式嵌入到绘图中的 Markdown 文档(MD-Transclusion)& 待办任务(Todo)",
|
||||
LINKS_HEAD_DESC: "包括:链接的打开和显示,MD-Transclusion 的显示,Todo 的显示等设置。",
|
||||
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>。",
|
||||
DRAG_MODIFIER_NAME: "修饰键",
|
||||
DRAG_MODIFIER_DESC: "在您按住点击链接或拖放元素时,可以触发某些行为。您可以为这些行为添加修饰键。" +
|
||||
"Excalidraw 不会检查您的设置是否合理,因此请谨慎设置,避免冲突。" +
|
||||
"以下选项在苹果和非苹果设备上区别很大,如果您在多个硬件平台上使用 Obsidian,需要分别进行设置。"+
|
||||
"选项里的 4 个开关依次代表 " +
|
||||
(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>` +
|
||||
@@ -249,22 +402,22 @@ FILENAME_HEAD: "文件名",
|
||||
MAINWORKSPACE_PANE_NAME: "在主工作区中打开",
|
||||
MAINWORKSPACE_PANE_DESC:
|
||||
`按住 ${labelCTRL()}+${labelSHIFT()} 并点击绘图里的内部链接时,插件默认会在当前窗口的新面板中打开该链接。<br>` +
|
||||
"若开启此项,Excalidraw 会在主工作区的面板中打开该链接。",
|
||||
"若开启此项,Excalidraw 会在主工作区的面板中打开该链接。",
|
||||
LINK_BRACKETS_NAME: "在链接的两侧显示 <code>[[中括号]]</code>",
|
||||
LINK_BRACKETS_DESC: `${
|
||||
"文本元素处于预览(PREVIEW)模式时,在内部链接的两侧显示中括号。<br>" +
|
||||
"您可为某个绘图单独设置此项,方法是在其 frontmatter 中添加形如 <code>"
|
||||
}${FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS}: true/false</code> 的键值对。`,
|
||||
}${FRONTMATTER_KEYS["link-brackets"].name}: true/false</code> 的键值对。`,
|
||||
LINK_PREFIX_NAME: "内部链接的前缀",
|
||||
LINK_PREFIX_DESC: `${
|
||||
"文本元素处于预览(PREVIEW)模式时,如果其中包含链接,则添加此前缀。<br>" +
|
||||
"您可为某个绘图单独设置此项,方法是在其 frontmatter 中添加形如 <code>"
|
||||
}${FRONTMATTER_KEY_CUSTOM_PREFIX}: "📍 "</code> 的键值对。`,
|
||||
}${FRONTMATTER_KEYS["link-prefix"].name}: "📍 "</code> 的键值对。`,
|
||||
URL_PREFIX_NAME: "外部链接的前缀",
|
||||
URL_PREFIX_DESC: `${
|
||||
"文本元素处于预览(PREVIEW)模式时,如果其中包含外部链接,则添加此前缀。<br>" +
|
||||
"您可为某个绘图单独设置此项,方法是在其 frontmatter 中添加形如 <code>"
|
||||
}${FRONTMATTER_KEY_CUSTOM_URL_PREFIX}: "🌐 "</code> 的键值对。`,
|
||||
}${FRONTMATTER_KEYS["url-prefix"].name}: "🌐 "</code> 的键值对。`,
|
||||
PARSE_TODO_NAME: "待办任务(Todo)",
|
||||
PARSE_TODO_DESC: "将文本元素中的 <code>- [ ]</code> 和 <code>- [x]</code> 前缀显示为方框。",
|
||||
TODO_NAME: "未完成项目",
|
||||
@@ -274,7 +427,7 @@ FILENAME_HEAD: "文件名",
|
||||
HOVERPREVIEW_NAME: "鼠标悬停预览内部链接",
|
||||
HOVERPREVIEW_DESC:
|
||||
`<b>开启:</b>在 Excalidraw <u>阅读模式(View)</u>下,鼠标悬停在 <code>[[内部链接]]</code> 上即可预览;` +
|
||||
"而在<u>普通模式(Normal)</u>下, 鼠标悬停在内部链接右上角的蓝色标识上即可预览。<br> " +
|
||||
"而在<u>普通模式(Normal)</u>下,鼠标悬停在内部链接右上角的蓝色标识上即可预览。<br> " +
|
||||
`<b>关闭:</b>鼠标悬停在 <code>[[内部链接]]</code> 上,并且按住 ${labelCTRL()} 才能预览。`,
|
||||
LINKOPACITY_NAME: "链接标识的透明度",
|
||||
LINKOPACITY_DESC:
|
||||
@@ -283,42 +436,46 @@ FILENAME_HEAD: "文件名",
|
||||
`按住 ${labelCTRL()} 并点击含有 [[链接]] 或 [别名](链接) 的文本来打开链接`,
|
||||
LINK_CTRL_CLICK_DESC:
|
||||
"如果此功能影响到您使用某些原版 Excalidraw 功能,可将其关闭。" +
|
||||
"关闭后,您只能通过绘图面板标题栏中的链接按钮来打开链接。",
|
||||
TRANSCLUSION_WRAP_NAME: "Transclusion 的折行方式",
|
||||
"关闭后,您可以使用 ${labelCTRL()} + ${labelMETA()} 或者元素右上角的链接指示器来打开链接。",
|
||||
TRANSCLUSION_WRAP_NAME: "MD-Transclusion 的折行方式",
|
||||
TRANSCLUSION_WRAP_DESC:
|
||||
"中的 number 表示嵌入的文本溢出时,在第几个字符处进行折行。<br>" +
|
||||
"此开关控制具体的折行方式。若开启,则严格在 number 处折行,禁止溢出;" +
|
||||
"若关闭,则允许在 number 位置后最近的空格处折行。",
|
||||
TRANSCLUSION_DEFAULT_WRAP_NAME: "Transclusion 的默认折行位置",
|
||||
TRANSCLUSION_DEFAULT_WRAP_NAME: "MD-Transclusion 的默认折行位置",
|
||||
TRANSCLUSION_DEFAULT_WRAP_DESC:
|
||||
"除了通过 <code>![[doc#^block]]{number}</code> 中的 number 来控制折行位置,您也可以在此设置 number 的默认值。<br>" +
|
||||
"一般设为 0 即可,表示不设置固定的默认值,这样当您需要嵌入文档到便签中时," +
|
||||
"Excalidraw 能更好地帮您自动处理。",
|
||||
PAGE_TRANSCLUSION_CHARCOUNT_NAME: "Transclusion 的最大显示字符数",
|
||||
PAGE_TRANSCLUSION_CHARCOUNT_NAME: "MD-Transclusion 的最大显示字符数",
|
||||
PAGE_TRANSCLUSION_CHARCOUNT_DESC:
|
||||
"以 <code>![[内部链接]]</code> 或 <code></code> 的形式将文档以文本形式嵌入到绘图中时," +
|
||||
"该文档在绘图中可显示的最大字符数量。",
|
||||
QUOTE_TRANSCLUSION_REMOVE_NAME: "隐藏 Transclusion 行首的引用符号",
|
||||
QUOTE_TRANSCLUSION_REMOVE_DESC: "不显示 Transclusion 中每一行行首的 > 符号,以提高纯文本 Transclusion 的可读性。<br>" +
|
||||
QUOTE_TRANSCLUSION_REMOVE_NAME: "隐藏 MD-Transclusion 行首的引用符号",
|
||||
QUOTE_TRANSCLUSION_REMOVE_DESC: "不显示 MD-Transclusion 中每一行行首的 > 符号,以提高纯文本 MD-Transclusion 的可读性。<br>" +
|
||||
"<b>开启:</b>隐藏 > 符号<br><b>关闭:</b>不隐藏 > 符号(注意,由于 Obsidian API 的原因,首行行首的 > 符号不会被隐藏)",
|
||||
GET_URL_TITLE_NAME: "使用 iframly 获取页面标题",
|
||||
GET_URL_TITLE_DESC:
|
||||
"拖放链接到 Excalidraw 时,使用 <code>http://iframely.server.crestify.com/iframely?url=</code> 来获取页面的标题。",
|
||||
PDF_TO_IMAGE: "以图像形式嵌入到绘图中的 PDF 文档",
|
||||
PDF_TO_IMAGE_SCALE_NAME: "分辨率",
|
||||
PDF_TO_IMAGE_SCALE_DESC: "分辨率越高,图像越清晰,但内存占用也越大。" +
|
||||
"此外,如果您想要复制这些图像到 Excalidraw.com,可能会超出其 2MB 大小的限制。",
|
||||
EMBED_TOEXCALIDRAW_HEAD: "嵌入到绘图中的文件",
|
||||
EMBED_TOEXCALIDRAW_DESC: "包括:以图像形式嵌入到绘图中的 PDF 文档、以交互形式嵌入到绘图中的 Markdown 文档(MD-Embeddable)、以图像形式嵌入的 Markdown 文档(MD-Embed)等。",
|
||||
MD_HEAD: "以图像形式嵌入到绘图中的 Markdown 文档(MD-Embed)",
|
||||
MD_HEAD_DESC:
|
||||
"除了 Transclusion,您还可以将 Markdown 文档以图像形式嵌入到绘图中。" +
|
||||
`方法是按住 ${labelCTRL()} 并从文件管理器中把文档拖入绘图,或者执行“以图像形式嵌入”系列命令。`,
|
||||
|
||||
MD_EMBED_CUSTOMDATA_HEAD_NAME: "以交互形式嵌入到绘图中的 Markdown 文档(MD-Embeddable)",
|
||||
MD_EMBED_CUSTOMDATA_HEAD_DESC: `这些选项不会影响到已存在的 MD-Embeddable。MD-Embeddable 的主题风格在“显示 & 行为”小节设置。`,
|
||||
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:
|
||||
@@ -339,21 +496,33 @@ 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> 的键值对。",
|
||||
EMBED_HEAD: "嵌入到 Markdown 文档中的绘图 & 导出",
|
||||
EMBED_CACHING: "启用预览图",
|
||||
EMBED_SIZING: "预览图的尺寸",
|
||||
EMBED_THEME_BACKGROUND: "预览图的主题和背景色",
|
||||
EMBED_IMAGE_CACHE_NAME: "为嵌入到 Markdown 文档中的绘图创建预览图",
|
||||
EMBED_IMAGE_CACHE_DESC: "为嵌入到文档中的绘图创建预览图。可提高下次嵌入的速度。" +
|
||||
"您可为某个 MD-Embed 单独设置此项,方法是在其源文件的 frontmatter 中添加形如 <code>excalidraw-css: 库中的 CSS 文件或 CSS 片段</code> 的键值对。",
|
||||
EMBED_HEAD: "嵌入到 Markdown 文档中的绘图",
|
||||
EMBED_DESC: `包括:嵌入到 Markdown 文档中的绘图的预览图类型(SVG、PNG)、源文件类型(Excalidraw 绘图文件、SVG、PNG)、缓存、图像大小、图像主题,以及嵌入的语法等。
|
||||
此外,还有自动导出 SVG 或 PNG 文件并保持与绘图文件状态同步的设置。`,
|
||||
EMBED_CANVAS: "Obsidian 白板支持",
|
||||
EMBED_CANVAS_NAME: "沉浸式嵌入",
|
||||
EMBED_CANVAS_DESC:
|
||||
"当嵌入绘图到 Obsidian 白板中时,隐藏元素的边界和背景。" +
|
||||
"注意:如果想要背景完全透明,您依然需要在 Excalidraw 中设置“导出的图像不包含背景”。",
|
||||
EMBED_CACHING: "预览图缓存",
|
||||
EXPORT_SUBHEAD: "导出",
|
||||
EMBED_SIZING: "图像尺寸",
|
||||
EMBED_THEME_BACKGROUND: "图像的主题和背景色",
|
||||
EMBED_IMAGE_CACHE_NAME: "为嵌入到 Markdown 文档中的绘图创建预览图缓存",
|
||||
EMBED_IMAGE_CACHE_DESC: "可提高下次嵌入的速度。" +
|
||||
"但如果绘图中又嵌入了子绘图,当子绘图改变时,您需要打开子绘图并手动保存,才能够更新父绘图的预览图。",
|
||||
EMBED_IMAGE_CACHE_CLEAR: "清除预览图",
|
||||
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 时,本插件会自动清理无用的备份。您确定要删除所有备份吗?",
|
||||
BACKUP_CACHE_CLEAR_CONFIRMATION: "该操作将删除所有绘图文件的备份。备份是绘图文件损坏时的一种补救手段。每次您打开 Obsidian 时,本插件会自动清理无用的备份。您确定要现在删除所有备份吗?",
|
||||
EMBED_REUSE_EXPORTED_IMAGE_NAME:
|
||||
"将之前已导出的图像作为预览图",
|
||||
EMBED_REUSE_EXPORTED_IMAGE_DESC:
|
||||
@@ -368,9 +537,9 @@ FILENAME_HEAD: "文件名",
|
||||
"<b>关闭:</b>为嵌入到 Markdown 文档中的绘图生成 <a href='' target='_blank'>PNG</a> 格式的预览图。注意:PNG 格式预览图不支持某些 <a href='https://www.youtube.com/watch?v=yZQoJg2RCKI&t=633s' target='_blank'>绘图元素的块引用特性</a>。",*/
|
||||
EMBED_PREVIEW_IMAGETYPE_NAME: "预览图的格式",
|
||||
EMBED_PREVIEW_IMAGETYPE_DESC:
|
||||
"<b>原始 SVG:</b>高品质、可交互。<br>" +
|
||||
"<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>" +
|
||||
@@ -380,12 +549,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 副本,当副本不存在时,该命令将会插入一条损坏的链接,您需要打开绘图文件并手动导出副本才能修复 —— " +
|
||||
"也就是说,该选项不会自动帮您生成 PNG/SVG 副本,而只会引用已有的 PNG/SVG 副本。",
|
||||
EMBED_MARKDOWN_COMMENT_NAME: "将链接作为注释嵌入",
|
||||
EMBED_MARKDOWN_COMMENT_DESC:
|
||||
"在图像下方以 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></code>。",
|
||||
@@ -396,13 +575,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 文件名与绘图文件同步",
|
||||
@@ -423,6 +606,26 @@ 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 格式,因为本插件的很多功能在旧格式中无法使用。",
|
||||
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>" +
|
||||
"如果您使用 <a href='https://github.com/deathau/sliding-panes-obsidian' target='_blank'>Sliding Panes 插件</a>," +
|
||||
"您可以开启此项来使 Excalidraw 绘图兼容此插件。<br>" +
|
||||
"注意,开启后会产生一些与 Obsidian 工作空间的兼容性问题。<br>" +
|
||||
"另外,Obsidian 现在已经原生支持 Stack Tabs(堆叠标签),基本实现了 Sliding Panes 插件的功能。",
|
||||
EXPORT_EXCALIDRAW_NAME: "自动导出 Excalidraw 旧格式副本",
|
||||
EXPORT_EXCALIDRAW_DESC: "和“自动导出 SVG 副本”类似,但是导出格式为 *.excalidraw。",
|
||||
SYNC_EXCALIDRAW_NAME:
|
||||
@@ -432,6 +635,7 @@ FILENAME_HEAD: "文件名",
|
||||
"则根据旧格式文件的内容来更新新格式文件。",
|
||||
COMPATIBILITY_MODE_NAME: "以旧格式创建新绘图",
|
||||
COMPATIBILITY_MODE_DESC:
|
||||
"⚠️ 慎用!99.9% 的情况下您不需要开启此项。" +
|
||||
"开启此功能后,您通过功能区按钮、命令面板、" +
|
||||
"文件浏览器等创建的绘图都将是旧格式(*.excalidraw)。" +
|
||||
"此外,您打开旧格式绘图文件时将不再收到警告消息。",
|
||||
@@ -442,16 +646,36 @@ FILENAME_HEAD: "文件名",
|
||||
LATEX_DEFAULT_NAME: "插入 LaTeX 时的默认表达式",
|
||||
LATEX_DEFAULT_DESC: "允许留空。允许使用类似 <code>\\color{white}</code> 的格式化表达式。",
|
||||
NONSTANDARD_HEAD: "非 Excalidraw.com 官方支持的特性",
|
||||
NONSTANDARD_DESC: "这些特性不受 Excalidraw.com 官方支持。当导出绘图到 Excalidraw.com 时,这些特性将会发生变化。",
|
||||
CUSTOM_PEN_NAME: "自定义画笔的数量",
|
||||
CUSTOM_PEN_DESC: "在画布上的 Obsidian 菜单旁边切换自定义画笔。长按画笔按钮可以修改其样式。",
|
||||
EXPERIMENTAL_HEAD: "实验性功能",
|
||||
EXPERIMENTAL_DESC:
|
||||
"以下部分设置不会立即生效,需要刷新文件资源管理器或者重启 Obsidian 才会生效。",
|
||||
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 菜单按钮旁边切换自定义画笔。长按画笔按钮可以修改其样式。",
|
||||
EXPERIMENTAL_HEAD: "杂项",
|
||||
EXPERIMENTAL_DESC: `包括:默认的 LaTeX 公式,字段建议,绘图文件的类型标识符,OCR 等设置。`,
|
||||
EA_HEAD: "Excalidraw 自动化",
|
||||
EA_DESC:
|
||||
"ExcalidrawAutomate 是用于 Excalidraw 自动化脚本的 API,但是目前说明文档还不够完善," +
|
||||
"建议阅读 <a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/docs/API/ExcalidrawAutomate.d.ts'>ExcalidrawAutomate.d.ts</a> 文件源码," +
|
||||
"参考 <a href='https://zsviczian.github.io/obsidian-excalidraw-plugin/'>ExcalidrawAutomate How-to</a> 网页(不过该网页" +
|
||||
"有一段时间未更新了),并开启下方的字段建议。字段建议功能能够在您键入时提示可用的" +
|
||||
"函数及相应的参数,而且附带描述,相当于最新的“文档”。",
|
||||
FIELD_SUGGESTER_NAME: "开启字段建议",
|
||||
FIELD_SUGGESTER_DESC:
|
||||
"开启后,当您在编辑器中输入 <code>excalidraw-</code> 或者 <code>ea.</code> 时,会弹出一个带有函数说明的自动补全提示菜单。<br>" +
|
||||
"该功能借鉴了 Breadcrumbs 和 Templater 插件。",
|
||||
STARTUP_SCRIPT_NAME: "起动脚本",
|
||||
STARTUP_SCRIPT_DESC:
|
||||
"插件启动时将自动执行该脚本。可用于为您的 Excalidraw 自动化脚本设置钩子。" +
|
||||
"起动脚本请用 javascript 代码编写,并保存为 Markdown 格式。",
|
||||
STARTUP_SCRIPT_BUTTON_CREATE: "创建起动脚本",
|
||||
STARTUP_SCRIPT_BUTTON_OPEN: "打开起动脚本",
|
||||
STARTUP_SCRIPT_EXISTS: "起动脚本已存在",
|
||||
FILETYPE_NAME: "在文件浏览器中为 excalidraw.md 文件添加类型标识符(如 ✏️)",
|
||||
FILETYPE_DESC:
|
||||
"可通过下一项设置来自定义类型标识符。",
|
||||
@@ -463,6 +687,17 @@ FILENAME_HEAD: "文件名",
|
||||
"开启此项,则可在 Obsidian 实时预览模式的编辑视图下,用形如 <code>![[绘图|宽度|样式]]</code> 的语法来嵌入绘图。<br>" +
|
||||
"该选项不会在已打开的文档中立刻生效 —— " +
|
||||
"你需要重新打开此文档来使其生效。",
|
||||
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>" +
|
||||
@@ -471,17 +706,18 @@ 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 自动化脚本包含设置项,当执行这些脚本时,它们会在该列表下添加设置项。",
|
||||
TASKBONE_HEAD: "Taskbone OCR(光学符号识别)",
|
||||
TASKBONE_DESC: "这是一个将 OCR 融入 Excalidraw 的实验性功能。请注意,Taskbone 是一项独立的外部服务,而不是由 Excalidraw 或 Obsidian-excalidraw-plugin 项目提供的。" +
|
||||
"OCR 能够对画布上用自由画笔工具写下的涂鸦或者嵌入的图像进行文本识别,并将识别出来的文本写入绘图文件的 frontmatter,同时复制到剪贴板。" +
|
||||
"之所以要写入 frontmatter 是为了便于您在 Obsidian 中能够搜索到这些文本。" +
|
||||
"注意,识别的过程不是在本地进行的,而是通过在线 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>.",
|
||||
TASKBONE_ENABLE_DESC: "启用意味着您同意 Taskbone <a href='https://www.taskbone.com/legal/terms/' target='_blank'>条款及细则</a> 以及 " +
|
||||
"<a href='https://www.taskbone.com/legal/privacy/' target='_blank'>隐私政策</a>。",
|
||||
TASKBONE_APIKEY_NAME: "Taskbone API Key",
|
||||
TASKBONE_APIKEY_DESC: "Taskbone 的免费 API key 提供了一定数量的每月识别次数。如果您非常频繁地使用此功能,或者想要支持 " +
|
||||
"Taskbone 的开发者(您懂的,没有人能用爱发电,Taskbone 开发者也需要投入资金来维持这项 OCR 服务)您可以" +
|
||||
@@ -489,9 +725,12 @@ FILENAME_HEAD: "文件名",
|
||||
|
||||
//openDrawings.ts
|
||||
SELECT_FILE: "选择一个文件后按回车。",
|
||||
SELECT_FILE_WITH_OPTION_TO_SCALE: `选择一个文件后按回车,或者 ${labelSHIFT()}+${labelMETA()}+ENTER 以 100% 尺寸插入。`,
|
||||
SELECT_COMMAND: "选择一个命令后按回车。",
|
||||
SELECT_FILE_WITH_OPTION_TO_SCALE: `选择一个文件后按回车,或者 ${labelSHIFT()}+${labelMETA()}+Enter 以 100% 尺寸插入。`,
|
||||
NO_MATCH: "查询不到匹配的文件。",
|
||||
NO_MATCHING_COMMAND: "查询不到匹配的命令。",
|
||||
SELECT_FILE_TO_LINK: "选择要插入(以内部链接形式嵌入)到当前绘图中的文件。",
|
||||
SELECT_COMMAND_PLACEHOLDER: "选择要插入到当前绘图中的命令。",
|
||||
SELECT_DRAWING: "选择要插入(以图像形式嵌入)到当前绘图中的图像或绘图文件。",
|
||||
TYPE_FILENAME: "键入要选择的绘图名称。",
|
||||
SELECT_FILE_OR_TYPE_NEW:
|
||||
@@ -502,6 +741,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",
|
||||
@@ -528,8 +774,32 @@ FILENAME_HEAD: "文件名",
|
||||
NARROW_TO_BLOCK: "缩放至块",
|
||||
SHOW_ENTIRE_FILE: "显示全部",
|
||||
ZOOM_TO_FIT: "缩放至合适大小",
|
||||
RELOAD: "重载",
|
||||
RELOAD: "重载链接",
|
||||
OPEN_IN_BROWSER: "在浏览器中打开",
|
||||
PROPERTIES: "属性",
|
||||
COPYCODE: "复制源文件",
|
||||
|
||||
//EmbeddableSettings.tsx
|
||||
ES_TITLE: "Embeddable 元素设置",
|
||||
ES_RENAME: "重命名",
|
||||
ES_ZOOM: "缩放",
|
||||
ES_YOUTUBE_START: "YouTube 起始时间",
|
||||
ES_YOUTUBE_START_DESC: "ss, mm:ss, hh:mm:ss",
|
||||
ES_YOUTUBE_START_INVALID: "YouTube 起始时间无效。请检查格式并重试",
|
||||
ES_FILENAME_VISIBLE: "显示文件名",
|
||||
ES_BACKGROUND_HEAD: "背景色",
|
||||
ES_BACKGROUND_MATCH_ELEMENT: "匹配元素背景色",
|
||||
ES_BACKGROUND_MATCH_CANVAS: "匹配画布背景色",
|
||||
ES_BACKGROUND_COLOR: "背景色",
|
||||
ES_BORDER_HEAD: "边框颜色",
|
||||
ES_BORDER_COLOR: "边框颜色",
|
||||
ES_BORDER_MATCH_ELEMENT: "匹配元素边框颜色",
|
||||
ES_BACKGROUND_OPACITY: "背景透明度",
|
||||
ES_BORDER_OPACITY: "边框透明度",
|
||||
ES_EMBEDDABLE_SETTINGS: "MD-Embeddable 设置",
|
||||
ES_USE_OBSIDIAN_DEFAULTS: "使用 Obsidian 默认设置",
|
||||
ES_ZOOM_100_RELATIVE_DESC: "使元素的缩放等级等于当前画布的缩放等级",
|
||||
ES_ZOOM_100: "Relative 100%",
|
||||
|
||||
//Prompts.ts
|
||||
PROMPT_FILE_DOES_NOT_EXIST: "文件不存在。要创建吗?",
|
||||
@@ -538,7 +808,11 @@ FILENAME_HEAD: "文件名",
|
||||
PROMPT_TITLE_NEW_FILE: "新建文件",
|
||||
PROMPT_TITLE_CONFIRMATION: "确认",
|
||||
PROMPT_BUTTON_CREATE_EXCALIDRAW: "创建 Excalidraw 绘图",
|
||||
PROMPT_BUTTON_CREATE_EXCALIDRAW_ARIA: "创建 Excalidraw 绘图并在新页签中打开",
|
||||
PROMPT_BUTTON_CREATE_MARKDOWN: "创建 Markdown 文档",
|
||||
PROMPT_BUTTON_CREATE_MARKDOWN_ARIA: "创建 Markdown 文档并在新页签中打开",
|
||||
PROMPT_BUTTON_EMBED_MARKDOWN: "嵌入",
|
||||
PROMPT_BUTTON_EMBED_MARKDOWN_ARIA: "将所选元素替换为 MD-Embeddable",
|
||||
PROMPT_BUTTON_NEVERMIND: "算了",
|
||||
PROMPT_BUTTON_OK: "OK",
|
||||
PROMPT_BUTTON_CANCEL: "取消",
|
||||
@@ -546,5 +820,19 @@ 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: "启用框架裁剪",
|
||||
};
|
||||
|
||||
1540
src/main.ts
1540
src/main.ts
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,11 @@
|
||||
import * as React from "react";
|
||||
import ExcalidrawView from "../ExcalidrawView";
|
||||
import { Notice } from "obsidian";
|
||||
|
||||
type ButtonProps = {
|
||||
title: string;
|
||||
action: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
longpress?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
icon: JSX.Element;
|
||||
view: ExcalidrawView;
|
||||
};
|
||||
|
||||
type ButtonState = {
|
||||
@@ -24,6 +23,10 @@ export class ActionButton extends React.Component<ButtonProps, ButtonState> {
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.render = () => null;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<button
|
||||
@@ -48,7 +51,8 @@ export class ActionButton extends React.Component<ButtonProps, ButtonState> {
|
||||
onPointerDown={(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
this.toastMessageTimeout = window.setTimeout(
|
||||
() => {
|
||||
this.props.view.excalidrawAPI?.setToast({message:this.props.title, duration: 3000, closable: true});
|
||||
new Notice(this.props.title, 3000);
|
||||
//this.props.view.excalidrawAPI?.setToast({message:this.props.title, duration: 3000, closable: true});
|
||||
this.toastMessageTimeout = 0;
|
||||
},
|
||||
400,
|
||||
@@ -58,7 +62,8 @@ export class ActionButton extends React.Component<ButtonProps, ButtonState> {
|
||||
if(this.props.longpress) {
|
||||
this.props.longpress(event);
|
||||
} else {
|
||||
this.props.view.excalidrawAPI?.setToast({message:"Cannot pin this action", duration: 3000, closable: true});
|
||||
new Notice("Cannot pin this action", 3000);
|
||||
//this.props.view.excalidrawAPI?.setToast({message:"Cannot pin this action", duration: 3000, closable: true});
|
||||
}
|
||||
this.longpressTimeout = 0;
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Copy, Globe, RotateCcw, Scan, Settings } from "lucide-react";
|
||||
import { Copy, Crop, Globe, RotateCcw, Scan, Settings, TextSelect } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { PenStyle } from "src/PenTypes";
|
||||
|
||||
@@ -26,9 +26,11 @@ export const ICONS = {
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
BackOfNote: (<TextSelect />),
|
||||
Reload: (<RotateCcw />),
|
||||
Copy: (<Copy /> ),
|
||||
Globe: (<Globe />),
|
||||
Crop: (<Crop />),
|
||||
ZoomToSelectedElement: (<Scan />),
|
||||
Properties: (<Settings />),
|
||||
ZoomToSection: (
|
||||
|
||||
@@ -7,13 +7,18 @@ import { ActionButton } from "./ActionButton";
|
||||
import { ICONS } from "./ActionIcons";
|
||||
import { t } from "src/lang/helpers";
|
||||
import { ScriptEngine } from "src/Scripts";
|
||||
import { ROOTELEMENTSIZE, mutateElement, nanoid, sceneCoordsToViewportCoords } from "src/constants/constants";
|
||||
import { MD_EX_SECTIONS, ROOTELEMENTSIZE, mutateElement, nanoid, sceneCoordsToViewportCoords } from "src/constants/constants";
|
||||
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "src/ExcalidrawData";
|
||||
import { processLinkText, useDefaultExcalidrawFrame } from "src/utils/CustomEmbeddableUtils";
|
||||
import { cleanSectionHeading } from "src/utils/ObsidianUtils";
|
||||
import { EmbeddableSettings } from "src/dialogs/EmbeddableSettings";
|
||||
import { openExternalLink } from "src/utils/ExcalidrawViewUtils";
|
||||
import { getEA } from "src";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
|
||||
export class EmbeddableMenu {
|
||||
private menuFadeTimeout: number = 0;
|
||||
private menuElementId: string = null;
|
||||
|
||||
constructor(
|
||||
private view:ExcalidrawView,
|
||||
@@ -21,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;
|
||||
@@ -33,26 +51,122 @@ export class EmbeddableMenu {
|
||||
file.extension === "md",
|
||||
)
|
||||
const link = `[[${path}${subpath}]]`;
|
||||
mutateElement (element,{link});
|
||||
const ea = getEA(view) as ExcalidrawAutomate;
|
||||
ea.copyViewElementsToEAforEditing([element]);
|
||||
ea.getElement(element.id).link = link;
|
||||
view.excalidrawData.elementLinks.set(element.id, link);
|
||||
view.setDirty(99);
|
||||
view.updateScene({appState: {activeEmbeddable: null}});
|
||||
ea.addElementsToView(false, true, true).then(() => ea.destroy());
|
||||
}
|
||||
|
||||
private menuFadeTimeout: number = 0;
|
||||
private menuElementId: string = null;
|
||||
private handleMouseEnter () {
|
||||
clearTimeout(this.menuFadeTimeout);
|
||||
this.containerRef.current?.style.setProperty("opacity", "1");
|
||||
};
|
||||
|
||||
private handleMouseLeave () {
|
||||
const self = this;
|
||||
this.menuFadeTimeout = window.setTimeout(() => {
|
||||
self.containerRef.current?.style.setProperty("opacity", "0.2");
|
||||
this.containerRef.current?.style.setProperty("opacity", "0.2");
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
private async actionMarkdownSelection (file: TFile, isExcalidrawFile: boolean, subpath: string, element: ExcalidrawEmbeddableElement) {
|
||||
this.view.updateScene({appState: {activeEmbeddable: null}});
|
||||
const sections = (await app.metadataCache.blockCache
|
||||
.getForFile({ isCancelled: () => false },file))
|
||||
.blocks.filter((b: any) => b.display && b.node?.type === "heading")
|
||||
.filter((b: any) => !isExcalidrawFile || !MD_EX_SECTIONS.includes(b.display));
|
||||
let values, display;
|
||||
if(isExcalidrawFile) {
|
||||
values = sections.map((b: any) => `#${cleanSectionHeading(b.display)}`);
|
||||
display = sections.map((b: any) => b.display);
|
||||
} else {
|
||||
values = [""].concat(
|
||||
sections.map((b: any) => `#${cleanSectionHeading(b.display)}`)
|
||||
);
|
||||
display = [t("SHOW_ENTIRE_FILE")].concat(
|
||||
sections.map((b: any) => b.display)
|
||||
);
|
||||
}
|
||||
const newSubpath = await ScriptEngine.suggester(
|
||||
app, display, values, "Select section from document"
|
||||
);
|
||||
if(!newSubpath && newSubpath!=="") return;
|
||||
if (newSubpath !== subpath) {
|
||||
this.updateElement(newSubpath, element, file);
|
||||
}
|
||||
}
|
||||
|
||||
private async actionMarkdownBlock (file: TFile, subpath: string, element: ExcalidrawEmbeddableElement) {
|
||||
if(!file) return;
|
||||
this.view.updateScene({appState: {activeEmbeddable: null}});
|
||||
const paragraphs = (await app.metadataCache.blockCache
|
||||
.getForFile({ isCancelled: () => false },file))
|
||||
.blocks.filter((b: any) => b.display && b.node &&
|
||||
(b.node.type === "paragraph" || b.node.type === "blockquote" || b.node.type === "listItem" || b.node.type === "table" || b.node.type === "callout")
|
||||
);
|
||||
const values = ["entire-file"].concat(paragraphs);
|
||||
const display = [t("SHOW_ENTIRE_FILE")].concat(
|
||||
paragraphs.map((b: any) => `${b.node?.id ? `#^${b.node.id}: ` : ``}${b.display.trim()}`));
|
||||
|
||||
const selectedBlock = await ScriptEngine.suggester(
|
||||
app, display, values, "Select section from document"
|
||||
);
|
||||
if(!selectedBlock) return;
|
||||
|
||||
if(selectedBlock==="entire-file") {
|
||||
if(subpath==="") return;
|
||||
this.updateElement("", element, file);
|
||||
return;
|
||||
}
|
||||
|
||||
let blockID = selectedBlock.node.id;
|
||||
if(blockID && (`#^${blockID}` === subpath)) return;
|
||||
if (!blockID) {
|
||||
const offset = selectedBlock.node?.position?.end?.offset;
|
||||
if(!offset) return;
|
||||
blockID = nanoid();
|
||||
const fileContents = await app.vault.cachedRead(file);
|
||||
if(!fileContents) return;
|
||||
await app.vault.modify(file, fileContents.slice(0, offset) + ` ^${blockID}` + fileContents.slice(offset));
|
||||
await sleep(200); //wait for cache to update
|
||||
}
|
||||
this.updateElement(`#^${blockID}`, element, file);
|
||||
}
|
||||
|
||||
private actionZoomToElement (element: ExcalidrawEmbeddableElement, maxLevel?: number) {
|
||||
if(!element) return;
|
||||
const api = this.view.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
api.zoomToFit([element], maxLevel ?? this.view.plugin.settings.zoomToFitMaxLevel, 0.1);
|
||||
}
|
||||
|
||||
private actionProperties (element: ExcalidrawEmbeddableElement, file: TFile) {
|
||||
if(!element) return;
|
||||
new EmbeddableSettings(this.view.plugin,this.view,file,element).open();
|
||||
}
|
||||
|
||||
private actionCrop (element: ExcalidrawEmbeddableElement) {
|
||||
if(!element) return;
|
||||
//@ts-ignore
|
||||
this.view.app.commands.executeCommandById("obsidian-excalidraw-plugin:crop-image");
|
||||
}
|
||||
|
||||
private actionReload (iframe: HTMLIFrameElement, link: string) {
|
||||
iframe.src = link;
|
||||
}
|
||||
|
||||
private actionOpen (iframe: HTMLIFrameElement, element: ExcalidrawEmbeddableElement) {
|
||||
openExternalLink(
|
||||
!iframe.src.startsWith("https://www.youtube.com") && !iframe.src.startsWith("https://player.vimeo.com")
|
||||
? iframe.src
|
||||
: element.link,
|
||||
this.view.app
|
||||
);
|
||||
}
|
||||
|
||||
private actionCopyCode (element: ExcalidrawEmbeddableElement, link: string) {
|
||||
if(!element) return;
|
||||
navigator.clipboard.writeText(atob(link.split(",")[1]));
|
||||
}
|
||||
|
||||
renderButtons(appState: AppState) {
|
||||
const view = this.view;
|
||||
@@ -98,6 +212,8 @@ export class EmbeddableMenu {
|
||||
const { subpath, file } = processLinkText(link, view);
|
||||
if(!file) return;
|
||||
const isMD = file.extension==="md";
|
||||
const isExcalidrawFile = view.plugin.isExcalidrawFile(file);
|
||||
const isPDF = file.extension==="pdf";
|
||||
const { x, y } = sceneCoordsToViewportCoords( { sceneX: element.x, sceneY: element.y }, appState);
|
||||
const top = `${y-2.5*ROOTELEMENTSIZE-appState.offsetTop}px`;
|
||||
const left = `${x-appState.offsetLeft}px`;
|
||||
@@ -126,89 +242,38 @@ export class EmbeddableMenu {
|
||||
<ActionButton
|
||||
key={"MarkdownSection"}
|
||||
title={t("NARROW_TO_HEADING")}
|
||||
action={async () => {
|
||||
const sections = (await app.metadataCache.blockCache
|
||||
.getForFile({ isCancelled: () => false },file))
|
||||
.blocks.filter((b: any) => b.display && b.node?.type === "heading");
|
||||
const values = [""].concat(
|
||||
sections.map((b: any) => `#${cleanSectionHeading(b.display)}`)
|
||||
);
|
||||
const 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 && (
|
||||
{isMD && !isExcalidrawFile && (
|
||||
<ActionButton
|
||||
key={"MarkdownBlock"}
|
||||
title={t("NARROW_TO_BLOCK")}
|
||||
action={async () => {
|
||||
if(!file) return;
|
||||
const paragrphs = (await app.metadataCache.blockCache
|
||||
.getForFile({ isCancelled: () => false },file))
|
||||
.blocks.filter((b: any) => b.display && b.node?.type === "paragraph");
|
||||
const values = ["entire-file"].concat(paragrphs);
|
||||
const display = [t("SHOW_ENTIRE_FILE")].concat(
|
||||
paragrphs.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={() => this.actionCrop(element)}
|
||||
icon={ICONS.Crop}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -246,56 +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={() => {
|
||||
view.openExternalLink(
|
||||
!iframe.src.startsWith("https://www.youtube.com") && !iframe.src.startsWith("https://player.vimeo.com")
|
||||
? iframe.src
|
||||
: element.link
|
||||
);
|
||||
}}
|
||||
action={() => this.actionOpen(iframe, element)}
|
||||
icon={ICONS.Globe}
|
||||
view={view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"ZoomToElement"}
|
||||
title={t("ZOOM_TO_FIT")}
|
||||
action={() => {
|
||||
if(!element) return;
|
||||
api.zoomToFit([element], view.plugin.settings.zoomToFitMaxLevel, 0.1);
|
||||
}}
|
||||
action={() => this.actionZoomToElement(element)}
|
||||
icon={ICONS.ZoomToSelectedElement}
|
||||
view={view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"Properties"}
|
||||
title={t("PROPERTIES")}
|
||||
action={() => {
|
||||
if(!element) return;
|
||||
new EmbeddableSettings(view.plugin,view,null,element).open();
|
||||
}}
|
||||
action={() => this.actionProperties(element, null)}
|
||||
icon={ICONS.Properties}
|
||||
view={view}
|
||||
/>
|
||||
{link?.startsWith("data:text/html") && (
|
||||
<ActionButton
|
||||
key={"CopyCode"}
|
||||
title={t("COPYCODE")}
|
||||
action={() => {
|
||||
if(!element) return;
|
||||
navigator.clipboard.writeText(atob(link.split(",")[1]));
|
||||
}}
|
||||
action={() => this.actionCopyCode(element, link)}
|
||||
icon={ICONS.Copy}
|
||||
view={view}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ import { t } from "src/lang/helpers";
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
|
||||
export const setPen = (pen: PenStyle, api: any) => {
|
||||
export function setPen (pen: PenStyle, api: any) {
|
||||
const st = api.getAppState();
|
||||
api.updateScene({
|
||||
appState: {
|
||||
@@ -38,7 +38,7 @@ export const setPen = (pen: PenStyle, api: any) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const resetStrokeOptions = (resetCustomPen:any, api: ExcalidrawImperativeAPI, clearCurrentStrokeOptions: boolean) => {
|
||||
export function resetStrokeOptions (resetCustomPen:any, api: ExcalidrawImperativeAPI, clearCurrentStrokeOptions: boolean) {
|
||||
api.updateScene({
|
||||
appState: {
|
||||
...resetCustomPen ? {
|
||||
@@ -57,6 +57,8 @@ export const resetStrokeOptions = (resetCustomPen:any, api: ExcalidrawImperative
|
||||
export class ObsidianMenu {
|
||||
private clickTimestamp:number[];
|
||||
private activePen: PenStyle;
|
||||
private longpressTimeout : { [key: number]: number } = {};
|
||||
private prevClickTimestamp: number = 0;
|
||||
constructor(
|
||||
private plugin: ExcalidrawPlugin,
|
||||
private toolsRef: React.MutableRefObject<any>,
|
||||
@@ -65,9 +67,98 @@ export class ObsidianMenu {
|
||||
this.clickTimestamp = Array.from({length: Object.keys(PENS).length}, () => 0);
|
||||
}
|
||||
|
||||
renderCustomPens = (isMobile: boolean, appState: AppState) => {
|
||||
private actionCustomPenLabelClick(index: number, pen: PenStyle) {
|
||||
const now = Date.now();
|
||||
const dblClick = now-this.clickTimestamp[index] < 500;
|
||||
//open pen settings on double click
|
||||
if(dblClick) {
|
||||
const penSettings = new PenSettingsModal(this.plugin,this.view,index);
|
||||
(async () => {
|
||||
await this.plugin.loadSettings();
|
||||
penSettings.open();
|
||||
})();
|
||||
return;
|
||||
}
|
||||
this.clickTimestamp[index] = now;
|
||||
|
||||
const api = this.view.excalidrawAPI;
|
||||
const st = api.getAppState();
|
||||
|
||||
//single second click to reset freedraw to default
|
||||
if(st.currentStrokeOptions === pen.penOptions && st.activeTool.type === "freedraw") {
|
||||
resetStrokeOptions(st.resetCustomPen, api, true);
|
||||
return;
|
||||
}
|
||||
|
||||
//apply pen settings to canvas
|
||||
this.activePen = {...pen};
|
||||
setPen(pen,api);
|
||||
api.setActiveTool({type:"freedraw"});
|
||||
}
|
||||
|
||||
private actionScriptButtonPonterUp(index: number, key: string) {
|
||||
if(this.longpressTimeout[index]) {
|
||||
this.view.ownerWindow.clearTimeout(this.longpressTimeout[index]);
|
||||
this.longpressTimeout[index] = 0;
|
||||
(async ()=>{
|
||||
const f = this.view.app.vault.getAbstractFileByPath(key);
|
||||
if (f && f instanceof TFile) {
|
||||
this.plugin.scriptEngine.executeScript(
|
||||
this.view,
|
||||
await this.view.app.vault.read(f),
|
||||
this.plugin.scriptEngine.getScriptName(f),
|
||||
f
|
||||
);
|
||||
}
|
||||
})()
|
||||
}
|
||||
}
|
||||
|
||||
private actionScriptButtonPointerDown(index: number, key: string) {
|
||||
const now = Date.now();
|
||||
if(this.longpressTimeout[index]>0) {
|
||||
this.view.ownerWindow.clearTimeout(this.longpressTimeout[index]);
|
||||
this.longpressTimeout[index] = 0;
|
||||
}
|
||||
if(now-this.prevClickTimestamp >= 500) {
|
||||
this.longpressTimeout[index] = this.view.ownerWindow.setTimeout(
|
||||
() => {
|
||||
this.longpressTimeout[index] = 0;
|
||||
(async () =>{
|
||||
await this.plugin.loadSettings();
|
||||
const index = this.plugin.settings.pinnedScripts.indexOf(key)
|
||||
if(index > -1) {
|
||||
this.plugin.settings.pinnedScripts.splice(index,1);
|
||||
this.view.excalidrawAPI?.setToast({message:`Pin removed: ${name}`, duration: 3000, closable: true});
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> {
|
||||
if (v.view instanceof ExcalidrawView) v.view.updatePinnedScripts()
|
||||
})
|
||||
})()
|
||||
},
|
||||
1500
|
||||
)
|
||||
}
|
||||
this.prevClickTimestamp = now;
|
||||
}
|
||||
|
||||
private actionShowHideMenu (isMobile: boolean, appState: AppState) {
|
||||
this.toolsRef.current.setTheme(appState.theme);
|
||||
this.toolsRef.current.toggleVisibility(
|
||||
appState.zenModeEnabled || isMobile,
|
||||
);
|
||||
}
|
||||
|
||||
private actionInsertAnyFile() {
|
||||
this.view.setCurrentPositionToCenter();
|
||||
const insertFileModal = new UniversalInsertFileModal(this.plugin, this.view);
|
||||
insertFileModal.open();
|
||||
}
|
||||
|
||||
public renderCustomPens (isMobile: boolean, appState: AppState) {
|
||||
return(
|
||||
appState.customPens?.map((key,index)=>{
|
||||
appState.customPens?.map((_,index)=>{
|
||||
const pen = this.plugin.settings.customPens[index]
|
||||
//Reset stroke setting when changing to a different tool
|
||||
if(
|
||||
@@ -75,7 +166,7 @@ export class ObsidianMenu {
|
||||
appState.activeTool.type !== "freedraw" &&
|
||||
appState.currentStrokeOptions === pen.penOptions
|
||||
) {
|
||||
setTimeout(()=> resetStrokeOptions(appState.resetCustomPen, this.view.excalidrawAPI, false))
|
||||
setTimeout(()=> resetStrokeOptions(appState.resetCustomPen, this.view.excalidrawAPI, false));
|
||||
}
|
||||
//if Pen settings are loaded, select custom pen when activating the freedraw element
|
||||
if (
|
||||
@@ -111,34 +202,7 @@ export class ObsidianMenu {
|
||||
"is-mobile": isMobile,
|
||||
},
|
||||
)}
|
||||
onClick={() => {
|
||||
const now = Date.now();
|
||||
const dblClick = now-this.clickTimestamp[index] < 500;
|
||||
//open pen settings on double click
|
||||
if(dblClick) {
|
||||
const penSettings = new PenSettingsModal(this.plugin,this.view,index);
|
||||
(async () => {
|
||||
await this.plugin.loadSettings();
|
||||
penSettings.open();
|
||||
})();
|
||||
return;
|
||||
}
|
||||
this.clickTimestamp[index] = now;
|
||||
|
||||
const api = this.view.excalidrawAPI;
|
||||
const st = api.getAppState();
|
||||
|
||||
//single second click to reset freedraw to default
|
||||
if(st.currentStrokeOptions === pen.penOptions && st.activeTool.type === "freedraw") {
|
||||
resetStrokeOptions(st.resetCustomPen, api, true);
|
||||
return;
|
||||
}
|
||||
|
||||
//apply pen settings to canvas
|
||||
this.activePen = {...pen};
|
||||
setPen(pen,api);
|
||||
api.setActiveTool({type:"freedraw"});
|
||||
}}
|
||||
onClick={ this.actionCustomPenLabelClick.bind(this,index, pen) }
|
||||
>
|
||||
<div
|
||||
className="ToolIcon__icon"
|
||||
@@ -157,10 +221,7 @@ export class ObsidianMenu {
|
||||
)
|
||||
}
|
||||
|
||||
private longpressTimeout : { [key: number]: number } = {};
|
||||
|
||||
renderPinnedScriptButtons = (isMobile: boolean, appState: AppState) => {
|
||||
let prevClickTimestamp = 0;
|
||||
public renderPinnedScriptButtons (isMobile: boolean, appState: AppState) {
|
||||
return (
|
||||
appState?.pinnedScripts?.map((key,index)=>{ //pinned scripts
|
||||
const scriptProp = this.plugin.scriptEngine.scriptIconMap[key];
|
||||
@@ -179,51 +240,8 @@ export class ObsidianMenu {
|
||||
"is-mobile": isMobile,
|
||||
},
|
||||
)}
|
||||
onPointerUp={() => {
|
||||
if(this.longpressTimeout[index]) {
|
||||
window.clearTimeout(this.longpressTimeout[index]);
|
||||
this.longpressTimeout[index] = 0;
|
||||
(async ()=>{
|
||||
const f = app.vault.getAbstractFileByPath(key);
|
||||
if (f && f instanceof TFile) {
|
||||
this.plugin.scriptEngine.executeScript(
|
||||
this.view,
|
||||
await app.vault.read(f),
|
||||
this.plugin.scriptEngine.getScriptName(f),
|
||||
f
|
||||
);
|
||||
}
|
||||
})()
|
||||
}
|
||||
}}
|
||||
onPointerDown={()=>{
|
||||
const now = Date.now();
|
||||
if(this.longpressTimeout[index]>0) {
|
||||
window.clearTimeout(this.longpressTimeout[index]);
|
||||
this.longpressTimeout[index] = 0;
|
||||
}
|
||||
if(now-prevClickTimestamp >= 500) {
|
||||
this.longpressTimeout[index] = window.setTimeout(
|
||||
() => {
|
||||
this.longpressTimeout[index] = 0;
|
||||
(async () =>{
|
||||
await this.plugin.loadSettings();
|
||||
const index = this.plugin.settings.pinnedScripts.indexOf(key)
|
||||
if(index > -1) {
|
||||
this.plugin.settings.pinnedScripts.splice(index,1);
|
||||
this.view.excalidrawAPI?.setToast({message:`Pin removed: ${name}`, duration: 3000, closable: true});
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> {
|
||||
if (v.view instanceof ExcalidrawView) v.view.updatePinnedScripts()
|
||||
})
|
||||
})()
|
||||
},
|
||||
1500
|
||||
)
|
||||
}
|
||||
prevClickTimestamp = now;
|
||||
}}
|
||||
onPointerUp={this.actionScriptButtonPonterUp.bind(this,index,key)}
|
||||
onPointerDown={this.actionScriptButtonPointerDown.bind(this,index,key)}
|
||||
>
|
||||
<div
|
||||
className="ToolIcon__icon"
|
||||
@@ -237,7 +255,7 @@ export class ObsidianMenu {
|
||||
)
|
||||
}
|
||||
|
||||
renderButton = (isMobile: boolean, appState: AppState) => {
|
||||
public renderButton (isMobile: boolean, appState: AppState) {
|
||||
return (
|
||||
<>
|
||||
<label
|
||||
@@ -248,12 +266,7 @@ export class ObsidianMenu {
|
||||
"is-mobile": isMobile,
|
||||
},
|
||||
)}
|
||||
onClick={() => {
|
||||
this.toolsRef.current.setTheme(appState.theme);
|
||||
this.toolsRef.current.toggleVisibility(
|
||||
appState.zenModeEnabled || isMobile,
|
||||
);
|
||||
}}
|
||||
onClick={this.actionShowHideMenu.bind(this,isMobile,appState)}
|
||||
>
|
||||
<div className="ToolIcon__icon" aria-label={t("OBSIDIAN_TOOLS_PANEL")}>
|
||||
{ICONS.obsidian}
|
||||
@@ -267,11 +280,7 @@ export class ObsidianMenu {
|
||||
"is-mobile": isMobile,
|
||||
},
|
||||
)}
|
||||
onClick={() => {
|
||||
this.view.setCurrentPositionToCenter();
|
||||
const insertFileModal = new UniversalInsertFileModal(this.plugin, this.view);
|
||||
insertFileModal.open();
|
||||
}}
|
||||
onClick={this.actionInsertAnyFile.bind(this)}
|
||||
>
|
||||
<div className="ToolIcon__icon" aria-label={t("UNIVERSAL_ADD_FILE")}>
|
||||
{ICONS["add-file"]}
|
||||
@@ -282,4 +291,19 @@ export class ObsidianMenu {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
destroy() {
|
||||
Object.values(this.longpressTimeout).forEach(
|
||||
t=>this.view.ownerWindow.clearTimeout(t)
|
||||
);
|
||||
this.longpressTimeout = {};
|
||||
this.activePen = null;
|
||||
this.plugin = null;
|
||||
this.toolsRef = null;
|
||||
this.view = null;
|
||||
this.clickTimestamp = null;
|
||||
this.renderButton = null;
|
||||
this.renderCustomPens = null;
|
||||
this.renderPinnedScriptButtons = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/
|
||||
import { isWinALTorMacOPT, isWinCTRLorMacCMD, isSHIFT } from "src/utils/ModifierkeyHelper";
|
||||
import { InsertPDFModal } from "src/dialogs/InsertPDFModal";
|
||||
import { ExportDialog } from "src/dialogs/ExportDialog";
|
||||
import { openExternalLink } from "src/utils/ExcalidrawViewUtils";
|
||||
import { UniversalInsertFileModal } from "src/dialogs/UniversalInsertFileModal";
|
||||
import { DEBUGGING, debug } from "src/utils/DebugHelper";
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
const dark = '<svg style="stroke:#ced4da;#212529;color:#ced4da;fill:#ced4da" ';
|
||||
@@ -21,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 = {
|
||||
@@ -35,7 +39,7 @@ export type PanelState = {
|
||||
isDirty: boolean;
|
||||
isFullscreen: boolean;
|
||||
isPreviewMode: boolean;
|
||||
scriptIconMap: ScriptIconMap;
|
||||
scriptIconMap: ScriptIconMap | null;
|
||||
};
|
||||
|
||||
const TOOLS_PANEL_WIDTH = 228;
|
||||
@@ -52,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,
|
||||
@@ -72,12 +88,14 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
}
|
||||
|
||||
updateScriptIconMap(scriptIconMap: ScriptIconMap) {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updateScriptIconMap,"ToolsPanel.updateScriptIconMap()");
|
||||
this.setState(() => {
|
||||
return { scriptIconMap };
|
||||
});
|
||||
}
|
||||
|
||||
setPreviewMode(isPreviewMode: boolean) {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setPreviewMode,"ToolsPanel.setPreviewMode()");
|
||||
this.setState(() => {
|
||||
return {
|
||||
isPreviewMode,
|
||||
@@ -86,6 +104,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
}
|
||||
|
||||
setFullscreen(isFullscreen: boolean) {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setFullscreen,"ToolsPanel.setFullscreen()");
|
||||
this.setState(() => {
|
||||
return {
|
||||
isFullscreen,
|
||||
@@ -94,6 +113,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
}
|
||||
|
||||
setDirty(isDirty: boolean) {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setDirty,"ToolsPanel.setDirty()");
|
||||
this.setState(()=> {
|
||||
return {
|
||||
isDirty,
|
||||
@@ -102,6 +122,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
}
|
||||
|
||||
setExcalidrawViewMode(isViewModeEnabled: boolean) {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setExcalidrawViewMode,"ToolsPanel.setExcalidrawViewMode()");
|
||||
this.setState(() => {
|
||||
return {
|
||||
excalidrawViewMode: isViewModeEnabled,
|
||||
@@ -110,6 +131,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
}
|
||||
|
||||
toggleVisibility(isMobileOrZen: boolean) {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.toggleVisibility,"ToolsPanel.toggleVisibility()");
|
||||
this.setTopCenter(isMobileOrZen);
|
||||
this.setState((prevState: PanelState) => {
|
||||
return {
|
||||
@@ -119,6 +141,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
}
|
||||
|
||||
setTheme(theme: "dark" | "light") {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setTheme,"ToolsPanel.setTheme()");
|
||||
this.setState((prevState: PanelState) => {
|
||||
return {
|
||||
theme,
|
||||
@@ -127,6 +150,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
}
|
||||
|
||||
setTopCenter(isMobileOrZen: boolean) {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setTopCenter,"ToolsPanel.setTopCenter()");
|
||||
this.setState(() => {
|
||||
return {
|
||||
left:
|
||||
@@ -142,6 +166,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
}
|
||||
|
||||
updatePosition(deltaY: number = 0, deltaX: number = 0) {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updatePosition,"ToolsPanel.updatePosition()");
|
||||
this.setState(() => {
|
||||
const {
|
||||
offsetTop,
|
||||
@@ -182,7 +207,234 @@ 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 (
|
||||
<div
|
||||
ref={this.containerRef}
|
||||
@@ -217,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"
|
||||
@@ -284,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"}
|
||||
@@ -348,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, 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>
|
||||
@@ -431,125 +589,85 @@ 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={this.actionLinkToElement.bind(this)}
|
||||
icon={ICONS.copyElementLink}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Insert actions</legend>
|
||||
<div className="buttonList buttonListIcon">
|
||||
<ActionButton
|
||||
key={"anyfile"}
|
||||
title={t("UNIVERSAL_ADD_FILE")}
|
||||
action={this.actionAddAnyFile.bind(this)}
|
||||
icon={ICONS["add-file"]}
|
||||
/>
|
||||
<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.actionInsertBackOfNote.bind(this)}
|
||||
icon={ICONS.BackOfNote}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"latex"}
|
||||
title={t("INSERT_LATEX")}
|
||||
action={(e) => {
|
||||
if(isWinALTorMacOPT(e)) {
|
||||
this.props.view.openExternalLink("https://youtu.be/r08wk-58DPk");
|
||||
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={"link-to-element"}
|
||||
title={t("INSERT_LINK_TO_ELEMENT")}
|
||||
action={(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
if(isWinALTorMacOPT(e)) {
|
||||
this.props.view.openExternalLink("https://youtu.be/yZQoJg2RCKI");
|
||||
return;
|
||||
}
|
||||
this.props.view.copyLinkToSelectedElementToClipboard(
|
||||
isWinCTRLorMacCMD(e) ? "group=" : (isSHIFT(e) ? "area=" : "")
|
||||
);
|
||||
}}
|
||||
icon={ICONS.copyElementLink}
|
||||
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={this.actionCropImage.bind(this)}
|
||||
icon={ICONS.Crop}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -563,11 +681,12 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
}
|
||||
|
||||
private renderScriptButtons(isDownloaded: boolean) {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.renderScriptButtons,"ToolsPanel.renderScriptButtons()");
|
||||
if (Object.keys(this.state.scriptIconMap).length === 0) {
|
||||
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
|
||||
@@ -602,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>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { createPNG, ExcalidrawAutomate } from "../ExcalidrawAutomate";
|
||||
import { ExcalidrawAutomate, createPNG } from "../ExcalidrawAutomate";
|
||||
import {Notice, requestUrl} from "obsidian"
|
||||
import ExcalidrawPlugin from "../main"
|
||||
import {log} from "../utils/Utils"
|
||||
import ExcalidrawView, { ExportSettings } from "../ExcalidrawView"
|
||||
import FrontmatterEditor from "src/utils/Frontmatter";
|
||||
import { ExcalidrawElement, ExcalidrawImageElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { EmbeddedFilesLoader } from "src/EmbeddedFileLoader";
|
||||
import { blobToBase64 } from "src/utils/FileUtils";
|
||||
import { getEA } from "src";
|
||||
import { log } from "src/utils/DebugHelper";
|
||||
|
||||
const TASKBONE_URL = "https://api.taskbone.com/"; //"https://excalidraw-preview.onrender.com/";
|
||||
const TASKBONE_OCR_FN = "execute?id=60f394af-85f6-40bc-9613-5d26dc283cbb";
|
||||
@@ -21,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({
|
||||
@@ -39,23 +44,9 @@ export default class Taskbone {
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
public async getTextForView(view: ExcalidrawView, forceReScan: boolean) {
|
||||
await view.forceSave(true);
|
||||
const viewElements = view.excalidrawAPI.getSceneElements().filter((el:ExcalidrawElement) =>
|
||||
el.type==="freedraw" ||
|
||||
( el.type==="image" &&
|
||||
!this.plugin.isExcalidrawFile(view.excalidrawData.getFile(el.fileId)?.file)
|
||||
));
|
||||
if(viewElements.length === 0) {
|
||||
new Notice ("Aborting OCR because there are no image or freedraw elements on the canvas.",4000);
|
||||
return;
|
||||
}
|
||||
const fe = new FrontmatterEditor(view.data);
|
||||
if(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)
|
||||
return;
|
||||
}
|
||||
const bb = this.plugin.ea.getBoundingBox(viewElements);
|
||||
public async getTextForElements(elements: ExcalidrawElement[], ea: ExcalidrawAutomate): Promise<string> {
|
||||
ea.copyViewElementsToEAforEditing(elements, true);
|
||||
const bb = ea.getBoundingBox(elements);
|
||||
const size = (bb.width*bb.height);
|
||||
const minRatio = Math.sqrt(360000/size);
|
||||
const maxRatio = Math.sqrt(size/16000000);
|
||||
@@ -75,30 +66,60 @@ export default class Taskbone {
|
||||
const exportSettings: ExportSettings = {
|
||||
withBackground: true,
|
||||
withTheme: true,
|
||||
isMask: false,
|
||||
};
|
||||
|
||||
const img =
|
||||
await createPNG(
|
||||
view.file.path + "#^taskbone",
|
||||
await ea.createPNG(
|
||||
null,
|
||||
scale,
|
||||
exportSettings,
|
||||
loader,
|
||||
"light",
|
||||
null,
|
||||
null,
|
||||
[],
|
||||
this.plugin,
|
||||
0
|
||||
);
|
||||
)
|
||||
return await this.getTextForImage(img);
|
||||
}
|
||||
|
||||
const text = await this.getTextForImage(img);
|
||||
if(text) {
|
||||
fe.setKey("taskbone-ocr",text);
|
||||
view.data = fe.data;
|
||||
view.save(false);
|
||||
window.navigator.clipboard.writeText(text);
|
||||
new Notice("I placed the recognized in the drawing's frontmatter and onto the system clipboard.");
|
||||
public async getTextForView(view: ExcalidrawView, {
|
||||
forceReScan,
|
||||
selectedElementsOnly = false,
|
||||
addToFrontmatter = true,
|
||||
}: {
|
||||
forceReScan: boolean,
|
||||
selectedElementsOnly?: boolean,
|
||||
addToFrontmatter?: boolean,
|
||||
}) {
|
||||
await view.forceSave(true);
|
||||
const ea = getEA(view) as ExcalidrawAutomate;
|
||||
const viewElements = (selectedElementsOnly ? ea.getViewSelectedElements() : ea.getViewElements())
|
||||
.filter((el:ExcalidrawElement) =>
|
||||
el.type==="freedraw" ||
|
||||
( el.type==="image" &&
|
||||
!this.plugin.isExcalidrawFile(view.excalidrawData.getFile(el.fileId)?.file)
|
||||
));
|
||||
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;
|
||||
}
|
||||
|
||||
const text = await this.getTextForElements(viewElements, ea);
|
||||
if(text) {
|
||||
if(addToFrontmatter) {
|
||||
fe.setKey("taskbone-ocr",text);
|
||||
view.data = fe.data;
|
||||
view.save(false);
|
||||
}
|
||||
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> {
|
||||
@@ -113,7 +134,18 @@ export default class Taskbone {
|
||||
}]
|
||||
};
|
||||
|
||||
const apiResponse = await requestUrl ({
|
||||
const apiResponse = await fetch(url,{
|
||||
method: "post",
|
||||
//@ts-ignore
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(input),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
authorization: `Bearer ${this.apiKey}`
|
||||
}});
|
||||
const content = await apiResponse?.json();
|
||||
|
||||
/*const apiResponse = await requestUrl ({
|
||||
url: url,
|
||||
method: "post",
|
||||
contentType: "application/json",
|
||||
@@ -123,7 +155,7 @@ export default class Taskbone {
|
||||
},
|
||||
throw: false
|
||||
});
|
||||
const content = apiResponse?.json;
|
||||
const content = apiResponse?.json;*/
|
||||
|
||||
if(!content || apiResponse.status !== 200) {
|
||||
new Notice("Something went wrong while processing your request. Please check developer console for more information");
|
||||
|
||||
465
src/settings.ts
465
src/settings.ts
@@ -33,15 +33,21 @@ import { EmbeddalbeMDFileCustomDataSettingsComponent } from "./dialogs/Embeddabl
|
||||
import { startupScript } from "./constants/starutpscript";
|
||||
import { ModifierKeySet, ModifierSetType } from "./utils/ModifierkeyHelper";
|
||||
import { ModifierKeySettingsComponent } from "./dialogs/ModifierKeySettings";
|
||||
import { ANNOTATED_PREFIX, CROPPED_PREFIX } from "./utils/CarveOut";
|
||||
import { EDITOR_FADEOUT } from "./CodeMirrorExtension/EditorHandler";
|
||||
import { setDebugging } from "./utils/DebugHelper";
|
||||
|
||||
export interface ExcalidrawSettings {
|
||||
folder: string;
|
||||
cropFolder: string;
|
||||
annotateFolder: string;
|
||||
embedUseExcalidrawFolder: boolean;
|
||||
templateFilePath: string;
|
||||
scriptFolderPath: string;
|
||||
compress: boolean;
|
||||
decompressForMDView: boolean;
|
||||
onceOffCompressFlagReset: boolean; //used to reset compress to true in 2.2.0
|
||||
autosave: boolean;
|
||||
autosaveInterval: number;
|
||||
autosaveIntervalDesktop: number;
|
||||
autosaveIntervalMobile: number;
|
||||
drawingFilenamePrefix: string;
|
||||
@@ -49,12 +55,17 @@ export interface ExcalidrawSettings {
|
||||
drawingFilnameEmbedPostfix: string;
|
||||
drawingFilenameDateTime: string;
|
||||
useExcalidrawExtension: boolean;
|
||||
cropPrefix: string;
|
||||
annotatePrefix: string;
|
||||
annotatePreserveSize: boolean;
|
||||
displaySVGInPreview: boolean; //No longer used since 1.9.13
|
||||
previewImageType: PreviewImageType; //Introduced with 1.9.13
|
||||
allowImageCache: boolean;
|
||||
allowImageCacheInScene: boolean;
|
||||
displayExportedImageIfAvailable: boolean;
|
||||
previewMatchObsidianTheme: boolean;
|
||||
width: string;
|
||||
height: string;
|
||||
dynamicStyling: DynamicStyle;
|
||||
isLeftHanded: boolean;
|
||||
iframeMatchExcalidrawTheme: boolean;
|
||||
@@ -63,12 +74,18 @@ export interface ExcalidrawSettings {
|
||||
matchThemeTrigger: boolean;
|
||||
defaultMode: string;
|
||||
defaultPenMode: "never" | "mobile" | "always";
|
||||
penModeCrosshairVisible: boolean;
|
||||
renderImageInMarkdownReadingMode: boolean,
|
||||
renderImageInHoverPreviewForMDNotes: boolean,
|
||||
renderImageInMarkdownToPDF: boolean,
|
||||
allowPinchZoom: boolean;
|
||||
allowWheelZoom: boolean;
|
||||
zoomToFitOnOpen: boolean;
|
||||
zoomToFitOnResize: boolean;
|
||||
zoomToFitMaxLevel: number;
|
||||
openInAdjacentPane: boolean;
|
||||
showSecondOrderLinks: boolean;
|
||||
focusOnFileTab: boolean;
|
||||
openInMainWorkspace: boolean;
|
||||
showLinkBrackets: boolean;
|
||||
linkPrefix: string;
|
||||
@@ -88,6 +105,7 @@ export interface ExcalidrawSettings {
|
||||
exportWithTheme: boolean;
|
||||
exportWithBackground: boolean;
|
||||
exportPaddingSVG: number;
|
||||
exportEmbedScene: boolean;
|
||||
keepInSync: boolean;
|
||||
autoexportSVG: boolean;
|
||||
autoexportPNG: boolean;
|
||||
@@ -101,8 +119,12 @@ export interface ExcalidrawSettings {
|
||||
experimentalFileType: boolean;
|
||||
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;
|
||||
@@ -170,18 +192,25 @@ export interface ExcalidrawSettings {
|
||||
Win: Record<ModifierSetType, ModifierKeySet>,
|
||||
},
|
||||
slidingPanesSupport: boolean;
|
||||
areaZoomLimit: number;
|
||||
longPressDesktop: number;
|
||||
longPressMobile: number;
|
||||
isDebugMode: boolean;
|
||||
}
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
|
||||
export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
folder: "Excalidraw",
|
||||
cropFolder: "",
|
||||
annotateFolder: "",
|
||||
embedUseExcalidrawFolder: false,
|
||||
templateFilePath: "Excalidraw/Template.excalidraw",
|
||||
scriptFolderPath: "Excalidraw/Scripts",
|
||||
compress: false,
|
||||
compress: true,
|
||||
decompressForMDView: false,
|
||||
onceOffCompressFlagReset: false,
|
||||
autosave: true,
|
||||
autosaveInterval: 15000,
|
||||
autosaveIntervalDesktop: 15000,
|
||||
autosaveIntervalMobile: 10000,
|
||||
drawingFilenamePrefix: "Drawing ",
|
||||
@@ -189,12 +218,17 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
drawingFilnameEmbedPostfix: " ",
|
||||
drawingFilenameDateTime: "YYYY-MM-DD HH.mm.ss",
|
||||
useExcalidrawExtension: true,
|
||||
cropPrefix: CROPPED_PREFIX,
|
||||
annotatePrefix: ANNOTATED_PREFIX,
|
||||
annotatePreserveSize: false,
|
||||
displaySVGInPreview: undefined,
|
||||
previewImageType: undefined,
|
||||
allowImageCache: true,
|
||||
allowImageCacheInScene: true,
|
||||
displayExportedImageIfAvailable: false,
|
||||
previewMatchObsidianTheme: false,
|
||||
width: "400",
|
||||
height: "",
|
||||
dynamicStyling: "colorful",
|
||||
isLeftHanded: false,
|
||||
iframeMatchExcalidrawTheme: true,
|
||||
@@ -203,6 +237,10 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
matchThemeTrigger: false,
|
||||
defaultMode: "normal",
|
||||
defaultPenMode: "never",
|
||||
penModeCrosshairVisible: true,
|
||||
renderImageInMarkdownReadingMode: false,
|
||||
renderImageInHoverPreviewForMDNotes: false,
|
||||
renderImageInMarkdownToPDF: false,
|
||||
allowPinchZoom: false,
|
||||
allowWheelZoom: false,
|
||||
zoomToFitOnOpen: true,
|
||||
@@ -216,6 +254,8 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
hoverPreviewWithoutCTRL: false,
|
||||
linkOpacity: 1,
|
||||
openInAdjacentPane: false,
|
||||
showSecondOrderLinks: true,
|
||||
focusOnFileTab: false,
|
||||
openInMainWorkspace: true,
|
||||
showLinkBrackets: true,
|
||||
allowCtrlClick: true,
|
||||
@@ -228,6 +268,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
exportWithTheme: true,
|
||||
exportWithBackground: true,
|
||||
exportPaddingSVG: 10, //since 1.6.17, not only SVG but also PNG
|
||||
exportEmbedScene: false,
|
||||
keepInSync: false,
|
||||
autoexportSVG: false,
|
||||
autoexportPNG: false,
|
||||
@@ -240,8 +281,12 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
experimentalFileType: false,
|
||||
experimentalFileTag: "✏️",
|
||||
experimentalLivePreview: true,
|
||||
fadeOutExcalidrawMarkup: false,
|
||||
loadPropertySuggestions: true,
|
||||
experimentalEnableFourthFont: false,
|
||||
experimantalFourthFont: "Virgil",
|
||||
addDummyTextElement: false,
|
||||
zoteroCompatibility: false,
|
||||
fieldSuggester: true,
|
||||
compatibilityMode: false,
|
||||
//loadCount: 0,
|
||||
@@ -402,6 +447,10 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
},
|
||||
},
|
||||
slidingPanesSupport: false,
|
||||
areaZoomLimit: 1,
|
||||
longPressDesktop: 500,
|
||||
longPressMobile: 500,
|
||||
isDebugMode: false,
|
||||
};
|
||||
|
||||
export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
@@ -550,6 +599,32 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("CROP_FOLDER_NAME"))
|
||||
.setDesc(fragWithHTML(t("CROP_FOLDER_DESC")))
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("e.g.: Excalidraw/Cropped")
|
||||
.setValue(this.plugin.settings.cropFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.cropFolder = value;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("ANNOTATE_FOLDER_NAME"))
|
||||
.setDesc(fragWithHTML(t("ANNOTATE_FOLDER_DESC")))
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("e.g.: Excalidraw/Annotations")
|
||||
.setValue(this.plugin.settings.annotateFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.annotateFolder = value;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("TEMPLATE_NAME"))
|
||||
.setDesc(fragWithHTML(t("TEMPLATE_DESC")))
|
||||
@@ -602,6 +677,18 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("DECOMPRESS_FOR_MD_NAME"))
|
||||
.setDesc(fragWithHTML(t("DECOMPRESS_FOR_MD_DESC")))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.decompressForMDView)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.decompressForMDView = value;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("AUTOSAVE_INTERVAL_DESKTOP_NAME"))
|
||||
.setDesc(fragWithHTML(t("AUTOSAVE_INTERVAL_DESKTOP_DESC")))
|
||||
@@ -614,9 +701,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 = app.isMobile
|
||||
? this.plugin.settings.autosaveIntervalMobile
|
||||
: this.plugin.settings.autosaveIntervalDesktop;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
@@ -633,9 +717,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 = app.isMobile
|
||||
? this.plugin.settings.autosaveIntervalMobile
|
||||
: this.plugin.settings.autosaveIntervalDesktop;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
@@ -746,6 +827,51 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("CROP_PREFIX_NAME"))
|
||||
.setDesc(fragWithHTML(t("CROP_PREFIX_DESC")))
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("e.g.: Cropped_ ")
|
||||
.setValue(this.plugin.settings.cropPrefix)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.cropPrefix = value.replaceAll(
|
||||
/[<>:"/\\|?*]/g,
|
||||
"_",
|
||||
);
|
||||
text.setValue(this.plugin.settings.cropPrefix);
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("ANNOTATE_PREFIX_NAME"))
|
||||
.setDesc(fragWithHTML(t("ANNOTATE_PREFIX_DESC")))
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("e.g.: Annotated_ ")
|
||||
.setValue(this.plugin.settings.annotatePrefix)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.annotatePrefix = value.replaceAll(
|
||||
/[<>:"/\\|?*]/g,
|
||||
"_",
|
||||
);
|
||||
text.setValue(this.plugin.settings.annotatePrefix);
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("ANNOTATE_PRESERVE_SIZE_NAME"))
|
||||
.setDesc(fragWithHTML(t("ANNOTATE_PRESERVE_SIZE_DESC")))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.annotatePreserveSize)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.annotatePreserveSize = value;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
//------------------------------------------------
|
||||
// AI Settings
|
||||
@@ -851,6 +977,42 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_NAME"))
|
||||
.setDesc(fragWithHTML(t("SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_DESC")))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.penModeCrosshairVisible)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.penModeCrosshairVisible = value;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
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) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.renderImageInMarkdownReadingMode)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.renderImageInMarkdownReadingMode = value;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
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")))
|
||||
@@ -866,43 +1028,43 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
addIframe(detailsEl, "H8Njp7ZXYag",999);
|
||||
addIframe(detailsEl, "H8Njp7ZXYag",999);
|
||||
|
||||
detailsEl = displayDetailsEl.createEl("details");
|
||||
detailsEl.createEl("summary", {
|
||||
text: t("THEME_HEAD"),
|
||||
cls: "excalidraw-setting-h3",
|
||||
});
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("DYNAMICSTYLE_NAME"))
|
||||
.setDesc(fragWithHTML(t("DYNAMICSTYLE_DESC")))
|
||||
.addDropdown((dropdown) =>
|
||||
dropdown
|
||||
.addOption("none","Dynamic Styling OFF")
|
||||
.addOption("colorful","Match color")
|
||||
.addOption("gray","Gray, match tone")
|
||||
.setValue(this.plugin.settings.dynamicStyling)
|
||||
.onChange(async (value) => {
|
||||
this.requestUpdateDynamicStyling = true;
|
||||
this.plugin.settings.dynamicStyling = value as DynamicStyle;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
addIframe(detailsEl, "fypDth_-8q0");
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("IFRAME_MATCH_THEME_NAME"))
|
||||
.setDesc(fragWithHTML(t("IFRAME_MATCH_THEME_DESC")))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.iframeMatchExcalidrawTheme)
|
||||
detailsEl = displayDetailsEl.createEl("details");
|
||||
detailsEl.createEl("summary", {
|
||||
text: t("THEME_HEAD"),
|
||||
cls: "excalidraw-setting-h3",
|
||||
});
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("DYNAMICSTYLE_NAME"))
|
||||
.setDesc(fragWithHTML(t("DYNAMICSTYLE_DESC")))
|
||||
.addDropdown((dropdown) =>
|
||||
dropdown
|
||||
.addOption("none","Dynamic Styling OFF")
|
||||
.addOption("colorful","Match color")
|
||||
.addOption("gray","Gray, match tone")
|
||||
.setValue(this.plugin.settings.dynamicStyling)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.iframeMatchExcalidrawTheme = value;
|
||||
this.applySettingsUpdate(true);
|
||||
this.requestUpdateDynamicStyling = true;
|
||||
this.plugin.settings.dynamicStyling = value as DynamicStyle;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
addIframe(detailsEl, "ICpoyMv6KSs");
|
||||
addIframe(detailsEl, "fypDth_-8q0");
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("IFRAME_MATCH_THEME_NAME"))
|
||||
.setDesc(fragWithHTML(t("IFRAME_MATCH_THEME_DESC")))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.iframeMatchExcalidrawTheme)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.iframeMatchExcalidrawTheme = value;
|
||||
this.applySettingsUpdate(true);
|
||||
}),
|
||||
);
|
||||
addIframe(detailsEl, "ICpoyMv6KSs");
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("MATCH_THEME_NAME"))
|
||||
@@ -1109,6 +1271,48 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
});
|
||||
detailsEl.createDiv({ text: t("DRAG_MODIFIER_DESC"), cls: "setting-item-description" });
|
||||
|
||||
let longPressDesktop: HTMLDivElement;
|
||||
new Setting(detailsEl)
|
||||
.setName(t("LONG_PRESS_DESKTOP_NAME"))
|
||||
.setDesc(fragWithHTML(t("LONG_PRESS_DESKTOP_DESC")))
|
||||
.addSlider((slider) =>
|
||||
slider
|
||||
.setLimits(300, 3000, 100)
|
||||
.setValue(this.plugin.settings.longPressDesktop)
|
||||
.onChange(async (value) => {
|
||||
longPressDesktop.innerText = ` ${value.toString()}`;
|
||||
this.plugin.settings.longPressDesktop = value;
|
||||
this.applySettingsUpdate(true);
|
||||
}),
|
||||
)
|
||||
.settingEl.createDiv("", (el) => {
|
||||
longPressDesktop = el;
|
||||
el.style.minWidth = "2.3em";
|
||||
el.style.textAlign = "right";
|
||||
el.innerText = ` ${this.plugin.settings.longPressDesktop.toString()}`;
|
||||
});
|
||||
|
||||
let longPressMobile: HTMLDivElement;
|
||||
new Setting(detailsEl)
|
||||
.setName(t("LONG_PRESS_MOBILE_NAME"))
|
||||
.setDesc(fragWithHTML(t("LONG_PRESS_MOBILE_DESC")))
|
||||
.addSlider((slider) =>
|
||||
slider
|
||||
.setLimits(300, 3000, 100)
|
||||
.setValue(this.plugin.settings.longPressMobile)
|
||||
.onChange(async (value) => {
|
||||
longPressMobile.innerText = ` ${value.toString()}`;
|
||||
this.plugin.settings.longPressMobile = value;
|
||||
this.applySettingsUpdate(true);
|
||||
}),
|
||||
)
|
||||
.settingEl.createDiv("", (el) => {
|
||||
longPressMobile = el;
|
||||
el.style.minWidth = "2.3em";
|
||||
el.style.textAlign = "right";
|
||||
el.innerText = ` ${this.plugin.settings.longPressMobile.toString()}`;
|
||||
});
|
||||
|
||||
new ModifierKeySettingsComponent(
|
||||
detailsEl,
|
||||
this.plugin.settings.modifierKeyConfig,
|
||||
@@ -1132,6 +1336,18 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
(el) => (el.innerHTML = t("LINKS_DESC")),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("SECOND_ORDER_LINKS_NAME"))
|
||||
.setDesc(fragWithHTML(t("SECOND_ORDER_LINKS_DESC")))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.showSecondOrderLinks)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.showSecondOrderLinks = value;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("ADJACENT_PANE_NAME"))
|
||||
.setDesc(fragWithHTML(t("ADJACENT_PANE_DESC")))
|
||||
@@ -1143,7 +1359,18 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("FOCUS_ON_EXISTING_TAB_NAME"))
|
||||
.setDesc(fragWithHTML(t("FOCUS_ON_EXISTING_TAB_DESC")))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.focusOnFileTab)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.focusOnFileTab = value;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("MAINWORKSPACE_PANE_NAME"))
|
||||
@@ -1508,6 +1735,19 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
this.applySettingsUpdate();
|
||||
})
|
||||
)
|
||||
new Setting(detailsEl)
|
||||
.setName(t("SCENE_IMAGE_CACHE_NAME"))
|
||||
.setDesc(fragWithHTML(t("SCENE_IMAGE_CACHE_DESC")))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.allowImageCacheInScene)
|
||||
.onChange((value) => {
|
||||
this.plugin.settings.allowImageCacheInScene = value;
|
||||
this.applySettingsUpdate();
|
||||
})
|
||||
)
|
||||
new Setting(detailsEl)
|
||||
.setName(t("EMBED_IMAGE_CACHE_CLEAR"))
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText(t("EMBED_IMAGE_CACHE_CLEAR"))
|
||||
@@ -1515,6 +1755,8 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
imageCache.clearImageCache();
|
||||
})
|
||||
)
|
||||
new Setting(detailsEl)
|
||||
.setName(t("BACKUP_CACHE_CLEAR"))
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText(t("BACKUP_CACHE_CLEAR"))
|
||||
@@ -1547,12 +1789,36 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
cls: "excalidraw-setting-h3",
|
||||
});
|
||||
addIframe(detailsEl, "wTtaXmRJ7wg",171);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME"))
|
||||
.setDesc(fragWithHTML(t("SHOW_DRAWING_OR_MD_IN_EXPORTPDF_DESC")))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.renderImageInMarkdownToPDF)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.renderImageInMarkdownToPDF = value;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("EXPORT_EMBED_SCENE_NAME"))
|
||||
.setDesc(fragWithHTML(t("EXPORT_EMBED_SCENE_DESC")))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.exportEmbedScene)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.exportEmbedScene = value;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
detailsEl = exportDetailsEl.createEl("details");
|
||||
detailsEl.createEl("summary", {
|
||||
text: t("EMBED_SIZING"),
|
||||
cls: "excalidraw-setting-h4",
|
||||
});
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("EMBED_WIDTH_NAME"))
|
||||
.setDesc(fragWithHTML(t("EMBED_WIDTH_DESC")))
|
||||
@@ -1567,6 +1833,20 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("EMBED_HEIGHT_NAME"))
|
||||
.setDesc(fragWithHTML(t("EMBED_HEIGHT_DESC")))
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("e.g.: 400")
|
||||
.setValue(this.plugin.settings.height)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.height = value;
|
||||
this.applySettingsUpdate();
|
||||
this.requestEmbedUpdate = true;
|
||||
}),
|
||||
);
|
||||
|
||||
let scaleText: HTMLDivElement;
|
||||
|
||||
new Setting(detailsEl)
|
||||
@@ -1857,7 +2137,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);
|
||||
});
|
||||
@@ -1924,6 +2204,35 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
cls: "excalidraw-setting-h1",
|
||||
});
|
||||
|
||||
detailsEl = nonstandardDetailsEl.createEl("details");
|
||||
detailsEl.createEl("summary", {
|
||||
text: t("RENDER_TWEAK_HEAD"),
|
||||
cls: "excalidraw-setting-h3",
|
||||
});
|
||||
|
||||
let areaZoomText: HTMLDivElement;
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("MAX_IMAGE_ZOOM_IN_NAME"))
|
||||
.setDesc(fragWithHTML(t("MAX_IMAGE_ZOOM_IN_DESC")))
|
||||
.addSlider((slider) =>
|
||||
slider
|
||||
.setLimits(1, 10, 0.5)
|
||||
.setValue(this.plugin.settings.areaZoomLimit)
|
||||
.onChange(async (value) => {
|
||||
areaZoomText.innerText = ` ${value.toString()}`;
|
||||
this.plugin.settings.areaZoomLimit = value;
|
||||
this.applySettingsUpdate();
|
||||
this.plugin.excalidrawConfig.updateValues(this.plugin);
|
||||
}),
|
||||
)
|
||||
.settingEl.createDiv("", (el) => {
|
||||
areaZoomText = el;
|
||||
el.style.minWidth = "2.3em";
|
||||
el.style.textAlign = "right";
|
||||
el.innerText = ` ${this.plugin.settings.areaZoomLimit.toString()}`;
|
||||
});
|
||||
|
||||
detailsEl = nonstandardDetailsEl.createEl("details");
|
||||
detailsEl.createEl("summary", {
|
||||
text: t("CUSTOM_PEN_HEAD"),
|
||||
@@ -2059,6 +2368,31 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("FADE_OUT_EXCALIDRAW_MARKUP_NAME"))
|
||||
.setDesc(fragWithHTML(t("FADE_OUT_EXCALIDRAW_MARKUP_DESC")))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.fadeOutExcalidrawMarkup)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.fadeOutExcalidrawMarkup = value;
|
||||
this.plugin.editorHandler.updateCMExtensionState(EDITOR_FADEOUT, value)
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
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"),
|
||||
@@ -2187,6 +2521,47 @@ 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"))
|
||||
.setDesc(fragWithHTML(t("DEBUGMODE_DESC")))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.isDebugMode)
|
||||
.onChange((value) => {
|
||||
this.plugin.settings.isDebugMode = value;
|
||||
setDebugging(value);
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("SLIDING_PANES_NAME"))
|
||||
.setDesc(fragWithHTML(t("SLIDING_PANES_DESC")))
|
||||
|
||||
@@ -4,37 +4,55 @@ import { createTreeWalker, walk } from "./walker";
|
||||
|
||||
export type ConversionResult = {
|
||||
hasErrors: boolean;
|
||||
errors: NodeListOf<Element> | null;
|
||||
errors: string;
|
||||
content: any; // Serialized Excalidraw JSON
|
||||
};
|
||||
|
||||
export const svgToExcalidraw = (svgString: string): ConversionResult => {
|
||||
const parser = new DOMParser();
|
||||
const svgDOM = parser.parseFromString(svgString, "image/svg+xml");
|
||||
try {
|
||||
const parser = new DOMParser();
|
||||
const svgDOM = parser.parseFromString(svgString, "image/svg+xml");
|
||||
|
||||
// was there a parsing error?
|
||||
const errorsElements = svgDOM.querySelectorAll("parsererror");
|
||||
const hasErrors = errorsElements.length > 0;
|
||||
let content = null;
|
||||
// was there a parsing error?
|
||||
const errorsElements = svgDOM.querySelectorAll("parsererror");
|
||||
const hasErrors = errorsElements.length > 0;
|
||||
let content = null;
|
||||
|
||||
if (hasErrors) {
|
||||
console.error(
|
||||
"There were errors while parsing the given SVG: ",
|
||||
[...errorsElements].map((el) => el.innerHTML),
|
||||
);
|
||||
} else {
|
||||
const tw = createTreeWalker(svgDOM);
|
||||
const scene = new ExcalidrawScene();
|
||||
const groups: Group[] = [];
|
||||
if (hasErrors) {
|
||||
console.error(
|
||||
"There were errors while parsing the given SVG: ",
|
||||
[...errorsElements].map((el) => el.innerHTML),
|
||||
);
|
||||
} else {
|
||||
const tw = createTreeWalker(svgDOM);
|
||||
const scene = new ExcalidrawScene();
|
||||
const groups: Group[] = [];
|
||||
|
||||
walk({ tw, scene, groups, root: svgDOM }, tw.nextNode());
|
||||
walk({ tw, scene, groups, root: svgDOM }, tw.nextNode());
|
||||
|
||||
content = scene.elements; //scene.toExJSON();
|
||||
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();
|
||||
}
|
||||
|
||||
return {
|
||||
hasErrors,
|
||||
errors: hasErrors ? `${[...errorsElements].map((el) => el.innerHTML)}` : "",
|
||||
content,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {
|
||||
hasErrors: true,
|
||||
errors: `${error}`,
|
||||
content:[],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
hasErrors,
|
||||
errors: hasErrors ? errorsElements : null,
|
||||
content,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
25
src/types.d.ts
vendored
25
src/types.d.ts
vendored
@@ -1,11 +1,12 @@
|
||||
import { ExcalidrawAutomate } from "./ExcalidrawAutomate";
|
||||
import { ExcalidrawLib } from "./ExcalidrawLib";
|
||||
|
||||
export type ConnectionPoint = "top" | "bottom" | "left" | "right" | null;
|
||||
|
||||
export type Packages = {
|
||||
react: any,
|
||||
reactDOM: any,
|
||||
excalidrawLib: any,
|
||||
excalidrawLib: typeof ExcalidrawLib,
|
||||
}
|
||||
|
||||
export type ValueOf<T> = T[keyof T];
|
||||
@@ -35,6 +36,9 @@ declare module "obsidian" {
|
||||
internalPlugins: any;
|
||||
isMobile(): boolean;
|
||||
getObsidianUrl(file:TFile): string;
|
||||
metadataTypeManager: {
|
||||
setType(name:string, type:string): void;
|
||||
};
|
||||
}
|
||||
interface Keymap {
|
||||
getRootScope(): Scope;
|
||||
@@ -55,10 +59,25 @@ declare module "obsidian" {
|
||||
},
|
||||
basePath: string;
|
||||
}
|
||||
interface Editor {
|
||||
insertText(data: string): void;
|
||||
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 }> };
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ container.appendChild(node.contentEl)
|
||||
import { TFile, WorkspaceLeaf, WorkspaceSplit } from "obsidian";
|
||||
import ExcalidrawView from "src/ExcalidrawView";
|
||||
import { getContainerForDocument, ConstructableWorkspaceSplit, isObsidianThemeDark } from "./ObsidianUtils";
|
||||
import { CustomMutationObserver, isDebugMode } from "./DebugHelper";
|
||||
import { CustomMutationObserver, DEBUGGING } from "./DebugHelper";
|
||||
|
||||
declare module "obsidian" {
|
||||
interface Workspace {
|
||||
@@ -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,
|
||||
@@ -80,8 +82,11 @@ export class CanvasNodeFactory {
|
||||
return node;
|
||||
}
|
||||
|
||||
public startEditing(node: ObsidianCanvasNode, theme: string) {
|
||||
public async startEditing(node: ObsidianCanvasNode, theme: string) {
|
||||
if (!this.initialized || !node) return;
|
||||
if (node.file === this.view.file) {
|
||||
await this.view.setEmbeddableIsEditingSelf();
|
||||
}
|
||||
node.startEditing();
|
||||
|
||||
const obsidianTheme = isObsidianThemeDark() ? "theme-dark" : "theme-light";
|
||||
@@ -107,26 +112,46 @@ export class CanvasNodeFactory {
|
||||
}
|
||||
}
|
||||
};
|
||||
const observer = isDebugMode
|
||||
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 });
|
||||
})();
|
||||
}
|
||||
|
||||
public stopEditing(node: ObsidianCanvasNode) {
|
||||
if(!this.initialized || !node) return;
|
||||
if(!node.child.editMode) return;
|
||||
if(node.file === this.view.file) {
|
||||
this.view.clearEmbeddableIsEditingSelf();
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
187
src/utils/CarveOut.ts
Normal file
187
src/utils/CarveOut.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { ExcalidrawEmbeddableElement, ExcalidrawFrameElement, ExcalidrawImageElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
|
||||
import { getEA } from "src";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
import { getCropFileNameAndFolder, getListOfTemplateFiles, splitFolderAndFilename } from "./FileUtils";
|
||||
import { Notice, TFile } from "obsidian";
|
||||
|
||||
export const CROPPED_PREFIX = "cropped_";
|
||||
export const ANNOTATED_PREFIX = "annotated_";
|
||||
|
||||
export const carveOutImage = async (sourceEA: ExcalidrawAutomate, viewImageEl: ExcalidrawImageElement) => {
|
||||
if(!viewImageEl?.fileId) return;
|
||||
if(!sourceEA?.targetView) return;
|
||||
|
||||
const targetEA = getEA(sourceEA.targetView) as ExcalidrawAutomate;
|
||||
|
||||
targetEA.copyViewElementsToEAforEditing([viewImageEl],true);
|
||||
const {height, width} = await sourceEA.getOriginalImageSize(viewImageEl);
|
||||
|
||||
if(!height || !width || height === 0 || width === 0) {
|
||||
targetEA.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
const newImage = targetEA.getElement(viewImageEl.id) as Mutable<ExcalidrawImageElement>;
|
||||
newImage.x = 0;
|
||||
newImage.y = 0;
|
||||
newImage.width = width;
|
||||
newImage.height = height;
|
||||
const scale = newImage.scale;
|
||||
const angle = newImage.angle;
|
||||
newImage.scale = [1,1];
|
||||
newImage.angle = 0;
|
||||
|
||||
const ef = sourceEA.targetView.excalidrawData.getFile(viewImageEl.fileId);
|
||||
let imageLink = "";
|
||||
let fname = "";
|
||||
if(ef.file) {
|
||||
fname = ef.file.basename;
|
||||
const ref = ef.linkParts?.ref ? `#${ef.linkParts.ref}` : ``;
|
||||
imageLink = `[[${ef.file.path}${ref}]]`;
|
||||
} else {
|
||||
const imagename = ef.hyperlink?.match(/^.*\/([^?]*)\??.*$/)?.[1];
|
||||
imageLink = ef.hyperlink;
|
||||
fname = viewImageEl
|
||||
? imagename.substring(0,imagename.lastIndexOf("."))
|
||||
: "_image";
|
||||
}
|
||||
|
||||
const {folderpath, filename} = await getCropFileNameAndFolder(sourceEA.plugin,sourceEA.targetView.file.path,fname);
|
||||
|
||||
const file = await createImageCropperFile(targetEA, newImage.id, imageLink, folderpath, filename);
|
||||
if(!file) {
|
||||
targetEA.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
//console.log(await app.vault.read(file));
|
||||
sourceEA.clear();
|
||||
sourceEA.copyViewElementsToEAforEditing([viewImageEl]);
|
||||
const sourceImageEl = sourceEA.getElement(viewImageEl.id) as Mutable<ExcalidrawImageElement>;
|
||||
sourceImageEl.isDeleted = true;
|
||||
|
||||
const replacingImageID = await sourceEA.addImage(sourceImageEl.x, sourceImageEl.y, file, true);
|
||||
const replacingImage = sourceEA.getElement(replacingImageID) as Mutable<ExcalidrawImageElement>;
|
||||
replacingImage.width = sourceImageEl.width;
|
||||
replacingImage.height = sourceImageEl.height;
|
||||
replacingImage.scale = scale;
|
||||
replacingImage.angle = angle;
|
||||
await sourceEA.addElementsToView(false, true, true);
|
||||
targetEA.destroy();
|
||||
}
|
||||
|
||||
export const carveOutPDF = async (sourceEA: ExcalidrawAutomate, embeddableEl: ExcalidrawEmbeddableElement, pdfPathWithPage: string, pdfFile: TFile) => {
|
||||
if(!embeddableEl || !pdfPathWithPage || !sourceEA?.targetView) return;
|
||||
|
||||
const targetEA = getEA(sourceEA.targetView) as ExcalidrawAutomate;
|
||||
|
||||
let {height, width} = embeddableEl;
|
||||
|
||||
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>;
|
||||
newImage.x = 0;
|
||||
newImage.y = 0;
|
||||
const angle = embeddableEl.angle;
|
||||
|
||||
const fname = pdfFile.basename;
|
||||
const imageLink = `[[${pdfPathWithPage}]]`;
|
||||
|
||||
const {folderpath, filename} = await getCropFileNameAndFolder(sourceEA.plugin,sourceEA.targetView.file.path,fname);
|
||||
|
||||
const file = await createImageCropperFile(targetEA, newImage.id, imageLink, folderpath, filename);
|
||||
if(!file) {
|
||||
targetEA.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
//console.log(await app.vault.read(file));
|
||||
sourceEA.clear();
|
||||
const replacingImageID = await sourceEA.addImage(embeddableEl.x + embeddableEl.width + 10, embeddableEl.y, file, true);
|
||||
const replacingImage = sourceEA.getElement(replacingImageID) as Mutable<ExcalidrawImageElement>;
|
||||
const imageAspectRatio = replacingImage.width / replacingImage.height;
|
||||
if(imageAspectRatio > 1) {
|
||||
replacingImage.width = embeddableEl.width;
|
||||
replacingImage.height = replacingImage.width / imageAspectRatio;
|
||||
} else {
|
||||
replacingImage.height = embeddableEl.height;
|
||||
replacingImage.width = replacingImage.height * imageAspectRatio;
|
||||
}
|
||||
replacingImage.angle = angle;
|
||||
await sourceEA.addElementsToView(false, true, true);
|
||||
targetEA.destroy();
|
||||
}
|
||||
|
||||
|
||||
export const createImageCropperFile = async (targetEA: ExcalidrawAutomate, imageID: string, imageLink:string, foldername: string, filename: string): Promise<TFile> => {
|
||||
const vault = targetEA.plugin.app.vault;
|
||||
const newImage = targetEA.getElement(imageID) as Mutable<ExcalidrawImageElement>;
|
||||
const { width, height } = newImage;
|
||||
const isPDF = imageLink.match(/\[\[([^#]*)#.*]]/)?.[1]?.endsWith(".pdf");
|
||||
|
||||
newImage.opacity = 100;
|
||||
newImage.locked = true;
|
||||
newImage.link = imageLink;
|
||||
|
||||
const frameID = targetEA.addFrame(0,0,width,height,"Adjust frame to crop image. Add elements for mask: White shows, Black hides.");
|
||||
const frame = targetEA.getElement(frameID) as Mutable<ExcalidrawFrameElement>;
|
||||
frame.link = imageLink;
|
||||
|
||||
newImage.frameId = frameID;
|
||||
|
||||
targetEA.style.opacity = 50;
|
||||
targetEA.style.fillStyle = "solid";
|
||||
targetEA.style.strokeStyle = "solid";
|
||||
targetEA.style.strokeColor = "black";
|
||||
targetEA.style.backgroundColor = "black";
|
||||
targetEA.style.roughness = 0;
|
||||
targetEA.style.roundness = null;
|
||||
targetEA.canvas.theme = "light";
|
||||
targetEA.canvas.viewBackgroundColor = isPDF ? "#5d5d5d" : "#3d3d3d";
|
||||
|
||||
const templates = getListOfTemplateFiles(targetEA.plugin);
|
||||
const templateFile = templates && templates.length > 0 ? templates[0] : null;
|
||||
if(templateFile && templateFile instanceof TFile) {
|
||||
const {appState} = await targetEA.getSceneFromFile(templateFile);
|
||||
if(appState) {
|
||||
targetEA.style.fontFamily = appState.currentItemFontFamily;
|
||||
targetEA.style.fontSize = appState.currentItemFontSize;
|
||||
}
|
||||
}
|
||||
|
||||
const newPath = await targetEA.create ({
|
||||
filename,
|
||||
foldername,
|
||||
onNewPane: true,
|
||||
frontmatterKeys: {
|
||||
"excalidraw-mask": true,
|
||||
"excalidraw-export-dark": false,
|
||||
"excalidraw-export-padding": 0,
|
||||
"excalidraw-export-transparent": true,
|
||||
...isPDF ? {"cssclasses": "excalidraw-cropped-pdfpage"} : {},
|
||||
}
|
||||
});
|
||||
|
||||
//console.log({newPath});
|
||||
|
||||
//wait for file to be created/indexed by Obsidian
|
||||
let file = vault.getAbstractFileByPath(newPath);
|
||||
let counter = 0;
|
||||
while((!file || !targetEA.isExcalidrawFile(file as TFile)) && counter < 50) {
|
||||
await sleep(100);
|
||||
file = vault.getAbstractFileByPath(newPath);
|
||||
counter++;
|
||||
}
|
||||
//console.log({counter, file});
|
||||
if(!file || !(file instanceof TFile)) {
|
||||
new Notice("File not found. NewExcalidraw Drawing is taking too long to create. Please try again.");
|
||||
return;
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
188
src/utils/CropImage.ts
Normal file
188
src/utils/CropImage.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { ExcalidrawElement, FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { BinaryFileData } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
|
||||
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 {
|
||||
private imageEA: ExcalidrawAutomate;
|
||||
private maskEA: ExcalidrawAutomate;
|
||||
private bbox: {topX: number, topY: number, width: number, height: number};
|
||||
|
||||
constructor (
|
||||
private elements: ExcalidrawElement[],
|
||||
files: Map<FileId,BinaryFileData>,
|
||||
) {
|
||||
const imageEA = getEA() as ExcalidrawAutomate;
|
||||
this.imageEA = imageEA;
|
||||
const maskEA = getEA() as ExcalidrawAutomate;
|
||||
this.maskEA = maskEA;
|
||||
|
||||
this.bbox = imageEA.getBoundingBox(elements);
|
||||
//this makes both the image and the mask the same size
|
||||
//Adding the bounding element first so it is at the bottom of the layers - does not override the image.
|
||||
this.setBoundingEl(imageEA, "transparent");
|
||||
this.setBoundingEl(maskEA, "white"); //the bbox should not mask the image. White lets everything through.
|
||||
|
||||
elements.forEach(el => {
|
||||
const newEl = cloneElement(el) as Mutable<ExcalidrawElement>;
|
||||
if(el.type !== "image" && el.type !== "frame") {
|
||||
newEl.opacity = 100;
|
||||
maskEA.elementsDict[el.id] = newEl;
|
||||
}
|
||||
if(el.type === "image") {
|
||||
imageEA.elementsDict[el.id] = newEl;
|
||||
}
|
||||
})
|
||||
|
||||
Object.values(files).forEach(file => {
|
||||
imageEA.imagesDict[file.id] = file;
|
||||
})
|
||||
}
|
||||
|
||||
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;
|
||||
ea.style.strokeColor = "transparent";
|
||||
//@ts-ignore: Setting this to string "0" will produce a rectangle with zero stroke width
|
||||
ea.style.strokeWidth = "0";
|
||||
ea.style.strokeStyle = "solid";
|
||||
ea.style.fillStyle = "solid";
|
||||
ea.style.roughness = 0;
|
||||
ea.addRect(topX, topY, width, height);
|
||||
}
|
||||
|
||||
private getViewBoxAndSize(): {viewBox: string, vbWidth: number, vbHeight: number, width: number, height: number} {
|
||||
const frames = this.elements.filter(el=>el.type === "frame");
|
||||
if(frames.length > 1) {
|
||||
new Notice("Multiple frames are not supported for image cropping. Discarding frames from mask.");
|
||||
}
|
||||
const images = this.imageEA.getElements().filter(el=>el.type === "image");
|
||||
const {x: frameX, y: frameY, width: frameWidth, height: frameHeight} = frames.length === 1
|
||||
? frames[0]
|
||||
: mapToXY(this.imageEA.getBoundingBox(images));
|
||||
const {topX, topY, width, height} = this.bbox;
|
||||
return {
|
||||
viewBox: `${frameX-topX} ${frameY-topY} ${frameWidth} ${frameHeight}`,
|
||||
vbWidth: frameWidth,
|
||||
vbHeight: frameHeight,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
private async getMaskSVG():Promise<{style: string, mask: string}> {
|
||||
const exportSettings:ExportSettings = {
|
||||
withBackground: false,
|
||||
withTheme: false,
|
||||
isMask: false,
|
||||
}
|
||||
|
||||
const maskSVG = await this.maskEA.createSVG(null,false,exportSettings,null,null,0);
|
||||
const defs = maskSVG.querySelector("defs");
|
||||
const styleEl = maskSVG.querySelector("style");
|
||||
const style = styleEl ? styleEl.outerHTML : "";
|
||||
defs.parentElement.removeChild(defs);
|
||||
return {style, mask:maskSVG.innerHTML};
|
||||
}
|
||||
|
||||
private async getImage() {
|
||||
const exportSettings:ExportSettings = {
|
||||
withBackground: false,
|
||||
withTheme: false,
|
||||
isMask: false,
|
||||
}
|
||||
const isRotated = this.imageEA.getElements().some(el=>el.type === "image" && el.angle !== 0);
|
||||
const images = Object.values(this.imageEA.imagesDict);
|
||||
if(!isRotated && (images.length === 1)) {
|
||||
return images[0].dataURL;
|
||||
}
|
||||
return await this.imageEA.createPNGBase64(null,1,exportSettings,null,null,0);
|
||||
}
|
||||
|
||||
private async buildSVG(): Promise<SVGSVGElement> {
|
||||
if(this.imageEA.getElements().filter(el=>el.type === "image").length === 0) {
|
||||
new Notice("No image found. Cannot crop.");
|
||||
return;
|
||||
}
|
||||
const maskID = nanoid();
|
||||
const imageID = nanoid();
|
||||
const {viewBox, vbWidth, vbHeight, width, height} = this.getViewBoxAndSize();
|
||||
const parser = new DOMParser();
|
||||
const {style, mask} = await this.getMaskSVG();
|
||||
const svgString = `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="${viewBox}" width="${vbWidth}" height="${vbHeight}">\n` +
|
||||
`<symbol id="${imageID}"><image width="100%" height="100%" href="${await this.getImage()}"/></symbol>\n` +
|
||||
`<defs>${style}\n<mask id="${maskID}" x="0" y="0" width="${width}" height="${height}" maskUnits="userSpaceOnUse">\n${mask}\n</mask>\n</defs>\n` +
|
||||
`<use x="0" y="0" width="${width}" height="${height}" mask="url(#${maskID})" mask-type="alpha" href="#${imageID}"/>\n</svg>`;
|
||||
return parser.parseFromString(
|
||||
svgString,
|
||||
"image/svg+xml",
|
||||
).firstElementChild as SVGSVGElement
|
||||
|
||||
}
|
||||
|
||||
async getCroppedPNG(): Promise<Blob> {
|
||||
//@ts-ignore
|
||||
const PLUGIN = app.plugins.plugins["obsidian-excalidraw-plugin"];
|
||||
const svg = embedFontsInSVG(await this.buildSVG(), PLUGIN);
|
||||
return new Promise((resolve, reject) => {
|
||||
const svgData = new XMLSerializer().serializeToString(svg);
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
if (!context) {
|
||||
reject('Unable to get 2D context');
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.width = svg.width.baseVal.value;
|
||||
canvas.height = svg.height.baseVal.value;
|
||||
|
||||
const image = new Image();
|
||||
image.onload = () => {
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
context.drawImage(image, 0, 0);
|
||||
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
if (blob) {
|
||||
resolve(blob);
|
||||
} else {
|
||||
reject(new Error('Failed to convert to PNG'));
|
||||
}
|
||||
},
|
||||
'image/png',
|
||||
1 // image quality (0 - 1)
|
||||
);
|
||||
};
|
||||
image.src = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svgData)))}`;
|
||||
});
|
||||
}
|
||||
|
||||
async getCroppedSVG() {
|
||||
return await this.buildSVG();
|
||||
}
|
||||
}
|
||||
|
||||
const mapToXY = ({topX, topY, width, height}: {topX: number, topY: number, width: number, height: number}): {x: number, y: number, width: number, height: number} => {
|
||||
return {
|
||||
x: topX,
|
||||
y: topY,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,18 @@
|
||||
export const isDebugMode = false;
|
||||
export const durationTreshold = 0; //0.05; //ms
|
||||
|
||||
export function setDebugging(value: boolean) {
|
||||
DEBUGGING = (process.env.NODE_ENV === 'development')
|
||||
? value
|
||||
: false;
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
export class CustomMutationObserver {
|
||||
private originalCallback: MutationCallback;
|
||||
private observer: MutationObserver | null;
|
||||
@@ -19,7 +31,7 @@ export class CustomMutationObserver {
|
||||
const endTime = performance.now(); // Get end time
|
||||
const executionTime = endTime - startTime;
|
||||
if (executionTime > durationTreshold) {
|
||||
console.log(`Excalidraw ${this.name} MutationObserver callback took ${executionTime}ms to execute`);
|
||||
console.log(`Excalidraw ${this.name} MutationObserver callback took ${executionTime}ms to execute`, observer);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -23,15 +23,17 @@ export const setDynamicStyle = (
|
||||
toolsStyle = toolsStyle.replace(/\-\-color\-primary.*/,"");
|
||||
toolspanel.setAttribute("style",toolsStyle);
|
||||
}
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
const doc = view.ownerDocument;
|
||||
//const doc = view.ownerDocument;
|
||||
const isLightTheme =
|
||||
view?.excalidrawAPI?.getAppState?.()?.theme === "light" ||
|
||||
view?.excalidrawData?.scene?.appState?.theme === "light";
|
||||
|
||||
if (color==="transparent") {
|
||||
color = "#ffffff";
|
||||
}
|
||||
|
||||
const darker = "#101010";
|
||||
const lighter = "#f0f0f0";
|
||||
const step = 10;
|
||||
@@ -98,7 +100,6 @@ export const setDynamicStyle = (
|
||||
[`--color-gray-50`]: str(text), //frame
|
||||
[`--color-surface-highlight`]: str(gray1()),
|
||||
//[`--color-gray-30`]: str(gray1),
|
||||
[`--color-gray-80`]: str(isDark?text.lighterBy(15):text.darkerBy(15)), //frame
|
||||
[`--sidebar-border-color`]: str(gray1()),
|
||||
[`--color-primary-light`]: str(accent().lighterBy(step)),
|
||||
[`--button-hover-bg`]: str(gray1()),
|
||||
@@ -111,8 +112,10 @@ export const setDynamicStyle = (
|
||||
[`--h2-color`]: str(text),
|
||||
[`--h3-color`]: str(text),
|
||||
[`--h4-color`]: str(text),
|
||||
[`color`]: str(text),
|
||||
[`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
|
||||
};
|
||||
|
||||
const styleString = Object.keys(styleObject)
|
||||
@@ -124,13 +127,26 @@ 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)),
|
||||
nameColor: str(isDark?gray2().lighterBy(40):gray2().darkerBy(40)),
|
||||
nameColor: str(isDark?gray2().lighterBy(50):gray2().darkerBy(50)),
|
||||
}
|
||||
const scene = api.getSceneElements();
|
||||
scene.filter(el=>el.type==="frame").forEach((e:ExcalidrawFrameElement)=>{
|
||||
@@ -153,11 +169,9 @@ export const setDynamicStyle = (
|
||||
dynamicStyle: styleObject
|
||||
}
|
||||
});
|
||||
view = null;
|
||||
ea = null;
|
||||
color = null;
|
||||
dynamicStyle = null;
|
||||
});
|
||||
const toolspanel = view.toolsPanelRef?.current?.containerRef?.current;
|
||||
if(toolspanel) {
|
||||
let toolsStyle = toolspanel.getAttribute("style");
|
||||
toolsStyle = toolsStyle.replace(/\-\-color\-primary.*/,"");
|
||||
toolspanel.setAttribute("style",toolsStyle+styleString);
|
||||
}
|
||||
}
|
||||
17
src/utils/ExcalidrawConfig.ts
Normal file
17
src/utils/ExcalidrawConfig.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { DEVICE } from "src/constants/constants";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
|
||||
export class ExcalidrawConfig {
|
||||
public areaLimit: number = 16777216;
|
||||
public widthHeightLimit: number = 32767;
|
||||
|
||||
constructor(plugin: ExcalidrawPlugin) {
|
||||
this.updateValues(plugin);
|
||||
}
|
||||
|
||||
updateValues(plugin: ExcalidrawPlugin) {
|
||||
if(DEVICE.isIOS) return;
|
||||
this.areaLimit = 16777216*plugin.settings.areaZoomLimit; //this.plugin.settings.areaLimit;
|
||||
this.widthHeightLimit = 32767*plugin.settings.areaZoomLimit; //his.plugin.settings.widthHeightLimit;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,24 @@
|
||||
|
||||
import { MAX_IMAGE_SIZE, IMAGE_TYPES, ANIMATED_IMAGE_TYPES } from "src/constants/constants";
|
||||
import { 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 } from "src/ExcalidrawData";
|
||||
import ExcalidrawView from "src/ExcalidrawView";
|
||||
import { ExcalidrawElement, ExcalidrawFrameElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { getLinkParts } 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> => {
|
||||
):Promise<string> {
|
||||
ea.clear();
|
||||
ea.style.strokeColor = "transparent";
|
||||
ea.style.backgroundColor = "transparent";
|
||||
@@ -25,17 +34,17 @@ export const insertImageToView = async (
|
||||
return id;
|
||||
}
|
||||
|
||||
export const insertEmbeddableToView = async (
|
||||
export async function insertEmbeddableToView (
|
||||
ea: ExcalidrawAutomate,
|
||||
position: { x: number, y: number },
|
||||
file?: TFile,
|
||||
link?: string,
|
||||
):Promise<string> => {
|
||||
):Promise<string> {
|
||||
ea.clear();
|
||||
ea.style.strokeColor = "transparent";
|
||||
ea.style.backgroundColor = "transparent";
|
||||
if(file && (IMAGE_TYPES.contains(file.extension) || ea.isExcalidrawFile(file)) && !ANIMATED_IMAGE_TYPES.contains(file.extension)) {
|
||||
return await insertImageToView(ea, position, file);
|
||||
return await insertImageToView(ea, position, link??file);
|
||||
} else {
|
||||
const id = ea.addEmbeddable(
|
||||
position.x,
|
||||
@@ -50,7 +59,7 @@ export const insertEmbeddableToView = async (
|
||||
}
|
||||
}
|
||||
|
||||
export const getLinkTextFromLink = (text: string): string => {
|
||||
export function getLinkTextFromLink (text: string): string {
|
||||
if (!text) return;
|
||||
if (text.match(REG_LINKINDEX_HYPERLINK)) return;
|
||||
|
||||
@@ -61,4 +70,314 @@ export const getLinkTextFromLink = (text: string): string => {
|
||||
if (linktext.match(REG_LINKINDEX_HYPERLINK)) return;
|
||||
|
||||
return linktext;
|
||||
}
|
||||
|
||||
export function 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) {
|
||||
return;
|
||||
}
|
||||
const search = app.workspace.getLeavesOfType("search");
|
||||
if (search.length == 0) {
|
||||
return;
|
||||
}
|
||||
//@ts-ignore
|
||||
search[0].view.setQuery(`tag:${tags.value[1]}`);
|
||||
app.workspace.revealLeaf(search[0]);
|
||||
|
||||
if (view && view.isFullscreen()) {
|
||||
view.exitFullscreen();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
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.startsWith("obsidian://") && link.match(REG_LINKINDEX_HYPERLINK)) {
|
||||
window.open(link, "_blank");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @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) {
|
||||
const linkset = new Set<string>();
|
||||
forwardLinks.forEach(link => {
|
||||
const linkparts = getLinkParts(link.link);
|
||||
const f = app.metadataCache.getFirstLinkpathDest(linkparts.path, excalidrawFile.path);
|
||||
if(f && f.path !== excalidrawFile.path) {
|
||||
if(secondOrderLinksSet.has(f.path)) return;
|
||||
secondOrderLinksSet.add(f.path);
|
||||
linkset.add(`[[${f.path}${linkparts.ref?"#"+linkparts.ref:""}|Second Order Link: ${f.basename}]]`);
|
||||
}
|
||||
});
|
||||
secondOrderLinks = [...linkset].join(" ");
|
||||
}
|
||||
return secondOrderLinks;
|
||||
}
|
||||
|
||||
export function getFrameBasedOnFrameNameOrId(
|
||||
frameName: string,
|
||||
elements: ExcalidrawElement[],
|
||||
): ExcalidrawFrameElement | null {
|
||||
const frames = elements
|
||||
.filter((el: ExcalidrawElement)=>el.type==="frame")
|
||||
.map((el: ExcalidrawFrameElement, idx: number)=>{
|
||||
return {el: el, id: el.id, name: el.name ?? `Frame ${String(idx+1).padStart(2,"0")}`};
|
||||
})
|
||||
.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) {
|
||||
setTimeout(()=>{
|
||||
api.updateScene({appState: {activeEmbeddable: {element: el, state: "active"}}});
|
||||
if(found) view.getEmbeddableLeafElementById(el.id)?.editNode?.();
|
||||
});
|
||||
}
|
||||
ea.destroy();
|
||||
return el.id;
|
||||
}
|
||||
|
||||
export function renderContextMenuAction(
|
||||
React: any,
|
||||
label: string,
|
||||
action: Function,
|
||||
onClose: (callback?: () => void) => void,
|
||||
) {
|
||||
return React.createElement (
|
||||
"li",
|
||||
{
|
||||
key: nanoid(),
|
||||
onClick: () => {
|
||||
onClose(()=>action())
|
||||
},
|
||||
},
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
className: "context-menu-item",
|
||||
},
|
||||
React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "context-menu-item__label",
|
||||
},
|
||||
label,
|
||||
),
|
||||
React.createElement(
|
||||
"kbd",
|
||||
{
|
||||
className: "context-menu-item__shortcut",
|
||||
},
|
||||
"", //this is where the shortcut may go in the future
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function tmpBruteForceCleanup (view: ExcalidrawView) {
|
||||
window.setTimeout(()=>{
|
||||
if(!view) return;
|
||||
// const cleanupHTMLElement = (el: Element) => {
|
||||
// //console.log(el);
|
||||
// while(el.firstElementChild) {
|
||||
// cleanupHTMLElement(el.firstElementChild);
|
||||
// el.removeChild(el.firstElementChild);
|
||||
// }
|
||||
// Object.keys(el).forEach((key) => {
|
||||
// //@ts-ignore
|
||||
// delete el[key];
|
||||
// });
|
||||
// el.empty();
|
||||
// }
|
||||
|
||||
// const cleanupLeaf = (l:any) => {
|
||||
// l.detach?.();
|
||||
// l.resizeObserver?.disconnect?.();
|
||||
// l.view?.unload?.();
|
||||
// l.component?.unload?.();
|
||||
// Object.keys(l).forEach((key) => {
|
||||
// const obj = l[key];
|
||||
// if (obj instanceof Element) {
|
||||
// // Recursively empty the DOM element's children
|
||||
// while (obj.firstChild) {
|
||||
// cleanupHTMLElement(obj.firstElementChild);
|
||||
// obj.removeChild(obj.firstElementChild);
|
||||
// }
|
||||
// obj.empty();
|
||||
// delete l[key];
|
||||
// return;
|
||||
// }
|
||||
// //@ts-ignore
|
||||
// delete l[key];
|
||||
// });
|
||||
// }
|
||||
|
||||
// //@ts-ignore
|
||||
// if(view.leaf && !view.leaf.parent) {
|
||||
// if(view.containerEl) {
|
||||
// cleanupHTMLElement(view.containerEl);
|
||||
// }
|
||||
// const leaves = new Set();
|
||||
// leaves.add(view.leaf);
|
||||
// while(leaves.has(view.leaf.getContainer())) {
|
||||
// leaves.add(view.leaf.getContainer());
|
||||
// }
|
||||
// const roots = new Set();
|
||||
// roots.add(view.leaf.getRoot());
|
||||
// leaves.forEach((leaf:WorkspaceLeaf) => {
|
||||
// roots.add(leaf.getRoot());
|
||||
// });
|
||||
// leaves.forEach((l:any) => cleanupLeaf(l));
|
||||
// leaves.clear();
|
||||
// roots.forEach((root:any) => cleanupLeaf(root));
|
||||
// roots.clear();
|
||||
// }
|
||||
|
||||
Object.keys(view).forEach((key) => {
|
||||
//@ts-ignore
|
||||
delete view[key];
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the text matches the transclusion pattern and if so,
|
||||
* check if the link in the transclusion can be resolved to a file in the vault.
|
||||
* if yes, call the callback function with the link and the file.
|
||||
* @param text
|
||||
* @param callback
|
||||
* @returns true if text is a transclusion and the link can be resolved to a file in the vault, false otherwise.
|
||||
*/
|
||||
export function isTextImageTransclusion (
|
||||
text: string,
|
||||
view: ExcalidrawView,
|
||||
callback: (link: string, file: TFile)=>void
|
||||
): boolean {
|
||||
const REG_TRANSCLUSION = /^!\[\[([^|\]]*)?.*?]]$|^!\[[^\]]*?]\((.*?)\)$/g;
|
||||
const match = text.trim().matchAll(REG_TRANSCLUSION).next(); //reset the iterator
|
||||
if(match?.value?.[0]) {
|
||||
const link = match.value[1] ?? match.value[2];
|
||||
const file = view.app.metadataCache.getFirstLinkpathDest(link?.split("#")[0], view.file.path);
|
||||
if(view.file === file) {
|
||||
new Notice(t("RECURSIVE_INSERT_ERROR"));
|
||||
return false;
|
||||
}
|
||||
if(file && file instanceof TFile) {
|
||||
if (file.extension !== "md" || view.plugin.isExcalidrawFile(file)) {
|
||||
callback(link, file);
|
||||
return true;
|
||||
} else {
|
||||
new Notice(t("USE_INSERT_FILE_MODAL"),5000);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { loadPdfJs, normalizePath, Notice, requestUrl, RequestUrlResponse, TAbstractFile, TFile, TFolder, Vault } from "obsidian";
|
||||
import { DEVICE, URLFETCHTIMEOUT } from "src/constants/constants";
|
||||
import { App, loadPdfJs, normalizePath, Notice, requestUrl, RequestUrlResponse, TAbstractFile, TFile, TFolder, Vault } from "obsidian";
|
||||
import { DEVICE, FRONTMATTER_KEYS, URLFETCHTIMEOUT } from "src/constants/constants";
|
||||
import { IMAGE_MIME_TYPES, MimeType } from "src/EmbeddedFileLoader";
|
||||
import { ExcalidrawSettings } from "src/settings";
|
||||
import { errorlog, getDataURL } from "./Utils";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
import { ANNOTATED_PREFIX, CROPPED_PREFIX } from "./CarveOut";
|
||||
import { getAttachmentsFolderAndFilePath } from "./ObsidianUtils";
|
||||
|
||||
/**
|
||||
* Splits a full path including a folderpath and a filename into separate folderpath and filename components
|
||||
@@ -332,6 +334,7 @@ export const getPathWithoutExtension = (f:TFile): string => {
|
||||
const VAULT_BASE_URL = DEVICE.isDesktop
|
||||
? app.vault.adapter.url.pathToFileURL(app.vault.adapter.basePath).toString()
|
||||
: "";
|
||||
|
||||
export const getInternalLinkOrFileURLLink = (
|
||||
path: string, plugin:ExcalidrawPlugin, alias?: string, sourceFile?: TFile
|
||||
):{link: string, isInternal: boolean, file?: TFile, url?: string} => {
|
||||
@@ -343,7 +346,7 @@ export const getInternalLinkOrFileURLLink = (
|
||||
const vault = plugin.app.vault;
|
||||
const fileURLString = vault.adapter.url.pathToFileURL(path).toString();
|
||||
if (fileURLString.startsWith(VAULT_BASE_URL)) {
|
||||
const internalPath = normalizePath(fileURLString.substring(VAULT_BASE_URL.length));
|
||||
const internalPath = normalizePath(unescape(fileURLString.substring(VAULT_BASE_URL.length)));
|
||||
const file = vault.getAbstractFileByPath(internalPath);
|
||||
if(file && file instanceof TFile) {
|
||||
const link = plugin.app.metadataCache.fileToLinktext(
|
||||
@@ -365,9 +368,111 @@ export const getInternalLinkOrFileURLLink = (
|
||||
*/
|
||||
export const getLink = (
|
||||
plugin: ExcalidrawPlugin,
|
||||
{ embed = true, path, alias }: { embed?: boolean; path: string; alias?: string }
|
||||
{ embed = true, path, alias }: { embed?: boolean; path: string; alias?: string },
|
||||
wikilinkOverride?: boolean
|
||||
):string => {
|
||||
return plugin.settings.embedWikiLink
|
||||
const isWikiLink = (typeof wikilinkOverride !== "undefined")
|
||||
? wikilinkOverride
|
||||
: plugin.settings.embedWikiLink;
|
||||
return isWikiLink
|
||||
? `${embed ? "!" : ""}[[${path}${alias ? `|${alias}` : ""}]]`
|
||||
: `${embed ? "!" : ""}[${alias ?? ""}](${encodeURI(path)})`
|
||||
}
|
||||
}
|
||||
|
||||
export const getAliasWithSize = (alias: string, size: string): string => {
|
||||
if(alias && alias !== "") {
|
||||
return `${alias}${size?`|${size}`:""}`;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
export const getCropFileNameAndFolder = async (plugin: ExcalidrawPlugin, hostPath: string, baseNewFileName: string):Promise<{folderpath: string, filename: string}> => {
|
||||
let prefix = plugin.settings.cropPrefix;
|
||||
if(!prefix || prefix.trim() === "") prefix = CROPPED_PREFIX;
|
||||
const filename = prefix + baseNewFileName + ".md";
|
||||
if(!plugin.settings.cropFolder || plugin.settings.cropFolder.trim() === "") {
|
||||
const folderpath = (await getAttachmentsFolderAndFilePath(plugin.app, hostPath, filename)).folder;
|
||||
return {folderpath, filename};
|
||||
}
|
||||
const folderpath = normalizePath(plugin.settings.cropFolder);
|
||||
await checkAndCreateFolder(folderpath);
|
||||
return {folderpath, filename};
|
||||
}
|
||||
|
||||
export const getAnnotationFileNameAndFolder = async (plugin: ExcalidrawPlugin, hostPath: string, baseNewFileName: string):Promise<{folderpath: string, filename: string}> => {
|
||||
let prefix = plugin.settings.annotatePrefix;
|
||||
if(!prefix || prefix.trim() === "") prefix = ANNOTATED_PREFIX;
|
||||
const filename = prefix + baseNewFileName + ".md";
|
||||
if(!plugin.settings.annotateFolder || plugin.settings.annotateFolder.trim() === "") {
|
||||
const folderpath = (await getAttachmentsFolderAndFilePath(plugin.app, hostPath, filename)).folder;
|
||||
return {folderpath, filename};
|
||||
}
|
||||
const folderpath = normalizePath(plugin.settings.annotateFolder);
|
||||
await checkAndCreateFolder(folderpath);
|
||||
return {folderpath, filename};
|
||||
}
|
||||
|
||||
export const getListOfTemplateFiles = (plugin: ExcalidrawPlugin):TFile[] | null => {
|
||||
const normalizedTemplatePath = normalizePath(plugin.settings.templateFilePath);
|
||||
const template = plugin.app.vault.getAbstractFileByPath(normalizedTemplatePath);
|
||||
if(template && template instanceof TFolder) {
|
||||
return plugin.app.vault.getFiles()
|
||||
.filter(f=>f.path.startsWith(template.path))
|
||||
.filter(f=>plugin.isExcalidrawFile(f))
|
||||
.sort((a,b)=>a.path.localeCompare(b.path))
|
||||
}
|
||||
if(template && template instanceof TFile) {
|
||||
return [template];
|
||||
}
|
||||
const templateFile = plugin.app.metadataCache.getFirstLinkpathDest(
|
||||
normalizedTemplatePath,
|
||||
"",
|
||||
);
|
||||
if(templateFile) {
|
||||
return [templateFile];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const fileShouldDefaultAsExcalidraw = (path:string, app:App):boolean => {
|
||||
if(!path) return false;
|
||||
const cache = app.metadataCache.getCache(path);
|
||||
return cache?.frontmatter &&
|
||||
cache.frontmatter[FRONTMATTER_KEYS["plugin"].name] &&
|
||||
!Boolean(cache.frontmatter[FRONTMATTER_KEYS["open-as-markdown"].name]);
|
||||
}
|
||||
|
||||
export const getExcalidrawEmbeddedFilesFiletree = (sourceFile: TFile, plugin: ExcalidrawPlugin):TFile[] => {
|
||||
if(!sourceFile || !plugin.isExcalidrawFile(sourceFile)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const fileList = new Set<TFile>();
|
||||
const app = plugin.app;
|
||||
|
||||
const addAttachedImages = (f:TFile) => Object
|
||||
.keys(app.metadataCache.resolvedLinks[f.path])
|
||||
.forEach(path => {
|
||||
const file = app.vault.getAbstractFileByPath(path);
|
||||
if (!file || !(file instanceof TFile)) return;
|
||||
const isExcalidraw = plugin.isExcalidrawFile(file);
|
||||
if (
|
||||
(file.extension === "md" && !isExcalidraw) ||
|
||||
fileList.has(file) //avoid infinite loops
|
||||
) {
|
||||
return;
|
||||
}
|
||||
fileList.add(file);
|
||||
if (isExcalidraw) {
|
||||
addAttachedImages(file);
|
||||
}
|
||||
});
|
||||
|
||||
addAttachedImages(sourceFile);
|
||||
return Array.from(fileList);
|
||||
}
|
||||
|
||||
export const hasExcalidrawEmbeddedImagesTreeChanged = (sourceFile: TFile, mtime:number, plugin: ExcalidrawPlugin):boolean => {
|
||||
const fileList = getExcalidrawEmbeddedFilesFiletree(sourceFile, plugin);
|
||||
return fileList.some(f=>f.stat.mtime > mtime);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export const getElementsAtPointer = (
|
||||
y <= pointer.y &&
|
||||
y + h >= pointer.y
|
||||
);
|
||||
});
|
||||
}).reverse();
|
||||
};
|
||||
|
||||
export const getTextElementAtPointer = (pointer: any, view: ExcalidrawView) => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { App, Notice, TFile } from "obsidian";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
import { convertSVGStringToElement } from "./Utils";
|
||||
import { FILENAMEPARTS, PreviewImageType } from "./UtilTypes";
|
||||
import { hasExcalidrawEmbeddedImagesTreeChanged } from "./FileUtils";
|
||||
|
||||
//@ts-ignore
|
||||
const DB_NAME = "Excalidraw " + app.appId;
|
||||
@@ -19,17 +20,18 @@ export type ImageKey = {
|
||||
isDark: boolean;
|
||||
previewImageType: PreviewImageType;
|
||||
scale: number;
|
||||
isTransparent: 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.previewImageType === PreviewImageType.SVGIMG
|
||||
? 1
|
||||
: key.previewImageType === PreviewImageType.PNG
|
||||
? 0
|
||||
: 2
|
||||
}#${key.scale}`; //key.isSVG ? 1 : 0
|
||||
}#${key.scale}${key.isTransparent?"#t":""}`; //key.isSVG ? 1 : 0
|
||||
|
||||
class ImageCache {
|
||||
private dbName: string;
|
||||
@@ -41,6 +43,19 @@ class ImageCache {
|
||||
private app: App;
|
||||
public initializationNotice: boolean = false;
|
||||
private obsidanURLCache = new Map<string, string>();
|
||||
private purgeInvalidCacheTimer: number = null;
|
||||
private purgeInvalidBackupTimer: number = null;
|
||||
|
||||
public destroy(): void {
|
||||
this.isInitializing = true;
|
||||
if(this.purgeInvalidCacheTimer) clearTimeout(this.purgeInvalidCacheTimer);
|
||||
if(this.purgeInvalidBackupTimer) clearTimeout(this.purgeInvalidBackupTimer);
|
||||
this.db = null;
|
||||
this.plugin = null;
|
||||
this.app = null;
|
||||
this.obsidanURLCache.clear();
|
||||
this.obsidanURLCache = null;
|
||||
}
|
||||
|
||||
constructor(dbName: string, cacheStoreName: string, backupStoreName: string) {
|
||||
this.dbName = dbName;
|
||||
@@ -127,8 +142,15 @@ class ImageCache {
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(async ()=>this.purgeInvalidCacheFiles(), 60000);
|
||||
setTimeout(async ()=>this.purgeInvalidBackupFiles(), 120000);
|
||||
this.purgeInvalidCacheTimer = window.setTimeout(async ()=>{
|
||||
this.purgeInvalidCacheTimer = null;
|
||||
this.purgeInvalidCacheFiles();
|
||||
}, 60000);
|
||||
|
||||
this.purgeInvalidBackupTimer = window.setTimeout(async ()=>{
|
||||
this.purgeInvalidBackupTimer = null;
|
||||
this.purgeInvalidBackupFiles();
|
||||
}, 120000);
|
||||
} finally {
|
||||
this.isInitializing = false;
|
||||
if(this.initializationNotice) {
|
||||
@@ -150,11 +172,11 @@ class ImageCache {
|
||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue | null>).result;
|
||||
if(cursor) {
|
||||
const key = cursor.key as string;
|
||||
const isLegacyKey = key.replaceAll(/[^#]/g,"").length < 9; // introduced hasGroupref, etc. in 1.9.28
|
||||
const isLegacyKey = key.split("#").length-1 < 11; // introduced hasGroupref, etc. in 1.9.28 // introduced hasClippedFrameref in 2.2.10
|
||||
const filepath = key.split("#")[0];
|
||||
const fileExists = files.some((f: TFile) => f.path === filepath);
|
||||
const file = fileExists ? files.find((f: TFile) => f.path === filepath) : null;
|
||||
if (isLegacyKey || !file || (file && file.stat.mtime > cursor.value.mtime) || (!cursor.value.blob && !cursor.value.svg)) {
|
||||
if (isLegacyKey || !file || (file && (file.stat.mtime > cursor.value.mtime)) || (!cursor.value.blob && !cursor.value.svg)) {
|
||||
deletePromises.push(
|
||||
new Promise<void>((innerResolve, innerReject) => {
|
||||
const deleteRequest = store.delete(cursor.primaryKey);
|
||||
@@ -291,7 +313,10 @@ class ImageCache {
|
||||
|
||||
const file = this.app.vault.getAbstractFileByPath(key_.filepath.split("#")[0]);
|
||||
if (!file || !(file instanceof TFile)) return undefined;
|
||||
if (cachedData && cachedData.mtime === file.stat.mtime) {
|
||||
if (cachedData && (cachedData.mtime >= file.stat.mtime)) {
|
||||
if(hasExcalidrawEmbeddedImagesTreeChanged(file, cachedData.mtime, this.plugin)) {
|
||||
return undefined;
|
||||
}
|
||||
if(cachedData.svg) {
|
||||
return convertSVGStringToElement(cachedData.svg);
|
||||
}
|
||||
@@ -316,6 +341,7 @@ class ImageCache {
|
||||
return this.getBackupData(filepath);
|
||||
}
|
||||
|
||||
//cache SVG should have the width and height parameters and not the embedded font
|
||||
public addImageToCache(key_: ImageKey, obsidianURL: string, image: Blob|SVGSVGElement): void {
|
||||
if (!this.isReady()) {
|
||||
return; // Database not initialized yet
|
||||
@@ -332,7 +358,7 @@ class ImageCache {
|
||||
} else {
|
||||
blob = image as Blob;
|
||||
}
|
||||
const data: FileCacheData = { mtime: file.stat.mtime, blob, svg};
|
||||
const data: FileCacheData = { mtime: Date.now(), blob, svg};
|
||||
const transaction = this.db.transaction(this.cacheStoreName, "readwrite");
|
||||
const store = transaction.objectStore(this.cacheStoreName);
|
||||
const key = getKey(key_);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export const modifierKeyTooltipMessages = ():ModifierKeyTooltipMessages => {
|
||||
// Add more messages for WebBrowserDragAction as needed
|
||||
},
|
||||
LocalFileDragAction: {
|
||||
"image-import": "Insert Image: import external or reuse existing if path in Vault",
|
||||
"image-import": "Import external file or reuse existing file if path is from the Vault",
|
||||
"image-url": `Insert Image: with local URI or internal-link if from Vault`,
|
||||
"link": "Insert Link: local URI or internal-link if from Vault",
|
||||
"embeddable": "Insert Interactive-Frame: local URI or internal-link if from Vault",
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import {
|
||||
App,
|
||||
normalizePath, parseFrontMatterEntry, TFile, Workspace, WorkspaceLeaf, WorkspaceSplit
|
||||
Editor,
|
||||
FrontMatterCache,
|
||||
MarkdownView,
|
||||
normalizePath, OpenViewState, parseFrontMatterEntry, TFile, View, 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 yaml, { Mark } from "js-yaml";
|
||||
|
||||
export const getParentOfClass = (element: Element, cssClass: string):HTMLElement | null => {
|
||||
let parent = element.parentElement;
|
||||
@@ -27,11 +31,11 @@ export const getLeaf = (
|
||||
const newTab = ():WorkspaceLeaf => {
|
||||
if(!plugin.settings.openInMainWorkspace) return app.workspace.getLeaf('tab');
|
||||
const [leafLoc, mainLeavesIds] = getLeafLoc(origo);
|
||||
if(leafLoc === 'main') return app.workspace.getLeaf('tab');
|
||||
if(leafLoc === 'main') return plugin.app.workspace.getLeaf('tab');
|
||||
return getNewOrAdjacentLeaf(plugin,origo);
|
||||
}
|
||||
const newTabGroup = ():WorkspaceLeaf => getNewOrAdjacentLeaf(plugin,origo);
|
||||
const newWindow = ():WorkspaceLeaf => app.workspace.openPopoutLeaf();
|
||||
const newWindow = ():WorkspaceLeaf => plugin.app.workspace.openPopoutLeaf();
|
||||
|
||||
switch(linkClickModifierType(ev)) {
|
||||
case "active-pane": return origo;
|
||||
@@ -254,4 +258,137 @@ export const getFileCSSClasses = (
|
||||
return [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
export const getActivePDFPageNumberFromPDFView = (view: View): number => view?.viewer?.child?.pdfViewer?.page;
|
||||
|
||||
export const openLeaf = ({
|
||||
plugin,
|
||||
fnGetLeaf,
|
||||
file,
|
||||
openState
|
||||
}:{
|
||||
plugin: ExcalidrawPlugin,
|
||||
fnGetLeaf: ()=>WorkspaceLeaf,
|
||||
file: TFile,
|
||||
openState?: OpenViewState
|
||||
}) : {
|
||||
leaf: WorkspaceLeaf
|
||||
promise: Promise<void>
|
||||
} => {
|
||||
let leaf:WorkspaceLeaf = null;
|
||||
if (plugin.settings.focusOnFileTab) {
|
||||
plugin.app.workspace.iterateAllLeaves((l) => {
|
||||
if(leaf) return;
|
||||
//@ts-ignore
|
||||
if (l?.view?.file === file) {
|
||||
plugin.app.workspace.setActiveLeaf(l,{focus: true});
|
||||
leaf = l;
|
||||
}
|
||||
});
|
||||
if(leaf) {
|
||||
if(openState) {
|
||||
const promise = leaf.openFile(file, openState);
|
||||
return {leaf, promise};
|
||||
}
|
||||
return {leaf, promise: Promise.resolve()};
|
||||
}
|
||||
}
|
||||
leaf = fnGetLeaf();
|
||||
const promise = leaf.openFile(file, openState);
|
||||
return {leaf, promise};
|
||||
}
|
||||
|
||||
export const 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();
|
||||
const templateContent = template.substring(templateFrontmatterEnd + 3); // Skip frontmatter and ---
|
||||
|
||||
// Parse template frontmatter
|
||||
const templateFrontmatterObj: FrontMatterCache = yaml.load(templateFrontmatter) || {};
|
||||
|
||||
// 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
|
||||
const targetFrontmatter = target.substring(4, targetFrontmatterEnd).trim();
|
||||
targetContent = target.substring(targetFrontmatterEnd + 3); // Skip frontmatter and ---
|
||||
|
||||
// Parse target frontmatter
|
||||
targetFrontmatterObj = yaml.load(targetFrontmatter) || {};
|
||||
} else {
|
||||
// If target doesn't have frontmatter, consider the entire content as target content
|
||||
targetContent = target.trim();
|
||||
}
|
||||
|
||||
// Merge frontmatter with target values taking precedence
|
||||
const mergedFrontmatter: FrontMatterCache = { ...templateFrontmatterObj };
|
||||
|
||||
// Merge arrays by combining and removing duplicates
|
||||
for (const key in targetFrontmatterObj) {
|
||||
if (Array.isArray(targetFrontmatterObj[key]) && Array.isArray(mergedFrontmatter[key])) {
|
||||
const combinedArray = [...new Set([...mergedFrontmatter[key], ...targetFrontmatterObj[key]])];
|
||||
mergedFrontmatter[key] = combinedArray;
|
||||
} else {
|
||||
mergedFrontmatter[key] = targetFrontmatterObj[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Convert merged frontmatter back to YAML
|
||||
const mergedFrontmatterYaml = yaml.dump(mergedFrontmatter);
|
||||
|
||||
// Concatenate frontmatter and content
|
||||
const mergedMarkdown = `---\n${mergedFrontmatterYaml}---\n${targetContent}\n\n${templateContent.trim()}\n`;
|
||||
|
||||
return mergedMarkdown;
|
||||
};
|
||||
|
||||
export const editorInsertText = (editor: Editor, text: string)=> {
|
||||
const cursor = editor.getCursor();
|
||||
const line = editor.getLine(cursor.line);
|
||||
const updatedLine = line.slice(0, cursor.ch) + text + line.slice(cursor.ch);
|
||||
editor.setLine(cursor.line, updatedLine);
|
||||
}
|
||||
|
||||
export const foldExcalidrawSection = (view: MarkdownView) => {
|
||||
if (!view || !(view instanceof MarkdownView)) return;
|
||||
|
||||
const foldStart = {
|
||||
ed: -1, // # Excalidraw Data
|
||||
te: -1, // ## Text Elements
|
||||
el: -1, // ## Element Links
|
||||
ef: -1, // ## Embedded Files
|
||||
d: -1, // ## Drawing
|
||||
};
|
||||
|
||||
const existingFolds = view.currentMode.getFoldInfo()?.folds ?? [];
|
||||
const lineCount = view.editor.lineCount();
|
||||
|
||||
for (let i = 0; i < lineCount; i++) {
|
||||
const line = view.editor.getLine(i);
|
||||
switch (line) {
|
||||
case "# Excalidraw Data": foldStart.ed = i; break;
|
||||
case "## Text Elements": foldStart.te = i; break;
|
||||
case "## Element Links": foldStart.el = i; break;
|
||||
case "## Embedded Files": foldStart.ef = i; break;
|
||||
case "## Drawing": foldStart.d = i; break;
|
||||
}
|
||||
if (line === "## Drawing") break;
|
||||
}
|
||||
|
||||
if (foldStart.ed > -1 && foldStart.d > -1) {
|
||||
const foldPositions = [
|
||||
...existingFolds,
|
||||
...(foldStart.te > -1 ? [{ from: foldStart.te, to: (foldStart.el > -1 ? foldStart.el : (foldStart.ef > -1 ? foldStart.ef : foldStart.d)) - 1 }] : []),
|
||||
...(foldStart.el > -1 ? [{ from: foldStart.el, to: (foldStart.ef > -1 ? foldStart.ef : foldStart.d) - 1 }] : []),
|
||||
...(foldStart.ef > -1 ? [{ from: foldStart.ef, to: foldStart.d - 1 }] : []),
|
||||
{ from: foldStart.d, to: lineCount - 1 },
|
||||
{ from: foldStart.ed, to: lineCount - 1 },
|
||||
];
|
||||
|
||||
view.currentMode.applyFoldInfo({ folds: foldPositions, lines: lineCount });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,8 +1,32 @@
|
||||
import { WorkspaceWindow } from "obsidian";
|
||||
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 {
|
||||
@@ -11,49 +35,45 @@ export class StylesManager {
|
||||
private styleDark: string;
|
||||
private plugin: ExcalidrawPlugin;
|
||||
|
||||
|
||||
|
||||
constructor(plugin: ExcalidrawPlugin) {
|
||||
this.plugin = plugin;
|
||||
plugin.app.workspace.onLayoutReady(async () => {
|
||||
(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() {
|
||||
@@ -126,4 +146,13 @@ export class StylesManager {
|
||||
this.stylesMap.set(doc, {light: lightStyleTag, dark: darkStyleTag});
|
||||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
for (const [doc, styleTags] of this.stylesMap) {
|
||||
doc.head.removeChild(styleTags.light);
|
||||
doc.head.removeChild(styleTags.dark);
|
||||
}
|
||||
this.plugin = null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,8 +5,9 @@ export type FILENAMEPARTS = {
|
||||
hasTaskbone: boolean,
|
||||
hasArearef: boolean,
|
||||
hasFrameref: boolean,
|
||||
blockref: string,
|
||||
hasClippedFrameref: boolean,
|
||||
hasSectionref: boolean,
|
||||
blockref: string,
|
||||
sectionref: string,
|
||||
linkpartReference: string,
|
||||
linkpartAlias: string
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
request,
|
||||
requestUrl,
|
||||
TFile,
|
||||
TFolder,
|
||||
} from "obsidian";
|
||||
import { Random } from "roughjs/bin/math";
|
||||
import { BinaryFileData, DataURL} from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
@@ -13,25 +14,25 @@ import {
|
||||
VIRGIL_FONT,
|
||||
} from "src/constants/constFonts";
|
||||
import {
|
||||
FRONTMATTER_KEY_EXPORT_DARK,
|
||||
FRONTMATTER_KEY_EXPORT_TRANSPARENT,
|
||||
FRONTMATTER_KEY_EXPORT_SVGPADDING,
|
||||
FRONTMATTER_KEY_EXPORT_PNGSCALE,
|
||||
FRONTMATTER_KEY_EXPORT_PADDING,
|
||||
exportToSvg,
|
||||
exportToBlob,
|
||||
IMAGE_TYPES
|
||||
IMAGE_TYPES,
|
||||
FRONTMATTER_KEYS,
|
||||
EXCALIDRAW_PLUGIN,
|
||||
getCommonBoundingBox,
|
||||
DEVICE,
|
||||
getContainerElement,
|
||||
} from "../constants/constants";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { ExcalidrawElement, ExcalidrawTextElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { ExportSettings } from "../ExcalidrawView";
|
||||
import { getDataURLFromURL, getIMGFilename, getMimeType, getURLImageExtension } from "./FileUtils";
|
||||
import { generateEmbeddableLink } from "./CustomEmbeddableUtils";
|
||||
import ExcalidrawScene from "src/svgToExcalidraw/elements/ExcalidrawScene";
|
||||
import { FILENAMEPARTS } from "./UtilTypes";
|
||||
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
|
||||
import { cleanBlockRef, cleanSectionHeading, getFileCSSClasses } from "./ObsidianUtils";
|
||||
import { updateElementLinksToObsidianLinks } from "src/ExcalidrawAutomate";
|
||||
import { CropImage } from "./CropImage";
|
||||
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
@@ -49,8 +50,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;
|
||||
}
|
||||
@@ -60,12 +62,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,
|
||||
@@ -83,12 +86,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(
|
||||
@@ -153,40 +161,17 @@ const rotate = (
|
||||
centerY,
|
||||
];
|
||||
|
||||
export const rotatedDimensions = (
|
||||
export function rotatedDimensions (
|
||||
element: ExcalidrawElement,
|
||||
): [number, number, number, number] => {
|
||||
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),
|
||||
];
|
||||
): [number, number, number, number] {
|
||||
const bb = getCommonBoundingBox([element]);
|
||||
return [bb.minX, bb.minY, bb.maxX - bb.minX, bb.maxY - bb.minY];
|
||||
};
|
||||
|
||||
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 = () => {
|
||||
@@ -198,19 +183,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;
|
||||
@@ -218,23 +231,23 @@ export const getFontDataURL = async (
|
||||
fontDef = ` @font-face {font-family: "${fontName}";src: url("${dataURL}")}`;
|
||||
//format("${f.extension === "ttf" ? "truetype" : f.extension}");}`;
|
||||
const split = fontDef.split(";base64,", 2);
|
||||
fontDef = `${split[0]};charset=utf-8;base64,${split[1]}`;
|
||||
fontDef = `${split[0]};charset=utf-8;base64,${split[1]}`;*/
|
||||
}
|
||||
return { fontDef, fontName, dataURL };
|
||||
};
|
||||
|
||||
export const base64StringToBlob = (base64String: string, mimeType: string): Blob => {
|
||||
export function base64StringToBlob (base64String: string, mimeType: string): Blob {
|
||||
const buffer = Buffer.from(base64String, 'base64');
|
||||
return new Blob([buffer], { type: mimeType });
|
||||
};
|
||||
|
||||
export const svgToBase64 = (svg: string): string => {
|
||||
export function svgToBase64 (svg: string): string {
|
||||
return `data:image/svg+xml;base64,${btoa(
|
||||
unescape(encodeURIComponent(svg.replaceAll(" ", " "))),
|
||||
)}`;
|
||||
};
|
||||
|
||||
export const getBinaryFileFromDataURL = async (dataURL: string): Promise<ArrayBuffer> => {
|
||||
export async function getBinaryFileFromDataURL (dataURL: string): Promise<ArrayBuffer> {
|
||||
if (!dataURL) {
|
||||
return null;
|
||||
}
|
||||
@@ -257,12 +270,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));
|
||||
@@ -271,26 +284,38 @@ export const getSVG = async (
|
||||
});
|
||||
}
|
||||
|
||||
elements = srcFile
|
||||
? updateElementLinksToObsidianLinks({
|
||||
elements,
|
||||
hostFile: srcFile,
|
||||
})
|
||||
: elements;
|
||||
|
||||
try {
|
||||
const svg = await exportToSvg({
|
||||
elements: srcFile
|
||||
? updateElementLinksToObsidianLinks({
|
||||
elements,
|
||||
hostFile: srcFile,
|
||||
})
|
||||
: elements,
|
||||
appState: {
|
||||
exportBackground: exportSettings.withBackground,
|
||||
exportWithDarkMode: exportSettings.withTheme
|
||||
? scene.appState?.theme != "light"
|
||||
: false,
|
||||
...scene.appState,
|
||||
},
|
||||
files: scene.files,
|
||||
exportPadding: padding,
|
||||
exportingFrame: null,
|
||||
renderEmbeddables: true,
|
||||
});
|
||||
let svg: SVGSVGElement;
|
||||
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,
|
||||
...exportSettings.frameRendering
|
||||
? {frameRendering: exportSettings.frameRendering}
|
||||
: {},
|
||||
},
|
||||
files: scene.files,
|
||||
exportPadding: exportSettings.frameRendering ? 0 : padding,
|
||||
exportingFrame: null,
|
||||
renderEmbeddables: true,
|
||||
});
|
||||
}
|
||||
if(svg) {
|
||||
svg.addClass("excalidraw-svg");
|
||||
if(srcFile instanceof TFile) {
|
||||
@@ -316,24 +341,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> {
|
||||
try {
|
||||
if(exportSettings.isMask) {
|
||||
const cropObject = new CropImage(scene.elements, scene.files);
|
||||
const blob = await cropObject.getCroppedPNG();
|
||||
cropObject.destroy();
|
||||
return blob;
|
||||
}
|
||||
|
||||
return await exportToBlob({
|
||||
elements: scene.elements,
|
||||
elements: scene.elements.filter((el:ExcalidrawElement)=>el.isDeleted !== true),
|
||||
appState: {
|
||||
...scene.appState,
|
||||
exportBackground: exportSettings.withBackground,
|
||||
exportWithDarkMode: exportSettings.withTheme
|
||||
? scene.appState?.theme != "light"
|
||||
? 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,
|
||||
@@ -342,16 +377,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;
|
||||
}
|
||||
@@ -368,33 +404,38 @@ export const getQuickImagePreview = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const embedFontsInSVG = (
|
||||
export function embedFontsInSVG(
|
||||
svg: SVGSVGElement,
|
||||
plugin: ExcalidrawPlugin,
|
||||
localOnly: boolean = false,
|
||||
): SVGSVGElement => {
|
||||
): SVGSVGElement {
|
||||
//replace font references with base64 fonts)
|
||||
const includesVirgil = !localOnly &&
|
||||
svg.querySelector("text[font-family^='Virgil']") != null;
|
||||
svg.querySelector("text[font-family^='Virgil']") !== null;
|
||||
const includesCascadia = !localOnly &&
|
||||
svg.querySelector("text[font-family^='Cascadia']") != null;
|
||||
svg.querySelector("text[font-family^='Cascadia']") !== null;
|
||||
const includesAssistant = !localOnly &&
|
||||
svg.querySelector("text[font-family^='Assistant']") != null;
|
||||
svg.querySelector("text[font-family^='Assistant']") !== null;
|
||||
const includesLocalFont =
|
||||
svg.querySelector("text[font-family^='LocalFont']") != null;
|
||||
svg.querySelector("text[font-family^='LocalFont']") !== null;
|
||||
const defs = svg.querySelector("defs");
|
||||
if (defs && (includesCascadia || includesVirgil || includesLocalFont || includesAssistant)) {
|
||||
defs.innerHTML = `<style>${includesVirgil ? VIRGIL_FONT : ""}${
|
||||
includesCascadia ? CASCADIA_FONT : ""}${
|
||||
includesAssistant ? ASSISTANT_FONT : ""
|
||||
}${includesLocalFont ? plugin.fourthFontDef : ""}</style>`;
|
||||
let style = defs.querySelector("style");
|
||||
if (!style) {
|
||||
style = document.createElement("style");
|
||||
defs.appendChild(style);
|
||||
}
|
||||
style.innerHTML = `${includesVirgil ? (VIRGIL_FONT + "\n") : ""}${
|
||||
includesCascadia ? (CASCADIA_FONT + "\n") : ""}${
|
||||
includesAssistant ? (ASSISTANT_FONT + "\n") : ""
|
||||
}${includesLocalFont ? (plugin.fourthFontDef + "\n") : ""}`;
|
||||
}
|
||||
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 = () => {
|
||||
@@ -406,7 +447,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) {
|
||||
@@ -416,15 +457,23 @@ export const addAppendUpdateCustomData = (el: Mutable<ExcalidrawElement>, newDat
|
||||
return el;
|
||||
};
|
||||
|
||||
export const scaleLoadedImage = (
|
||||
export function scaleLoadedImage (
|
||||
scene: any,
|
||||
files: any,
|
||||
): { dirty: boolean; scene: any } => {
|
||||
files: any
|
||||
): { dirty: boolean; scene: any } {
|
||||
let dirty = false;
|
||||
if (!files || !scene) {
|
||||
return { dirty, scene };
|
||||
}
|
||||
for (const f of files) {
|
||||
|
||||
for (const f of files.filter((f:any)=>{
|
||||
if(!Boolean(EXCALIDRAW_PLUGIN)) return true; //this should never happen
|
||||
const ef = EXCALIDRAW_PLUGIN.filesMaster.get(f.id);
|
||||
if(!ef) return true; //mermaid SVG or equation
|
||||
const file = EXCALIDRAW_PLUGIN.app.vault.getAbstractFileByPath(ef.path.replace(/#.*$/,"").replace(/\|.*$/,""));
|
||||
if(!file || (file instanceof TFolder)) return false;
|
||||
return (file as TFile).extension==="md" || EXCALIDRAW_PLUGIN.isExcalidrawFile(file as TFile)
|
||||
})) {
|
||||
const [w_image, h_image] = [f.size.width, f.size.height];
|
||||
const imageAspectRatio = f.size.width / f.size.height;
|
||||
scene.elements
|
||||
@@ -437,7 +486,7 @@ export const scaleLoadedImage = (
|
||||
}
|
||||
if(f.shouldScale) {
|
||||
const elementAspectRatio = w_old / h_old;
|
||||
if (imageAspectRatio != elementAspectRatio) {
|
||||
if (imageAspectRatio !== elementAspectRatio) {
|
||||
dirty = true;
|
||||
const h_new = Math.sqrt((w_old * h_old * h_image) / w_image);
|
||||
const w_new = Math.sqrt((w_old * h_old * w_image) / h_image);
|
||||
@@ -460,7 +509,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;}`;
|
||||
@@ -473,10 +522,10 @@ 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 = app.isMobile?document:leaf.view.containerEl.ownerDocument;
|
||||
const ownerDocument = DEVICE.isMobile?document:leaf.view.containerEl.ownerDocument;
|
||||
if(!ownerDocument) return;
|
||||
if(visitedDocs.has(ownerDocument)) return;
|
||||
visitedDocs.add(ownerDocument);
|
||||
@@ -494,7 +543,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);
|
||||
@@ -512,23 +561,41 @@ export const getLinkParts = (fname: string, file?: TFile): LinkParts => {
|
||||
};
|
||||
};
|
||||
|
||||
export const compress = (data: string): string => {
|
||||
return LZString.compressToBase64(data).replace(/(.{64})/g, "$1\n\n");
|
||||
export function compress (data: string): string {
|
||||
return LZString.compressToBase64(data).replace(/(.{256})/g, "$1\n\n");
|
||||
};
|
||||
|
||||
export const decompress = (data: string): string => {
|
||||
export function decompress (data: string): string {
|
||||
return LZString.decompressFromBase64(data.replaceAll("\n", "").replaceAll("\r", ""));
|
||||
};
|
||||
|
||||
export const hasExportTheme = (
|
||||
export function isMaskFile (
|
||||
plugin: ExcalidrawPlugin,
|
||||
file: TFile,
|
||||
): boolean => {
|
||||
): boolean {
|
||||
if (file) {
|
||||
const fileCache = plugin.app.metadataCache.getFileCache(file);
|
||||
if (
|
||||
fileCache?.frontmatter &&
|
||||
fileCache.frontmatter[FRONTMATTER_KEY_EXPORT_DARK] != null
|
||||
fileCache.frontmatter[FRONTMATTER_KEYS["mask"].name] !== null &&
|
||||
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["mask"].name] !== "undefined")
|
||||
) {
|
||||
return Boolean(fileCache.frontmatter[FRONTMATTER_KEYS["mask"].name]);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export function hasExportTheme (
|
||||
plugin: ExcalidrawPlugin,
|
||||
file: TFile,
|
||||
): boolean {
|
||||
if (file) {
|
||||
const fileCache = plugin.app.metadataCache.getFileCache(file);
|
||||
if (
|
||||
fileCache?.frontmatter &&
|
||||
fileCache.frontmatter[FRONTMATTER_KEYS["export-dark"].name] !== null &&
|
||||
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["export-dark"].name] !== "undefined")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -536,18 +603,19 @@ 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 (
|
||||
fileCache?.frontmatter &&
|
||||
fileCache.frontmatter[FRONTMATTER_KEY_EXPORT_DARK] != null
|
||||
fileCache.frontmatter[FRONTMATTER_KEYS["export-dark"].name] !== null &&
|
||||
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["export-dark"].name] !== "undefined")
|
||||
) {
|
||||
return fileCache.frontmatter[FRONTMATTER_KEY_EXPORT_DARK]
|
||||
return fileCache.frontmatter[FRONTMATTER_KEYS["export-dark"].name]
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
@@ -555,15 +623,33 @@ export const getExportTheme = (
|
||||
return plugin.settings.exportWithTheme ? theme : "light";
|
||||
};
|
||||
|
||||
export const hasExportBackground = (
|
||||
export function shouldEmbedScene (
|
||||
plugin: ExcalidrawPlugin,
|
||||
file: TFile,
|
||||
): boolean => {
|
||||
file: TFile
|
||||
): boolean {
|
||||
if (file) {
|
||||
const fileCache = plugin.app.metadataCache.getFileCache(file);
|
||||
if (
|
||||
fileCache?.frontmatter &&
|
||||
fileCache.frontmatter[FRONTMATTER_KEY_EXPORT_TRANSPARENT] != null
|
||||
fileCache.frontmatter[FRONTMATTER_KEYS["export-embed-scene"].name] !== null &&
|
||||
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["export-embed-scene"].name] !== "undefined")
|
||||
) {
|
||||
return fileCache.frontmatter[FRONTMATTER_KEYS["export-embed-scene"].name];
|
||||
}
|
||||
}
|
||||
return plugin.settings.exportEmbedScene;
|
||||
};
|
||||
|
||||
export function hasExportBackground (
|
||||
plugin: ExcalidrawPlugin,
|
||||
file: TFile,
|
||||
): boolean {
|
||||
if (file) {
|
||||
const fileCache = plugin.app.metadataCache.getFileCache(file);
|
||||
if (
|
||||
fileCache?.frontmatter &&
|
||||
fileCache.frontmatter[FRONTMATTER_KEYS["export-transparent"].name] !== null &&
|
||||
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["export-transparent"].name] !== "undefined")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -571,43 +657,50 @@ 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 (
|
||||
fileCache?.frontmatter &&
|
||||
fileCache.frontmatter[FRONTMATTER_KEY_EXPORT_TRANSPARENT] != null
|
||||
fileCache.frontmatter[FRONTMATTER_KEYS["export-transparent"].name] !== null &&
|
||||
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["export-transparent"].name] !== "undefined")
|
||||
) {
|
||||
return !fileCache.frontmatter[FRONTMATTER_KEY_EXPORT_TRANSPARENT];
|
||||
return !fileCache.frontmatter[FRONTMATTER_KEYS["export-transparent"].name];
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
||||
if (fileCache.frontmatter[FRONTMATTER_KEY_EXPORT_PADDING] != null) {
|
||||
if (
|
||||
fileCache.frontmatter[FRONTMATTER_KEYS["export-padding"].name] !== null &&
|
||||
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["export-padding"].name] !== "undefined")
|
||||
) {
|
||||
const val = parseInt(
|
||||
fileCache.frontmatter[FRONTMATTER_KEY_EXPORT_PADDING],
|
||||
fileCache.frontmatter[FRONTMATTER_KEYS["export-padding"].name],
|
||||
);
|
||||
if (!isNaN(val)) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
//depricated. Retained for backward compatibility
|
||||
if (fileCache.frontmatter[FRONTMATTER_KEY_EXPORT_SVGPADDING] != null) {
|
||||
//deprecated. Retained for backward compatibility
|
||||
if (
|
||||
fileCache.frontmatter[FRONTMATTER_KEYS["export-svgpadding"].name] !== null &&
|
||||
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["export-svgpadding"].name] !== "undefined")
|
||||
) {
|
||||
const val = parseInt(
|
||||
fileCache.frontmatter[FRONTMATTER_KEY_EXPORT_SVGPADDING],
|
||||
fileCache.frontmatter[FRONTMATTER_KEYS["export-svgpadding"].name],
|
||||
);
|
||||
if (!isNaN(val)) {
|
||||
return val;
|
||||
@@ -618,15 +711,16 @@ 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 (
|
||||
fileCache?.frontmatter &&
|
||||
fileCache.frontmatter[FRONTMATTER_KEY_EXPORT_PNGSCALE] != null
|
||||
fileCache.frontmatter[FRONTMATTER_KEYS["export-pngscale"].name] !== null &&
|
||||
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["export-pngscale"].name] !== "undefined")
|
||||
) {
|
||||
const val = parseFloat(
|
||||
fileCache.frontmatter[FRONTMATTER_KEY_EXPORT_PNGSCALE],
|
||||
fileCache.frontmatter[FRONTMATTER_KEYS["export-pngscale"].name],
|
||||
);
|
||||
if (!isNaN(val) && val > 0) {
|
||||
return val;
|
||||
@@ -636,7 +730,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*)/);
|
||||
|
||||
@@ -651,9 +745,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,
|
||||
@@ -662,6 +756,7 @@ export const getEmbeddedFilenameParts = (fname:string): FILENAMEPARTS => {
|
||||
hasTaskbone: false,
|
||||
hasArearef: false,
|
||||
hasFrameref: false,
|
||||
hasClippedFrameref: false,
|
||||
blockref: "",
|
||||
hasSectionref: false,
|
||||
sectionref: "",
|
||||
@@ -676,6 +771,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],
|
||||
@@ -684,14 +780,17 @@ export const getEmbeddedFilenameParts = (fname:string): FILENAMEPARTS => {
|
||||
}
|
||||
}
|
||||
|
||||
export const fragWithHTML = (html: string) =>
|
||||
createFragment((frag) => (frag.createDiv().innerHTML = html));
|
||||
export function fragWithHTML (html: string) {
|
||||
return createFragment((frag) => (frag.createDiv().innerHTML = html));
|
||||
}
|
||||
|
||||
export const errorlog = (data: {}) => {
|
||||
export function errorlog (data: {}) {
|
||||
console.error({ plugin: "Excalidraw", ...data });
|
||||
};
|
||||
|
||||
export const sleep = async (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
export async function sleep (ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**REACT 18
|
||||
//see also: https://github.com/zsviczian/obsidian-excalidraw-plugin/commit/b67d70c5196f30e2968f9da919d106ee66f2a5eb
|
||||
@@ -699,28 +798,43 @@ export const sleep = async (ms: number) => new Promise((resolve) => setTimeout(r
|
||||
export const awaitNextAnimationFrame = async () => new Promise(requestAnimationFrame);
|
||||
*/
|
||||
|
||||
export const log = console.log.bind(window.console);
|
||||
export const debug = console.log.bind(window.console);
|
||||
//export const debug = function(){};
|
||||
|
||||
|
||||
export const getContainerElement = (
|
||||
export function _getContainerElement (
|
||||
element:
|
||||
| (ExcalidrawElement & { containerId: ExcalidrawElement["id"] | null })
|
||||
| null,
|
||||
scene: ExcalidrawScene,
|
||||
) => {
|
||||
if (!element) {
|
||||
scene: any,
|
||||
) {
|
||||
if (!element || !scene?.elements || element.type !== "text") {
|
||||
return null;
|
||||
}
|
||||
if (element.containerId) {
|
||||
return scene.elements.filter(el=>el.id === element.containerId)[0] ?? null;
|
||||
return getContainerElement(element as ExcalidrawTextElement, arrayToMap(scene.elements))
|
||||
//return scene.elements.find((el:ExcalidrawElement)=>el.id === element.containerId) ?? null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const updateFrontmatterInString = (data:string, keyValuePairs: [string,string][]):string => {
|
||||
if(!data) return data;
|
||||
/**
|
||||
* Transforms array of objects containing `id` attribute,
|
||||
* or array of ids (strings), into a Map, keyd by `id`.
|
||||
*/
|
||||
export function arrayToMap <T extends { id: string } | string>(
|
||||
items: readonly T[] | Map<string, T>,
|
||||
) {
|
||||
if (items instanceof Map) {
|
||||
return items;
|
||||
}
|
||||
return items.reduce((acc: Map<string, T>, element) => {
|
||||
acc.set(typeof element === "string" ? element : element.id, element);
|
||||
return acc;
|
||||
}, new Map());
|
||||
};
|
||||
|
||||
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");
|
||||
data = data.match(r)
|
||||
@@ -730,22 +844,27 @@ export const updateFrontmatterInString = (data:string, keyValuePairs: [string,st
|
||||
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
|
||||
@@ -769,7 +888,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.")) {
|
||||
@@ -779,7 +898,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;
|
||||
@@ -789,9 +908,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: {
|
||||
|
||||
29
src/utils/WeakArray.ts
Normal file
29
src/utils/WeakArray.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export class WeakArray<T extends object> {
|
||||
private weakArray: WeakRef<T>[] = [];
|
||||
|
||||
constructor() {}
|
||||
|
||||
push(obj: T) {
|
||||
this.weakArray.push(new WeakRef(obj));
|
||||
}
|
||||
|
||||
forEach(callback: (obj: T, index: number) => void) {
|
||||
this.weakArray.forEach((ref, index) => {
|
||||
const obj = ref.deref();
|
||||
if (obj) {
|
||||
callback(obj, index);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeObjects(objectsToRemove: Set<T>) {
|
||||
this.weakArray = this.weakArray.filter((ref) => {
|
||||
const obj = ref.deref();
|
||||
return obj && !objectsToRemove.has(obj);
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.weakArray = [];
|
||||
}
|
||||
}
|
||||
62
styles.css
62
styles.css
@@ -485,6 +485,8 @@ hr.excalidraw-setting-hr {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1456
|
||||
.canvas-node:not(.is-editing):has(.excalidraw-canvas-immersive) {
|
||||
::-webkit-scrollbar,
|
||||
::-webkit-scrollbar-horizontal {
|
||||
@@ -497,6 +499,7 @@ hr.excalidraw-setting-hr {
|
||||
border: unset;
|
||||
box-shadow: unset;
|
||||
}
|
||||
*/
|
||||
|
||||
.excalidraw .canvas-node .ex-md-font-hand-drawn {
|
||||
--font-text: "Virgil";
|
||||
@@ -532,4 +535,63 @@ hr.excalidraw-setting-hr {
|
||||
.excalidraw__embeddable-container .workspace-leaf-content .audio-container,
|
||||
.excalidraw__embeddable-container .workspace-leaf-content .video-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.excalidraw__embeddable-container .canvas-node-container {
|
||||
border: 2px solid var(--canvas-color);
|
||||
}
|
||||
|
||||
.excalidraw__embeddable-container .canvas-node {
|
||||
--shadow-border-themed-inset: inset 0 0 0 1px rgb(var(--canvas-color));;
|
||||
--shadow-border-themed: 0 0 0 2px rgb(var(--canvas-color));
|
||||
}
|
||||
|
||||
.excalidraw__embeddable-container .canvas-node.is-selected.is-themed .canvas-node-container,
|
||||
.excalidraw__embeddable-container .canvas-node.is-focused.is-themed .canvas-node-container {
|
||||
border-color: var(--canvas-color);
|
||||
}
|
||||
|
||||
img.excalidraw-cropped-pdfpage,
|
||||
.excalidraw-cropped-pdfpage svg {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.excalidraw .pdf-toolbar,
|
||||
.excalidraw .pdf-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ex-opacity-30 {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.ex-opacity-15 {
|
||||
opacity: 0.15;
|
||||
}
|
||||
|
||||
.ex-opacity-8 {
|
||||
opacity: 0.08;
|
||||
}
|
||||
|
||||
.ex-opacity-5 {
|
||||
opacity: 0.05;
|
||||
}
|
||||
|
||||
.ex-opacity-0 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.popover .excalidraw-svg {
|
||||
width: 100%;
|
||||
max-width: inherit;
|
||||
height: 100%;
|
||||
max-height: inherit;
|
||||
}
|
||||
|
||||
root {
|
||||
--excalidraw-caret-color: initial;
|
||||
}
|
||||
|
||||
textarea.excalidraw-wysiwyg, .excalidraw input {
|
||||
caret-color: var(--excalidraw-caret-color);
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"baseUrl": ".",
|
||||
"sourceMap": false,
|
||||
"module": "ES2015",
|
||||
"target": "es2017", //es2017 because script engine requires for async execution
|
||||
"target": "es2018", //es2017 because script engine requires for async execution
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"baseUrl": ".",
|
||||
"sourceMap": false,
|
||||
"module": "es2015",
|
||||
"target": "es2017", //es2017 because script engine requires for async execution
|
||||
"target": "es2018", //es2017 because script engine requires for async execution //es2018 for named capture groups
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
|
||||
Reference in New Issue
Block a user