diff --git a/README.md b/README.md index 648dcb7..73c10ff 100644 --- a/README.md +++ b/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 Use this template 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 `/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: - - ![](asset/action.png) - -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. \ No newline at end of file diff --git a/README_zh_CN.md b/README_zh_CN.md deleted file mode 100644 index 0ac9fca..0000000 --- a/README_zh_CN.md +++ /dev/null @@ -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. 通过 Use this template 按钮将该库文件复制到你自己的库中,请注意库名和插件名称一致,默认分支必须为 `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` 为思源的插件目录 `/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**,打开配置 - - ![](asset/action.png) - -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 创建日记),请手动为文档添加该属性 diff --git a/icon.png b/icon.png index 47d51af..15dd634 100644 Binary files a/icon.png and b/icon.png differ diff --git a/package.json b/package.json index 2b7cdec..78f409d 100644 --- a/package.json +++ b/package.json @@ -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" } -} \ No newline at end of file +} diff --git a/plugin.json b/plugin.json index fd63a3c..8413186 100644 --- a/plugin.json +++ b/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" ] } diff --git a/preview.png b/preview.png index 95b93c7..fb8c7e5 100644 Binary files a/preview.png and b/preview.png differ diff --git a/public/i18n/en_US.json b/public/i18n/en_US.json deleted file mode 100644 index 7a11256..0000000 --- a/public/i18n/en_US.json +++ /dev/null @@ -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 offical sample, make sure that you've known about the pipeline for plugin developing." - }, - "hintTitle":"About", - "hintDesc":"plugin-sample-vite-svelte
@frostime
@88250
@zxkmm" -} \ No newline at end of file diff --git a/public/i18n/zh_CN.json b/public/i18n/zh_CN.json deleted file mode 100644 index 6600f6a..0000000 --- a/public/i18n/zh_CN.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "addTopBarIcon": "使用插件添加一个顶栏按钮", - "cancel": "取消", - "save": "保存", - "byeMenu": "再见,菜单!", - "helloPlugin": "你好,插件!", - "byePlugin": "再见,插件!", - "showDialog": "弹出一个对话框", - "removedData": "数据已删除", - "confirmRemove": "确认删除 ${name} 中的数据?", - "insertEmoji": "插入表情", - "removeSpace": "移除空格", - "getTab": "在日志中打印出已打开的所有自定义页签", - "name": "思源", - "hello": { - "makesure": "使用这个模板之前,请阅读官方教程, 确保自己已经理解了插件的基本开发流程。" - }, - "hintTitle": "关于", - "hintDesc": "🔗 plugin-sample-vite-svelte
💻 @frostime
💻 @88250
💻 @zxkmm" -} \ No newline at end of file diff --git a/public/webapp/button.js b/public/webapp/button.js new file mode 100644 index 0000000..610799d --- /dev/null +++ b/public/webapp/button.js @@ -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 = ` + + + + `; + 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); + } + +} \ No newline at end of file diff --git a/public/webapp/draw.js b/public/webapp/draw.js new file mode 100644 index 0000000..9d9adfc --- /dev/null +++ b/public/webapp/draw.js @@ -0,0 +1,47 @@ +const FALLBACK = "

Nothing here yet! Click me to open the editor.

" +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}`; +} \ No newline at end of file diff --git a/public/webapp/index.css b/public/webapp/index.css new file mode 100644 index 0000000..e1dc21d --- /dev/null +++ b/public/webapp/index.css @@ -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; + } +} \ No newline at end of file diff --git a/public/webapp/index.html b/public/webapp/index.html new file mode 100644 index 0000000..a1fd2f6 --- /dev/null +++ b/public/webapp/index.html @@ -0,0 +1,26 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/const.ts b/src/const.ts new file mode 100644 index 0000000..1acaa80 --- /dev/null +++ b/src/const.ts @@ -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="; \ No newline at end of file diff --git a/src/editorTab.ts b/src/editorTab.ts new file mode 100644 index 0000000..bef75c7 --- /dev/null +++ b/src/editorTab.ts @@ -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%'; + +} \ No newline at end of file diff --git a/src/file.ts b/src/file.ts new file mode 100644 index 0000000..5ce0d20 --- /dev/null +++ b/src/file.ts @@ -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; + +} diff --git a/src/hello.svelte b/src/hello.svelte deleted file mode 100644 index 967c7f6..0000000 --- a/src/hello.svelte +++ /dev/null @@ -1,63 +0,0 @@ - - - -
-
appId:
-
-
${app?.appId}
-
-
-
API demo:
-
-
- System current time: {time} -
-
-
-
Protyle demo: id = {blockID}
-
-
-
- diff --git a/src/helper.ts b/src/helper.ts new file mode 100644 index 0000000..18386c7 --- /dev/null +++ b/src/helper.ts @@ -0,0 +1,57 @@ +import { Plugin } from 'siyuan'; +import {DATA_PATH, EMBED_PATH} from "@/const"; + +const drawIcon: string = ` + + + +`; + +export function loadIcons(p: Plugin) { + const icons = drawIcon; + p.addIcons(icons); +} + +export function getMenuHTML(icon: string, text: string): string { + return ` +
+ + + + ${text} +
+ `; +} + +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}"}) +// ![Drawing](assets/${id}.svg) +export function getPreviewHTML(id: string): string { + return ` + + ` +} \ No newline at end of file diff --git a/src/index.scss b/src/index.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/index.ts b/src/index.ts index 5b50c49..e19ecb2 100644 --- a/src/index.ts +++ b/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(` - - - - -`); - - 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 = `
- - - -
`; - 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 = `
- -
Custom Dock
-
-
- ${dock.data.text} -
-
`; - } else { - dock.element.innerHTML = `
-
- - - -
-
- ${dock.data.text} -
-
`; - } - }, - 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: `
${this.i18n.insertEmoji}😊
`, - 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: `
`, - 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: `
`, - // 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, - }); - } - } -} +} \ No newline at end of file diff --git a/src/setting-example.svelte b/src/setting-example.svelte deleted file mode 100644 index 2a2c809..0000000 --- a/src/setting-example.svelte +++ /dev/null @@ -1,139 +0,0 @@ - - -
-
    - {#each groups as group} - -
  • { - focusGroup = group; - }} - on:keydown={() => {}} - > - {group} -
  • - {/each} -
-
- { console.debug("Click:", detail.key); }} - > -
- 💡 This is our default settings. -
-
- { console.debug("Click:", detail.key); }} - > - -
-
- - -