diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index f06235c..0000000 --- a/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -dist diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index 0781605..0000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,38 +0,0 @@ -module.exports = { - extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:svelte/recommended", - "turbo", - "prettier", - ], - - parser: "@typescript-eslint/parser", - - overrides: [ - { - files: ["*.svelte"], - parser: "svelte-eslint-parser", - // Parse the script in `.svelte` as TypeScript by adding the following configuration. - parserOptions: { - parser: "@typescript-eslint/parser", - }, - }, - ], - - plugins: ["@typescript-eslint", "prettier"], - - rules: { - // Note: you must disable the base rule as it can report incorrect errors - semi: "off", - quotes: "off", - "no-undef": "off", - "@typescript-eslint/no-var-requires": "off", - "@typescript-eslint/no-this-alias": "off", - "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/no-unused-vars": "off", - "@typescript-eslint/no-explicit-any": "off", - "turbo/no-undeclared-env-vars": "off", - "prettier/prettier": "error", - }, -} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7bd6c4e..49834e5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,12 +17,12 @@ jobs: - name: Install Node.js uses: actions/setup-node@v3 with: - node-version: 18 + node-version: 20 registry-url: "https://registry.npmjs.org" # Install pnpm - name: Install pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v4 id: pnpm-install with: version: 8 @@ -59,4 +59,4 @@ jobs: artifactErrorsFailBuild: true artifacts: "package.zip" token: ${{ secrets.GITHUB_TOKEN }} - prerelease: true + prerelease: false diff --git a/.gitignore b/.gitignore index 764e2d4..c2a2064 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .vscode .DS_Store pnpm-lock.yaml +package-lock.json package.zip node_modules dev diff --git a/CHANGELOG.md b/CHANGELOG.md index 4411dd8..af04807 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,42 @@ # Changelog +## v0.3.5 2024-04-30 + +* [Add `direction` to plugin method `Setting.addItem`](https://github.com/siyuan-note/siyuan/issues/11183) + + +## 0.3.4 2024-02-20 + +* [Add plugin event bus `click-flashcard-action`](https://github.com/siyuan-note/siyuan/issues/10318) + +## 0.3.3 2024-01-24 + +* Update dock icon class + +## 0.3.2 2024-01-09 + +* [Add plugin `protyleOptions`](https://github.com/siyuan-note/siyuan/issues/10090) +* [Add plugin api `uninstall`](https://github.com/siyuan-note/siyuan/issues/10063) +* [Add plugin method `updateCards`](https://github.com/siyuan-note/siyuan/issues/10065) +* [Add plugin function `lockScreen`](https://github.com/siyuan-note/siyuan/issues/10063) +* [Add plugin event bus `lock-screen`](https://github.com/siyuan-note/siyuan/pull/9967) +* [Add plugin event bus `open-menu-inbox`](https://github.com/siyuan-note/siyuan/pull/9967) + + +## 0.3.1 2023-12-06 + +* [Support `Dock Plugin` and `Command Palette` on mobile](https://github.com/siyuan-note/siyuan/issues/9926) + +## 0.3.0 2023-12-05 + +* Upgrade Siyuan to 0.9.0 +* Support more platforms + +## 0.2.9 2023-11-28 + +* [Add plugin method `openMobileFileById`](https://github.com/siyuan-note/siyuan/issues/9738) + + ## 0.2.8 2023-11-15 * [`resize` cannot be triggered after dragging to unpin the dock](https://github.com/siyuan-note/siyuan/issues/9640) diff --git a/README.md b/README.md index 4f79c10..648dcb7 100644 --- a/README.md +++ b/README.md @@ -3,27 +3,46 @@ [中文版](./README_zh_CN.md) -> Consistent with [siyuan/plugin-sample](https://github.com/siyuan-note/plugin-sample) [v0.2.8](https://github.com/siyuan-note/plugin-sample/tree/v0.2.8) +> 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) 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 + + > **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. Make a copy of this repo as a template with the `Use this template` button, please note that the repo name must be the same as the plugin name, the default branch must be `main` +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. -2. Clone your repo to a local development folder at any place - - Notice: we **don't recommand** you to place the folder under your `{workspace}/data/plugins/` folder. +### Setting the Target Directory for the make-link Command -3. Install NodeJS and pnpm, then run pnpm i in the command line under your repo folder -4. **Auto create development symbolic links** - - Make sure that SiYuan is running - - Run `pnpm run make-link`, the script will detect all the siyuan workspace, please select the targe workspace and the script will automatically create the symbolic link under the `{workspace}/data/plugins/` folder +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 @@ -37,22 +56,24 @@ Got target directory: H:\Media\SiYuan/data/plugins Done! Created symlink H:\Media\SiYuan/data/plugins/plugin-sample-vite-svelte ``` -4. **Manually create development symbolic links** - - Open `./scripts/make_dev_link.js` file, set `targetDir` to your SiYuan plugin directory `/data/plugins` - - Run `pnpm run make-link`, succeed if following message is shown: - ```bash - >>> pnpm run make-link - > plugin-sample-vite-svelte@0.0.1 make-link H:\SrcCode\plugin-sample-vite-svelte - > node ./scripts/make_dev_link.js +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: - Done! Created symlink H:/SiYuanDevSpace/data/plugins/plugin-sample-vite-svelte - ``` -5. **Create development symbolic links by using environment variable** - - You can set environment variable `SIYUAN_PLUGIN_DIR` as `/data/plugins` -6. Execute pnpm run dev for real-time compilation -7. Open SiYuan marketplace and enable plugin in downloaded tab +3. **Set Environment Variable to Create Symbolic Link** + - Set the system environment variable `SIYUAN_PLUGIN_DIR` to the path `workspace/data/plugins`. -> Notice: as the `make-link` script rely on the `fetch` function, please **ensure that at least version v18 of nodejs is installed** if you want to use make-link script. +### 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 @@ -62,8 +83,11 @@ 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 - * src/i18n/*.json language configuration files + * 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. @@ -230,3 +254,25 @@ If you insist on removing all svelte dependencies so that they do not pollute yo - 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 + diff --git a/README_zh_CN.md b/README_zh_CN.md index 5b3e6e8..0ac9fca 100644 --- a/README_zh_CN.md +++ b/README_zh_CN.md @@ -4,20 +4,40 @@ [English](./README.md) -> 本例同 [siyuan/plugin-sample](https://github.com/siyuan-note/plugin-sample) [v0.2.8](https://github.com/siyuan-note/plugin-sample/tree/v0.2.8) +> 本例同 [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` +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` 安装所需要的依赖 -3. **自动创建符号链接** +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 @@ -33,23 +53,26 @@ Got target directory: H:\Media\SiYuan/data/plugins Done! Created symlink H:\Media\SiYuan/data/plugins/plugin-sample-vite-svelte ``` -4. **手动创建符号链接** +2. **手动配置目标目录** - 打开 `./scripts/make_dev_link.js` 文件,更改 `targetDir` 为思源的插件目录 `/data/plugins` - 运行 `pnpm run make-link` 命令, 如果看到类似以下的消息,说明创建成功: - ```bash - ❯❯❯ pnpm run make-link - > plugin-sample-vite-svelte@0.0.1 make-link H:\SrcCode\plugin-sample-vite-svelte - > node ./scripts/make_dev_link.js - Done! Created symlink H:/SiYuanDevSpace/data/plugins/plugin-sample-vite-svelte - ``` -5. **设置环境变量创建符号链接** - - 你也可以设置系统的环境变量 `SIYUAN_PLUGIN_DIR` 为 `/data/plugins` 的路径 -6. 执行 `pnpm run dev` 进行实时编译 -7. 在思源中打开集市并在下载选项卡中启用插件 +3. **设置环境变量创建符号链接** + - 设置系统的环境变量 `SIYUAN_PLUGIN_DIR` 为 `工作空间/data/plugins` 的路径 -> 注意由于使用的 make-link 脚本依赖于 `fetch`,所以如果想要使用 make-link **请保证至少安装 v18 版本的 nodejs** +### 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 脚本执行权限 ## 国际化 @@ -59,9 +82,12 @@ * 插件自身的元信息,比如插件描述和自述文件 * plugin.json 中的 `description` 和 `readme` 字段,以及对应的 README*.md 文件 * 插件中使用的文本,比如按钮文字和提示信息 - * src/i18n/*.json 语言配置文件 + * public/i18n/*.json 语言配置文件 * 代码中使用 `this.i18.key` 获取文本 * 最后在 plugin.json 中的 `i18n` 字段中声明该插件支持的语言 +* yaml 支持 + * 本模板特别支持基于 Yaml 语法的 I18n,见 `public/i18n/zh_CN.yaml` + * 编译时,会自动把定义的 yaml 文件翻译成 json 文件放到 dist 或 dev 目录下 建议插件至少支持英文和简体中文,这样可以方便更多人使用。 @@ -221,3 +247,25 @@ PR 社区集市仓库。 - 第二十行: `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/package.json b/package.json index e4c06a2..2b7cdec 100644 --- a/package.json +++ b/package.json @@ -1,33 +1,37 @@ { "name": "plugin-sample-vite-svelte", - "version": "0.2.6", + "version": "0.3.6", "type": "module", "description": "This is a sample plugin based on vite and svelte for Siyuan (https://b3log.org/siyuan)", "repository": "", "homepage": "", - "author": "", + "author": "frostime", "license": "MIT", "scripts": { - "make-link": "node --no-warnings ./scripts/make_dev_link.js", - "dev": "vite build --watch", - "build": "vite build" + "dev": "cross-env NODE_ENV=development VITE_SOURCEMAP=inline vite build --watch", + "build": "cross-env NODE_ENV=production vite build", + "make-link": "node --no-warnings ./scripts/make_dev_link.js", + "make-link-win": "powershell.exe -NoProfile -ExecutionPolicy Bypass -File ./scripts/elevate.ps1 -scriptPath ./scripts/make_dev_link.js", + "update-version": "node --no-warnings ./scripts/update_version.js", + "make-install": "vite build && node --no-warnings ./scripts/make_install.js" }, "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^2.0.3", + "@sveltejs/vite-plugin-svelte": "^3.1.0", "@tsconfig/svelte": "^4.0.1", - "@types/node": "^20.2.0", - "eslint": "^8.42.0", + "@types/node": "^20.3.0", + "cross-env": "^7.0.3", "fast-glob": "^3.2.12", - "glob": "^7.2.3", + "glob": "^10.0.0", + "js-yaml": "^4.1.0", "minimist": "^1.2.8", "rollup-plugin-livereload": "^2.0.5", - "sass": "^1.62.1", - "siyuan": "0.8.8", - "svelte": "^3.57.0", + "sass": "^1.63.3", + "siyuan": "1.0.4", + "svelte": "^4.2.19", "ts-node": "^10.9.1", - "typescript": "^5.0.4", - "vite": "^4.3.7", - "vite-plugin-static-copy": "^0.15.0", + "typescript": "^5.1.3", + "vite": "^5.2.9", + "vite-plugin-static-copy": "^1.0.2", "vite-plugin-zip-pack": "^1.0.5" } -} +} \ No newline at end of file diff --git a/plugin.json b/plugin.json index 7bb3bea..fd63a3c 100644 --- a/plugin.json +++ b/plugin.json @@ -2,15 +2,22 @@ "name": "plugin-sample-vite-svelte", "author": "frostime", "url": "https://github.com/siyuan-note/plugin-sample-vite-svelte", - "version": "0.2.8", - "minAppVersion": "2.10.14", + "version": "0.3.6", + "minAppVersion": "3.0.12", "backends": [ "windows", "linux", - "darwin" + "darwin", + "ios", + "android", + "harmony", + "docker" ], "frontends": [ "desktop", + "mobile", + "browser-desktop", + "browser-mobile", "desktop-window" ], "displayName": { @@ -27,10 +34,12 @@ }, "funding": { "custom": [ - "https://afdian.net/a/frostime" + "" ] }, "keywords": [ - "plugin", "sample", "插件样例" + "plugin", + "sample", + "插件样例" ] } diff --git a/public/i18n/README.md b/public/i18n/README.md new file mode 100644 index 0000000..af8de98 --- /dev/null +++ b/public/i18n/README.md @@ -0,0 +1,12 @@ +思源支持的 i18n 文件范围,可以在控制台 `siyuan.config.langs` 中查看。以下是目前(2024-10-24)支持的语言方案: + +The range of i18n files supported by SiYuan can be viewed in the console under `siyuan.config.langs`. Below are the language schemes currently supported as of now (October 24, 2024) : + +```js +>>> siyuan.config.langs.map( lang => lang.name) +['de_DE', 'en_US', 'es_ES', 'fr_FR', 'he_IL', 'it_IT', 'ja_JP', 'pl_PL', 'ru_RU', 'zh_CHT', 'zh_CN'] +``` + +在插件开发中,默认使用 JSON 格式作为国际化(i18n)的载体文件。如果您更喜欢使用 YAML 语法,可以将 JSON 文件替换为 YAML 文件(例如 `en_US.yaml`),并在其中编写 i18n 文本。本模板提供了相关的 Vite 插件,可以在编译时自动将 YAML 文件转换为 JSON 文件(请参见 `/yaml-plugin.js`)。本 MD 文件 和 YAML 文件会在 `npm run build` 时自动从 `dist` 目录下删除,仅保留必要的 JSON 文件共插件系统使用。 + +In plugin development, JSON format is used by default as the carrier file for internationalization (i18n). If you prefer to use YAML syntax, you can replace the JSON file with a YAML file (e.g., `en_US.yaml`) and write the i18n text within it. This template provides a related Vite plugin that can automatically convert YAML files to JSON files during the compilation process (see `/yaml-plugin.js`). This markdown file and YAML files will be automatically removed from the `dist` directory during `npm run build`, leaving only the necessary JSON files for plugin system to use. diff --git a/src/i18n/en_US.json b/public/i18n/en_US.json similarity index 80% rename from src/i18n/en_US.json rename to public/i18n/en_US.json index c7ed102..7a11256 100644 --- a/src/i18n/en_US.json +++ b/public/i18n/en_US.json @@ -14,5 +14,7 @@ "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/src/i18n/zh_CN.json b/public/i18n/zh_CN.json similarity index 78% rename from src/i18n/zh_CN.json rename to public/i18n/zh_CN.json index 49a7af8..6600f6a 100644 --- a/src/i18n/zh_CN.json +++ b/public/i18n/zh_CN.json @@ -14,5 +14,7 @@ "name": "思源", "hello": { "makesure": "使用这个模板之前,请阅读官方教程, 确保自己已经理解了插件的基本开发流程。" - } + }, + "hintTitle": "关于", + "hintDesc": "🔗 plugin-sample-vite-svelte💻 @frostime💻 @88250💻 @zxkmm" } \ No newline at end of file diff --git a/scripts/elevate.ps1 b/scripts/elevate.ps1 new file mode 100644 index 0000000..151b8ba --- /dev/null +++ b/scripts/elevate.ps1 @@ -0,0 +1,24 @@ +# Copyright (c) 2024 by frostime. All Rights Reserved. +# @Author : frostime +# @Date : 2024-09-06 19:15:53 +# @FilePath : /scripts/elevate.ps1 +# @LastEditTime : 2024-09-06 19:39:13 +# @Description : Force to elevate the script to admin privilege. + +param ( + [string]$scriptPath +) + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$projectDir = Split-Path -Parent $scriptDir + +if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { + $args = "-NoProfile -ExecutionPolicy Bypass -File `"" + $MyInvocation.MyCommand.Path + "`" -scriptPath `"" + $scriptPath + "`"" + Start-Process powershell.exe -Verb RunAs -ArgumentList $args -WorkingDirectory $projectDir + exit +} + +Set-Location -Path $projectDir +& node $scriptPath + +pause diff --git a/scripts/make_dev_link.js b/scripts/make_dev_link.js index 845d298..2be3d2b 100644 --- a/scripts/make_dev_link.js +++ b/scripts/make_dev_link.js @@ -1,106 +1,29 @@ +/* + * Copyright (c) 2024 by frostime. All Rights Reserved. + * @Author : frostime + * @Date : 2023-07-15 15:31:31 + * @FilePath : /scripts/make_dev_link.js + * @LastEditTime : 2024-09-06 18:13:53 + * @Description : + */ +// make_dev_link.js import fs from 'fs'; -import http from 'node:http'; -import readline from 'node:readline'; +import { log, error, getSiYuanDir, chooseTarget, getThisPluginName, makeSymbolicLink } from './utils.js'; - -//************************************ Write you dir here ************************************ - -//Please write the "workspace/data/plugins" directory here -//请在这里填写你的 "workspace/data/plugins" 目录 let targetDir = ''; -//Like this -// let targetDir = `H:\\SiYuanDevSpace\\data\\plugins`; -//******************************************************************************************** - -const log = (info) => console.log(`\x1B[36m%s\x1B[0m`, info); -const error = (info) => console.log(`\x1B[31m%s\x1B[0m`, info); - -let POST_HEADER = { - // "Authorization": `Token ${token}`, - "Content-Type": "application/json", -} - -async function myfetch(url, options) { - //使用 http 模块,从而兼容那些不支持 fetch 的 nodejs 版本 - return new Promise((resolve, reject) => { - let req = http.request(url, options, (res) => { - let data = ''; - res.on('data', (chunk) => { - data += chunk; - }); - res.on('end', () => { - resolve({ - ok: true, - status: res.statusCode, - json: () => JSON.parse(data) - }); - }); - }); - req.on('error', (e) => { - reject(e); - }); - req.end(); - }); -} - -async function getSiYuanDir() { - let url = 'http://127.0.0.1:6806/api/system/getWorkspaces'; - let conf = {}; - try { - let response = await myfetch(url, { - method: 'POST', - headers: POST_HEADER - }); - if (response.ok) { - conf = await response.json(); - } else { - error(`\tHTTP-Error: ${response.status}`); - return null; - } - } catch (e) { - error(`\tError: ${e}`); - error("\tPlease make sure SiYuan is running!!!"); - return null; - } - return conf.data; -} - -async function chooseTarget(workspaces) { - let count = workspaces.length; - log(`>>> Got ${count} SiYuan ${count > 1 ? 'workspaces' : 'workspace'}`) - for (let i = 0; i < workspaces.length; i++) { - log(`\t[${i}] ${workspaces[i].path}`); - } - - if (count == 1) { - return `${workspaces[0].path}/data/plugins`; - } else { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - let index = await new Promise((resolve, reject) => { - rl.question(`\tPlease select a workspace[0-${count-1}]: `, (answer) => { - resolve(answer); - }); - }); - rl.close(); - return `${workspaces[index].path}/data/plugins`; - } -} - -log('>>> Try to visit constant "targetDir" in make_dev_link.js...') +/** + * 1. Get the parent directory to install the plugin + */ +log('>>> Try to visit constant "targetDir" in make_dev_link.js...'); if (targetDir === '') { - log('>>> Constant "targetDir" is empty, try to get SiYuan directory automatically....') + log('>>> Constant "targetDir" is empty, try to get SiYuan directory automatically....'); let res = await getSiYuanDir(); - - if (res === null || res === undefined || res.length === 0) { - log('>>> Can not get SiYuan directory automatically, try to visit environment variable "SIYUAN_PLUGIN_DIR"....'); - // console.log(process.env) + if (!res || res.length === 0) { + log('>>> Can not get SiYuan directory automatically, try to visit environment variable "SIYUAN_PLUGIN_DIR"....'); let env = process.env?.SIYUAN_PLUGIN_DIR; - if (env !== undefined && env !== null && env !== '') { + if (env) { targetDir = env; log(`\tGot target directory from environment variable "SIYUAN_PLUGIN_DIR": ${targetDir}`); } else { @@ -111,76 +34,33 @@ if (targetDir === '') { targetDir = await chooseTarget(res); } - log(`>>> Successfully got target directory: ${targetDir}`); } - -//Check if (!fs.existsSync(targetDir)) { - error(`Failed! plugin directory not exists: "${targetDir}"`); - error(`Please set the plugin directory in scripts/make_dev_link.js`); + error(`Failed! Plugin directory not exists: "${targetDir}"`); + error('Please set the plugin directory in scripts/make_dev_link.js'); process.exit(1); } - -//check if plugin.json exists -if (!fs.existsSync('./plugin.json')) { - //change dir to parent - process.chdir('../'); - if (!fs.existsSync('./plugin.json')) { - error('Failed! plugin.json not found'); - process.exit(1); - } -} - -//load plugin.json -const plugin = JSON.parse(fs.readFileSync('./plugin.json', 'utf8')); -const name = plugin?.name; -if (!name || name === '') { - error('Failed! Please set plugin name in plugin.json'); - process.exit(1); -} - -//dev directory +/** + * 2. The dev directory, which contains the compiled plugin code + */ const devDir = `${process.cwd()}/dev`; -//mkdir if not exists if (!fs.existsSync(devDir)) { fs.mkdirSync(devDir); } -function cmpPath(path1, path2) { - path1 = path1.replace(/\\/g, '/'); - path2 = path2.replace(/\\/g, '/'); - // sepertor at tail - if (path1[path1.length - 1] !== '/') { - path1 += '/'; - } - if (path2[path2.length - 1] !== '/') { - path2 += '/'; - } - return path1 === path2; -} +/** + * 3. The target directory to make symbolic link to dev directory + */ +const name = getThisPluginName(); +if (name === null) { + process.exit(1); +} const targetPath = `${targetDir}/${name}`; -//如果已经存在,就退出 -if (fs.existsSync(targetPath)) { - let isSymbol = fs.lstatSync(targetPath).isSymbolicLink(); - - if (isSymbol) { - let srcPath = fs.readlinkSync(targetPath); - - if (cmpPath(srcPath, devDir)) { - log(`Good! ${targetPath} is already linked to ${devDir}`); - } else { - error(`Error! Already exists symbolic link ${targetPath}\nBut it links to ${srcPath}`); - } - } else { - error(`Failed! ${targetPath} already exists and is not a symbolic link`); - } - -} else { - //创建软链接 - fs.symlinkSync(devDir, targetPath, 'junction'); - log(`Done! Created symlink ${targetPath}`); -} +/** + * 4. Make symbolic link + */ +makeSymbolicLink(devDir, targetPath); diff --git a/scripts/make_install.js b/scripts/make_install.js new file mode 100644 index 0000000..cb2a4ac --- /dev/null +++ b/scripts/make_install.js @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 by frostime. All Rights Reserved. + * @Author : frostime + * @Date : 2024-03-28 20:03:59 + * @FilePath : /scripts/make_install.js + * @LastEditTime : 2024-09-06 18:08:19 + * @Description : + */ +// make_install.js +import fs from 'fs'; +import { log, error, getSiYuanDir, chooseTarget, copyDirectory, getThisPluginName } from './utils.js'; + +let targetDir = ''; + +/** + * 1. Get the parent directory to install the plugin + */ +log('>>> Try to visit constant "targetDir" in make_install.js...'); +if (targetDir === '') { + log('>>> Constant "targetDir" is empty, try to get SiYuan directory automatically....'); + let res = await getSiYuanDir(); + + if (res === null || res === undefined || res.length === 0) { + error('>>> Can not get SiYuan directory automatically'); + process.exit(1); + } else { + targetDir = await chooseTarget(res); + } + log(`>>> Successfully got target directory: ${targetDir}`); +} +if (!fs.existsSync(targetDir)) { + error(`Failed! Plugin directory not exists: "${targetDir}"`); + error('Please set the plugin directory in scripts/make_install.js'); + process.exit(1); +} + +/** + * 2. The dist directory, which contains the compiled plugin code + */ +const distDir = `${process.cwd()}/dist`; +if (!fs.existsSync(distDir)) { + fs.mkdirSync(distDir); +} + +/** + * 3. The target directory to install the plugin + */ +const name = getThisPluginName(); +if (name === null) { + process.exit(1); +} +const targetPath = `${targetDir}/${name}`; + +/** + * 4. Copy the compiled plugin code to the target directory + */ +copyDirectory(distDir, targetPath); diff --git a/scripts/update_version.js b/scripts/update_version.js new file mode 100644 index 0000000..775c98a --- /dev/null +++ b/scripts/update_version.js @@ -0,0 +1,141 @@ +// const fs = require('fs'); +// const path = require('path'); +// const readline = require('readline'); +import fs from 'node:fs'; +import path from 'node:path'; +import readline from 'node:readline'; + +// Utility to read JSON file +function readJsonFile(filePath) { + return new Promise((resolve, reject) => { + fs.readFile(filePath, 'utf8', (err, data) => { + if (err) return reject(err); + try { + const jsonData = JSON.parse(data); + resolve(jsonData); + } catch (e) { + reject(e); + } + }); + }); +} + +// Utility to write JSON file +function writeJsonFile(filePath, jsonData) { + return new Promise((resolve, reject) => { + fs.writeFile(filePath, JSON.stringify(jsonData, null, 2), 'utf8', (err) => { + if (err) return reject(err); + resolve(); + }); + }); +} + +// Utility to prompt the user for input +function promptUser(query) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + return new Promise((resolve) => rl.question(query, (answer) => { + rl.close(); + resolve(answer); + })); +} + +// Function to parse the version string +function parseVersion(version) { + const [major, minor, patch] = version.split('.').map(Number); + return { major, minor, patch }; +} + +// Function to auto-increment version parts +function incrementVersion(version, type) { + let { major, minor, patch } = parseVersion(version); + + switch (type) { + case 'major': + major++; + minor = 0; + patch = 0; + break; + case 'minor': + minor++; + patch = 0; + break; + case 'patch': + patch++; + break; + default: + break; + } + + return `${major}.${minor}.${patch}`; +} + +// Main script +(async function () { + try { + const pluginJsonPath = path.join(process.cwd(), 'plugin.json'); + const packageJsonPath = path.join(process.cwd(), 'package.json'); + + // Read both JSON files + const pluginData = await readJsonFile(pluginJsonPath); + const packageData = await readJsonFile(packageJsonPath); + + // Get the current version from both files (assuming both have the same version) + const currentVersion = pluginData.version || packageData.version; + console.log(`\n🌟 Current version: \x1b[36m${currentVersion}\x1b[0m\n`); + + // Calculate potential new versions for auto-update + const newPatchVersion = incrementVersion(currentVersion, 'patch'); + const newMinorVersion = incrementVersion(currentVersion, 'minor'); + const newMajorVersion = incrementVersion(currentVersion, 'major'); + + // Prompt the user with formatted options + console.log('🔄 How would you like to update the version?\n'); + console.log(` 1️⃣ Auto update \x1b[33mpatch\x1b[0m version (new version: \x1b[32m${newPatchVersion}\x1b[0m)`); + console.log(` 2️⃣ Auto update \x1b[33mminor\x1b[0m version (new version: \x1b[32m${newMinorVersion}\x1b[0m)`); + console.log(` 3️⃣ Auto update \x1b[33mmajor\x1b[0m version (new version: \x1b[32m${newMajorVersion}\x1b[0m)`); + console.log(` 4️⃣ Input version \x1b[33mmanually\x1b[0m`); + // Press 0 to skip version update + console.log(' 0️⃣ Quit without updating\n'); + + const updateChoice = await promptUser('👉 Please choose (1/2/3/4): '); + + let newVersion; + + switch (updateChoice.trim()) { + case '1': + newVersion = newPatchVersion; + break; + case '2': + newVersion = newMinorVersion; + break; + case '3': + newVersion = newMajorVersion; + break; + case '4': + newVersion = await promptUser('✍️ Please enter the new version (in a.b.c format): '); + break; + case '0': + console.log('\n🛑 Skipping version update.'); + return; + default: + console.log('\n❌ Invalid option, no version update.'); + return; + } + + // Update the version in both plugin.json and package.json + pluginData.version = newVersion; + packageData.version = newVersion; + + // Write the updated JSON back to files + await writeJsonFile(pluginJsonPath, pluginData); + await writeJsonFile(packageJsonPath, packageData); + + console.log(`\n✅ Version successfully updated to: \x1b[32m${newVersion}\x1b[0m\n`); + + } catch (error) { + console.error('❌ Error:', error); + } +})(); diff --git a/scripts/utils.js b/scripts/utils.js new file mode 100644 index 0000000..210b6b1 --- /dev/null +++ b/scripts/utils.js @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2024 by frostime. All Rights Reserved. + * @Author : frostime + * @Date : 2024-09-06 17:42:57 + * @FilePath : /scripts/utils.js + * @LastEditTime : 2024-09-06 19:23:12 + * @Description : + */ +// common.js +import fs from 'fs'; +import path from 'node:path'; +import http from 'node:http'; +import readline from 'node:readline'; + +// Logging functions +export const log = (info) => console.log(`\x1B[36m%s\x1B[0m`, info); +export const error = (info) => console.log(`\x1B[31m%s\x1B[0m`, info); + +// HTTP POST headers +export const POST_HEADER = { + "Content-Type": "application/json", +}; + +// Fetch function compatible with older Node.js versions +export async function myfetch(url, options) { + return new Promise((resolve, reject) => { + let req = http.request(url, options, (res) => { + let data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + res.on('end', () => { + resolve({ + ok: true, + status: res.statusCode, + json: () => JSON.parse(data) + }); + }); + }); + req.on('error', (e) => { + reject(e); + }); + req.end(); + }); +} + +/** + * Fetch SiYuan workspaces from port 6806 + * @returns {Promise} + */ +export async function getSiYuanDir() { + let url = 'http://127.0.0.1:6806/api/system/getWorkspaces'; + let conf = {}; + try { + let response = await myfetch(url, { + method: 'POST', + headers: POST_HEADER + }); + if (response.ok) { + conf = await response.json(); + } else { + error(`\tHTTP-Error: ${response.status}`); + return null; + } + } catch (e) { + error(`\tError: ${e}`); + error("\tPlease make sure SiYuan is running!!!"); + return null; + } + return conf?.data; // 保持原始返回值 +} + +/** + * Choose target workspace + * @param {{path: string}[]} workspaces + * @returns {string} The path of the selected workspace + */ +export async function chooseTarget(workspaces) { + let count = workspaces.length; + log(`>>> Got ${count} SiYuan ${count > 1 ? 'workspaces' : 'workspace'}`); + workspaces.forEach((workspace, i) => { + log(`\t[${i}] ${workspace.path}`); + }); + + if (count === 1) { + return `${workspaces[0].path}/data/plugins`; + } else { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + let index = await new Promise((resolve) => { + rl.question(`\tPlease select a workspace[0-${count - 1}]: `, (answer) => { + resolve(answer); + }); + }); + rl.close(); + return `${workspaces[index].path}/data/plugins`; + } +} + +/** + * Check if two paths are the same + * @param {string} path1 + * @param {string} path2 + * @returns {boolean} + */ +export function cmpPath(path1, path2) { + path1 = path1.replace(/\\/g, '/'); + path2 = path2.replace(/\\/g, '/'); + if (path1[path1.length - 1] !== '/') { + path1 += '/'; + } + if (path2[path2.length - 1] !== '/') { + path2 += '/'; + } + return path1 === path2; +} + +export function getThisPluginName() { + if (!fs.existsSync('./plugin.json')) { + process.chdir('../'); + if (!fs.existsSync('./plugin.json')) { + error('Failed! plugin.json not found'); + return null; + } + } + + const plugin = JSON.parse(fs.readFileSync('./plugin.json', 'utf8')); + const name = plugin?.name; + if (!name) { + error('Failed! Please set plugin name in plugin.json'); + return null; + } + + return name; +} + +export function copyDirectory(srcDir, dstDir) { + if (!fs.existsSync(dstDir)) { + fs.mkdirSync(dstDir); + log(`Created directory ${dstDir}`); + } + + fs.readdirSync(srcDir, { withFileTypes: true }).forEach((file) => { + const src = path.join(srcDir, file.name); + const dst = path.join(dstDir, file.name); + + if (file.isDirectory()) { + copyDirectory(src, dst); + } else { + fs.copyFileSync(src, dst); + log(`Copied file: ${src} --> ${dst}`); + } + }); + log(`All files copied!`); +} + + +export function makeSymbolicLink(srcPath, targetPath) { + if (!fs.existsSync(targetPath)) { + // fs.symlinkSync(srcPath, targetPath, 'junction'); + //Go 1.23 no longer supports junctions as symlinks + //Please refer to https://github.com/siyuan-note/siyuan/issues/12399 + fs.symlinkSync(srcPath, targetPath, 'dir'); + log(`Done! Created symlink ${targetPath}`); + return; + } + + //Check the existed target path + let isSymbol = fs.lstatSync(targetPath).isSymbolicLink(); + if (!isSymbol) { + error(`Failed! ${targetPath} already exists and is not a symbolic link`); + return; + } + let existedPath = fs.readlinkSync(targetPath); + if (cmpPath(existedPath, srcPath)) { + log(`Good! ${targetPath} is already linked to ${srcPath}`); + } else { + error(`Error! Already exists symbolic link ${targetPath}\nBut it links to ${existedPath}`); + } +} diff --git a/src/api.ts b/src/api.ts index 9a4ff3f..0595371 100644 --- a/src/api.ts +++ b/src/api.ts @@ -6,10 +6,10 @@ * API 文档见 [API_zh_CN.md](https://github.com/siyuan-note/siyuan/blob/master/API_zh_CN.md) */ -import { fetchSyncPost, IWebSocketData } from "siyuan"; +import { fetchPost, fetchSyncPost, IWebSocketData } from "siyuan"; -async function request(url: string, data: any) { +export async function request(url: string, data: any) { let response: IWebSocketData = await fetchSyncPost(url, data); let res = response.code === 0 ? response.data : null; return res; @@ -224,6 +224,24 @@ export async function moveBlock(id: BlockId, previousID?: PreviousID, parentID?: } +export async function foldBlock(id: BlockId) { + let data = { + id: id + } + let url = '/api/block/foldBlock'; + return request(url, data); +} + + +export async function unfoldBlock(id: BlockId) { + let data = { + id: id + } + let url = '/api/block/unfoldBlock'; + return request(url, data); +} + + export async function getBlockKramdown(id: BlockId): Promise { let data = { id: id @@ -310,14 +328,35 @@ export async function getFile(path: string): Promise { path: path } let url = '/api/file/getFile'; - try { - let file = await fetchSyncPost(url, data); - return file; - } catch (error_msg) { + return new Promise((resolve, _) => { + fetchPost(url, data, (content: any) => { + resolve(content) + }); + }); +} + + +/** + * fetchPost will secretly convert data into json, this func merely return Blob + * @param endpoint + * @returns + */ +export const getFileBlob = async (path: string): Promise => { + const endpoint = '/api/file/getFile' + let response = await fetch(endpoint, { + method: 'POST', + body: JSON.stringify({ + path: path + }) + }); + if (!response.ok) { return null; } + let data = await response.blob(); + return data; } + export async function putFile(path: string, isDir: boolean, file: any) { let form = new FormData(); form.append('path', path); diff --git a/src/hello.svelte b/src/hello.svelte index cc9f0dd..967c7f6 100644 --- a/src/hello.svelte +++ b/src/hello.svelte @@ -1,3 +1,11 @@ + + +{#if type === "checkbox"} + + +{:else if type === "textinput"} + + +{:else if type === "textarea"} + +{:else if type === "number"} + +{:else if type === "button"} + + + {button.label} + +{:else if type === "select"} + + + {#each Object.entries(options) as [value, text]} + {text} + {/each} + +{:else if type == "slider"} + + + + +{/if} diff --git a/src/libs/components/Form/form-wrap.svelte b/src/libs/components/Form/form-wrap.svelte new file mode 100644 index 0000000..4d8092e --- /dev/null +++ b/src/libs/components/Form/form-wrap.svelte @@ -0,0 +1,53 @@ + + + +{#if direction === "row"} + + + {title} + {@html description} + + + + + + +{:else} + + + {title} + + {@html description} + + + + + +{/if} + + diff --git a/src/libs/components/Form/index.ts b/src/libs/components/Form/index.ts new file mode 100644 index 0000000..a5c81c0 --- /dev/null +++ b/src/libs/components/Form/index.ts @@ -0,0 +1,6 @@ +import FormInput from './form-input.svelte'; +import FormWrap from './form-wrap.svelte'; + +const Form = { Wrap: FormWrap, Input: FormInput }; +export default Form; +export { FormInput, FormWrap }; diff --git a/src/libs/b3-typography.svelte b/src/libs/components/b3-typography.svelte similarity index 100% rename from src/libs/b3-typography.svelte rename to src/libs/components/b3-typography.svelte diff --git a/src/libs/components/setting-panel.svelte b/src/libs/components/setting-panel.svelte new file mode 100644 index 0000000..783e2b6 --- /dev/null +++ b/src/libs/components/setting-panel.svelte @@ -0,0 +1,51 @@ + + + + + + {#each settingItems as item (item.key)} + + + + {/each} + \ No newline at end of file diff --git a/src/libs/const.ts b/src/libs/const.ts new file mode 100644 index 0000000..2dd3e06 --- /dev/null +++ b/src/libs/const.ts @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2024 by frostime. All Rights Reserved. + * @Author : frostime + * @Date : 2024-06-08 20:36:30 + * @FilePath : /src/libs/const.ts + * @LastEditTime : 2024-06-08 20:48:06 + * @Description : + */ + + +export const BlockType2NodeType: {[key in BlockType]: string} = { + d: 'NodeDocument', + p: 'NodeParagraph', + query_embed: 'NodeBlockQueryEmbed', + l: 'NodeList', + i: 'NodeListItem', + h: 'NodeHeading', + iframe: 'NodeIFrame', + tb: 'NodeThematicBreak', + b: 'NodeBlockquote', + s: 'NodeSuperBlock', + c: 'NodeCodeBlock', + widget: 'NodeWidget', + t: 'NodeTable', + html: 'NodeHTMLBlock', + m: 'NodeMathBlock', + av: 'NodeAttributeView', + audio: 'NodeAudio' +} + + +export const NodeIcons = { + NodeAttributeView: { + icon: "iconDatabase" + }, + NodeAudio: { + icon: "iconRecord" + }, + NodeBlockQueryEmbed: { + icon: "iconSQL" + }, + NodeBlockquote: { + icon: "iconQuote" + }, + NodeCodeBlock: { + icon: "iconCode" + }, + NodeDocument: { + icon: "iconFile" + }, + NodeHTMLBlock: { + icon: "iconHTML5" + }, + NodeHeading: { + icon: "iconHeadings", + subtypes: { + h1: { icon: "iconH1" }, + h2: { icon: "iconH2" }, + h3: { icon: "iconH3" }, + h4: { icon: "iconH4" }, + h5: { icon: "iconH5" }, + h6: { icon: "iconH6" } + } + }, + NodeIFrame: { + icon: "iconLanguage" + }, + NodeList: { + subtypes: { + o: { icon: "iconOrderedList" }, + t: { icon: "iconCheck" }, + u: { icon: "iconList" } + } + }, + NodeListItem: { + icon: "iconListItem" + }, + NodeMathBlock: { + icon: "iconMath" + }, + NodeParagraph: { + icon: "iconParagraph" + }, + NodeSuperBlock: { + icon: "iconSuper" + }, + NodeTable: { + icon: "iconTable" + }, + NodeThematicBreak: { + icon: "iconLine" + }, + NodeVideo: { + icon: "iconVideo" + }, + NodeWidget: { + icon: "iconBoth" + } +}; diff --git a/src/libs/dialog.ts b/src/libs/dialog.ts new file mode 100644 index 0000000..d0fe582 --- /dev/null +++ b/src/libs/dialog.ts @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2024 by frostime. All Rights Reserved. + * @Author : frostime + * @Date : 2024-03-23 21:37:33 + * @FilePath : /src/libs/dialog.ts + * @LastEditTime : 2024-10-16 14:31:04 + * @Description : Kits about dialogs + */ +import { Dialog } from "siyuan"; +import { type SvelteComponent } from "svelte"; + +export const inputDialog = (args: { + title: string, placeholder?: string, defaultText?: string, + confirm?: (text: string) => void, cancel?: () => void, + width?: string, height?: string +}) => { + const dialog = new Dialog({ + title: args.title, + content: ` + ${args?.defaultText ?? ''} + + + ${window.siyuan.languages.cancel} + ${window.siyuan.languages.confirm} +`, + width: args.width ?? "520px", + height: args.height + }); + const target: HTMLTextAreaElement = dialog.element.querySelector(".b3-dialog__content>div.ft__breakword>textarea"); + const btnsElement = dialog.element.querySelectorAll(".b3-button"); + btnsElement[0].addEventListener("click", () => { + if (args?.cancel) { + args.cancel(); + } + dialog.destroy(); + }); + btnsElement[1].addEventListener("click", () => { + if (args?.confirm) { + args.confirm(target.value); + } + dialog.destroy(); + }); +}; + +export const inputDialogSync = async (args: { + title: string, placeholder?: string, defaultText?: string, + width?: string, height?: string +}) => { + return new Promise((resolve) => { + let newargs = { + ...args, confirm: (text) => { + resolve(text); + }, cancel: () => { + resolve(null); + } + }; + inputDialog(newargs); + }); +} + + +interface IConfirmDialogArgs { + title: string; + content: string | HTMLElement; + confirm?: (ele?: HTMLElement) => void; + cancel?: (ele?: HTMLElement) => void; + width?: string; + height?: string; +} + +export const confirmDialog = (args: IConfirmDialogArgs) => { + const { title, content, confirm, cancel, width, height } = args; + + const dialog = new Dialog({ + title, + content: ` + + + + + ${window.siyuan.languages.cancel} + ${window.siyuan.languages.confirm} +`, + width: width, + height: height + }); + + const target: HTMLElement = dialog.element.querySelector(".b3-dialog__content>div.ft__breakword"); + if (typeof content === "string") { + target.innerHTML = content; + } else { + target.appendChild(content); + } + + const btnsElement = dialog.element.querySelectorAll(".b3-button"); + btnsElement[0].addEventListener("click", () => { + if (cancel) { + cancel(target); + } + dialog.destroy(); + }); + btnsElement[1].addEventListener("click", () => { + if (confirm) { + confirm(target); + } + dialog.destroy(); + }); +}; + + +export const confirmDialogSync = async (args: IConfirmDialogArgs) => { + return new Promise((resolve) => { + let newargs = { + ...args, confirm: (ele: HTMLElement) => { + resolve(ele); + }, cancel: (ele: HTMLElement) => { + resolve(ele); + } + }; + confirmDialog(newargs); + }); +}; + + +export const simpleDialog = (args: { + title: string, ele: HTMLElement | DocumentFragment, + width?: string, height?: string, + callback?: () => void; +}) => { + const dialog = new Dialog({ + title: args.title, + content: ``, + width: args.width, + height: args.height, + destroyCallback: args.callback + }); + dialog.element.querySelector(".dialog-content").appendChild(args.ele); + return { + dialog, + close: dialog.destroy.bind(dialog) + }; +} + + +export const svelteDialog = (args: { + title: string, constructor: (container: HTMLElement) => SvelteComponent, + width?: string, height?: string, + callback?: () => void; +}) => { + let container = document.createElement('div') + container.style.display = 'contents'; + let component = args.constructor(container); + const { dialog, close } = simpleDialog({ + ...args, ele: container, callback: () => { + component.$destroy(); + if (args.callback) args.callback(); + } + }); + return { + component, + dialog, + close + } +} diff --git a/src/libs/index.d.ts b/src/libs/index.d.ts index b46f655..27a27ed 100644 --- a/src/libs/index.d.ts +++ b/src/libs/index.d.ts @@ -1,20 +1,43 @@ -type TSettingItemType = "checkbox" | "select" | "textinput" | "textarea" | "slider" | "button"; -interface ISettingItem { +/* + * Copyright (c) 2024 by frostime. All Rights Reserved. + * @Author : frostime + * @Date : 2024-04-19 18:30:12 + * @FilePath : /src/libs/index.d.ts + * @LastEditTime : 2024-04-30 16:39:54 + * @Description : + */ +type TSettingItemType = "checkbox" | "select" | "textinput" | "textarea" | "number" | "slider" | "button" | "hint" | "custom"; + +interface ISettingItemCore { + type: TSettingItemType; key: string; value: any; - type: TSettingItemType; - title: string; - description?: string; + placeholder?: string; slider?: { min: number; max: number; step: number; }; - select?: { - options: {val: any; text: string}[]; - }; + options?: { [key: string | number]: string }; button?: { label: string; callback: () => void; } } + +interface ISettingItem extends ISettingItemCore { + title: string; + description: string; + direction?: "row" | "column"; +} + + +//Interface for setting-utils +interface ISettingUtilsItem extends ISettingItem { + action?: { + callback: () => void; + } + createElement?: (currentVal: any) => HTMLElement; + getEleVal?: (ele: HTMLElement) => any; + setEleVal?: (ele: HTMLElement, val: any) => void; +} diff --git a/src/libs/promise-pool.ts b/src/libs/promise-pool.ts new file mode 100644 index 0000000..e20b0b8 --- /dev/null +++ b/src/libs/promise-pool.ts @@ -0,0 +1,48 @@ +export default class PromiseLimitPool { + private maxConcurrent: number; + private currentRunning = 0; + private queue: (() => void)[] = []; + private promises: Promise[] = []; + + constructor(maxConcurrent: number) { + this.maxConcurrent = maxConcurrent; + } + + add(fn: () => Promise): void { + const promise = new Promise((resolve, reject) => { + const run = async () => { + try { + this.currentRunning++; + const result = await fn(); + resolve(result); + } catch (error) { + reject(error); + } finally { + this.currentRunning--; + this.next(); + } + }; + + if (this.currentRunning < this.maxConcurrent) { + run(); + } else { + this.queue.push(run); + } + }); + this.promises.push(promise); + } + + async awaitAll(): Promise { + return Promise.all(this.promises); + } + + /** + * Handles the next task in the queue. + */ + private next(): void { + if (this.queue.length > 0 && this.currentRunning < this.maxConcurrent) { + const nextRun = this.queue.shift()!; + nextRun(); + } + } +} diff --git a/src/libs/setting-item.svelte b/src/libs/setting-item.svelte deleted file mode 100644 index b728cf4..0000000 --- a/src/libs/setting-item.svelte +++ /dev/null @@ -1,92 +0,0 @@ - - - - - {title} - - {text} - - - - - {#if type === "checkbox"} - - - {:else if type === "input"} - - - {:else if type === "button"} - - - {settingValue} - - {:else if type === "select"} - - - {#each Object.entries(options) as [value, text]} - {text} - {/each} - - {:else if type == "slider"} - - - - - {/if} - diff --git a/src/libs/setting-panel.svelte b/src/libs/setting-panel.svelte deleted file mode 100644 index ccf424e..0000000 --- a/src/libs/setting-panel.svelte +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - This setting panel is provided by a svelte component - - - See: - /lib/setting-pannel.svelte - - - - - { - showMessage( - `Checkbox changed: ${event.detail.key} = ${event.detail.value}` - ); - }} - /> - { - showMessage( - `Input changed: ${event.detail.key} = ${event.detail.value}` - ); - }} - /> - { - showMessage("Button clicked"); - }} - /> - { - showMessage( - `Select changed: ${event.detail.key} = ${event.detail.value}` - ); - }} - /> - { - showMessage( - `Slide changed: ${event.detail.key} = ${event.detail.value}` - ); - }} - /> - diff --git a/src/libs/setting-utils.ts b/src/libs/setting-utils.ts index b092a27..ae316e2 100644 --- a/src/libs/setting-utils.ts +++ b/src/libs/setting-utils.ts @@ -1,39 +1,116 @@ /* * Copyright (c) 2023 by frostime. All Rights Reserved. * @Author : frostime - * @Date : 2023-09-16 18:05:00 + * @Date : 2023-12-17 18:28:19 * @FilePath : /src/libs/setting-utils.ts - * @LastEditTime : 2023-10-28 16:52:01 - * @Description : A utility for siyuan plugin settings + * @LastEditTime : 2024-05-01 17:44:16 + * @Description : */ import { Plugin, Setting } from 'siyuan'; + +/** + * The default function to get the value of the element + * @param type + * @returns + */ +const createDefaultGetter = (type: TSettingItemType) => { + let getter: (ele: HTMLElement) => any; + switch (type) { + case 'checkbox': + getter = (ele: HTMLInputElement) => { + return ele.checked; + }; + break; + case 'select': + case 'slider': + case 'textinput': + case 'textarea': + getter = (ele: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement) => { + return ele.value; + }; + break; + case 'number': + getter = (ele: HTMLInputElement) => { + return parseInt(ele.value); + } + break; + default: + getter = () => null; + break; + } + return getter; +} + + +/** + * The default function to set the value of the element + * @param type + * @returns + */ +const createDefaultSetter = (type: TSettingItemType) => { + let setter: (ele: HTMLElement, value: any) => void; + switch (type) { + case 'checkbox': + setter = (ele: HTMLInputElement, value: any) => { + ele.checked = value; + }; + break; + case 'select': + case 'slider': + case 'textinput': + case 'textarea': + case 'number': + setter = (ele: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement, value: any) => { + ele.value = value; + }; + break; + default: + setter = () => {}; + break; + } + return setter; + +} + + export class SettingUtils { plugin: Plugin; name: string; file: string; - settings: Map = new Map(); + settings: Map = new Map(); elements: Map = new Map(); - constructor(plugin: Plugin, name?: string, callback?: (data: any) => void, width?: string, height?: string) { - this.name = name ?? 'settings'; - this.plugin = plugin; + constructor(args: { + plugin: Plugin, + name?: string, + callback?: (data: any) => void, + width?: string, + height?: string + }) { + this.name = args.name ?? 'settings'; + this.plugin = args.plugin; this.file = this.name.endsWith('.json') ? this.name : `${this.name}.json`; this.plugin.setting = new Setting({ - width: width, - height: height, + width: args.width, + height: args.height, confirmCallback: () => { for (let key of this.settings.keys()) { - this.updateValue(key); + this.updateValueFromElement(key); } let data = this.dump(); - if (callback !== undefined) { - callback(data); - } else { - this.plugin.data[this.name] = data; - this.save(); + if (args.callback !== undefined) { + args.callback(data); + } + this.plugin.data[this.name] = data; + this.save(data); + }, + destroyCallback: () => { + //Restore the original value + for (let key of this.settings.keys()) { + this.updateElementFromValue(key); } } }); @@ -51,14 +128,15 @@ export class SettingUtils { return data; } - async save() { - let data = this.dump(); + async save(data?: any) { + data = data ?? this.dump(); await this.plugin.saveData(this.file, this.dump()); + console.debug('Save config:', data); return data; } /** - * Get setting item value + * read the data after saving * @param key key name * @returns setting item value */ @@ -66,6 +144,88 @@ export class SettingUtils { return this.settings.get(key)?.value; } + /** + * Set data to this.settings, + * but do not save it to the configuration file + * @param key key name + * @param value value + */ + set(key: string, value: any) { + let item = this.settings.get(key); + if (item) { + item.value = value; + this.updateElementFromValue(key); + } + } + + /** + * Set and save setting item value + * If you want to set and save immediately you can use this method + * @param key key name + * @param value value + */ + async setAndSave(key: string, value: any) { + let item = this.settings.get(key); + if (item) { + item.value = value; + this.updateElementFromValue(key); + await this.save(); + } + } + + /** + * Read in the value of element instead of setting obj in real time + * @param key key name + * @param apply whether to apply the value to the setting object + * if true, the value will be applied to the setting object + * @returns value in html + */ + take(key: string, apply: boolean = false) { + let item = this.settings.get(key); + let element = this.elements.get(key) as any; + if (!element) { + return + } + if (apply) { + this.updateValueFromElement(key); + } + return item.getEleVal(element); + } + + /** + * Read data from html and save it + * @param key key name + * @param value value + * @return value in html + */ + async takeAndSave(key: string) { + let value = this.take(key, true); + await this.save(); + return value; + } + + /** + * Disable setting item + * @param key key name + */ + disable(key: string) { + let element = this.elements.get(key) as any; + if (element) { + element.disabled = true; + } + } + + /** + * Enable setting item + * @param key key name + */ + enable(key: string) { + let element = this.elements.get(key) as any; + if (element) { + element.disabled = false; + } + } + /** * 将设置项目导出为 JSON 对象 * @returns object @@ -79,9 +239,59 @@ export class SettingUtils { return data; } - addItem(item: ISettingItem) { + addItem(item: ISettingUtilsItem) { this.settings.set(item.key, item); + const IsCustom = item.type === 'custom'; + let error = IsCustom && (item.createElement === undefined || item.getEleVal === undefined || item.setEleVal === undefined); + if (error) { + console.error('The custom setting item must have createElement, getEleVal and setEleVal methods'); + return; + } + + if (item.getEleVal === undefined) { + item.getEleVal = createDefaultGetter(item.type); + } + if (item.setEleVal === undefined) { + item.setEleVal = createDefaultSetter(item.type); + } + + if (item.createElement === undefined) { + let itemElement = this.createDefaultElement(item); + this.elements.set(item.key, itemElement); + this.plugin.setting.addItem({ + title: item.title, + description: item?.description, + direction: item?.direction, + createActionElement: () => { + this.updateElementFromValue(item.key); + let element = this.getElement(item.key); + return element; + } + }); + } else { + this.plugin.setting.addItem({ + title: item.title, + description: item?.description, + direction: item?.direction, + createActionElement: () => { + let val = this.get(item.key); + let element = item.createElement(val); + this.elements.set(item.key, element); + return element; + } + }); + } + } + + createDefaultElement(item: ISettingUtilsItem) { let itemElement: HTMLElement; + //阻止思源内置的回车键确认 + const preventEnterConfirm = (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + e.stopImmediatePropagation(); + } + } switch (item.type) { case 'checkbox': let element: HTMLInputElement = document.createElement('input'); @@ -89,17 +299,21 @@ export class SettingUtils { element.checked = item.value; element.className = "b3-switch fn__flex-center"; itemElement = element; + element.onchange = item.action?.callback ?? (() => { }); break; case 'select': let selectElement: HTMLSelectElement = document.createElement('select'); selectElement.className = "b3-select fn__flex-center fn__size200"; - for (let option of item.select?.options ?? []) { + let options = item?.options ?? {}; + for (let val in options) { let optionElement = document.createElement('option'); - optionElement.value = option.val; - optionElement.text = option.text; + let text = options[val]; + optionElement.value = val; + optionElement.text = text; selectElement.appendChild(optionElement); } selectElement.value = item.value; + selectElement.onchange = item.action?.callback ?? (() => { }); itemElement = selectElement; break; case 'slider': @@ -113,6 +327,7 @@ export class SettingUtils { sliderElement.value = item.value; sliderElement.onchange = () => { sliderElement.ariaLabel = sliderElement.value; + item.action?.callback(); } itemElement = sliderElement; break; @@ -120,78 +335,63 @@ export class SettingUtils { let textInputElement: HTMLInputElement = document.createElement('input'); textInputElement.className = 'b3-text-field fn__flex-center fn__size200'; textInputElement.value = item.value; + textInputElement.onchange = item.action?.callback ?? (() => { }); itemElement = textInputElement; + textInputElement.addEventListener('keydown', preventEnterConfirm); break; case 'textarea': let textareaElement: HTMLTextAreaElement = document.createElement('textarea'); textareaElement.className = "b3-text-field fn__block"; textareaElement.value = item.value; + textareaElement.onchange = item.action?.callback ?? (() => { }); itemElement = textareaElement; break; + case 'number': + let numberElement: HTMLInputElement = document.createElement('input'); + numberElement.type = 'number'; + numberElement.className = 'b3-text-field fn__flex-center fn__size200'; + numberElement.value = item.value; + itemElement = numberElement; + numberElement.addEventListener('keydown', preventEnterConfirm); + break; case 'button': let buttonElement: HTMLButtonElement = document.createElement('button'); buttonElement.className = "b3-button b3-button--outline fn__flex-center fn__size200"; buttonElement.innerText = item.button?.label ?? 'Button'; - buttonElement.onclick = item.button?.callback ?? (() => {}); + buttonElement.onclick = item.button?.callback ?? (() => { }); itemElement = buttonElement; break; + case 'hint': + let hintElement: HTMLElement = document.createElement('div'); + hintElement.className = 'b3-label fn__flex-center'; + itemElement = hintElement; + break; } - this.elements.set(item.key, itemElement); - this.plugin.setting.addItem({ - title: item.title, - description: item?.description, - createActionElement: () => { - let element = this.getElement(item.key); - return element; - } - }) + return itemElement; } - private getElement(key: string) { - let item = this.settings.get(key); + /** + * return the setting element + * @param key key name + * @returns element + */ + getElement(key: string) { + // let item = this.settings.get(key); let element = this.elements.get(key) as any; - switch (item.type) { - case 'checkbox': - element.checked = item.value; - break; - case 'select': - element.value = item.value; - break; - case 'slider': - element.value = item.value; - element.ariaLabel = item.value; - break; - case 'textinput': - element.value = item.value; - break; - case 'textarea': - element.value = item.value; - break; - } return element; } - private updateValue(key: string) { + private updateValueFromElement(key: string) { let item = this.settings.get(key); + if (item.type === 'button') return; let element = this.elements.get(key) as any; - // console.debug(element, element?.value); - switch (item.type) { - case 'checkbox': - item.value = element.checked; - break; - case 'select': - item.value = element.value; - break; - case 'slider': - item.value = element.value; - break; - case 'textinput': - item.value = element.value; - break; - case 'textarea': - item.value = element.value; - break; - } + item.value = item.getEleVal(element); } + private updateElementFromValue(key: string) { + let item = this.settings.get(key); + if (item.type === 'button') return; + let element = this.elements.get(key) as any; + item.setEleVal(element, item.value); + } } \ No newline at end of file diff --git a/src/setting-example.svelte b/src/setting-example.svelte new file mode 100644 index 0000000..2a2c809 --- /dev/null +++ b/src/setting-example.svelte @@ -0,0 +1,139 @@ + + + + + {#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); }} + > + + + + + + diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 1891c1a..f224b3e 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1,10 +1,13 @@ -/** - * Copyright (c) 2023 frostime. All rights reserved. +/* + * Copyright (c) 2024 by frostime. All Rights Reserved. + * @Author : frostime + * @Date : 2023-08-15 10:28:10 + * @FilePath : /src/types/index.d.ts + * @LastEditTime : 2024-06-08 20:50:53 + * @Description : Frequently used data structures in SiYuan */ -/** - * Frequently used data structures in SiYuan - */ + type DocumentId = string; type BlockId = string; type NotebookId = string; @@ -28,7 +31,25 @@ type NotebookConf = { dailyNoteTemplatePath: string; } -type BlockType = "d" | "s" | "h" | "t" | "i" | "p" | "f" | "audio" | "video" | "other"; +type BlockType = + | 'd' + | 'p' + | 'query_embed' + | 'l' + | 'i' + | 'h' + | 'iframe' + | 'tb' + | 'b' + | 's' + | 'c' + | 'widget' + | 't' + | 'html' + | 'm' + | 'av' + | 'audio'; + type BlockSubType = "d1" | "d2" | "s1" | "s2" | "s3" | "t1" | "t2" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "table" | "task" | "toggle" | "latex" | "quote" | "html" | "code" | "footnote" | "cite" | "collection" | "bookmark" | "attachment" | "comment" | "mindmap" | "spreadsheet" | "calendar" | "image" | "audio" | "video" | "other"; @@ -70,6 +91,7 @@ type doOperation = { interface Window { siyuan: { + config: any; notebooks: any; menus: any; dialogs: any; @@ -78,5 +100,7 @@ interface Window { user: any; ws: any; languages: any; + emojis: any; }; + Lute: any; } diff --git a/svelte.config.js b/svelte.config.js index 7c8df62..d62a343 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -1,7 +1,26 @@ +/* + * Copyright (c) 2024 by frostime. All Rights Reserved. + * @Author : frostime + * @Date : 2023-05-19 19:49:13 + * @FilePath : /svelte.config.js + * @LastEditTime : 2024-04-19 19:01:55 + * @Description : + */ import { vitePreprocess } from "@sveltejs/vite-plugin-svelte" +const NoWarns = new Set([ + "a11y-click-events-have-key-events", + "a11y-no-static-element-interactions", + "a11y-no-noninteractive-element-interactions" +]); + export default { // Consult https://svelte.dev/docs#compile-time-svelte-preprocess // for more information about preprocessors preprocess: vitePreprocess(), + onwarn: (warning, handler) => { + // suppress warnings on `vite dev` and `vite build`; but even without this, things still work + if (NoWarns.has(warning.code)) return; + handler(warning); + } } diff --git a/tsconfig.json b/tsconfig.json index e196929..0fcc1ad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -47,7 +47,8 @@ "src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", - "src/**/*.vue" + "src/**/*.vue", + "src/**/*.svelte" ], "references": [ { diff --git a/vite.config.ts b/vite.config.ts index 2af9f2f..cb7511b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,19 +1,22 @@ import { resolve } from "path" import { defineConfig, loadEnv } from "vite" -import minimist from "minimist" import { viteStaticCopy } from "vite-plugin-static-copy" import livereload from "rollup-plugin-livereload" import { svelte } from "@sveltejs/vite-plugin-svelte" import zipPack from "vite-plugin-zip-pack"; import fg from 'fast-glob'; -const args = minimist(process.argv.slice(2)) -const isWatch = args.watch || args.w || false -const devDistDir = "./dev" -const distDir = isWatch ? devDistDir : "./dist" +import vitePluginYamlI18n from './yaml-plugin'; -console.log("isWatch=>", isWatch) -console.log("distDir=>", distDir) +const env = process.env; +const isSrcmap = env.VITE_SOURCEMAP === 'inline'; +const isDev = env.NODE_ENV === 'development'; + +const outputDir = isDev ? "dev" : "dist"; + +console.log("isDev=>", isDev); +console.log("isSrcmap=>", isSrcmap); +console.log("outputDir=>", outputDir); export default defineConfig({ resolve: { @@ -25,92 +28,69 @@ export default defineConfig({ plugins: [ svelte(), + vitePluginYamlI18n({ + inDir: 'public/i18n', + outDir: `${outputDir}/i18n` + }), + viteStaticCopy({ targets: [ - { - src: "./README*.md", - dest: "./", - }, - { - src: "./icon.png", - dest: "./", - }, - { - src: "./preview.png", - dest: "./", - }, - { - src: "./plugin.json", - dest: "./", - }, - { - src: "./src/i18n/**", - dest: "./i18n/", - }, + { src: "./README*.md", dest: "./" }, + { src: "./plugin.json", dest: "./" }, + { src: "./preview.png", dest: "./" }, + { src: "./icon.png", dest: "./" } ], }), + ], - // https://github.com/vitejs/vite/issues/1930 - // https://vitejs.dev/guide/env-and-mode.html#env-files - // https://github.com/vitejs/vite/discussions/3058#discussioncomment-2115319 - // 在这里自定义变量 define: { - "process.env.DEV_MODE": `"${isWatch}"`, + "process.env.DEV_MODE": JSON.stringify(isDev), + "process.env.NODE_ENV": JSON.stringify(env.NODE_ENV) }, build: { - // 输出路径 - outDir: distDir, + outDir: outputDir, emptyOutDir: false, - - // 构建后是否生成 source map 文件 - sourcemap: false, - - // 设置为 false 可以禁用最小化混淆 - // 或是用来指定是应用哪种混淆器 - // boolean | 'terser' | 'esbuild' - // 不压缩,用于调试 - minify: !isWatch, + minify: true, + sourcemap: isSrcmap ? 'inline' : false, lib: { - // Could also be a dictionary or array of multiple entry points entry: resolve(__dirname, "src/index.ts"), - // the proper extensions will be added fileName: "index", formats: ["cjs"], }, rollupOptions: { plugins: [ - ...( - isWatch ? [ - livereload(devDistDir), - { - //监听静态资源文件 - name: 'watch-external', - async buildStart() { - const files = await fg([ - 'src/i18n/*.json', - './README*.md', - './plugin.json' - ]); - for (let file of files) { - this.addWatchFile(file); - } + ...(isDev ? [ + livereload(outputDir), + { + name: 'watch-external', + async buildStart() { + const files = await fg([ + 'public/i18n/**', + './README*.md', + './plugin.json' + ]); + for (let file of files) { + this.addWatchFile(file); } } - ] : [ - zipPack({ - inDir: './dist', - outDir: './', - outFileName: 'package.zip' - }) - ] - ) + } + ] : [ + // Clean up unnecessary files under dist dir + cleanupDistFiles({ + patterns: ['i18n/*.yaml', 'i18n/*.md'], + distDir: outputDir + }), + zipPack({ + inDir: './dist', + outDir: './', + outFileName: 'package.zip' + }) + ]) ], - // make sure to externalize deps that shouldn't be bundled - // into your library external: ["siyuan", "process"], output: { @@ -124,4 +104,60 @@ export default defineConfig({ }, }, } -}) +}); + + +/** + * Clean up some dist files after compiled + * @author frostime + * @param options: + * @returns + */ +function cleanupDistFiles(options: { patterns: string[], distDir: string }) { + const { + patterns, + distDir + } = options; + + return { + name: 'rollup-plugin-cleanup', + enforce: 'post', + writeBundle: { + sequential: true, + order: 'post' as 'post', + async handler() { + const fg = await import('fast-glob'); + const fs = await import('fs'); + // const path = await import('path'); + + // 使用 glob 语法,确保能匹配到文件 + const distPatterns = patterns.map(pat => `${distDir}/${pat}`); + console.debug('Cleanup searching patterns:', distPatterns); + + const files = await fg.default(distPatterns, { + dot: true, + absolute: true, + onlyFiles: false + }); + + // console.info('Files to be cleaned up:', files); + + for (const file of files) { + try { + if (fs.default.existsSync(file)) { + const stat = fs.default.statSync(file); + if (stat.isDirectory()) { + fs.default.rmSync(file, { recursive: true }); + } else { + fs.default.unlinkSync(file); + } + console.log(`Cleaned up: ${file}`); + } + } catch (error) { + console.error(`Failed to clean up ${file}:`, error); + } + } + } + } + }; +} diff --git a/yaml-plugin.js b/yaml-plugin.js new file mode 100644 index 0000000..01c85e2 --- /dev/null +++ b/yaml-plugin.js @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024 by frostime. All Rights Reserved. + * @Author : frostime + * @Date : 2024-04-05 21:27:55 + * @FilePath : /yaml-plugin.js + * @LastEditTime : 2024-04-05 22:53:34 + * @Description : 去妮玛的 json 格式,我就是要用 yaml 写 i18n + */ +// plugins/vite-plugin-parse-yaml.js +import fs from 'fs'; +import yaml from 'js-yaml'; +import { resolve } from 'path'; + +export default function vitePluginYamlI18n(options = {}) { + // Default options with a fallback + const DefaultOptions = { + inDir: 'src/i18n', + outDir: 'dist/i18n', + }; + + const finalOptions = { ...DefaultOptions, ...options }; + + return { + name: 'vite-plugin-yaml-i18n', + buildStart() { + console.log('🌈 Parse I18n: YAML to JSON..'); + const inDir = finalOptions.inDir; + const outDir = finalOptions.outDir + + if (!fs.existsSync(outDir)) { + fs.mkdirSync(outDir, { recursive: true }); + } + + //Parse yaml file, output to json + const files = fs.readdirSync(inDir); + for (const file of files) { + if (file.endsWith('.yaml') || file.endsWith('.yml')) { + console.log(`-- Parsing ${file}`) + //检查是否有同名的json文件 + const jsonFile = file.replace(/\.(yaml|yml)$/, '.json'); + if (files.includes(jsonFile)) { + console.log(`---- File ${jsonFile} already exists, skipping...`); + continue; + } + try { + const filePath = resolve(inDir, file); + const fileContents = fs.readFileSync(filePath, 'utf8'); + const parsed = yaml.load(fileContents); + const jsonContent = JSON.stringify(parsed, null, 2); + const outputFilePath = resolve(outDir, file.replace(/\.(yaml|yml)$/, '.json')); + console.log(`---- Writing to ${outputFilePath}`); + fs.writeFileSync(outputFilePath, jsonContent); + } catch (error) { + this.error(`---- Error parsing YAML file ${file}: ${error.message}`); + } + } + } + }, + }; +}
/lib/setting-pannel.svelte