This commit is contained in:
parent
777f31761c
commit
eaca7fc192
20 changed files with 469 additions and 1718 deletions
304
README.md
304
README.md
|
@ -1,278 +1,42 @@
|
|||
|
||||
# SiYuan plugin sample with vite and svelte
|
||||
# SiYuan js-draw Plugin
|
||||
|
||||
[中文版](./README_zh_CN.md)
|
||||
This plugin allows you to embed js-draw whiteboards anywhere in your SiYuan documents.
|
||||
|
||||
> Consistent with [siyuan/plugin-sample](https://github.com/siyuan-note/plugin-sample) [v0.3.5](https://github.com/siyuan-note/plugin-sample/tree/v0.3.5)
|
||||
## Usage instructions
|
||||
1. Install the plugin
|
||||
- Grab a release from the [Releases page](https://git.massive.box/massivebox/siyuan-jsdraw-plugin/releases)
|
||||
- Unzip it in the folder `./data/plugins`, relatively to your SiYuan workspace.
|
||||
> The plugin is not yet available in the official marketplace. I will try to publish it there soon!
|
||||
2. Insert a drawing in your documents by typing `/Insert Drawing` in your document, and selecting the correct menu entry
|
||||
3. The whiteboard editor will open in a new tab. Draw as you like, then click the Save button. It will also add a
|
||||
drawing block to your document.
|
||||
4. Click the Gear icon > Refresh to refresh the drawing block, if it's still displaying the old drawing.
|
||||
5. Click the drawing block to open the editor again.
|
||||
|
||||
## Planned features
|
||||
- [ ] Auto-reload drawing blocks on drawing change
|
||||
- [ ] Rename whiteboards
|
||||
- [ ] Improve internationalization framework
|
||||
- [ ] Default background color and grid options
|
||||
- [ ] Respecting user theme for the editor
|
||||
- And more!
|
||||
|
||||
## Contributing
|
||||
Contributions are always welcome! Right now, I'm working on the core functionality and fixing bugs.
|
||||
After that is done, I will need help with the internationalization, as, unfortunately, I don't speak Chinese.
|
||||
Please [contact me](mailto:box@massive.box) if you'd like to help!
|
||||
|
||||
1. Using vite for packaging
|
||||
2. Use symbolic linking instead of putting the project into the plugins directory program development
|
||||
3. Built-in support for the svelte framework
|
||||
## Thanks to
|
||||
This project couldn't have been possible without (in no particular order):
|
||||
- The [SiYuan](https://github.com/siyuan-note/siyuan) project
|
||||
- [js-draw](https://github.com/personalizedrefrigerator/js-draw)
|
||||
- [SiYuan plugin sample with vite and svelte](https://github.com/siyuan-note/plugin-sample-vite-svelte)
|
||||
- [siyuan-drawio-plugin](https://github.com/zt8989/siyuan-drawio-plugin) and
|
||||
[siyuan-plugin-whiteboard](https://github.com/zuoez02/siyuan-plugin-whiteboard) for inspiration and bits of code
|
||||
|
||||
> **If don't want svelte, turn to this template**: [frostime/plugin-sample-vite](https://github.com/frostime/plugin-sample-vite)
|
||||
>
|
||||
> **We also provide with a vite+solidjs template**: [frostime/plugin-sample-vite-solidjs](https://github.com/frostime/plugin-sample-vite-solidjs)
|
||||
|
||||
4. Provides a github action template to automatically generate package.zip and upload to new release
|
||||
|
||||
|
||||
> [!TIP]
|
||||
> You can also use our maintained [siyuan-plugin-cli](https://www.npmjs.com/package/siyuan-plugin-cli) command-line tool to directly build plugins in your local terminal.
|
||||
>
|
||||
> Additionally, for the `make-link` related commands mentioned in this plugin, all future updates will be made in [siyuan-plugin-cli](https://www.npmjs.com/package/siyuan-plugin-cli).
|
||||
>
|
||||
> The built-in `make-link` scripts may also be removed in a future version, in favor of using the `siyuan-plugin-cli` tool, aiming to simplify the workload of maintaining multiple plugin templates.
|
||||
|
||||
|
||||
## Get started
|
||||
|
||||
1. Use the <kbd>Use this template</kbd> button to make a copy of this repo as a template. Note that the repository name should match the plugin name, and the default branch must be `main`.
|
||||
2. Clone your repository to the local development folder.
|
||||
* Note: Unlike `plugin-sample`, this example does not recommend directly downloading the code to `{workspace}/data/plugins/`.
|
||||
3. Install [NodeJS](https://nodejs.org/en/download) and [pnpm](https://pnpm.io/installation), then run `pnpm i` in the development folder to install the required dependencies.
|
||||
4. Run the `pnpm run make-link` command to create a symbolic link (Windows developers, please refer to the "make-link on Windows" section below).
|
||||
5. Execute `pnpm run dev` for real-time compilation.
|
||||
6. Open the marketplace in SiYuan and enable the plugin in the download tab.
|
||||
|
||||
### Setting the Target Directory for the make-link Command
|
||||
|
||||
The `make-link` command creates a symbolic link that binds your `dev` directory to the SiYuan plugin directory. You can configure the target SiYuan workspace and create the symbolic link in three ways:
|
||||
|
||||
1. **Select Workspace**
|
||||
- Open SiYuan, ensure the SiYuan kernel is running.
|
||||
- Run `pnpm run make-link`, the script will automatically detect all SiYuan workspaces, please manually enter the number to select the workspace.
|
||||
```bash
|
||||
>>> pnpm run make-link
|
||||
> plugin-sample-vite-svelte@0.0.3 make-link H:\SrcCode\开源项目\plugin-sample-vite-svelte
|
||||
> node --no-warnings ./scripts/make_dev_link.js
|
||||
|
||||
"targetDir" is empty, try to get SiYuan directory automatically....
|
||||
Got 2 SiYuan workspaces
|
||||
[0] H:\Media\SiYuan
|
||||
[1] H:\临时文件夹\SiYuanDevSpace
|
||||
Please select a workspace[0-1]: 0
|
||||
Got target directory: H:\Media\SiYuan/data/plugins
|
||||
Done! Created symlink H:\Media\SiYuan/data/plugins/plugin-sample-vite-svelte
|
||||
```
|
||||
2. **Manually Configure Target Directory**
|
||||
- Open the `./scripts/make_dev_link.js` file, change `targetDir` to the SiYuan plugin directory `<siyuan workspace>/data/plugins`.
|
||||
- Run the `pnpm run make-link` command. If you see a message similar to the one below, it indicates successful creation:
|
||||
|
||||
3. **Set Environment Variable to Create Symbolic Link**
|
||||
- Set the system environment variable `SIYUAN_PLUGIN_DIR` to the path `workspace/data/plugins`.
|
||||
|
||||
### make-link on Windows
|
||||
|
||||
Due to SiYuan upgrading to Go 1.23, the old version of junction links cannot be recognized normally on Windows, so it has been changed to create `dir` symbolic links.
|
||||
|
||||
> https://github.com/siyuan-note/siyuan/issues/12399
|
||||
|
||||
However, creating directory symbolic links on Windows using NodeJs may require administrator privileges. You have the following options:
|
||||
|
||||
1. Run `pnpm run make-link` in a command line with administrator privileges.
|
||||
2. Configure Windows settings, enable developer mode in [System Settings - Update & Security - Developer Mode] then run `pnpm run make-link`.
|
||||
3. Run `pnpm run make-link-win`, this command will use a PowerShell script to request administrator privileges, requiring the system to enable PowerShell script execution permissions.
|
||||
|
||||
## I18n
|
||||
|
||||
In terms of internationalization, our main consideration is to support multiple languages. Specifically, we need to
|
||||
complete the following tasks:
|
||||
|
||||
* Meta information about the plugin itself, such as plugin description and readme
|
||||
* `description` and `readme` fields in plugin.json, and the corresponding README*.md file
|
||||
* Text used in the plugin, such as button text and tooltips
|
||||
* public/i18n/*.json language configuration files
|
||||
* Use `this.i18.key` to get the text in the code
|
||||
* YAML Support
|
||||
* This template specifically supports I18n based on YAML syntax, see `public/i18n/zh_CN.yaml`
|
||||
* During compilation, the defined YAML files will be automatically translated into JSON files and placed in the dist or dev directory.
|
||||
|
||||
It is recommended that the plugin supports at least English and Simplified Chinese, so that more people can use it more
|
||||
conveniently.
|
||||
|
||||
## plugin.json
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "plugin-sample-vite-svelte",
|
||||
"author": "frostime",
|
||||
"url": "https://github.com/siyuan-note/plugin-sample-vite-svelte",
|
||||
"version": "0.1.3",
|
||||
"minAppVersion": "2.8.8",
|
||||
"backends": ["windows", "linux", "darwin"],
|
||||
"frontends": ["desktop"],
|
||||
"displayName": {
|
||||
"en_US": "Plugin sample with vite and svelte",
|
||||
"zh_CN": "插件样例 vite + svelte 版"
|
||||
},
|
||||
"description": {
|
||||
"en_US": "SiYuan plugin sample with vite and svelte",
|
||||
"zh_CN": "使用 vite 和 svelte 开发的思源插件样例"
|
||||
},
|
||||
"readme": {
|
||||
"en_US": "README_en_US.md",
|
||||
"zh_CN": "README.md"
|
||||
},
|
||||
"funding": {
|
||||
"openCollective": "",
|
||||
"patreon": "",
|
||||
"github": "",
|
||||
"custom": [
|
||||
"https://ld246.com/sponsor"
|
||||
]
|
||||
},
|
||||
"keywords": [
|
||||
"sample", "示例"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
* `name`: Plugin name, must be the same as the repo name, and must be unique globally (no duplicate plugin names in the
|
||||
marketplace)
|
||||
* `author`: Plugin author name
|
||||
* `url`: Plugin repo URL
|
||||
* `version`: Plugin version number, it is recommended to follow the [semver](https://semver.org/) specification
|
||||
* `minAppVersion`: Minimum version number of SiYuan required to use this plugin
|
||||
* `backends`: Backend environment required by the plugin, optional values are `windows`, `linux`, `darwin`, `docker`, `android`, `ios` and `all`
|
||||
* `windows`: Windows desktop
|
||||
* `linux`: Linux desktop
|
||||
* `darwin`: macOS desktop
|
||||
* `docker`: Docker
|
||||
* `android`: Android APP
|
||||
* `ios`: iOS APP
|
||||
* `all`: All environments
|
||||
* `frontends`: Frontend environment required by the plugin, optional values are `desktop`, `desktop-window`, `mobile`, `browser-desktop`, `browser-mobile` and `all`
|
||||
* `desktop`: Desktop
|
||||
* `desktop-window`: Desktop window converted from tab
|
||||
* `mobile`: Mobile APP
|
||||
* `browser-desktop`: Desktop browser
|
||||
* `browser-mobile`: Mobile browser
|
||||
* `all`: All environments
|
||||
* `displayName`: Template display name, mainly used for display in the marketplace list, supports multiple languages
|
||||
* `default`: Default language, must exist
|
||||
* `zh_CN`, `en_US` and other languages: optional, it is recommended to provide at least Chinese and English
|
||||
* `description`: Plugin description, mainly used for display in the marketplace list, supports multiple languages
|
||||
* `default`: Default language, must exist
|
||||
* `zh_CN`, `en_US` and other languages: optional, it is recommended to provide at least Chinese and English
|
||||
* `readme`: readme file name, mainly used to display in the marketplace details page, supports multiple languages
|
||||
* `default`: Default language, must exist
|
||||
* `zh_CN`, `en_US` and other languages: optional, it is recommended to provide at least Chinese and English
|
||||
* `funding`: Plugin sponsorship information
|
||||
* `openCollective`: Open Collective name
|
||||
* `patreon`: Patreon name
|
||||
* `github`: GitHub login name
|
||||
* `custom`: Custom sponsorship link list
|
||||
* `keywords`: Search keyword list, used for marketplace search function
|
||||
|
||||
## Package
|
||||
|
||||
No matter which method is used to compile and package, we finally need to generate a package.zip, which contains at
|
||||
least the following files:
|
||||
|
||||
* i18n/*
|
||||
* icon.png (160*160)
|
||||
* index.css
|
||||
* index.js
|
||||
* plugin.json
|
||||
* preview.png (1024*768)
|
||||
* README*.md
|
||||
|
||||
## List on the marketplace
|
||||
|
||||
* `pnpm run build` to generate package.zip
|
||||
* Create a new GitHub release using your new version number as the "Tag version". See here for an
|
||||
example: https://github.com/siyuan-note/plugin-sample/releases
|
||||
* Upload the file package.zip as binary attachments
|
||||
* Publish the release
|
||||
|
||||
If it is the first release, please create a pull request to
|
||||
the [Community Bazaar](https://github.com/siyuan-note/bazaar) repository and modify the plugins.json file in it. This
|
||||
file is the index of all community plugin repositories, the format is:
|
||||
|
||||
```json
|
||||
{
|
||||
"repos": [
|
||||
"username/reponame"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
After the PR is merged, the bazaar will automatically update the index and deploy through GitHub Actions. When releasing
|
||||
a new version of the plugin in the future, you only need to follow the above steps to create a new release, and you
|
||||
don't need to PR the community bazaar repo.
|
||||
|
||||
Under normal circumstances, the community bazaar repo will automatically update the index and deploy every hour,
|
||||
and you can check the deployment status at https://github.com/siyuan-note/bazaar/actions.
|
||||
|
||||
## Use Github Action
|
||||
|
||||
The github action is included in this sample, you can use it to publish your new realse to marketplace automatically:
|
||||
|
||||
1. In your repo setting page `https://github.com/OWNER/REPO/settings/actions`, down to **Workflow Permissions** and open the configuration like this:
|
||||
|
||||

|
||||
|
||||
2. Push a tag in the format `v*` and github will automatically create a new release with new bulit package.zip
|
||||
|
||||
3. By default, it will only publish a pre-release, if you don't think this is necessary, change the settings in release.yml
|
||||
|
||||
```yaml
|
||||
- name: Release
|
||||
uses: ncipollo/release-action@v1
|
||||
with.
|
||||
allowUpdates: true
|
||||
artifactErrorsFailBuild: true
|
||||
artifacts: 'package.zip'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
prerelease: true # change this to false
|
||||
```
|
||||
|
||||
|
||||
## How to remove svelte dependencies
|
||||
|
||||
> Pure vite without svelte: https://github.com/frostime/plugin-sample-vite
|
||||
|
||||
This plugin is packaged in vite and provides a dependency on the svelte framework. However, in practice some developers may not want to use svelte and only want to use the vite package.
|
||||
|
||||
In fact you can use this template without using svelte without any modifications at all. The compilation-related parts of the svelte compilation are loaded into the vite workflow as plugins, so even if you don't have svelte in your project, it won't matter much.
|
||||
|
||||
If you insist on removing all svelte dependencies so that they do not pollute your workspace, you can perform the following steps. 1.
|
||||
|
||||
1. delete the
|
||||
```json
|
||||
{
|
||||
"@sveltejs/vite-plugin-svelte": "^2.0.3",
|
||||
"@tsconfig/svelte": "^4.0.1",
|
||||
"svelte": "^3.57.0"
|
||||
}
|
||||
```
|
||||
2. delete the `svelte.config.js` file
|
||||
3. delete the following line from the `vite.config.js` file
|
||||
- Line 6: `import { svelte } from "@sveltejs/vite-plugin-svelte"`
|
||||
- Line 20: `svelte(),`
|
||||
4. delete line 37 of `tsconfig.json` from `"svelte"` 5.
|
||||
5. re-run `pnpm i`
|
||||
|
||||
## Developer's Guide
|
||||
|
||||
Developers of SiYuan need to pay attention to the following specifications.
|
||||
|
||||
### 1. File Reading and Writing Specifications
|
||||
|
||||
If plugins or external extensions require direct reading or writing of files under the `data` directory, please use the kernel API to achieve this. **Do not call `fs` or other electron or nodejs APIs directly**, as it may result in data loss during synchronization and cause damage to cloud data.
|
||||
|
||||
Related APIs can be found at: `/api/file/*` (e.g., `/api/file/getFile`).
|
||||
|
||||
### 2. Daily Note Attribute Specifications
|
||||
|
||||
When creating a daily note in SiYuan, a custom-dailynote-yyyymmdd attribute will be automatically added to the document to distinguish it from regular documents.
|
||||
|
||||
> For more details, please refer to [Github Issue #9807](https://github.com/siyuan-note/siyuan/issues/9807).
|
||||
|
||||
Developers should pay attention to the following when developing the functionality to manually create Daily Notes:
|
||||
|
||||
* If `/api/filetree/createDailyNote` is called to create a daily note, the attribute will be automatically added to the document, and developers do not need to handle it separately
|
||||
* If a document is created manually by developer's code (e.g., using the `createDocWithMd` API to create a daily note), please manually add this attribute to the document
|
||||
Make sure you check them out and support them as well!
|
||||
|
||||
## License
|
||||
The original plugin framework is developed by SiYuan 思源笔记 and licensed under the MIT license.
|
||||
All changes made by me are copyright MassiveBox 2025, and licensed under the MIT license.
|
271
README_zh_CN.md
271
README_zh_CN.md
|
@ -1,271 +0,0 @@
|
|||
|
||||
# 使用 vite + svelte 的思源笔记插件示例
|
||||
|
||||
[English](./README.md)
|
||||
|
||||
|
||||
> 本例同 [siyuan/plugin-sample](https://github.com/siyuan-note/plugin-sample) [v0.3.5](https://github.com/siyuan-note/plugin-sample/tree/v0.3.5)
|
||||
|
||||
1. 使用 vite 打包
|
||||
2. 使用符号链接、而不是把项目放到插件目录下的模式进行开发
|
||||
3. 内置对 svelte 框架的支持
|
||||
|
||||
> **如果不想要 svelte,请移步这个模板:** [frostime/plugin-sample-vite](https://github.com/frostime/plugin-sample-vite)
|
||||
>
|
||||
> **这里还提供了一个 vite+solidjs 的模板**: [frostime/plugin-sample-vite-solidjs](https://github.com/frostime/plugin-sample-vite-solidjs)
|
||||
|
||||
4. 提供一个github action 模板,能自动生成package.zip并上传到新版本中
|
||||
|
||||
## 开始
|
||||
|
||||
1. 通过 <kbd>Use this template</kbd> 按钮将该库文件复制到你自己的库中,请注意库名和插件名称一致,默认分支必须为 `main`
|
||||
2. 将你的库克隆到本地开发文件夹中
|
||||
* 注意: 同 `plugin-sample` 不同, 本样例并不推荐直接把代码下载到 `{workspace}/data/plugins/`
|
||||
3. 安装 [NodeJS](https://nodejs.org/en/download) 和 [pnpm](https://pnpm.io/installation),然后在开发文件夹下执行 `pnpm i` 安装所需要的依赖
|
||||
4. 运行 `pnpm run make-link` 命令创建符号链接 (Windows 下的开发者请参阅下方「Windows 下的 make-link」小节)
|
||||
5. 执行 `pnpm run dev` 进行实时编译
|
||||
6. 在思源中打开集市并在下载选项卡中启用插件
|
||||
|
||||
> [!TIP]
|
||||
> 你也可以使用我们维护的 [siyuan-plugin-cli](https://www.npmjs.com/package/siyuan-plugin-cli) 命令行工具,在本地终端中直接构建插件。
|
||||
>
|
||||
> 此外,对于本插件以下提及到的 `make-link` 相关的命令,后续所有更新将在 [siyuan-plugin-cli](https://www.npmjs.com/package/siyuan-plugin-cli) 中进行。
|
||||
>
|
||||
> 模板内置的 `make-link` 脚本也可能会在未来某个版本中移除,转而使用 `siyuan-plugin-cli` 工具,意在简化同时维护多个插件模板的工作量。
|
||||
|
||||
### 设置 make-link 命令的目标目录
|
||||
|
||||
make-link 命令会创建符号链接将你的 `dev` 目录绑定到思源的插件目录下。你可以有三种方式来配置目标的思源工作空间并创建符号链接:
|
||||
|
||||
1. **选择工作空间**
|
||||
- 打开思源笔记, 确保思源内核正在运行
|
||||
- 运行 `pnpm run make-link`, 脚本会自动检测所有思源的工作空间, 请在命令行中手动输入序号以选择工作空间
|
||||
```bash
|
||||
>>> pnpm run make-link
|
||||
> plugin-sample-vite-svelte@0.0.3 make-link H:\SrcCode\开源项目\plugin-sample-vite-svelte
|
||||
> node --no-warnings ./scripts/make_dev_link.js
|
||||
|
||||
"targetDir" is empty, try to get SiYuan directory automatically....
|
||||
Got 2 SiYuan workspaces
|
||||
[0] H:\Media\SiYuan
|
||||
[1] H:\临时文件夹\SiYuanDevSpace
|
||||
Please select a workspace[0-1]: 0
|
||||
Got target directory: H:\Media\SiYuan/data/plugins
|
||||
Done! Created symlink H:\Media\SiYuan/data/plugins/plugin-sample-vite-svelte
|
||||
```
|
||||
2. **手动配置目标目录**
|
||||
- 打开 `./scripts/make_dev_link.js` 文件,更改 `targetDir` 为思源的插件目录 `<siyuan workspace>/data/plugins`
|
||||
- 运行 `pnpm run make-link` 命令, 如果看到类似以下的消息,说明创建成功:
|
||||
|
||||
3. **设置环境变量创建符号链接**
|
||||
- 设置系统的环境变量 `SIYUAN_PLUGIN_DIR` 为 `工作空间/data/plugins` 的路径
|
||||
|
||||
|
||||
### Windows 下的 make-link
|
||||
|
||||
由于思源升级了 Go 1.23,旧版创建的 junction link 在 windows 下无法被正常识别,故而改为创建 `dir` 符号链接。
|
||||
|
||||
> https://github.com/siyuan-note/siyuan/issues/12399
|
||||
|
||||
|
||||
不过 Windows 下使用 NodeJs 创建目录符号链接可能需要管理员权限,你可以有如下几种选择:
|
||||
|
||||
1. 在具有管理员权限的命令行中运行 `pnpm run make-link`
|
||||
2. 配置 Windows 设置,在 [系统设置-更新与安全-开发者模式] 中启用开发者模式,然后再运行 `pnpm run make-link`
|
||||
3. 运行 `pnpm run make-link-win`,该命令会使用一个 powershell 脚本来寻求管理员权限,需要在系统中开启 PowerShell 脚本执行权限
|
||||
|
||||
|
||||
## 国际化
|
||||
|
||||
国际化方面我们主要考虑的是支持多语言,具体需要完成以下工作:
|
||||
|
||||
* 插件自身的元信息,比如插件描述和自述文件
|
||||
* plugin.json 中的 `description` 和 `readme` 字段,以及对应的 README*.md 文件
|
||||
* 插件中使用的文本,比如按钮文字和提示信息
|
||||
* public/i18n/*.json 语言配置文件
|
||||
* 代码中使用 `this.i18.key` 获取文本
|
||||
* 最后在 plugin.json 中的 `i18n` 字段中声明该插件支持的语言
|
||||
* yaml 支持
|
||||
* 本模板特别支持基于 Yaml 语法的 I18n,见 `public/i18n/zh_CN.yaml`
|
||||
* 编译时,会自动把定义的 yaml 文件翻译成 json 文件放到 dist 或 dev 目录下
|
||||
|
||||
建议插件至少支持英文和简体中文,这样可以方便更多人使用。
|
||||
|
||||
## plugin.json
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "plugin-sample-vite-svelte",
|
||||
"author": "frostime",
|
||||
"url": "https://github.com/siyuan-note/plugin-sample-vite-svelte",
|
||||
"version": "0.1.3",
|
||||
"minAppVersion": "2.8.8",
|
||||
"backends": ["windows", "linux", "darwin"],
|
||||
"frontends": ["desktop"],
|
||||
"displayName": {
|
||||
"en_US": "Plugin sample with vite and svelte",
|
||||
"zh_CN": "插件样例 vite + svelte 版"
|
||||
},
|
||||
"description": {
|
||||
"en_US": "SiYuan plugin sample with vite and svelte",
|
||||
"zh_CN": "使用 vite 和 svelte 开发的思源插件样例"
|
||||
},
|
||||
"readme": {
|
||||
"en_US": "README_en_US.md",
|
||||
"zh_CN": "README.md"
|
||||
},
|
||||
"funding": {
|
||||
"openCollective": "",
|
||||
"patreon": "",
|
||||
"github": "",
|
||||
"custom": [
|
||||
"https://ld246.com/sponsor"
|
||||
]
|
||||
},
|
||||
"keywords": [
|
||||
"sample", "示例"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
* `name`:插件名称,必须和库名一致,且全局唯一(集市中不能有重名插件)
|
||||
* `author`:插件作者名
|
||||
* `url`:插件仓库地址
|
||||
* `version`:插件版本号,建议遵循 [semver](https://semver.org/lang/zh-CN/) 规范
|
||||
* `minAppVersion`:插件支持的最低思源笔记版本号
|
||||
* `backends`:插件需要的后端环境,可选值为 `windows`, `linux`, `darwin`, `docker`, `android`, `ios` and `all`
|
||||
* `windows`:Windows 桌面端
|
||||
* `linux`:Linux 桌面端
|
||||
* `darwin`:macOS 桌面端
|
||||
* `docker`:Docker 端
|
||||
* `android`:Android 端
|
||||
* `ios`:iOS 端
|
||||
* `all`:所有环境
|
||||
* `frontends`:插件需要的前端环境,可选值为 `desktop`, `desktop-window`, `mobile`, `browser-desktop`, `browser-mobile` and `all`
|
||||
* `desktop`:桌面端
|
||||
* `desktop-window`:桌面端页签转换的独立窗口
|
||||
* `mobile`:移动端
|
||||
* `browser-desktop`:桌面端浏览器
|
||||
* `browser-mobile`:移动端浏览器
|
||||
* `all`:所有环境
|
||||
* `displayName`:模板显示名称,主要用于模板集市列表中显示,支持多语言
|
||||
* `default`:默认语言,必须存在
|
||||
* `zh_CN`、`en_US` 等其他语言:可选,建议至少提供中文和英文
|
||||
* `description`:插件描述,主要用于插件集市列表中显示,支持多语言
|
||||
* `default`:默认语言,必须存在
|
||||
* `zh_CN`、`en_US` 等其他语言:可选,建议至少提供中文和英文
|
||||
* `readme`:自述文件名,主要用于插件集市详情页中显示,支持多语言
|
||||
* `default`:默认语言,必须存在
|
||||
* `zh_CN`、`en_US` 等其他语言:可选,建议至少提供中文和英文
|
||||
* `funding`:插件赞助信息
|
||||
* `openCollective`:Open Collective 名称
|
||||
* `patreon`:Patreon 名称
|
||||
* `github`:GitHub 登录名
|
||||
* `custom`:自定义赞助链接列表
|
||||
* `keywords`:搜索关键字列表,用于集市搜索功能
|
||||
|
||||
## 打包
|
||||
|
||||
无论使用何种方式编译打包,我们最终需要生成一个 package.zip,它至少包含如下文件:
|
||||
|
||||
* i18n/*
|
||||
* icon.png (160*160)
|
||||
* index.css
|
||||
* index.js
|
||||
* plugin.json
|
||||
* preview.png (1024*768)
|
||||
* README*.md
|
||||
|
||||
## 上架集市
|
||||
|
||||
* 执行 `pnpm run build` 生成 package.zip
|
||||
* 在 GitHub 上创建一个新的发布,使用插件版本号作为 “Tag
|
||||
version”,示例 https://github.com/siyuan-note/plugin-sample/releases
|
||||
* 上传 package.zip 作为二进制附件
|
||||
* 提交发布
|
||||
|
||||
如果是第一次发布版本,还需要创建一个 PR 到 [Community Bazaar](https://github.com/siyuan-note/bazaar) 社区集市仓库,修改该库的
|
||||
plugins.json。该文件是所有社区插件库的索引,格式为:
|
||||
|
||||
```json
|
||||
{
|
||||
"repos": [
|
||||
"username/reponame"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
PR 被合并以后集市会通过 GitHub Actions 自动更新索引并部署。后续发布新版本插件时只需要按照上述步骤创建新的发布即可,不需要再
|
||||
PR 社区集市仓库。
|
||||
|
||||
正常情况下,社区集市仓库每隔 1 小时会自动更新索引并部署,可在 https://github.com/siyuan-note/bazaar/actions 查看部署状态。
|
||||
|
||||
## 使用 Github action 自动发布
|
||||
|
||||
样例中自带了 github action,可以自动打包发布,请遵循以下操作:
|
||||
|
||||
1. 设置项目 `https://github.com/OWNER/REPO/settings/actions` 页面向下划到 **Workflow Permissions**,打开配置
|
||||
|
||||

|
||||
|
||||
2. 需要发布版本的时候,push 一个格式为 `v*` 的 tag,github 就会自动打包发布 release(包括 package.zip)
|
||||
|
||||
3. 默认使用保守策略进行 pre-release 发布,如果觉得没有必要,可以更改 release.yml 中的设置:
|
||||
|
||||
```yaml
|
||||
- name: Release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifactErrorsFailBuild: true
|
||||
artifacts: 'package.zip'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
prerelease: true # 把这个改为 false
|
||||
```
|
||||
|
||||
## 如何去掉 svelte 依赖
|
||||
|
||||
> 无 Svelte 依赖版: https://github.com/frostime/plugin-sample-vite
|
||||
|
||||
本插件使用 vite 打包,并提供了 svelte 框架依赖。不过实际情况下可能有些开发者并不想要 svelte,只希望使用 vite 打包。
|
||||
|
||||
实际上你可以完全不做任何修改,就可以在不使用 svelte 的前提下使用这个模板。与 svelte 编译的编译相关的部分是以插件的形式载入到 vite 的工作流中,所以即使你的项目里面没有 svelte,也不会有太大的影响。
|
||||
|
||||
如果你执意希望删除掉所有 svelte 依赖以免它们污染你的工作空间,可以执行一下步骤:
|
||||
|
||||
1. 删掉 package.json 中的
|
||||
```json
|
||||
{
|
||||
"@sveltejs/vite-plugin-svelte": "^2.0.3",
|
||||
"@tsconfig/svelte": "^4.0.1",
|
||||
"svelte": "^3.57.0"
|
||||
}
|
||||
```
|
||||
2. 删掉 `svelte.config.js` 文件
|
||||
3. 删掉 `vite.config.js` 文件中的
|
||||
- 第六行: `import { svelte } from "@sveltejs/vite-plugin-svelte"`
|
||||
- 第二十行: `svelte(),`
|
||||
4. 删掉 `tsconfig.json` 中 37 行 `"svelte"`
|
||||
5. 重新执行 `pnpm i`
|
||||
|
||||
|
||||
## 开发者须知
|
||||
|
||||
思源开发者需注意以下规范。
|
||||
|
||||
### 1. 读写文件规范
|
||||
|
||||
插件或者外部扩展如果有直接读取或者写入 data 下文件的需求,请通过调用内核 API 来实现,**不要自行调用 `fs` 或者其他 electron、nodejs API**,否则可能会导致数据同步时分块丢失,造成云端数据损坏。
|
||||
|
||||
相关 API 见 `/api/file/*`(例如 `/api/file/getFile` 等)。
|
||||
|
||||
### 2. Daily Note 属性规范
|
||||
|
||||
思源在创建日记的时候会自动为文档添加 custom-dailynote-yyyymmdd 属性,以方便将日记文档同普通文档区分。
|
||||
|
||||
> 详情请见 [Github Issue #9807](https://github.com/siyuan-note/siyuan/issues/9807)。
|
||||
|
||||
开发者在开发手动创建 Daily Note 的功能时请注意:
|
||||
|
||||
* 如果调用了 `/api/filetree/createDailyNote` 创建日记,那么文档会自动添加这个属性,无需开发者特别处理
|
||||
* 如果是开发者代码手动创建文档(例如使用 `createDocWithMd` API 创建日记),请手动为文档添加该属性
|
BIN
icon.png
BIN
icon.png
Binary file not shown.
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 8.4 KiB |
|
@ -33,5 +33,9 @@
|
|||
"vite": "^5.2.9",
|
||||
"vite-plugin-static-copy": "^1.0.2",
|
||||
"vite-plugin-zip-pack": "^1.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@js-draw/material-icons": "^1.29.0",
|
||||
"js-draw": "^1.29.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
23
plugin.json
23
plugin.json
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "plugin-sample-vite-svelte",
|
||||
"author": "frostime",
|
||||
"url": "https://github.com/siyuan-note/plugin-sample-vite-svelte",
|
||||
"version": "0.3.6",
|
||||
"name": "siyuan-jsdraw-plugin",
|
||||
"author": "massivebox",
|
||||
"url": "https://git.massive.box/massivebox/siyuan-jsdraw-plugin",
|
||||
"version": "0.1.0",
|
||||
"minAppVersion": "3.0.12",
|
||||
"backends": [
|
||||
"windows",
|
||||
|
@ -21,16 +21,13 @@
|
|||
"desktop-window"
|
||||
],
|
||||
"displayName": {
|
||||
"en_US": "Plugin sample with vite and svelte",
|
||||
"zh_CN": "插件样例 vite + svelte 版"
|
||||
"en_US": "JS-Draw Whiteboard"
|
||||
},
|
||||
"description": {
|
||||
"en_US": "SiYuan plugin sample with vite and svelte",
|
||||
"zh_CN": "使用 vite 和 svelte 开发的思源插件样例"
|
||||
"en_US": "Include a whiteboard for freehand drawing anywhere in your documents."
|
||||
},
|
||||
"readme": {
|
||||
"en_US": "README.md",
|
||||
"zh_CN": "README_zh_CN.md"
|
||||
"en_US": "README.md"
|
||||
},
|
||||
"funding": {
|
||||
"custom": [
|
||||
|
@ -39,7 +36,9 @@
|
|||
},
|
||||
"keywords": [
|
||||
"plugin",
|
||||
"sample",
|
||||
"插件样例"
|
||||
"drawing",
|
||||
"freehand",
|
||||
"tablet",
|
||||
"whiteboard"
|
||||
]
|
||||
}
|
||||
|
|
BIN
preview.png
BIN
preview.png
Binary file not shown.
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 73 KiB |
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"addTopBarIcon": "Add a top bar icon by plugin",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"byeMenu": "Bye, Menu!",
|
||||
"helloPlugin": "Hello, Plugin!",
|
||||
"byePlugin": "Bye, Plugin!",
|
||||
"showDialog": "Show dialog",
|
||||
"removedData": "Data deleted",
|
||||
"confirmRemove": "Confirm to delete the data in ${name}?",
|
||||
"insertEmoji": "Insert Emoji",
|
||||
"removeSpace": "Remove Space",
|
||||
"getTab": "Print out all opened custom tabs in the debugger",
|
||||
"name": "SiYuan",
|
||||
"hello": {
|
||||
"makesure": "Before using this template, please read the <a href=\"https://github.com/siyuan-note/plugin-sample\">offical sample</a>, make sure that you've known about the pipeline for plugin developing."
|
||||
},
|
||||
"hintTitle":"About",
|
||||
"hintDesc":"<a href='https://github.com/siyuan-note/plugin-sample-vite-svelte'>plugin-sample-vite-svelte</a><br>@frostime<br>@88250<br>@zxkmm"
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"addTopBarIcon": "使用插件添加一个顶栏按钮",
|
||||
"cancel": "取消",
|
||||
"save": "保存",
|
||||
"byeMenu": "再见,菜单!",
|
||||
"helloPlugin": "你好,插件!",
|
||||
"byePlugin": "再见,插件!",
|
||||
"showDialog": "弹出一个对话框",
|
||||
"removedData": "数据已删除",
|
||||
"confirmRemove": "确认删除 ${name} 中的数据?",
|
||||
"insertEmoji": "插入表情",
|
||||
"removeSpace": "移除空格",
|
||||
"getTab": "在日志中打印出已打开的所有自定义页签",
|
||||
"name": "思源",
|
||||
"hello": {
|
||||
"makesure": "使用这个模板之前,请阅读<a href=\"https://github.com/siyuan-note/plugin-sample\">官方教程</a>, 确保自己已经理解了插件的基本开发流程。"
|
||||
},
|
||||
"hintTitle": "关于",
|
||||
"hintDesc": "<a href='https://github.com/siyuan-note/plugin-sample-vite-svelte'>🔗 plugin-sample-vite-svelte</a><br>💻 @frostime<br>💻 @88250<br>💻 @zxkmm"
|
||||
}
|
56
public/webapp/button.js
Normal file
56
public/webapp/button.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
function copyEditLink(fileID) {
|
||||
navigator.clipboard.writeText(getEditLink(fileID));
|
||||
}
|
||||
|
||||
function refreshPage() {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function addButton(document, fileID) {
|
||||
|
||||
// Add floating button
|
||||
const floatingButton = document.createElement('button');
|
||||
floatingButton.id = 'floatingButton';
|
||||
floatingButton.innerHTML = '⚙️';
|
||||
document.body.appendChild(floatingButton);
|
||||
|
||||
// Add popup menu
|
||||
const popupMenu = document.createElement('div');
|
||||
popupMenu.id = 'popupMenu';
|
||||
popupMenu.innerHTML = `
|
||||
<button onclick="refreshPage()">Refresh</button>
|
||||
<button onclick="copyEditLink('${fileID}')">Copy Direct Edit Link</button>
|
||||
|
||||
`;
|
||||
document.body.appendChild(popupMenu);
|
||||
|
||||
// Show/hide floating button on mouse move
|
||||
document.body.addEventListener('mousemove', () => {
|
||||
floatingButton.style.display = 'block';
|
||||
});
|
||||
|
||||
document.body.addEventListener('mouseleave', () => {
|
||||
floatingButton.style.display = 'none';
|
||||
});
|
||||
|
||||
// Toggle popup menu on button click
|
||||
floatingButton.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
popupMenu.style.display = popupMenu.style.display === 'block' ? 'none' : 'block';
|
||||
});
|
||||
|
||||
// Hide popup menu when clicking outside
|
||||
document.addEventListener('click', () => {
|
||||
popupMenu.style.display = 'none';
|
||||
});
|
||||
|
||||
// Set CSS variable for correct scaling of SVG
|
||||
const svg = document.body.querySelector('svg');
|
||||
if (svg) {
|
||||
const viewBox = svg.getAttribute('viewBox')?.split(' ') || [];
|
||||
const width = parseFloat(viewBox[2]) || svg.clientWidth;
|
||||
const height = parseFloat(viewBox[3]) || svg.clientHeight;
|
||||
document.documentElement.style.setProperty('--svg-aspect-ratio', width/height);
|
||||
}
|
||||
|
||||
}
|
47
public/webapp/draw.js
Normal file
47
public/webapp/draw.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
const FALLBACK = "<p>Nothing here yet! Click me to open the editor.</p>"
|
||||
async function getFile(path) {
|
||||
|
||||
const response = await fetch('/api/file/getFile', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({path: path})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.log('Failed to fetch HTML content');
|
||||
return null;
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const resTxt = await blob.text();
|
||||
|
||||
// if we got a 404 api response, we will return null
|
||||
try {
|
||||
const res = JSON.parse(resTxt);
|
||||
if(res.code === 404) {
|
||||
return null;
|
||||
}
|
||||
}catch {}
|
||||
|
||||
return resTxt;
|
||||
|
||||
}
|
||||
|
||||
async function getSVG(fileID) {
|
||||
|
||||
const resp = await getFile("/data/assets/" + fileID + '.svg');
|
||||
if(resp == null) {
|
||||
return FALLBACK;
|
||||
}
|
||||
return resp;
|
||||
|
||||
}
|
||||
|
||||
function getEditLink(fileID) {
|
||||
const data = encodeURIComponent(
|
||||
JSON.stringify({
|
||||
id: fileID
|
||||
})
|
||||
)
|
||||
return `siyuan://plugins/siyuan-jsdraw-pluginwhiteboard/?icon=iconDraw&title=Drawing&data=${data}`;
|
||||
}
|
91
public/webapp/index.css
Normal file
91
public/webapp/index.css
Normal file
|
@ -0,0 +1,91 @@
|
|||
a > div > p {
|
||||
color: var(--text, black);
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden; /* Prevent scrollbars */
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
div {
|
||||
max-width: min(100vw, 100vh * var(--svg-aspect-ratio));
|
||||
max-height: min(100vh, 100vw / var(--svg-aspect-ratio));
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Floating button styles */
|
||||
#floatingButton {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
display: none; /* Initially hidden */
|
||||
}
|
||||
|
||||
/* Popup menu styles */
|
||||
#popupMenu {
|
||||
position: fixed;
|
||||
bottom: 70px;
|
||||
right: 20px;
|
||||
background-color: var(--popup-bg, white);
|
||||
border: 1px solid var(--popup-border, #ccc);
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
display: none; /* Initially hidden */
|
||||
max-height: calc(100vh - 90px); /* Adjust based on window height */
|
||||
overflow-y: auto; /* Add scroll if content overflows */
|
||||
}
|
||||
|
||||
#popupMenu button {
|
||||
display: block;
|
||||
margin: 5px 0;
|
||||
padding: 5px 10px;
|
||||
background-color: var(--button-bg, #f8f9fa);
|
||||
border: 1px solid var(--button-border, #ccc);
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
color: var(--button-text, black);
|
||||
}
|
||||
|
||||
#popupMenu button:hover {
|
||||
background-color: var(--button-hover-bg, #e2e6ea);
|
||||
}
|
||||
|
||||
/* Dark theme styles */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text: white;
|
||||
--popup-bg: #333;
|
||||
--popup-border: #555;
|
||||
--button-bg: #444;
|
||||
--button-border: #666;
|
||||
--button-text: #fff;
|
||||
--button-hover-bg: #555;
|
||||
}
|
||||
}
|
26
public/webapp/index.html
Normal file
26
public/webapp/index.html
Normal file
|
@ -0,0 +1,26 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="draw.js"></script>
|
||||
<script src="button.js"></script>
|
||||
<script>
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const fileID = urlParams.get('id');
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const editLink = document.createElement('a');
|
||||
editLink.href = getEditLink(fileID);
|
||||
document.body.appendChild(editLink);
|
||||
|
||||
const htmlContainer = document.createElement('div');
|
||||
htmlContainer.innerHTML = await getSVG(fileID);
|
||||
editLink.appendChild(htmlContainer);
|
||||
|
||||
addButton(document, fileID);
|
||||
});
|
||||
</script>
|
||||
<link rel="stylesheet" href="index.css">
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
7
src/const.ts
Normal file
7
src/const.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export const SVG_MIME = "image/svg+xml";
|
||||
export const JSON_MIME = "application/json";
|
||||
export const DATA_PATH = "/data/assets";
|
||||
export const STORAGE_PATH = "/data/storage/petal/siyuan-jsdraw-plugin";
|
||||
export const TOOLBAR_PATH = STORAGE_PATH + "/toolbar.json";
|
||||
export const CONFIG_PATH = STORAGE_PATH + "/conf.json";
|
||||
export const EMBED_PATH = "/plugins/siyuan-jsdraw-plugin/webapp/?id=";
|
75
src/editorTab.ts
Normal file
75
src/editorTab.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import {ITabModel, openTab, Plugin} from "siyuan"
|
||||
import Editor, {BaseWidget, EditorEventType} from "js-draw";
|
||||
import { MaterialIconProvider } from '@js-draw/material-icons';
|
||||
import 'js-draw/styles';
|
||||
import {getFile, saveFile} from "@/file";
|
||||
import {JSON_MIME, SVG_MIME, TOOLBAR_PATH} from "@/const";
|
||||
import {idToPath} from "@/helper";
|
||||
|
||||
export function openEditorTab(p: Plugin, fileID: string) {
|
||||
openTab({
|
||||
app: p.app,
|
||||
custom: {
|
||||
title: 'Drawing',
|
||||
icon: 'iconDraw',
|
||||
id: "siyuan-jsdraw-pluginwhiteboard",
|
||||
data: { id: fileID }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function saveCallback(editor: Editor, fileID: string, saveButton: BaseWidget) {
|
||||
const svgElem = editor.toSVG();
|
||||
try {
|
||||
saveFile(idToPath(fileID), SVG_MIME, svgElem.outerHTML);
|
||||
saveButton.setDisabled(true);
|
||||
setTimeout(() => { // @todo improve save button feedback
|
||||
saveButton.setDisabled(false);
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
alert("Error saving drawing! Enter developer mode to find the error, and a copy of the current status.");
|
||||
console.error(error);
|
||||
console.log("Couldn't save SVG: ", svgElem.outerHTML)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function createEditor(i: ITabModel) {
|
||||
|
||||
const fileID = i.data.id;
|
||||
if(fileID == null) {
|
||||
alert("File ID missing - couldn't open file.")
|
||||
return;
|
||||
}
|
||||
|
||||
const editor = new Editor(i.element, {
|
||||
iconProvider: new MaterialIconProvider(),
|
||||
});
|
||||
|
||||
const toolbar = editor.addToolbar();
|
||||
|
||||
// restore toolbar state
|
||||
getFile(TOOLBAR_PATH).then(toolbarState => {
|
||||
if(toolbarState!= null) {
|
||||
toolbar.deserializeState(toolbarState)
|
||||
}
|
||||
});
|
||||
// restore drawing
|
||||
getFile(idToPath(fileID)).then(svg => {
|
||||
if(svg != null) {
|
||||
editor.loadFromSVG(svg);
|
||||
}
|
||||
});
|
||||
|
||||
// save logic
|
||||
const saveButton = toolbar.addSaveButton(() => saveCallback(editor, fileID, saveButton));
|
||||
|
||||
// save toolbar config on tool change (toolbar state is not saved in SVGs!)
|
||||
editor.notifier.on(EditorEventType.ToolUpdated, () => {
|
||||
saveFile(TOOLBAR_PATH, JSON_MIME, toolbar.serializeState());
|
||||
});
|
||||
|
||||
editor.dispatch(editor.setBackgroundStyle({ autoresize: true }), false);
|
||||
editor.getRootElement().style.height = '100%';
|
||||
|
||||
}
|
37
src/file.ts
Normal file
37
src/file.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import {getFileBlob, putFile} from "@/api";
|
||||
|
||||
function toFile(title: string, content: string, mimeType: string){
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
return new File([blob], title, { type: mimeType });
|
||||
}
|
||||
|
||||
export function saveFile(path: string, mimeType: string, content: string) {
|
||||
|
||||
const file = toFile(path.split('/').pop(), content, mimeType);
|
||||
|
||||
try {
|
||||
putFile(path, false, file);
|
||||
} catch (error) {
|
||||
console.error("Error saving file:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export async function getFile(path: string) {
|
||||
|
||||
const blob = await getFileBlob(path);
|
||||
const jsonText = await blob.text();
|
||||
|
||||
// if we got a 404 api response, we will return null
|
||||
try {
|
||||
const res = JSON.parse(jsonText);
|
||||
if(res.code == 404) {
|
||||
return null;
|
||||
}
|
||||
}catch {}
|
||||
|
||||
// js-draw expects a string!
|
||||
return jsonText;
|
||||
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
<!--
|
||||
Copyright (c) 2024 by frostime. All Rights Reserved.
|
||||
Author : frostime
|
||||
Date : 2023-11-19 12:30:45
|
||||
FilePath : /src/hello.svelte
|
||||
LastEditTime : 2024-10-16 14:37:50
|
||||
Description :
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { version, sql as query } from "@/api";
|
||||
import { showMessage, fetchPost, Protyle } from "siyuan";
|
||||
|
||||
export let app;
|
||||
|
||||
let time: string = "";
|
||||
let ver: string = "";
|
||||
|
||||
let divProtyle: HTMLDivElement;
|
||||
let protyle: any;
|
||||
let blockID: string = '';
|
||||
|
||||
onMount(async () => {
|
||||
ver = await version();
|
||||
fetchPost("/api/system/currentTime", {}, (response) => {
|
||||
time = new Date(response.data).toString();
|
||||
});
|
||||
protyle = await initProtyle();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
showMessage("Hello panel closed");
|
||||
protyle.destroy();
|
||||
});
|
||||
|
||||
async function initProtyle() {
|
||||
let sql = "SELECT * FROM blocks ORDER BY RANDOM () LIMIT 1;";
|
||||
let blocks: Block[] = await query(sql);
|
||||
blockID = blocks[0].id;
|
||||
return new Protyle(app, divProtyle, {
|
||||
blockId: blockID
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="b3-dialog__content">
|
||||
<div>appId:</div>
|
||||
<div class="fn__hr"></div>
|
||||
<div class="plugin-sample__time">${app?.appId}</div>
|
||||
<div class="fn__hr"></div>
|
||||
<div class="fn__hr"></div>
|
||||
<div>API demo:</div>
|
||||
<div class="fn__hr" />
|
||||
<div class="plugin-sample__time">
|
||||
System current time: <span id="time">{time}</span>
|
||||
</div>
|
||||
<div class="fn__hr" />
|
||||
<div class="fn__hr" />
|
||||
<div>Protyle demo: id = {blockID}</div>
|
||||
<div class="fn__hr" />
|
||||
<div id="protyle" style="height: 360px;" bind:this={divProtyle}/>
|
||||
</div>
|
||||
|
57
src/helper.ts
Normal file
57
src/helper.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { Plugin } from 'siyuan';
|
||||
import {DATA_PATH, EMBED_PATH} from "@/const";
|
||||
|
||||
const drawIcon: string = `
|
||||
<symbol id="iconDraw" viewBox="0 0 28 28">
|
||||
<path clip-rule="evenodd" d="M26.4097 9.61208C27.196 8.8358 27.1969 7.57578 26.4117 6.79842L21.1441 1.58305C20.3597 0.806412 19.0875 0.805538 18.302 1.5811L3.55214 16.1442C3.15754 16.5338 2.87982 17.024 2.74985 17.5603L1.05726 24.5451C0.697341 26.0304 2.09375 27.3461 3.57566 26.918L10.3372 24.9646C10.8224 24.8244 11.2642 24.5658 11.622 24.2125L26.4097 9.61208ZM20.4642 12.6725L10.2019 22.8047C10.0827 22.9225 9.9354 23.0087 9.77366 23.0554L4.17079 24.6741C3.65448 24.8232 3.16963 24.359 3.2962 23.8367L4.70476 18.024C4.74809 17.8453 4.84066 17.6819 4.97219 17.552L15.195 7.45865L20.4642 12.6725ZM21.8871 11.2676L16.618 6.05372L19.0185 3.68356C19.4084 3.29865 20.0354 3.29908 20.4247 3.68454L24.271 7.49266C24.6666 7.88436 24.6661 8.52374 24.27 8.91488L21.8871 11.2676Z" fill-rule="evenodd"/>
|
||||
</symbol>
|
||||
`;
|
||||
|
||||
export function loadIcons(p: Plugin) {
|
||||
const icons = drawIcon;
|
||||
p.addIcons(icons);
|
||||
}
|
||||
|
||||
export function getMenuHTML(icon: string, text: string): string {
|
||||
return `
|
||||
<div class="b3-list-item__first">
|
||||
<svg class="b3-list-item__graphic">
|
||||
<use xlink:href="#${icon}"></use>
|
||||
</svg>
|
||||
<span class="b3-list-item__text">${text}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export function generateSiyuanId() {
|
||||
const now = new Date();
|
||||
|
||||
const year = now.getFullYear().toString();
|
||||
const month = (now.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = now.getDate().toString().padStart(2, '0');
|
||||
const hours = now.getHours().toString().padStart(2, '0');
|
||||
const minutes = now.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = now.getSeconds().toString().padStart(2, '0');
|
||||
|
||||
const timestamp = `${year}${month}${day}${hours}${minutes}${seconds}`;
|
||||
|
||||
const characters = 'abcdefghijklmnopqrstuvwxyz';
|
||||
let random = '';
|
||||
for (let i = 0; i < 7; i++) {
|
||||
random += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
|
||||
return `${timestamp}-${random}`;
|
||||
}
|
||||
|
||||
export function idToPath(id: string) {
|
||||
return DATA_PATH + '/' + id + '.svg';
|
||||
}
|
||||
|
||||
// [Edit](siyuan://plugins/siyuan-jsdraw-pluginwhiteboard/?icon=iconDraw&title=Drawing&data={"id":"${id}"})
|
||||
// 
|
||||
export function getPreviewHTML(id: string): string {
|
||||
return `
|
||||
<iframe src="${EMBED_PATH + id}&antiCache=0"></iframe>
|
||||
`
|
||||
}
|
945
src/index.ts
945
src/index.ts
|
@ -1,943 +1,44 @@
|
|||
import {
|
||||
Plugin,
|
||||
showMessage,
|
||||
confirm,
|
||||
Dialog,
|
||||
Menu,
|
||||
openTab,
|
||||
adaptHotkey,
|
||||
getFrontend,
|
||||
getBackend,
|
||||
IModel,
|
||||
Protyle,
|
||||
openWindow,
|
||||
IOperation,
|
||||
Constants,
|
||||
openMobileFileById,
|
||||
lockScreen,
|
||||
ICard,
|
||||
ICardData
|
||||
} from "siyuan";
|
||||
import "@/index.scss";
|
||||
import {Plugin, Protyle} from 'siyuan';
|
||||
import {getPreviewHTML, loadIcons, getMenuHTML, generateSiyuanId} from "@/helper";
|
||||
import {createEditor, openEditorTab} from "@/editorTab";
|
||||
|
||||
import HelloExample from "@/hello.svelte";
|
||||
import SettingExample from "@/setting-example.svelte";
|
||||
export default class DrawJSPlugin extends Plugin {
|
||||
onload() {
|
||||
|
||||
import { SettingUtils } from "./libs/setting-utils";
|
||||
import { svelteDialog } from "./libs/dialog";
|
||||
|
||||
const STORAGE_NAME = "menu-config";
|
||||
const TAB_TYPE = "custom_tab";
|
||||
const DOCK_TYPE = "dock_tab";
|
||||
|
||||
export default class PluginSample extends Plugin {
|
||||
|
||||
customTab: () => IModel;
|
||||
private isMobile: boolean;
|
||||
private blockIconEventBindThis = this.blockIconEvent.bind(this);
|
||||
private settingUtils: SettingUtils;
|
||||
|
||||
async onload() {
|
||||
this.data[STORAGE_NAME] = { readonlyText: "Readonly" };
|
||||
|
||||
console.log("loading plugin-sample", this.i18n);
|
||||
|
||||
const frontEnd = getFrontend();
|
||||
this.isMobile = frontEnd === "mobile" || frontEnd === "browser-mobile";
|
||||
// 图标的制作参见帮助文档
|
||||
this.addIcons(`<symbol id="iconFace" viewBox="0 0 32 32">
|
||||
<path d="M13.667 17.333c0 0.92-0.747 1.667-1.667 1.667s-1.667-0.747-1.667-1.667 0.747-1.667 1.667-1.667 1.667 0.747 1.667 1.667zM20 15.667c-0.92 0-1.667 0.747-1.667 1.667s0.747 1.667 1.667 1.667 1.667-0.747 1.667-1.667-0.747-1.667-1.667-1.667zM29.333 16c0 7.36-5.973 13.333-13.333 13.333s-13.333-5.973-13.333-13.333 5.973-13.333 13.333-13.333 13.333 5.973 13.333 13.333zM14.213 5.493c1.867 3.093 5.253 5.173 9.12 5.173 0.613 0 1.213-0.067 1.787-0.16-1.867-3.093-5.253-5.173-9.12-5.173-0.613 0-1.213 0.067-1.787 0.16zM5.893 12.627c2.28-1.293 4.040-3.4 4.88-5.92-2.28 1.293-4.040 3.4-4.88 5.92zM26.667 16c0-1.040-0.16-2.040-0.44-2.987-0.933 0.2-1.893 0.32-2.893 0.32-4.173 0-7.893-1.92-10.347-4.92-1.4 3.413-4.187 6.093-7.653 7.4 0.013 0.053 0 0.12 0 0.187 0 5.88 4.787 10.667 10.667 10.667s10.667-4.787 10.667-10.667z"></path>
|
||||
</symbol>
|
||||
<symbol id="iconSaving" viewBox="0 0 32 32">
|
||||
<path d="M20 13.333c0-0.733 0.6-1.333 1.333-1.333s1.333 0.6 1.333 1.333c0 0.733-0.6 1.333-1.333 1.333s-1.333-0.6-1.333-1.333zM10.667 12h6.667v-2.667h-6.667v2.667zM29.333 10v9.293l-3.76 1.253-2.24 7.453h-7.333v-2.667h-2.667v2.667h-7.333c0 0-3.333-11.28-3.333-15.333s3.28-7.333 7.333-7.333h6.667c1.213-1.613 3.147-2.667 5.333-2.667 1.107 0 2 0.893 2 2 0 0.28-0.053 0.533-0.16 0.773-0.187 0.453-0.347 0.973-0.427 1.533l3.027 3.027h2.893zM26.667 12.667h-1.333l-4.667-4.667c0-0.867 0.12-1.72 0.347-2.547-1.293 0.333-2.347 1.293-2.787 2.547h-8.227c-2.573 0-4.667 2.093-4.667 4.667 0 2.507 1.627 8.867 2.68 12.667h2.653v-2.667h8v2.667h2.68l2.067-6.867 3.253-1.093v-4.707z"></path>
|
||||
</symbol>`);
|
||||
|
||||
const topBarElement = this.addTopBar({
|
||||
icon: "iconFace",
|
||||
title: this.i18n.addTopBarIcon,
|
||||
position: "right",
|
||||
callback: () => {
|
||||
if (this.isMobile) {
|
||||
this.addMenu();
|
||||
} else {
|
||||
let rect = topBarElement.getBoundingClientRect();
|
||||
// 如果被隐藏,则使用更多按钮
|
||||
if (rect.width === 0) {
|
||||
rect = document.querySelector("#barMore").getBoundingClientRect();
|
||||
}
|
||||
if (rect.width === 0) {
|
||||
rect = document.querySelector("#barPlugins").getBoundingClientRect();
|
||||
}
|
||||
this.addMenu(rect);
|
||||
}
|
||||
loadIcons(this);
|
||||
//const id = Math.random().toString(36).substring(7);
|
||||
this.addTab({
|
||||
'type': "whiteboard",
|
||||
init() {
|
||||
createEditor(this);
|
||||
}
|
||||
});
|
||||
|
||||
const statusIconTemp = document.createElement("template");
|
||||
statusIconTemp.innerHTML = `<div class="toolbar__item ariaLabel" aria-label="Remove plugin-sample Data">
|
||||
<svg>
|
||||
<use xlink:href="#iconTrashcan"></use>
|
||||
</svg>
|
||||
</div>`;
|
||||
statusIconTemp.content.firstElementChild.addEventListener("click", () => {
|
||||
confirm("⚠️", this.i18n.confirmRemove.replace("${name}", this.name), () => {
|
||||
this.removeData(STORAGE_NAME).then(() => {
|
||||
this.data[STORAGE_NAME] = { readonlyText: "Readonly" };
|
||||
showMessage(`[${this.name}]: ${this.i18n.removedData}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
this.addStatusBar({
|
||||
element: statusIconTemp.content.firstElementChild as HTMLElement,
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
langKey: "showDialog",
|
||||
hotkey: "⇧⌘O",
|
||||
callback: () => {
|
||||
this.showDialog();
|
||||
},
|
||||
fileTreeCallback: (file: any) => {
|
||||
console.log(file, "fileTreeCallback");
|
||||
},
|
||||
editorCallback: (protyle: any) => {
|
||||
console.log(protyle, "editorCallback");
|
||||
},
|
||||
dockCallback: (element: HTMLElement) => {
|
||||
console.log(element, "dockCallback");
|
||||
},
|
||||
});
|
||||
this.addCommand({
|
||||
langKey: "getTab",
|
||||
hotkey: "⇧⌘M",
|
||||
globalCallback: () => {
|
||||
console.log(this.getOpenedTab());
|
||||
},
|
||||
});
|
||||
|
||||
this.addDock({
|
||||
config: {
|
||||
position: "LeftBottom",
|
||||
size: { width: 200, height: 0 },
|
||||
icon: "iconSaving",
|
||||
title: "Custom Dock",
|
||||
hotkey: "⌥⌘W",
|
||||
},
|
||||
data: {
|
||||
text: "This is my custom dock"
|
||||
},
|
||||
type: DOCK_TYPE,
|
||||
resize() {
|
||||
console.log(DOCK_TYPE + " resize");
|
||||
},
|
||||
update() {
|
||||
console.log(DOCK_TYPE + " update");
|
||||
},
|
||||
init: (dock) => {
|
||||
if (this.isMobile) {
|
||||
dock.element.innerHTML = `<div class="toolbar toolbar--border toolbar--dark">
|
||||
<svg class="toolbar__icon"><use xlink:href="#iconEmoji"></use></svg>
|
||||
<div class="toolbar__text">Custom Dock</div>
|
||||
</div>
|
||||
<div class="fn__flex-1 plugin-sample__custom-dock">
|
||||
${dock.data.text}
|
||||
</div>
|
||||
</div>`;
|
||||
} else {
|
||||
dock.element.innerHTML = `<div class="fn__flex-1 fn__flex-column">
|
||||
<div class="block__icons">
|
||||
<div class="block__logo">
|
||||
<svg class="block__logoicon"><use xlink:href="#iconEmoji"></use></svg>
|
||||
Custom Dock
|
||||
</div>
|
||||
<span class="fn__flex-1 fn__space"></span>
|
||||
<span data-type="min" class="block__icon b3-tooltips b3-tooltips__sw" aria-label="Min ${adaptHotkey("⌘W")}"><svg class="block__logoicon"><use xlink:href="#iconMin"></use></svg></span>
|
||||
</div>
|
||||
<div class="fn__flex-1 plugin-sample__custom-dock">
|
||||
${dock.data.text}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
},
|
||||
destroy() {
|
||||
console.log("destroy dock:", DOCK_TYPE);
|
||||
}
|
||||
});
|
||||
|
||||
this.settingUtils = new SettingUtils({
|
||||
plugin: this, name: STORAGE_NAME
|
||||
});
|
||||
this.settingUtils.addItem({
|
||||
key: "Input",
|
||||
value: "",
|
||||
type: "textinput",
|
||||
title: "Readonly text",
|
||||
description: "Input description",
|
||||
action: {
|
||||
// Called when focus is lost and content changes
|
||||
callback: () => {
|
||||
// Return data and save it in real time
|
||||
let value = this.settingUtils.takeAndSave("Input");
|
||||
console.log(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.settingUtils.addItem({
|
||||
key: "InputArea",
|
||||
value: "",
|
||||
type: "textarea",
|
||||
title: "Readonly text",
|
||||
description: "Input description",
|
||||
// Called when focus is lost and content changes
|
||||
action: {
|
||||
callback: () => {
|
||||
// Read data in real time
|
||||
let value = this.settingUtils.take("InputArea");
|
||||
console.log(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.settingUtils.addItem({
|
||||
key: "Check",
|
||||
value: true,
|
||||
type: "checkbox",
|
||||
title: "Checkbox text",
|
||||
description: "Check description",
|
||||
action: {
|
||||
callback: () => {
|
||||
// Return data and save it in real time
|
||||
let value = !this.settingUtils.get("Check");
|
||||
this.settingUtils.set("Check", value);
|
||||
console.log(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.settingUtils.addItem({
|
||||
key: "Select",
|
||||
value: 1,
|
||||
type: "select",
|
||||
title: "Select",
|
||||
description: "Select description",
|
||||
options: {
|
||||
1: "Option 1",
|
||||
2: "Option 2"
|
||||
},
|
||||
action: {
|
||||
callback: () => {
|
||||
// Read data in real time
|
||||
let value = this.settingUtils.take("Select");
|
||||
console.log(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.settingUtils.addItem({
|
||||
key: "Slider",
|
||||
value: 50,
|
||||
type: "slider",
|
||||
title: "Slider text",
|
||||
description: "Slider description",
|
||||
direction: "column",
|
||||
slider: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
},
|
||||
action:{
|
||||
callback: () => {
|
||||
// Read data in real time
|
||||
let value = this.settingUtils.take("Slider");
|
||||
console.log(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.settingUtils.addItem({
|
||||
key: "Btn",
|
||||
value: "",
|
||||
type: "button",
|
||||
title: "Button",
|
||||
description: "Button description",
|
||||
button: {
|
||||
label: "Button",
|
||||
callback: () => {
|
||||
showMessage("Button clicked");
|
||||
}
|
||||
}
|
||||
});
|
||||
this.settingUtils.addItem({
|
||||
key: "Custom Element",
|
||||
value: "",
|
||||
type: "custom",
|
||||
direction: "row",
|
||||
title: "Custom Element",
|
||||
description: "Custom Element description",
|
||||
//Any custom element must offer the following methods
|
||||
createElement: (currentVal: any) => {
|
||||
let div = document.createElement('div');
|
||||
div.style.border = "1px solid var(--b3-theme-primary)";
|
||||
div.contentEditable = "true";
|
||||
div.textContent = currentVal;
|
||||
return div;
|
||||
},
|
||||
getEleVal: (ele: HTMLElement) => {
|
||||
return ele.textContent;
|
||||
},
|
||||
setEleVal: (ele: HTMLElement, val: any) => {
|
||||
ele.textContent = val;
|
||||
}
|
||||
});
|
||||
this.settingUtils.addItem({
|
||||
key: "Hint",
|
||||
value: "",
|
||||
type: "hint",
|
||||
title: this.i18n.hintTitle,
|
||||
description: this.i18n.hintDesc,
|
||||
});
|
||||
|
||||
try {
|
||||
this.settingUtils.load();
|
||||
} catch (error) {
|
||||
console.error("Error loading settings storage, probably empty config json:", error);
|
||||
}
|
||||
|
||||
|
||||
this.protyleSlash = [{
|
||||
filter: ["insert emoji 😊", "插入表情 😊", "crbqwx"],
|
||||
html: `<div class="b3-list-item__first"><span class="b3-list-item__text">${this.i18n.insertEmoji}</span><span class="b3-list-item__meta">😊</span></div>`,
|
||||
id: "insertEmoji",
|
||||
callback(protyle: Protyle) {
|
||||
protyle.insert("😊");
|
||||
id: "insert-drawing",
|
||||
filter: ["Insert Drawing", "Add drawing", "whiteboard", "freehand", "graphics", "jsdraw"],
|
||||
html: getMenuHTML("iconDraw", this.i18n.insertDrawing),
|
||||
callback: (protyle: Protyle) => {
|
||||
const uid = generateSiyuanId();
|
||||
protyle.insert(getPreviewHTML(uid), true, false);
|
||||
openEditorTab(this, uid);
|
||||
}
|
||||
}];
|
||||
|
||||
this.protyleOptions = {
|
||||
toolbar: ["block-ref",
|
||||
"a",
|
||||
"|",
|
||||
"text",
|
||||
"strong",
|
||||
"em",
|
||||
"u",
|
||||
"s",
|
||||
"mark",
|
||||
"sup",
|
||||
"sub",
|
||||
"clear",
|
||||
"|",
|
||||
"code",
|
||||
"kbd",
|
||||
"tag",
|
||||
"inline-math",
|
||||
"inline-memo",
|
||||
"|",
|
||||
{
|
||||
name: "insert-smail-emoji",
|
||||
icon: "iconEmoji",
|
||||
hotkey: "⇧⌘I",
|
||||
tipPosition: "n",
|
||||
tip: this.i18n.insertEmoji,
|
||||
click(protyle: Protyle) {
|
||||
protyle.insert("😊");
|
||||
}
|
||||
}],
|
||||
};
|
||||
|
||||
console.log(this.i18n.helloPlugin);
|
||||
}
|
||||
|
||||
onLayoutReady() {
|
||||
// this.loadData(STORAGE_NAME);
|
||||
this.settingUtils.load();
|
||||
console.log(`frontend: ${getFrontend()}; backend: ${getBackend()}`);
|
||||
|
||||
console.log(
|
||||
"Official settings value calling example:\n" +
|
||||
this.settingUtils.get("InputArea") + "\n" +
|
||||
this.settingUtils.get("Slider") + "\n" +
|
||||
this.settingUtils.get("Select") + "\n"
|
||||
);
|
||||
|
||||
let tabDiv = document.createElement("div");
|
||||
new HelloExample({
|
||||
target: tabDiv,
|
||||
props: {
|
||||
app: this.app,
|
||||
}
|
||||
});
|
||||
this.customTab = this.addTab({
|
||||
type: TAB_TYPE,
|
||||
init() {
|
||||
this.element.appendChild(tabDiv);
|
||||
console.log(this.element);
|
||||
},
|
||||
beforeDestroy() {
|
||||
console.log("before destroy tab:", TAB_TYPE);
|
||||
},
|
||||
destroy() {
|
||||
console.log("destroy tab:", TAB_TYPE);
|
||||
}
|
||||
});
|
||||
// This function is automatically called when the layout is loaded.
|
||||
}
|
||||
|
||||
async onunload() {
|
||||
console.log(this.i18n.byePlugin);
|
||||
showMessage("Goodbye SiYuan Plugin");
|
||||
console.log("onunload");
|
||||
onunload() {
|
||||
// This function is automatically called when the plugin is disabled.
|
||||
}
|
||||
|
||||
uninstall() {
|
||||
console.log("uninstall");
|
||||
// This function is automatically called when the plugin is uninstalled.
|
||||
}
|
||||
|
||||
async updateCards(options: ICardData) {
|
||||
options.cards.sort((a: ICard, b: ICard) => {
|
||||
if (a.blockID < b.blockID) {
|
||||
return -1;
|
||||
}
|
||||
if (a.blockID > b.blockID) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom setting pannel provided by svelte
|
||||
*/
|
||||
openDIYSetting(): void {
|
||||
let dialog = new Dialog({
|
||||
title: "SettingPannel",
|
||||
content: `<div id="SettingPanel" style="height: 100%;"></div>`,
|
||||
width: "800px",
|
||||
destroyCallback: (options) => {
|
||||
console.log("destroyCallback", options);
|
||||
//You'd better destroy the component when the dialog is closed
|
||||
pannel.$destroy();
|
||||
}
|
||||
});
|
||||
let pannel = new SettingExample({
|
||||
target: dialog.element.querySelector("#SettingPanel"),
|
||||
});
|
||||
}
|
||||
|
||||
private eventBusPaste(event: any) {
|
||||
// 如果需异步处理请调用 preventDefault, 否则会进行默认处理
|
||||
event.preventDefault();
|
||||
// 如果使用了 preventDefault,必须调用 resolve,否则程序会卡死
|
||||
event.detail.resolve({
|
||||
textPlain: event.detail.textPlain.trim(),
|
||||
});
|
||||
}
|
||||
|
||||
private eventBusLog({ detail }: any) {
|
||||
console.log(detail);
|
||||
}
|
||||
|
||||
private blockIconEvent({ detail }: any) {
|
||||
detail.menu.addItem({
|
||||
iconHTML: "",
|
||||
label: this.i18n.removeSpace,
|
||||
click: () => {
|
||||
const doOperations: IOperation[] = [];
|
||||
detail.blockElements.forEach((item: HTMLElement) => {
|
||||
const editElement = item.querySelector('[contenteditable="true"]');
|
||||
if (editElement) {
|
||||
editElement.textContent = editElement.textContent.replace(/ /g, "");
|
||||
doOperations.push({
|
||||
id: item.dataset.nodeId,
|
||||
data: item.outerHTML,
|
||||
action: "update"
|
||||
});
|
||||
}
|
||||
});
|
||||
detail.protyle.getInstance().transaction(doOperations);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private showDialog() {
|
||||
// let dialog = new Dialog({
|
||||
// title: `SiYuan ${Constants.SIYUAN_VERSION}`,
|
||||
// content: `<div id="helloPanel" class="b3-dialog__content"></div>`,
|
||||
// width: this.isMobile ? "92vw" : "720px",
|
||||
// destroyCallback() {
|
||||
// // hello.$destroy();
|
||||
// },
|
||||
// });
|
||||
// new HelloExample({
|
||||
// target: dialog.element.querySelector("#helloPanel"),
|
||||
// props: {
|
||||
// app: this.app,
|
||||
// }
|
||||
// });
|
||||
svelteDialog({
|
||||
title: `SiYuan ${Constants.SIYUAN_VERSION}`,
|
||||
width: this.isMobile ? "92vw" : "720px",
|
||||
constructor: (container: HTMLElement) => {
|
||||
return new HelloExample({
|
||||
target: container,
|
||||
props: {
|
||||
app: this.app,
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private addMenu(rect?: DOMRect) {
|
||||
const menu = new Menu("topBarSample", () => {
|
||||
console.log(this.i18n.byeMenu);
|
||||
});
|
||||
menu.addItem({
|
||||
icon: "iconInfo",
|
||||
label: "Dialog(open help first)",
|
||||
accelerator: this.commands[0].customHotkey,
|
||||
click: () => {
|
||||
this.showDialog();
|
||||
}
|
||||
});
|
||||
if (!this.isMobile) {
|
||||
menu.addItem({
|
||||
icon: "iconFace",
|
||||
label: "Open Custom Tab",
|
||||
click: () => {
|
||||
const tab = openTab({
|
||||
app: this.app,
|
||||
custom: {
|
||||
icon: "iconFace",
|
||||
title: "Custom Tab",
|
||||
data: {
|
||||
text: "This is my custom tab",
|
||||
},
|
||||
id: this.name + TAB_TYPE
|
||||
},
|
||||
});
|
||||
console.log(tab);
|
||||
}
|
||||
});
|
||||
menu.addItem({
|
||||
icon: "iconImage",
|
||||
label: "Open Asset Tab(open help first)",
|
||||
click: () => {
|
||||
const tab = openTab({
|
||||
app: this.app,
|
||||
asset: {
|
||||
path: "assets/paragraph-20210512165953-ag1nib4.svg"
|
||||
}
|
||||
});
|
||||
console.log(tab);
|
||||
}
|
||||
});
|
||||
menu.addItem({
|
||||
icon: "iconFile",
|
||||
label: "Open Doc Tab(open help first)",
|
||||
click: async () => {
|
||||
const tab = await openTab({
|
||||
app: this.app,
|
||||
doc: {
|
||||
id: "20200812220555-lj3enxa",
|
||||
}
|
||||
});
|
||||
console.log(tab);
|
||||
}
|
||||
});
|
||||
menu.addItem({
|
||||
icon: "iconSearch",
|
||||
label: "Open Search Tab",
|
||||
click: () => {
|
||||
const tab = openTab({
|
||||
app: this.app,
|
||||
search: {
|
||||
k: "SiYuan"
|
||||
}
|
||||
});
|
||||
console.log(tab);
|
||||
}
|
||||
});
|
||||
menu.addItem({
|
||||
icon: "iconRiffCard",
|
||||
label: "Open Card Tab",
|
||||
click: () => {
|
||||
const tab = openTab({
|
||||
app: this.app,
|
||||
card: {
|
||||
type: "all"
|
||||
}
|
||||
});
|
||||
console.log(tab);
|
||||
}
|
||||
});
|
||||
menu.addItem({
|
||||
icon: "iconLayout",
|
||||
label: "Open Float Layer(open help first)",
|
||||
click: () => {
|
||||
this.addFloatLayer({
|
||||
ids: ["20210428212840-8rqwn5o", "20201225220955-l154bn4"],
|
||||
defIds: ["20230415111858-vgohvf3", "20200813131152-0wk5akh"],
|
||||
x: window.innerWidth - 768 - 120,
|
||||
y: 32
|
||||
});
|
||||
}
|
||||
});
|
||||
menu.addItem({
|
||||
icon: "iconOpenWindow",
|
||||
label: "Open Doc Window(open help first)",
|
||||
click: () => {
|
||||
openWindow({
|
||||
doc: {id: "20200812220555-lj3enxa"}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
menu.addItem({
|
||||
icon: "iconFile",
|
||||
label: "Open Doc(open help first)",
|
||||
click: () => {
|
||||
openMobileFileById(this.app, "20200812220555-lj3enxa");
|
||||
}
|
||||
});
|
||||
}
|
||||
menu.addItem({
|
||||
icon: "iconLock",
|
||||
label: "Lockscreen",
|
||||
click: () => {
|
||||
lockScreen(this.app);
|
||||
}
|
||||
});
|
||||
menu.addItem({
|
||||
icon: "iconScrollHoriz",
|
||||
label: "Event Bus",
|
||||
type: "submenu",
|
||||
submenu: [{
|
||||
icon: "iconSelect",
|
||||
label: "On ws-main",
|
||||
click: () => {
|
||||
this.eventBus.on("ws-main", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconClose",
|
||||
label: "Off ws-main",
|
||||
click: () => {
|
||||
this.eventBus.off("ws-main", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconSelect",
|
||||
label: "On click-blockicon",
|
||||
click: () => {
|
||||
this.eventBus.on("click-blockicon", this.blockIconEventBindThis);
|
||||
}
|
||||
}, {
|
||||
icon: "iconClose",
|
||||
label: "Off click-blockicon",
|
||||
click: () => {
|
||||
this.eventBus.off("click-blockicon", this.blockIconEventBindThis);
|
||||
}
|
||||
}, {
|
||||
icon: "iconSelect",
|
||||
label: "On click-pdf",
|
||||
click: () => {
|
||||
this.eventBus.on("click-pdf", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconClose",
|
||||
label: "Off click-pdf",
|
||||
click: () => {
|
||||
this.eventBus.off("click-pdf", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconSelect",
|
||||
label: "On click-editorcontent",
|
||||
click: () => {
|
||||
this.eventBus.on("click-editorcontent", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconClose",
|
||||
label: "Off click-editorcontent",
|
||||
click: () => {
|
||||
this.eventBus.off("click-editorcontent", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconSelect",
|
||||
label: "On click-editortitleicon",
|
||||
click: () => {
|
||||
this.eventBus.on("click-editortitleicon", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconClose",
|
||||
label: "Off click-editortitleicon",
|
||||
click: () => {
|
||||
this.eventBus.off("click-editortitleicon", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconSelect",
|
||||
label: "On click-flashcard-action",
|
||||
click: () => {
|
||||
this.eventBus.on("click-flashcard-action", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconClose",
|
||||
label: "Off click-flashcard-action",
|
||||
click: () => {
|
||||
this.eventBus.off("click-flashcard-action", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconSelect",
|
||||
label: "On open-noneditableblock",
|
||||
click: () => {
|
||||
this.eventBus.on("open-noneditableblock", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconClose",
|
||||
label: "Off open-noneditableblock",
|
||||
click: () => {
|
||||
this.eventBus.off("open-noneditableblock", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconSelect",
|
||||
label: "On loaded-protyle-static",
|
||||
click: () => {
|
||||
this.eventBus.on("loaded-protyle-static", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconClose",
|
||||
label: "Off loaded-protyle-static",
|
||||
click: () => {
|
||||
this.eventBus.off("loaded-protyle-static", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconSelect",
|
||||
label: "On loaded-protyle-dynamic",
|
||||
click: () => {
|
||||
this.eventBus.on("loaded-protyle-dynamic", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconClose",
|
||||
label: "Off loaded-protyle-dynamic",
|
||||
click: () => {
|
||||
this.eventBus.off("loaded-protyle-dynamic", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconSelect",
|
||||
label: "On switch-protyle",
|
||||
click: () => {
|
||||
this.eventBus.on("switch-protyle", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconClose",
|
||||
label: "Off switch-protyle",
|
||||
click: () => {
|
||||
this.eventBus.off("switch-protyle", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconSelect",
|
||||
label: "On destroy-protyle",
|
||||
click: () => {
|
||||
this.eventBus.on("destroy-protyle", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconClose",
|
||||
label: "Off destroy-protyle",
|
||||
click: () => {
|
||||
this.eventBus.off("destroy-protyle", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconSelect",
|
||||
label: "On open-menu-doctree",
|
||||
click: () => {
|
||||
this.eventBus.on("open-menu-doctree", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconClose",
|
||||
label: "Off open-menu-doctree",
|
||||
click: () => {
|
||||
this.eventBus.off("open-menu-doctree", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconSelect",
|
||||
label: "On open-menu-blockref",
|
||||
click: () => {
|
||||
this.eventBus.on("open-menu-blockref", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconClose",
|
||||
label: "Off open-menu-blockref",
|
||||
click: () => {
|
||||
this.eventBus.off("open-menu-blockref", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconSelect",
|
||||
label: "On open-menu-fileannotationref",
|
||||
click: () => {
|
||||
this.eventBus.on("open-menu-fileannotationref", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconClose",
|
||||
label: "Off open-menu-fileannotationref",
|
||||
click: () => {
|
||||
this.eventBus.off("open-menu-fileannotationref", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconSelect",
|
||||
label: "On open-menu-tag",
|
||||
click: () => {
|
||||
this.eventBus.on("open-menu-tag", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconClose",
|
||||
label: "Off open-menu-tag",
|
||||
click: () => {
|
||||
this.eventBus.off("open-menu-tag", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconSelect",
|
||||
label: "On open-menu-link",
|
||||
click: () => {
|
||||
this.eventBus.on("open-menu-link", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconClose",
|
||||
label: "Off open-menu-link",
|
||||
click: () => {
|
||||
this.eventBus.off("open-menu-link", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconSelect",
|
||||
label: "On open-menu-image",
|
||||
click: () => {
|
||||
this.eventBus.on("open-menu-image", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconClose",
|
||||
label: "Off open-menu-image",
|
||||
click: () => {
|
||||
this.eventBus.off("open-menu-image", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconSelect",
|
||||
label: "On open-menu-av",
|
||||
click: () => {
|
||||
this.eventBus.on("open-menu-av", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconClose",
|
||||
label: "Off open-menu-av",
|
||||
click: () => {
|
||||
this.eventBus.off("open-menu-av", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconSelect",
|
||||
label: "On open-menu-content",
|
||||
click: () => {
|
||||
this.eventBus.on("open-menu-content", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconClose",
|
||||
label: "Off open-menu-content",
|
||||
click: () => {
|
||||
this.eventBus.off("open-menu-content", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconSelect",
|
||||
label: "On open-menu-breadcrumbmore",
|
||||
click: () => {
|
||||
this.eventBus.on("open-menu-breadcrumbmore", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconClose",
|
||||
label: "Off open-menu-breadcrumbmore",
|
||||
click: () => {
|
||||
this.eventBus.off("open-menu-breadcrumbmore", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconSelect",
|
||||
label: "On open-menu-inbox",
|
||||
click: () => {
|
||||
this.eventBus.on("open-menu-inbox", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconClose",
|
||||
label: "Off open-menu-inbox",
|
||||
click: () => {
|
||||
this.eventBus.off("open-menu-inbox", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconSelect",
|
||||
label: "On input-search",
|
||||
click: () => {
|
||||
this.eventBus.on("input-search", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconClose",
|
||||
label: "Off input-search",
|
||||
click: () => {
|
||||
this.eventBus.off("input-search", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconSelect",
|
||||
label: "On paste",
|
||||
click: () => {
|
||||
this.eventBus.on("paste", this.eventBusPaste);
|
||||
}
|
||||
}, {
|
||||
icon: "iconClose",
|
||||
label: "Off paste",
|
||||
click: () => {
|
||||
this.eventBus.off("paste", this.eventBusPaste);
|
||||
}
|
||||
}, {
|
||||
icon: "iconSelect",
|
||||
label: "On open-siyuan-url-plugin",
|
||||
click: () => {
|
||||
this.eventBus.on("open-siyuan-url-plugin", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconClose",
|
||||
label: "Off open-siyuan-url-plugin",
|
||||
click: () => {
|
||||
this.eventBus.off("open-siyuan-url-plugin", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconSelect",
|
||||
label: "On open-siyuan-url-block",
|
||||
click: () => {
|
||||
this.eventBus.on("open-siyuan-url-block", this.eventBusLog);
|
||||
}
|
||||
}, {
|
||||
icon: "iconClose",
|
||||
label: "Off open-siyuan-url-block",
|
||||
click: () => {
|
||||
this.eventBus.off("open-siyuan-url-block", this.eventBusLog);
|
||||
}
|
||||
}]
|
||||
});
|
||||
menu.addSeparator();
|
||||
menu.addItem({
|
||||
icon: "iconSettings",
|
||||
label: "Official Setting Dialog",
|
||||
click: () => {
|
||||
this.openSetting();
|
||||
}
|
||||
});
|
||||
menu.addItem({
|
||||
icon: "iconSettings",
|
||||
label: "A custom setting dialog (by svelte)",
|
||||
click: () => {
|
||||
this.openDIYSetting();
|
||||
}
|
||||
});
|
||||
menu.addItem({
|
||||
icon: "iconSparkles",
|
||||
label: this.data[STORAGE_NAME].readonlyText || "Readonly",
|
||||
type: "readonly",
|
||||
});
|
||||
if (this.isMobile) {
|
||||
menu.fullscreen();
|
||||
} else {
|
||||
menu.open({
|
||||
x: rect.right,
|
||||
y: rect.bottom,
|
||||
isLeft: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,139 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { showMessage } from "siyuan";
|
||||
import SettingPanel from "./libs/components/setting-panel.svelte";
|
||||
|
||||
let groups: string[] = ["🌈 Group 1", "✨ Group 2"];
|
||||
let focusGroup = groups[0];
|
||||
|
||||
const group1Items: ISettingItem[] = [
|
||||
{
|
||||
type: 'checkbox',
|
||||
title: 'checkbox',
|
||||
description: 'checkbox',
|
||||
key: 'a',
|
||||
value: true
|
||||
},
|
||||
{
|
||||
type: 'textinput',
|
||||
title: 'text',
|
||||
description: 'This is a text',
|
||||
key: 'b',
|
||||
value: 'This is a text',
|
||||
placeholder: 'placeholder'
|
||||
},
|
||||
{
|
||||
type: 'textarea',
|
||||
title: 'textarea',
|
||||
description: 'This is a textarea',
|
||||
key: 'b2',
|
||||
value: 'This is a textarea',
|
||||
placeholder: 'placeholder',
|
||||
direction: 'row'
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
title: 'select',
|
||||
description: 'select',
|
||||
key: 'c',
|
||||
value: 'x',
|
||||
options: {
|
||||
x: 'x',
|
||||
y: 'y',
|
||||
z: 'z'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const group2Items: ISettingItem[] = [
|
||||
{
|
||||
type: 'button',
|
||||
title: 'button',
|
||||
description: 'This is a button',
|
||||
key: 'e',
|
||||
value: 'Click Button',
|
||||
button: {
|
||||
label: 'Click Me',
|
||||
callback: () => {
|
||||
showMessage('Hello, world!');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'slider',
|
||||
title: 'slider',
|
||||
description: 'slider',
|
||||
key: 'd',
|
||||
value: 50,
|
||||
slider: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
/********** Events **********/
|
||||
interface ChangeEvent {
|
||||
group: string;
|
||||
key: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
const onChanged = ({ detail }: CustomEvent<ChangeEvent>) => {
|
||||
if (detail.group === groups[0]) {
|
||||
// setting.set(detail.key, detail.value);
|
||||
//Please add your code here
|
||||
//Udpate the plugins setting data, don't forget to call plugin.save() for data persistence
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="fn__flex-1 fn__flex config__panel">
|
||||
<ul class="b3-tab-bar b3-list b3-list--background">
|
||||
{#each groups as group}
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<li
|
||||
data-name="editor"
|
||||
class:b3-list-item--focus={group === focusGroup}
|
||||
class="b3-list-item"
|
||||
on:click={() => {
|
||||
focusGroup = group;
|
||||
}}
|
||||
on:keydown={() => {}}
|
||||
>
|
||||
<span class="b3-list-item__text">{group}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<div class="config__tab-wrap">
|
||||
<SettingPanel
|
||||
group={groups[0]}
|
||||
settingItems={group1Items}
|
||||
display={focusGroup === groups[0]}
|
||||
on:changed={onChanged}
|
||||
on:click={({ detail }) => { console.debug("Click:", detail.key); }}
|
||||
>
|
||||
<div class="fn__flex b3-label">
|
||||
💡 This is our default settings.
|
||||
</div>
|
||||
</SettingPanel>
|
||||
<SettingPanel
|
||||
group={groups[1]}
|
||||
settingItems={group2Items}
|
||||
display={focusGroup === groups[1]}
|
||||
on:changed={onChanged}
|
||||
on:click={({ detail }) => { console.debug("Click:", detail.key); }}
|
||||
>
|
||||
</SettingPanel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.config__panel {
|
||||
height: 100%;
|
||||
}
|
||||
.config__panel > ul > li {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue