├── .github └── workflows │ ├── tauri-action-publish.yml │ └── tauri-action-test.yml ├── .gitignore ├── .vscode └── extensions.json ├── README-CN.md ├── README.md ├── images └── screenshot.webp ├── index.html ├── package.json ├── pnpm-lock.yaml ├── public ├── tauri.svg └── vite.svg ├── src-tauri ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── shared │ └── tags.db │ │ ├── danbooru.csv │ │ └── quality.csv ├── src │ ├── main.rs │ ├── tagutils.rs │ └── translate.rs └── tauri.conf.json ├── src ├── App.vue ├── assets │ └── vue.svg ├── components │ ├── DeleteIsolatedTxt.ts │ ├── DeleteIsolatedTxt.vue │ ├── Home.vue │ ├── ImageFilter.vue │ ├── ImageList.vue │ ├── Settings.vue │ ├── Tag.vue │ ├── TagEditor.vue │ ├── TagInput.vue │ └── TagList.vue ├── lib │ ├── state.ts │ └── utils.ts ├── main.ts ├── styles.css └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.github/workflows/tauri-action-publish.yml: -------------------------------------------------------------------------------- 1 | name: 'publish' 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | create-release: 10 | permissions: 11 | contents: write 12 | runs-on: ubuntu-20.04 13 | outputs: 14 | release_id: ${{ steps.create-release.outputs.result }} 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: setup node 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 16 22 | - name: get version 23 | run: echo "PACKAGE_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV 24 | - name: create release 25 | id: create-release 26 | uses: actions/github-script@v6 27 | with: 28 | script: | 29 | const { data } = await github.rest.repos.createRelease({ 30 | owner: context.repo.owner, 31 | repo: context.repo.repo, 32 | tag_name: `v${process.env.PACKAGE_VERSION}`, 33 | name: `sd-tagtool v${process.env.PACKAGE_VERSION}`, 34 | body: 'Take a look at the assets to download and install this app.', 35 | draft: true, 36 | prerelease: false 37 | }) 38 | return data.id 39 | 40 | build-tauri: 41 | needs: create-release 42 | permissions: 43 | contents: write 44 | strategy: 45 | fail-fast: false 46 | matrix: 47 | platform: [macos-latest, ubuntu-20.04, windows-latest] 48 | 49 | runs-on: ${{ matrix.platform }} 50 | steps: 51 | - uses: actions/checkout@v3 52 | - name: setup node 53 | uses: actions/setup-node@v3 54 | with: 55 | node-version: 16 56 | - name: setup pnpm 57 | uses: pnpm/action-setup@v2 58 | with: 59 | version: 8.6 60 | - name: install Rust stable 61 | uses: dtolnay/rust-toolchain@stable 62 | - name: install dependencies (ubuntu only) 63 | if: matrix.platform == 'ubuntu-20.04' 64 | run: | 65 | sudo apt-get update 66 | sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf 67 | - name: install frontend dependencies 68 | run: pnpm install 69 | - uses: tauri-apps/tauri-action@v0 70 | env: 71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 72 | with: 73 | releaseId: ${{ needs.create-release.outputs.release_id }} 74 | 75 | publish-release: 76 | permissions: 77 | contents: write 78 | runs-on: ubuntu-20.04 79 | needs: [create-release, build-tauri] 80 | 81 | steps: 82 | - name: publish release 83 | id: publish-release 84 | uses: actions/github-script@v6 85 | env: 86 | release_id: ${{ needs.create-release.outputs.release_id }} 87 | with: 88 | script: | 89 | github.rest.repos.updateRelease({ 90 | owner: context.repo.owner, 91 | repo: context.repo.repo, 92 | release_id: process.env.release_id, 93 | draft: false, 94 | prerelease: false 95 | }) -------------------------------------------------------------------------------- /.github/workflows/tauri-action-test.yml: -------------------------------------------------------------------------------- 1 | name: 'test-on-pr' 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | test-tauri: 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | platform: [macos-latest, ubuntu-20.04, windows-latest] 16 | 17 | runs-on: ${{ matrix.platform }} 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: setup node 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: 16 24 | - name: setup pnpm 25 | uses: pnpm/action-setup@v2 26 | with: 27 | version: 8.6 28 | - name: install Rust stable 29 | uses: dtolnay/rust-toolchain@stable 30 | - name: install dependencies (ubuntu only) 31 | if: matrix.platform == 'ubuntu-20.04' 32 | run: | 33 | sudo apt-get update 34 | sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf 35 | - name: install frontend dependencies 36 | run: pnpm install 37 | - uses: tauri-apps/tauri-action@v0 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Vue.volar", 4 | "tauri-apps.tauri-vscode", 5 | "rust-lang.rust-analyzer" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /README-CN.md: -------------------------------------------------------------------------------- 1 | # sd-tagtool 2 | 3 | 这是一个简单的 stable diffusion 数据集标签编辑器。可用于编辑自动标签工具生成的数据集。这个工具受到了 [BooruDatasetTagManager](https://github.com/starik222/BooruDatasetTagManager) 的启发。 4 | 5 | ## 特性 6 | 7 | - 支持所有图像标签的显示和管理 8 | - 支持撤销/重做 9 | - 标签输入智能提示(支持模糊匹配) 10 | - 支持拖拽标签 11 | - 可批量插入/删除标签,可指定插入位置 12 | - 可通过标签过滤数据集 13 | - 可自定义的标签高亮 14 | - 自动翻译(现在写死了翻译到中文,并且可能需要魔法上网) 15 | - 反应很快 16 | 17 | ## 截图 18 | ![screenshot.png](images/screenshot.webp) 19 | 20 | ## 下载 / 安装 21 | 22 | sd-tagtool 支持 Windows,macOS 和 Linux。安装步骤如下: 23 | 1. 在 [Release](https://github.com/skiars/sd-tagtool/releases) 页面找到最新的版本; 24 | 2. 依据操作系统从 **Assets** 列表中下载安装包文件,例如 Windows 安装包的后缀是 **_.msi_** 或者 **_.exe_**; 25 | 3. 运行安装文件。 26 | 27 | **备注**:sd-tagtool 可能需要较新的 Windows 10 或者 Windows 11 才能运行。此外,我较少测试 Linux 和 MacOS 的兼容情况。 28 | 29 | ## 使用方法 30 | 31 | 基本的用法大家可以自己尝试,这里只补充几个细节。 32 | 33 | ### 基本的标签编辑 34 | 35 | 当你选中一个图片之后,拖拽图片的标签即可排序。点击标签上的 `×` 图标即可删除。不过当你选择了多张图片之后就不能再对标签进行排序了。 36 | 37 | 你还可以打开 *edit all tags* 开关,这样可以: 38 | - 删除整个数据集中的标签(全部标签显示在窗口下方),例如删除全部标签中的 "1girl" 会导致所有图片中的 "1girl" 标签被删除; 39 | - 插入或替换标签时会操作数据集的所有图片,而不只是选中的图片。 40 | 41 | ### 标签高亮 42 | 43 | 你可以在标签的的上下文菜单(点击鼠标右键)的 *Pick color* 为标签选择一个醒目的颜色,这样就可以快速找到它。当你不需要标签高亮时可以通过上下文菜单的 *Clear picked color* 来清除。 44 | 45 | ### 选择标签 46 | 47 | 在标签列表中用鼠标左键点击某个标签即可选择该标签。按住 `Ctrl` 键可以选择多个标签,按住 `Shift` 可以按范围选中多个标签。选中后的标签可以通过右键菜单来拷贝或者添加到过滤器。 48 | 49 | ### 标签过滤 50 | 51 | 在顶部的标签过滤栏中填写需要过滤的标签,然后点击 *Filter* 按钮即可过滤数据集。通过 *exclude* 复选框可以选择两种过滤模式: 52 | - **包含模式**:图片具有过滤器中的所有标签时会在过滤后的列表中展示; 53 | - **排除模式**:图片不具有过滤器中的所有标签时会在过滤后的列表中展示。 54 | 55 | 当你编辑了数据集之后,需要重新点击 *Filter* 按钮来更新过滤后的数据集。你可以手动输入标签,也可以在标签列表中点击鼠标右键并通过 *Add filter* 菜单将选中的标签添加到过滤器中。 56 | 57 | ### 插入标签 58 | 59 | 在标签编辑栏的输入框中输入标签然后点 *Insert* 按钮即可将新的标签插入到选中的数据集中(你可以选择多张图片)。如你所见标签输入框中可以填写多个 tag。 60 | 61 | 插入的位置由 *position* 输入框决定。目前支持 3 种模式: 62 | - **auto**:如果图片中不存在要插入的标签就将该标签插到末尾,否则什么也不做; 63 | - **正数**:把标签插入到从头部往后数的第几个位置,如果图片已经存在该标签则会将其移动到指定位置; 64 | - **负数**:和正数类似,不过是从尾部往前数。 65 | 66 | 插入位置可以超出图片实际的标签数量,此时会将标签插到头部或尾部位置。 67 | 68 | 直接在全部标签列表中双击某个标签也会将其插入到选中图片的标签集中。此时的插入位置也是由 *position* 参数决定的。 69 | 70 | ### 替换标签 71 | 72 | 点击标签编辑栏左侧的 `>` 按钮可以打开标签替换栏。在 *replace with* 输入框中输入用于替换的标签,点击 *Replace* 按钮即可替换标签。标签的替换是一一对应的,例如: 73 | - `a,b,c` 替换为 `d,e,f` 表示 `a` 替换为 `d`,`b` 替换为 `e`,`c` 替换为 `f`; 74 | - `a,b,c` 替换为 `d,e` 表示 `a` 替换为 `d`,`b` 替换为 `e`,`f` 删除; 75 | - `a,b` 替换为 `d,e,f` 表示 `a` 替换为 `d`,`b` 替换为 `e,f`。 76 | 77 | 每个标签的具体替换过程是: 78 | - 替换后的标签会出现在原标签的位置; 79 | - 如果替换后的标签已经存在于图片的标签列表中,会去除后面的重复项; 80 | - 如果图片没有原标签则不执行对应的替换; 81 | - 你还可以利用将标签替换为空来删除标签。 82 | 83 | ### 撤销 / 重做 84 | 85 | 点击 *Edit* 菜单中的 *Undo* 和 *Redo* 可以进行撤销和重做,还可以使用对应的快捷键来操作。 86 | 87 | 撤销和重做都没有步数限制,但是目前没有比较合理的交互反馈(这会导致你可能不知道发生了什么操作)。另外,打开新的目录之后会清除撤销历史记录。 88 | 89 | ### 翻译 90 | 91 | 点 *View* 的 *Translate tags* 菜单可以打开或关闭自动翻译。 92 | 93 | ### 删除孤立 txt 文件 94 | 95 | 当你删除了数据集文件夹中的图片文件之后,可以使用 *Tools* 菜单下的 *Delete isolated txt* 删除残留的 *\*.txt* 文件。该命令会弹出一个对话框让你确认需要删除的文件。 96 | 97 | ## 开发 98 | 99 | 本项目基于 [Tauri](https://tauri.app/) 和 [Vue.js](https://vuejs.org/) 开发。构建此项目需要先安装 Nojde.js 和 pnpm,在 Windows 上可以使用以下命令来安装: 100 | ``` bash 101 | winget install nodejs # 安装 Node.js 102 | winget install pnpm # 安装 pnpm 103 | ``` 104 | 然后参考此[文档](https://tauri.app/v1/guides/getting-started/prerequisites) 配置 Rust 工具链和 WebView2。如果你使用 Windows,那么也可以用 winget 来安装 rustup: 105 | ``` bash 106 | winget install rustup 107 | ``` 108 | 109 | 一切妥当之后使用以下命令来构建: 110 | ``` bash 111 | pnpm install # 安装项目所有的依赖项 112 | pnpm tauri dev # 构建调试版本程序并启动 113 | ``` 114 | 据我测试,Linux 桌面下 `pnpm tauri dev` 不正常,只能用 Windows 或者 macOS 来调试。 115 | 116 | 使用以下命令构建发布版本: 117 | ``` bash 118 | pnpm tauri build 119 | ``` 120 | 121 | 面向用户的安装包由 GitHub Action 自动构建,具体的配置在[这里](.github/workflows). 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sd-tagtool 2 | 3 | [中文看这里](README-CN.md) 4 | 5 | This is a simple tag editor for stable diffusion datasets. It can be used to edit datasets generated by automatic labeling tools. This tool is inspired by [BooruDatasetTagManager](https://github.com/starik222/BooruDatasetTagManager). 6 | 7 | ### Features 8 | 9 | - Support display and management of all image tags 10 | - Support undo/redo 11 | - Intelligent prompt for tag input (support fuzzy matching) 12 | - Support drag and drop for tags 13 | - Tags can be inserted/deleted in batches, and the insertion position can be specified 14 | - Datasets can be filtered by tags 15 | - Customized tag highlighting 16 | - Automatic translation (translation to Chinese is now hard-coded, and may require magic to surf the Internet) 17 | - Quick response 18 | 19 | ## Screenshot 20 | 21 | ![screenshot.png](images/screenshot.webp) 22 | 23 | ## Download / Install 24 | 25 | sd-tagtool supports Windows, macOS, and Linux. The installation steps are as follows: 26 | 1. Find the latest version on the [Release](https://github.com/skiars/sd-tagtool/releases) page; 27 | 2. Download the installation package file from the **Assets** list according to the OS, for example, the suffix of the Windows installation file is **_.msi_** or **_.exe_**; 28 | 3. Run the installation file. 29 | 30 | **Note**: sd-tagtool may require newer Windows 10 or Windows 11. Also, I don't often test for compatibility with Linux and macOS. 31 | 32 | ## Usage 33 | 34 | You can try the basic usage by yourself, only a few details are added here. 35 | 36 | ### Basic tag editing 37 | 38 | When you select a picture, drag the tag of the image to sort it, or click the `×` icon on the tag to delete. But the tags cannot be sorted when multiple images are selected. 39 | 40 | You can also turn on the *edit all tags* switch, which will: 41 | - Delete tags in the entire data set (all tags are displayed at the bottom of the window). For example, deleting "1girl" in all tags will cause the "1girl" tag in all images to be deleted; 42 | - When inserting or replacing tags, all images in the dataset will be operated, not just the selected images. 43 | 44 | ### Tag highlight 45 | 46 | You can choose a highlight color for the tag in *Pick color* of the tag's context menu (click the right mouse button), so that you can find it quickly. When you don't need the tag highlighting anymore, you can clear it by *Clear picked color* in the context menu. 47 | 48 | ### Select tags 49 | 50 | Click a tag with the left mouse button in the tag list to select it. Hold down the `Ctrl` key to select multiple tags, and hold down `Shift` to select multiple tags by range. The selected tags can be copied or added to the filter through the content menu. 51 | 52 | ### Tags filter 53 | 54 | Enter the tags to be filtered in the tags filter input at the top, and then click the *Filter* button to filter the dataset. Two filtering modes can be selected via the *exclude* checkbox: 55 | - **Include mode**: when the image has all the tags in the filter, it will be displayed in the filtered list; 56 | - **Exclude mode**: Images that do not have all the tags in the filter will be shown in the filtered list. 57 | 58 | After editing the dataset, you need to click the *Filter* button again to update the filtered dataset. You can enter tags manually, or right-click in the tags list and add selected tags to the filter via the *Add filter* menu. 59 | 60 | ### Insert tags 61 | 62 | Enter a tag in the tags input box and click the *Insert* button to insert a new tag into the selected dataset (you can select multiple images). As you can see tags input box can fill in multiple tags. 63 | 64 | The insertion position is specified by the *position* box. These modes are currently supported: 65 | - **auto**: Insert tags to tail if there is no label to be inserted in the image, otherwise do nothing; 66 | - **Positive number**: Insert tags into the position counting from the head, if the tag already exists in the image, it will be moved to the specified position; 67 | - **Negative number**: Similar to positive numbers, but counting from the tail to the front. 68 | 69 | The insertion position can exceed the actual number of tags in the image, and the tags will be inserted at the head or tail position at this time. 70 | 71 | Double-clicking a tag directly in the list of all labels will also insert it into the tag set of the selected images. The insertion position at this time is also determined by the *position*. 72 | 73 | ### Replace tags 74 | 75 | Click the `>` button on the left side of the tag editing bar to open the tag replacement bar. Enter the tags for replacement in the *replace with* input box, and click the *Replace* button to replace the tags. Tag replacement is one-to-one correspondence, for example: 76 | - Replace `a,b,c` with `d,e,f` means: `a` is replaced by `d`, `b` is replaced by `e`, `c` is replaced by `f`; 77 | - Replace `a,b,c` with `d,e` means: `a` is replaced with `d`, `b` is replaced with `e`, `f` is deleted; 78 | - Replace `a,b` with `d,e,f` means: `a` replaced with `d` and `b` replaced with `e,f`. 79 | 80 | The specific replacement process for each tag is: 81 | - The replaced tag will appear in the position of the original tag; 82 | - If the replaced tag already exists in the image tag list, subsequent duplicates will be removed; 83 | - If the image does not have the original tag, the corresponding replacement will not be performed; 84 | - You can also delete tags by replacing them with nothing. 85 | 86 | ### Undo / Redo 87 | 88 | Click *Undo* and *Redo* in the *Edit* menu to undo and redo, and you can also use the shortcut keys to do it. 89 | 90 | There is no step limit for undo and redo, but there is currently no reasonable interactive feedback (this will lead to you may not know what happened). Also, the undo history is cleared after opening a new directory. 91 | 92 | ### Translation 93 | 94 | Click the *Translate tags* menu of *View* to enable automatic translation. 95 | 96 | ### Delete isolated txt files 97 | 98 | After delete the image files in the dataset folder, you can delete the remaining *\*.txt* files using *Delete isolated txt* in the *Tools* menu. The command will pop up a dialog to confirm the files that need to be deleted. 99 | 100 | ## Development 101 | 102 | This project is based on [Tauri](https://tauri.app/) and [Vue.js](https://vuejs.org/). To build this project, you need to install Nojde.js and pnpm. Use these commands to install to Windows: 103 | ``` bash 104 | winget install nodejs # install Node.js 105 | winget install pnpm # install pnpm 106 | ``` 107 | Then follow this [document](https://tauri.app/v1/guides/getting-started/prerequisites) to configure the Rust toolchain and WebView2. You can also install rustup with winget, if you use Windows: 108 | ``` bash 109 | winget install rustup 110 | ``` 111 | 112 | Use the following commands to build when everything is in place: 113 | ``` bash 114 | pnpm install # install all dependencies 115 | pnpm tauri dev # build the debug program and start 116 | ``` 117 | According to my test, `pnpm tauri dev` is abnormal under Linux desktop, and can only be debugged with Windows or macOS. 118 | 119 | Build the release version with the these command: 120 | ``` bash 121 | pnpm tauri build 122 | ``` 123 | 124 | The user-facing installation package is automatically built by GitHub Action, and the specific configuration is [here](.github/workflows). 125 | -------------------------------------------------------------------------------- /images/screenshot.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skiars/sd-tagtool/70e3e07125ea7d550322fd22ad7ffe064c61cffc/images/screenshot.webp -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | sd-tagtool 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sd-tagtool", 3 | "private": true, 4 | "version": "1.2.4", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc --noEmit && vite build", 9 | "preview": "vite preview", 10 | "tauri": "tauri" 11 | }, 12 | "dependencies": { 13 | "@tauri-apps/api": "^1.5.3", 14 | "fuse.js": "^6.6.2", 15 | "primeicons": "^6.0.1", 16 | "primevue": "^3.45.0", 17 | "vue": "^3.3.13", 18 | "vue-router": "^4.2.5", 19 | "vuedraggable": "^4.1.0" 20 | }, 21 | "devDependencies": { 22 | "@tauri-apps/cli": "^1.5.9", 23 | "@types/node": "^18.19.3", 24 | "@vitejs/plugin-vue": "^4.6.0", 25 | "typescript": "^4.9.5", 26 | "vite": "^4.5.1", 27 | "vue-tsc": "^1.8.26" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | dependencies: 8 | '@tauri-apps/api': 9 | specifier: ^1.5.3 10 | version: 1.5.3 11 | fuse.js: 12 | specifier: ^6.6.2 13 | version: 6.6.2 14 | primeicons: 15 | specifier: ^6.0.1 16 | version: 6.0.1 17 | primevue: 18 | specifier: ^3.45.0 19 | version: 3.45.0(vue@3.3.13) 20 | vue: 21 | specifier: ^3.3.13 22 | version: 3.3.13(typescript@4.9.5) 23 | vue-router: 24 | specifier: ^4.2.5 25 | version: 4.2.5(vue@3.3.13) 26 | vuedraggable: 27 | specifier: ^4.1.0 28 | version: 4.1.0(vue@3.3.13) 29 | 30 | devDependencies: 31 | '@tauri-apps/cli': 32 | specifier: ^1.5.9 33 | version: 1.5.9 34 | '@types/node': 35 | specifier: ^18.19.3 36 | version: 18.19.3 37 | '@vitejs/plugin-vue': 38 | specifier: ^4.6.0 39 | version: 4.6.0(vite@4.5.1)(vue@3.3.13) 40 | typescript: 41 | specifier: ^4.9.5 42 | version: 4.9.5 43 | vite: 44 | specifier: ^4.5.1 45 | version: 4.5.1(@types/node@18.19.3) 46 | vue-tsc: 47 | specifier: ^1.8.26 48 | version: 1.8.26(typescript@4.9.5) 49 | 50 | packages: 51 | 52 | /@babel/helper-string-parser@7.23.4: 53 | resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} 54 | engines: {node: '>=6.9.0'} 55 | 56 | /@babel/helper-validator-identifier@7.22.20: 57 | resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} 58 | engines: {node: '>=6.9.0'} 59 | 60 | /@babel/parser@7.23.6: 61 | resolution: {integrity: sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==} 62 | engines: {node: '>=6.0.0'} 63 | hasBin: true 64 | dependencies: 65 | '@babel/types': 7.23.6 66 | 67 | /@babel/types@7.23.6: 68 | resolution: {integrity: sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==} 69 | engines: {node: '>=6.9.0'} 70 | dependencies: 71 | '@babel/helper-string-parser': 7.23.4 72 | '@babel/helper-validator-identifier': 7.22.20 73 | to-fast-properties: 2.0.0 74 | 75 | /@esbuild/android-arm64@0.18.20: 76 | resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} 77 | engines: {node: '>=12'} 78 | cpu: [arm64] 79 | os: [android] 80 | requiresBuild: true 81 | dev: true 82 | optional: true 83 | 84 | /@esbuild/android-arm@0.18.20: 85 | resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} 86 | engines: {node: '>=12'} 87 | cpu: [arm] 88 | os: [android] 89 | requiresBuild: true 90 | dev: true 91 | optional: true 92 | 93 | /@esbuild/android-x64@0.18.20: 94 | resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} 95 | engines: {node: '>=12'} 96 | cpu: [x64] 97 | os: [android] 98 | requiresBuild: true 99 | dev: true 100 | optional: true 101 | 102 | /@esbuild/darwin-arm64@0.18.20: 103 | resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} 104 | engines: {node: '>=12'} 105 | cpu: [arm64] 106 | os: [darwin] 107 | requiresBuild: true 108 | dev: true 109 | optional: true 110 | 111 | /@esbuild/darwin-x64@0.18.20: 112 | resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} 113 | engines: {node: '>=12'} 114 | cpu: [x64] 115 | os: [darwin] 116 | requiresBuild: true 117 | dev: true 118 | optional: true 119 | 120 | /@esbuild/freebsd-arm64@0.18.20: 121 | resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} 122 | engines: {node: '>=12'} 123 | cpu: [arm64] 124 | os: [freebsd] 125 | requiresBuild: true 126 | dev: true 127 | optional: true 128 | 129 | /@esbuild/freebsd-x64@0.18.20: 130 | resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} 131 | engines: {node: '>=12'} 132 | cpu: [x64] 133 | os: [freebsd] 134 | requiresBuild: true 135 | dev: true 136 | optional: true 137 | 138 | /@esbuild/linux-arm64@0.18.20: 139 | resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} 140 | engines: {node: '>=12'} 141 | cpu: [arm64] 142 | os: [linux] 143 | requiresBuild: true 144 | dev: true 145 | optional: true 146 | 147 | /@esbuild/linux-arm@0.18.20: 148 | resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} 149 | engines: {node: '>=12'} 150 | cpu: [arm] 151 | os: [linux] 152 | requiresBuild: true 153 | dev: true 154 | optional: true 155 | 156 | /@esbuild/linux-ia32@0.18.20: 157 | resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} 158 | engines: {node: '>=12'} 159 | cpu: [ia32] 160 | os: [linux] 161 | requiresBuild: true 162 | dev: true 163 | optional: true 164 | 165 | /@esbuild/linux-loong64@0.18.20: 166 | resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} 167 | engines: {node: '>=12'} 168 | cpu: [loong64] 169 | os: [linux] 170 | requiresBuild: true 171 | dev: true 172 | optional: true 173 | 174 | /@esbuild/linux-mips64el@0.18.20: 175 | resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} 176 | engines: {node: '>=12'} 177 | cpu: [mips64el] 178 | os: [linux] 179 | requiresBuild: true 180 | dev: true 181 | optional: true 182 | 183 | /@esbuild/linux-ppc64@0.18.20: 184 | resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} 185 | engines: {node: '>=12'} 186 | cpu: [ppc64] 187 | os: [linux] 188 | requiresBuild: true 189 | dev: true 190 | optional: true 191 | 192 | /@esbuild/linux-riscv64@0.18.20: 193 | resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} 194 | engines: {node: '>=12'} 195 | cpu: [riscv64] 196 | os: [linux] 197 | requiresBuild: true 198 | dev: true 199 | optional: true 200 | 201 | /@esbuild/linux-s390x@0.18.20: 202 | resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} 203 | engines: {node: '>=12'} 204 | cpu: [s390x] 205 | os: [linux] 206 | requiresBuild: true 207 | dev: true 208 | optional: true 209 | 210 | /@esbuild/linux-x64@0.18.20: 211 | resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} 212 | engines: {node: '>=12'} 213 | cpu: [x64] 214 | os: [linux] 215 | requiresBuild: true 216 | dev: true 217 | optional: true 218 | 219 | /@esbuild/netbsd-x64@0.18.20: 220 | resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} 221 | engines: {node: '>=12'} 222 | cpu: [x64] 223 | os: [netbsd] 224 | requiresBuild: true 225 | dev: true 226 | optional: true 227 | 228 | /@esbuild/openbsd-x64@0.18.20: 229 | resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} 230 | engines: {node: '>=12'} 231 | cpu: [x64] 232 | os: [openbsd] 233 | requiresBuild: true 234 | dev: true 235 | optional: true 236 | 237 | /@esbuild/sunos-x64@0.18.20: 238 | resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} 239 | engines: {node: '>=12'} 240 | cpu: [x64] 241 | os: [sunos] 242 | requiresBuild: true 243 | dev: true 244 | optional: true 245 | 246 | /@esbuild/win32-arm64@0.18.20: 247 | resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} 248 | engines: {node: '>=12'} 249 | cpu: [arm64] 250 | os: [win32] 251 | requiresBuild: true 252 | dev: true 253 | optional: true 254 | 255 | /@esbuild/win32-ia32@0.18.20: 256 | resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} 257 | engines: {node: '>=12'} 258 | cpu: [ia32] 259 | os: [win32] 260 | requiresBuild: true 261 | dev: true 262 | optional: true 263 | 264 | /@esbuild/win32-x64@0.18.20: 265 | resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} 266 | engines: {node: '>=12'} 267 | cpu: [x64] 268 | os: [win32] 269 | requiresBuild: true 270 | dev: true 271 | optional: true 272 | 273 | /@jridgewell/sourcemap-codec@1.4.15: 274 | resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} 275 | 276 | /@tauri-apps/api@1.5.3: 277 | resolution: {integrity: sha512-zxnDjHHKjOsrIzZm6nO5Xapb/BxqUq1tc7cGkFXsFkGTsSWgCPH1D8mm0XS9weJY2OaR73I3k3S+b7eSzJDfqA==} 278 | engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'} 279 | dev: false 280 | 281 | /@tauri-apps/cli-darwin-arm64@1.5.9: 282 | resolution: {integrity: sha512-7C2Jf8f0gzv778mLYb7Eszqqv1bm9Wzews81MRTqKrUIcC+eZEtDXLex+JaEkEzFEUrgIafdOvMBVEavF030IA==} 283 | engines: {node: '>= 10'} 284 | cpu: [arm64] 285 | os: [darwin] 286 | requiresBuild: true 287 | dev: true 288 | optional: true 289 | 290 | /@tauri-apps/cli-darwin-x64@1.5.9: 291 | resolution: {integrity: sha512-LHKytpkofPYgH8RShWvwDa3hD1ws131x7g7zNasJPfOiCWLqYVQFUuQVmjEUt8+dpHe/P/err5h4z+YZru2d0A==} 292 | engines: {node: '>= 10'} 293 | cpu: [x64] 294 | os: [darwin] 295 | requiresBuild: true 296 | dev: true 297 | optional: true 298 | 299 | /@tauri-apps/cli-linux-arm-gnueabihf@1.5.9: 300 | resolution: {integrity: sha512-teGK20IYKx+dVn8wFq/Lg57Q9ce7foq1KHSfyHi464LVt1T0V1rsmULSgZpQPPj/NYPF5BG78PcWYv64yH86jw==} 301 | engines: {node: '>= 10'} 302 | cpu: [arm] 303 | os: [linux] 304 | requiresBuild: true 305 | dev: true 306 | optional: true 307 | 308 | /@tauri-apps/cli-linux-arm64-gnu@1.5.9: 309 | resolution: {integrity: sha512-onJ/DW5Crw38qVx+wquY4uBbfCxVhzhdJmlCYqnYyXsZZmSiPUfSyhV58y+5TYB0q1hG8eYdB5x8VAwzByhGzw==} 310 | engines: {node: '>= 10'} 311 | cpu: [arm64] 312 | os: [linux] 313 | libc: [glibc] 314 | requiresBuild: true 315 | dev: true 316 | optional: true 317 | 318 | /@tauri-apps/cli-linux-arm64-musl@1.5.9: 319 | resolution: {integrity: sha512-23AYoLD3acakLp9NtheKQDJl8F66eTOflxoPzdJNRy13hUSxb+W9qpz4rRA+CIzkjICFvO2i3UWjeV9QwDVpsQ==} 320 | engines: {node: '>= 10'} 321 | cpu: [arm64] 322 | os: [linux] 323 | libc: [musl] 324 | requiresBuild: true 325 | dev: true 326 | optional: true 327 | 328 | /@tauri-apps/cli-linux-x64-gnu@1.5.9: 329 | resolution: {integrity: sha512-9PQA1rE7gh41W2ylyKd5qOGOds55ymaYPml9KOpM0g+cxmCXa+8Wf9K5NKvACnJldJJ6cekWzIyB4eN6o5T+yQ==} 330 | engines: {node: '>= 10'} 331 | cpu: [x64] 332 | os: [linux] 333 | libc: [glibc] 334 | requiresBuild: true 335 | dev: true 336 | optional: true 337 | 338 | /@tauri-apps/cli-linux-x64-musl@1.5.9: 339 | resolution: {integrity: sha512-5hdbNFeDsrJ/pXZ4cSQV4bJwUXPPxXxN3/pAtNUqIph7q+vLcBXOXIMoS64iuyaluJC59lhEwlWZFz+EPv0Hqg==} 340 | engines: {node: '>= 10'} 341 | cpu: [x64] 342 | os: [linux] 343 | libc: [musl] 344 | requiresBuild: true 345 | dev: true 346 | optional: true 347 | 348 | /@tauri-apps/cli-win32-arm64-msvc@1.5.9: 349 | resolution: {integrity: sha512-O18JufjSB3hSJYu5WWByONouGeX7DraLAtXLErsG1r/VS3zHd/zyuzycrVUaObNXk5bfGlIP0Ypt+RvZJILN2w==} 350 | engines: {node: '>= 10'} 351 | cpu: [arm64] 352 | os: [win32] 353 | requiresBuild: true 354 | dev: true 355 | optional: true 356 | 357 | /@tauri-apps/cli-win32-ia32-msvc@1.5.9: 358 | resolution: {integrity: sha512-FQxtxTZu0JVBihfd/lmpxo7jyMOesjWQehfyVUqtgMfm5+Pvvw0Y+ZioeDi1TZkFVrT3QDYy8R4LqDLSZVMQRA==} 359 | engines: {node: '>= 10'} 360 | cpu: [ia32] 361 | os: [win32] 362 | requiresBuild: true 363 | dev: true 364 | optional: true 365 | 366 | /@tauri-apps/cli-win32-x64-msvc@1.5.9: 367 | resolution: {integrity: sha512-EeI1+L518cIBLKw0qUFwnLIySBeSmPQjPLIlNwSukHSro4tAQPHycEVGgKrdToiCWgaZJBA0e5aRSds0Du2TWg==} 368 | engines: {node: '>= 10'} 369 | cpu: [x64] 370 | os: [win32] 371 | requiresBuild: true 372 | dev: true 373 | optional: true 374 | 375 | /@tauri-apps/cli@1.5.9: 376 | resolution: {integrity: sha512-knSt/9AvCTeyfC6wkyeouF9hBW/0Mzuw+5vBKEvzaGPQsfFJo1ZCp5FkdiZpGBBfnm09BhugasGRTGofzatfqQ==} 377 | engines: {node: '>= 10'} 378 | hasBin: true 379 | optionalDependencies: 380 | '@tauri-apps/cli-darwin-arm64': 1.5.9 381 | '@tauri-apps/cli-darwin-x64': 1.5.9 382 | '@tauri-apps/cli-linux-arm-gnueabihf': 1.5.9 383 | '@tauri-apps/cli-linux-arm64-gnu': 1.5.9 384 | '@tauri-apps/cli-linux-arm64-musl': 1.5.9 385 | '@tauri-apps/cli-linux-x64-gnu': 1.5.9 386 | '@tauri-apps/cli-linux-x64-musl': 1.5.9 387 | '@tauri-apps/cli-win32-arm64-msvc': 1.5.9 388 | '@tauri-apps/cli-win32-ia32-msvc': 1.5.9 389 | '@tauri-apps/cli-win32-x64-msvc': 1.5.9 390 | dev: true 391 | 392 | /@types/node@18.19.3: 393 | resolution: {integrity: sha512-k5fggr14DwAytoA/t8rPrIz++lXK7/DqckthCmoZOKNsEbJkId4Z//BqgApXBUGrGddrigYa1oqheo/7YmW4rg==} 394 | dependencies: 395 | undici-types: 5.26.5 396 | dev: true 397 | 398 | /@vitejs/plugin-vue@4.6.0(vite@4.5.1)(vue@3.3.13): 399 | resolution: {integrity: sha512-XHuyFdAikWRmHuAd89FOyUGIjrBU5KlxJtyi2hVeR9ySGFxQwE0bl5xAQju/ArMq5azdBivY4d+D2yPKwoYWUg==} 400 | engines: {node: ^14.18.0 || >=16.0.0} 401 | peerDependencies: 402 | vite: ^4.0.0 || ^5.0.0 403 | vue: ^3.2.25 404 | dependencies: 405 | vite: 4.5.1(@types/node@18.19.3) 406 | vue: 3.3.13(typescript@4.9.5) 407 | dev: true 408 | 409 | /@volar/language-core@1.11.1: 410 | resolution: {integrity: sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==} 411 | dependencies: 412 | '@volar/source-map': 1.11.1 413 | dev: true 414 | 415 | /@volar/source-map@1.11.1: 416 | resolution: {integrity: sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==} 417 | dependencies: 418 | muggle-string: 0.3.1 419 | dev: true 420 | 421 | /@volar/typescript@1.11.1: 422 | resolution: {integrity: sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==} 423 | dependencies: 424 | '@volar/language-core': 1.11.1 425 | path-browserify: 1.0.1 426 | dev: true 427 | 428 | /@vue/compiler-core@3.3.13: 429 | resolution: {integrity: sha512-bwi9HShGu7uaZLOErZgsH2+ojsEdsjerbf2cMXPwmvcgZfVPZ2BVZzCVnwZBxTAYd6Mzbmf6izcUNDkWnBBQ6A==} 430 | dependencies: 431 | '@babel/parser': 7.23.6 432 | '@vue/shared': 3.3.13 433 | estree-walker: 2.0.2 434 | source-map-js: 1.0.2 435 | 436 | /@vue/compiler-dom@3.3.13: 437 | resolution: {integrity: sha512-EYRDpbLadGtNL0Gph+HoKiYqXLqZ0xSSpR5Dvnu/Ep7ggaCbjRDIus1MMxTS2Qm0koXED4xSlvTZaTnI8cYAsw==} 438 | dependencies: 439 | '@vue/compiler-core': 3.3.13 440 | '@vue/shared': 3.3.13 441 | 442 | /@vue/compiler-sfc@3.3.13: 443 | resolution: {integrity: sha512-DQVmHEy/EKIgggvnGRLx21hSqnr1smUS9Aq8tfxiiot8UR0/pXKHN9k78/qQ7etyQTFj5em5nruODON7dBeumw==} 444 | dependencies: 445 | '@babel/parser': 7.23.6 446 | '@vue/compiler-core': 3.3.13 447 | '@vue/compiler-dom': 3.3.13 448 | '@vue/compiler-ssr': 3.3.13 449 | '@vue/reactivity-transform': 3.3.13 450 | '@vue/shared': 3.3.13 451 | estree-walker: 2.0.2 452 | magic-string: 0.30.5 453 | postcss: 8.4.32 454 | source-map-js: 1.0.2 455 | 456 | /@vue/compiler-ssr@3.3.13: 457 | resolution: {integrity: sha512-d/P3bCeUGmkJNS1QUZSAvoCIW4fkOKK3l2deE7zrp0ypJEy+En2AcypIkqvcFQOcw3F0zt2VfMvNsA9JmExTaw==} 458 | dependencies: 459 | '@vue/compiler-dom': 3.3.13 460 | '@vue/shared': 3.3.13 461 | 462 | /@vue/devtools-api@6.5.1: 463 | resolution: {integrity: sha512-+KpckaAQyfbvshdDW5xQylLni1asvNSGme1JFs8I1+/H5pHEhqUKMEQD/qn3Nx5+/nycBq11qAEi8lk+LXI2dA==} 464 | dev: false 465 | 466 | /@vue/language-core@1.8.26(typescript@4.9.5): 467 | resolution: {integrity: sha512-9cmza/Y2YTiOnKZ0Mi9zsNn7Irw+aKirP+5LLWVSNaL3fjKJjW1cD3HGBckasY2RuVh4YycvdA9/Q6EBpVd/7Q==} 468 | peerDependencies: 469 | typescript: '*' 470 | peerDependenciesMeta: 471 | typescript: 472 | optional: true 473 | dependencies: 474 | '@volar/language-core': 1.11.1 475 | '@volar/source-map': 1.11.1 476 | '@vue/compiler-dom': 3.3.13 477 | '@vue/shared': 3.3.13 478 | computeds: 0.0.1 479 | minimatch: 9.0.3 480 | muggle-string: 0.3.1 481 | path-browserify: 1.0.1 482 | typescript: 4.9.5 483 | vue-template-compiler: 2.7.16 484 | dev: true 485 | 486 | /@vue/reactivity-transform@3.3.13: 487 | resolution: {integrity: sha512-oWnydGH0bBauhXvh5KXUy61xr9gKaMbtsMHk40IK9M4gMuKPJ342tKFarY0eQ6jef8906m35q37wwA8DMZOm5Q==} 488 | dependencies: 489 | '@babel/parser': 7.23.6 490 | '@vue/compiler-core': 3.3.13 491 | '@vue/shared': 3.3.13 492 | estree-walker: 2.0.2 493 | magic-string: 0.30.5 494 | 495 | /@vue/reactivity@3.3.13: 496 | resolution: {integrity: sha512-fjzCxceMahHhi4AxUBzQqqVhuA21RJ0COaWTbIBl1PruGW1CeY97louZzLi4smpYx+CHfFPPU/CS8NybbGvPKQ==} 497 | dependencies: 498 | '@vue/shared': 3.3.13 499 | 500 | /@vue/runtime-core@3.3.13: 501 | resolution: {integrity: sha512-1TzA5TvGuh2zUwMJgdfvrBABWZ7y8kBwBhm7BXk8rvdx2SsgcGfz2ruv2GzuGZNvL1aKnK8CQMV/jFOrxNQUMA==} 502 | dependencies: 503 | '@vue/reactivity': 3.3.13 504 | '@vue/shared': 3.3.13 505 | 506 | /@vue/runtime-dom@3.3.13: 507 | resolution: {integrity: sha512-JJkpE8R/hJKXqVTgUoODwS5wqKtOsmJPEqmp90PDVGygtJ4C0PtOkcEYXwhiVEmef6xeXcIlrT3Yo5aQ4qkHhQ==} 508 | dependencies: 509 | '@vue/runtime-core': 3.3.13 510 | '@vue/shared': 3.3.13 511 | csstype: 3.1.3 512 | 513 | /@vue/server-renderer@3.3.13(vue@3.3.13): 514 | resolution: {integrity: sha512-vSnN+nuf6iSqTL3Qgx/9A+BT+0Zf/VJOgF5uMZrKjYPs38GMYyAU1coDyBNHauehXDaP+zl73VhwWv0vBRBHcg==} 515 | peerDependencies: 516 | vue: 3.3.13 517 | dependencies: 518 | '@vue/compiler-ssr': 3.3.13 519 | '@vue/shared': 3.3.13 520 | vue: 3.3.13(typescript@4.9.5) 521 | 522 | /@vue/shared@3.3.13: 523 | resolution: {integrity: sha512-/zYUwiHD8j7gKx2argXEMCUXVST6q/21DFU0sTfNX0URJroCe3b1UF6vLJ3lQDfLNIiiRl2ONp7Nh5UVWS6QnA==} 524 | 525 | /balanced-match@1.0.2: 526 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 527 | dev: true 528 | 529 | /brace-expansion@2.0.1: 530 | resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 531 | dependencies: 532 | balanced-match: 1.0.2 533 | dev: true 534 | 535 | /computeds@0.0.1: 536 | resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==} 537 | dev: true 538 | 539 | /csstype@3.1.3: 540 | resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} 541 | 542 | /de-indent@1.0.2: 543 | resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} 544 | dev: true 545 | 546 | /esbuild@0.18.20: 547 | resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} 548 | engines: {node: '>=12'} 549 | hasBin: true 550 | requiresBuild: true 551 | optionalDependencies: 552 | '@esbuild/android-arm': 0.18.20 553 | '@esbuild/android-arm64': 0.18.20 554 | '@esbuild/android-x64': 0.18.20 555 | '@esbuild/darwin-arm64': 0.18.20 556 | '@esbuild/darwin-x64': 0.18.20 557 | '@esbuild/freebsd-arm64': 0.18.20 558 | '@esbuild/freebsd-x64': 0.18.20 559 | '@esbuild/linux-arm': 0.18.20 560 | '@esbuild/linux-arm64': 0.18.20 561 | '@esbuild/linux-ia32': 0.18.20 562 | '@esbuild/linux-loong64': 0.18.20 563 | '@esbuild/linux-mips64el': 0.18.20 564 | '@esbuild/linux-ppc64': 0.18.20 565 | '@esbuild/linux-riscv64': 0.18.20 566 | '@esbuild/linux-s390x': 0.18.20 567 | '@esbuild/linux-x64': 0.18.20 568 | '@esbuild/netbsd-x64': 0.18.20 569 | '@esbuild/openbsd-x64': 0.18.20 570 | '@esbuild/sunos-x64': 0.18.20 571 | '@esbuild/win32-arm64': 0.18.20 572 | '@esbuild/win32-ia32': 0.18.20 573 | '@esbuild/win32-x64': 0.18.20 574 | dev: true 575 | 576 | /estree-walker@2.0.2: 577 | resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} 578 | 579 | /fsevents@2.3.3: 580 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 581 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 582 | os: [darwin] 583 | requiresBuild: true 584 | dev: true 585 | optional: true 586 | 587 | /fuse.js@6.6.2: 588 | resolution: {integrity: sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==} 589 | engines: {node: '>=10'} 590 | dev: false 591 | 592 | /he@1.2.0: 593 | resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} 594 | hasBin: true 595 | dev: true 596 | 597 | /lru-cache@6.0.0: 598 | resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} 599 | engines: {node: '>=10'} 600 | dependencies: 601 | yallist: 4.0.0 602 | dev: true 603 | 604 | /magic-string@0.30.5: 605 | resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} 606 | engines: {node: '>=12'} 607 | dependencies: 608 | '@jridgewell/sourcemap-codec': 1.4.15 609 | 610 | /minimatch@9.0.3: 611 | resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} 612 | engines: {node: '>=16 || 14 >=14.17'} 613 | dependencies: 614 | brace-expansion: 2.0.1 615 | dev: true 616 | 617 | /muggle-string@0.3.1: 618 | resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==} 619 | dev: true 620 | 621 | /nanoid@3.3.7: 622 | resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} 623 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 624 | hasBin: true 625 | 626 | /path-browserify@1.0.1: 627 | resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} 628 | dev: true 629 | 630 | /picocolors@1.0.0: 631 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} 632 | 633 | /postcss@8.4.32: 634 | resolution: {integrity: sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==} 635 | engines: {node: ^10 || ^12 || >=14} 636 | dependencies: 637 | nanoid: 3.3.7 638 | picocolors: 1.0.0 639 | source-map-js: 1.0.2 640 | 641 | /primeicons@6.0.1: 642 | resolution: {integrity: sha512-KDeO94CbWI4pKsPnYpA1FPjo79EsY9I+M8ywoPBSf9XMXoe/0crjbUK7jcQEDHuc0ZMRIZsxH3TYLv4TUtHmAA==} 643 | dev: false 644 | 645 | /primevue@3.45.0(vue@3.3.13): 646 | resolution: {integrity: sha512-qtA2YDi6SSBkGRb33lUXL2WnzMd7VZd4IC/YLXiUZLnxCGxqtB4iYj+jGqINNDcIFVqIYYMrc1UhiX+C68GQEQ==} 647 | peerDependencies: 648 | vue: ^3.0.0 649 | dependencies: 650 | vue: 3.3.13(typescript@4.9.5) 651 | dev: false 652 | 653 | /rollup@3.29.4: 654 | resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==} 655 | engines: {node: '>=14.18.0', npm: '>=8.0.0'} 656 | hasBin: true 657 | optionalDependencies: 658 | fsevents: 2.3.3 659 | dev: true 660 | 661 | /semver@7.5.4: 662 | resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} 663 | engines: {node: '>=10'} 664 | hasBin: true 665 | dependencies: 666 | lru-cache: 6.0.0 667 | dev: true 668 | 669 | /sortablejs@1.14.0: 670 | resolution: {integrity: sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==} 671 | dev: false 672 | 673 | /source-map-js@1.0.2: 674 | resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} 675 | engines: {node: '>=0.10.0'} 676 | 677 | /to-fast-properties@2.0.0: 678 | resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} 679 | engines: {node: '>=4'} 680 | 681 | /typescript@4.9.5: 682 | resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} 683 | engines: {node: '>=4.2.0'} 684 | hasBin: true 685 | 686 | /undici-types@5.26.5: 687 | resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} 688 | dev: true 689 | 690 | /vite@4.5.1(@types/node@18.19.3): 691 | resolution: {integrity: sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==} 692 | engines: {node: ^14.18.0 || >=16.0.0} 693 | hasBin: true 694 | peerDependencies: 695 | '@types/node': '>= 14' 696 | less: '*' 697 | lightningcss: ^1.21.0 698 | sass: '*' 699 | stylus: '*' 700 | sugarss: '*' 701 | terser: ^5.4.0 702 | peerDependenciesMeta: 703 | '@types/node': 704 | optional: true 705 | less: 706 | optional: true 707 | lightningcss: 708 | optional: true 709 | sass: 710 | optional: true 711 | stylus: 712 | optional: true 713 | sugarss: 714 | optional: true 715 | terser: 716 | optional: true 717 | dependencies: 718 | '@types/node': 18.19.3 719 | esbuild: 0.18.20 720 | postcss: 8.4.32 721 | rollup: 3.29.4 722 | optionalDependencies: 723 | fsevents: 2.3.3 724 | dev: true 725 | 726 | /vue-router@4.2.5(vue@3.3.13): 727 | resolution: {integrity: sha512-DIUpKcyg4+PTQKfFPX88UWhlagBEBEfJ5A8XDXRJLUnZOvcpMF8o/dnL90vpVkGaPbjvXazV/rC1qBKrZlFugw==} 728 | peerDependencies: 729 | vue: ^3.2.0 730 | dependencies: 731 | '@vue/devtools-api': 6.5.1 732 | vue: 3.3.13(typescript@4.9.5) 733 | dev: false 734 | 735 | /vue-template-compiler@2.7.16: 736 | resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==} 737 | dependencies: 738 | de-indent: 1.0.2 739 | he: 1.2.0 740 | dev: true 741 | 742 | /vue-tsc@1.8.26(typescript@4.9.5): 743 | resolution: {integrity: sha512-jMEJ4aqU/l1hdgmeExH5h1TFoN+hbho0A2ZAhHy53/947DGm7Qj/bpB85VpECOCwV00h7JYNVnvoD2ceOorB4Q==} 744 | hasBin: true 745 | peerDependencies: 746 | typescript: '*' 747 | dependencies: 748 | '@volar/typescript': 1.11.1 749 | '@vue/language-core': 1.8.26(typescript@4.9.5) 750 | semver: 7.5.4 751 | typescript: 4.9.5 752 | dev: true 753 | 754 | /vue@3.3.13(typescript@4.9.5): 755 | resolution: {integrity: sha512-LDnUpQvDgsfc0u/YgtAgTMXJlJQqjkxW1PVcOnJA5cshPleULDjHi7U45pl2VJYazSSvLH8UKcid/kzH8I0a0Q==} 756 | peerDependencies: 757 | typescript: '*' 758 | peerDependenciesMeta: 759 | typescript: 760 | optional: true 761 | dependencies: 762 | '@vue/compiler-dom': 3.3.13 763 | '@vue/compiler-sfc': 3.3.13 764 | '@vue/runtime-dom': 3.3.13 765 | '@vue/server-renderer': 3.3.13(vue@3.3.13) 766 | '@vue/shared': 3.3.13 767 | typescript: 4.9.5 768 | 769 | /vuedraggable@4.1.0(vue@3.3.13): 770 | resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==} 771 | peerDependencies: 772 | vue: ^3.0.1 773 | dependencies: 774 | sortablejs: 1.14.0 775 | vue: 3.3.13(typescript@4.9.5) 776 | dev: false 777 | 778 | /yallist@4.0.0: 779 | resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} 780 | dev: true 781 | -------------------------------------------------------------------------------- /public/tauri.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sd-tagtool" 3 | version = "1.2.4" 4 | description = "A stable diffusion training dataset tag editor." 5 | authors = ["skiars"] 6 | license = "" 7 | repository = "" 8 | edition = "2021" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [build-dependencies] 13 | tauri-build = { version = "1.5.1", features = [] } 14 | 15 | [dependencies] 16 | tauri = { version = "1.5.4", features = [ "process-exit", "window-close", "window-set-title", "os-all", "protocol-asset", "path-all", "dialog-all", "fs-all", "shell-open"] } 17 | serde = { version = "1.0", features = ["derive"] } 18 | serde_json = "1.0" 19 | reqwest = { version = "0.11" } 20 | tokio = { version = "1.28.2", features = ["full"] } 21 | regex = "1.8.4" 22 | lazy_static = "1.4.0" 23 | futures = "0.3.28" 24 | csv = "1.2.2" 25 | simsearch = "0.2.4" 26 | 27 | [features] 28 | # this feature is used for production builds or when `devPath` points to the filesystem 29 | # DO NOT REMOVE!! 30 | custom-protocol = ["tauri/custom-protocol"] 31 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skiars/sd-tagtool/70e3e07125ea7d550322fd22ad7ffe064c61cffc/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skiars/sd-tagtool/70e3e07125ea7d550322fd22ad7ffe064c61cffc/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skiars/sd-tagtool/70e3e07125ea7d550322fd22ad7ffe064c61cffc/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skiars/sd-tagtool/70e3e07125ea7d550322fd22ad7ffe064c61cffc/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skiars/sd-tagtool/70e3e07125ea7d550322fd22ad7ffe064c61cffc/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skiars/sd-tagtool/70e3e07125ea7d550322fd22ad7ffe064c61cffc/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skiars/sd-tagtool/70e3e07125ea7d550322fd22ad7ffe064c61cffc/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skiars/sd-tagtool/70e3e07125ea7d550322fd22ad7ffe064c61cffc/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skiars/sd-tagtool/70e3e07125ea7d550322fd22ad7ffe064c61cffc/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skiars/sd-tagtool/70e3e07125ea7d550322fd22ad7ffe064c61cffc/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skiars/sd-tagtool/70e3e07125ea7d550322fd22ad7ffe064c61cffc/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skiars/sd-tagtool/70e3e07125ea7d550322fd22ad7ffe064c61cffc/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skiars/sd-tagtool/70e3e07125ea7d550322fd22ad7ffe064c61cffc/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skiars/sd-tagtool/70e3e07125ea7d550322fd22ad7ffe064c61cffc/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skiars/sd-tagtool/70e3e07125ea7d550322fd22ad7ffe064c61cffc/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skiars/sd-tagtool/70e3e07125ea7d550322fd22ad7ffe064c61cffc/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/shared/tags.db/quality.csv: -------------------------------------------------------------------------------- 1 | masterpiece 2 | best quality -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | mod translate; 5 | mod tagutils; 6 | 7 | use std::fs; 8 | use std::path::PathBuf; 9 | use std::sync::Mutex; 10 | use std::collections::HashSet; 11 | use tauri::{AppHandle, CustomMenuItem, Manager, Menu, 12 | MenuItem, Runtime, State, Submenu, WindowMenuEvent}; 13 | use translate::{TranslateCache, translate}; 14 | use tagutils::{QueryTag, TagData, TagHint, TagHintDB}; 15 | 16 | #[derive(Default)] 17 | struct CmdState { 18 | translate_enabled: Mutex, 19 | preview_enabled: Mutex, 20 | translate_cache: TranslateCache, 21 | tags_db: Mutex, 22 | } 23 | 24 | #[tauri::command] 25 | fn listdir(path: &str) -> Vec { 26 | tagutils::listdir_images(&path).iter() 27 | .filter_map(|e| tagutils::read_tags(e, path)) 28 | .collect::>() 29 | } 30 | 31 | #[tauri::command] 32 | fn list_isolated_txt(path: &str) -> Vec { 33 | let images: HashSet = HashSet::from_iter( 34 | tagutils::listdir_images(path).iter().map(|e| 35 | PathBuf::from(e).file_stem().unwrap().to_str().unwrap().to_string() 36 | )); 37 | tagutils::listdir_files(path).iter() 38 | .filter_map(|abs_path| { 39 | abs_path.strip_prefix(&path).ok().and_then(|path| 40 | tagutils::is_isolated_txt(&images, &path).then(|| ()).and( 41 | path.to_str().map(|e| e.to_string()) 42 | ) 43 | ) 44 | }) 45 | .collect::>() 46 | } 47 | 48 | #[tauri::command] 49 | fn save_tags(path: &str, text: &str) -> bool { 50 | let mut pb = PathBuf::from(path); 51 | pb.set_extension("txt"); 52 | fs::write(pb, text).is_ok() 53 | } 54 | 55 | #[tauri::command] 56 | fn parse_tags(text: &str) -> Vec { 57 | tagutils::parse_tags(text) 58 | } 59 | 60 | #[tauri::command] 61 | async fn translate_tag(text: &str, tl: &str, state: State<'_, CmdState>) -> Result { 62 | Ok(translate(&state.translate_cache, tl, text).await) 63 | } 64 | 65 | #[tauri::command] 66 | fn query_tag(text: &str, state: State<'_, CmdState>) -> Vec { 67 | let db = state.tags_db.lock().unwrap(); 68 | let matched = db.search.search(text); 69 | matched.iter().take(20).map(|tag| { 70 | let hint = db.database.get(tag).unwrap(); 71 | match hint { 72 | TagHint::Just(x) => QueryTag { 73 | tag: tag.clone(), 74 | suggest: None, 75 | usage_count: Some(x.clone()), 76 | }, 77 | TagHint::Alias(x) => QueryTag { 78 | tag: tag.clone(), 79 | suggest: Some(x.clone()), 80 | usage_count: None, 81 | } 82 | } 83 | }).collect::>() 84 | } 85 | 86 | #[tauri::command] 87 | async fn load_tags_db(app: AppHandle) -> Result<(), ()> { 88 | let tags_db_path = app.path_resolver() 89 | .resolve_resource("shared/tags.db") 90 | .expect("failed to resolve tags.db path"); 91 | let state: State = app.state(); 92 | let mut db = state.tags_db.lock().unwrap(); 93 | *db = TagHintDB::new(); // clean tag records 94 | db.read_db(tags_db_path); 95 | Ok(()) 96 | } 97 | 98 | #[tauri::command] 99 | fn load_config(app: AppHandle) -> Result { 100 | let resolver = app.path_resolver(); 101 | let path = resolver.app_config_dir().map( 102 | |e| e.join("config.json")).expect("failed to resolve config path"); 103 | let rdr = fs::File::open(path).or(Err(()))?; 104 | serde_json::from_reader(rdr).or(Err(())) 105 | } 106 | 107 | #[tauri::command] 108 | fn save_config(model: serde_json::Value, app: AppHandle) { 109 | let resolver = app.path_resolver(); 110 | let dir = resolver.app_config_dir().expect("failed to resolve config path"); 111 | fs::create_dir_all(&dir).unwrap_or(()); 112 | fs::File::create(dir.join("config.json")).and_then(|f| { 113 | serde_json::to_writer_pretty(f, &model).unwrap_or(()); 114 | Ok(()) 115 | }).unwrap_or(()); 116 | } 117 | 118 | #[tauri::command] 119 | async fn refresh_cache(state: State<'_, CmdState>) -> Result<(), ()> { 120 | state.translate_cache.lock().await.clear(); 121 | Ok(()) 122 | } 123 | 124 | fn window_menu() -> Menu { 125 | // here `"quit".to_string()` defines the menu item id, and the second parameter is the menu item label. 126 | let open = CustomMenuItem::new("open".to_string(), "Open folder") 127 | .accelerator("CmdOrCtrl+O"); 128 | let save = CustomMenuItem::new("save".to_string(), "Save") 129 | .accelerator("CmdOrCtrl+S"); 130 | let reload = CustomMenuItem::new("reload".to_string(), "Reload folder") 131 | .accelerator("CmdOrCtrl+R"); 132 | let quit = CustomMenuItem::new("quit".to_string(), "Quit") 133 | .accelerator("CmdOrCtrl+Q"); 134 | let settings = CustomMenuItem::new("settings".to_string(), "Settings"); 135 | let file = Submenu::new("File", Menu::new() 136 | .add_item(open).add_item(save).add_item(reload) 137 | .add_native_item(MenuItem::Separator).add_item(settings) 138 | .add_native_item(MenuItem::Separator).add_item(quit)); 139 | 140 | let undo = CustomMenuItem::new("undo".to_string(), "Undo") 141 | .accelerator("CmdOrCtrl+Z"); 142 | let redo = CustomMenuItem::new("redo".to_string(), "Redo") 143 | .accelerator("CmdOrCtrl+Shift+Z"); 144 | let edit = Submenu::new("Edit", Menu::new() 145 | .add_item(undo).add_item(redo)); 146 | 147 | let trans = CustomMenuItem::new("translate".to_string(), "Translate tags"); 148 | let preview = CustomMenuItem::new("preview".to_string(), "Image Preview"); 149 | let view = Submenu::new("View", Menu::new() 150 | .add_item(trans).add_item(preview)); 151 | 152 | let delete_txt = CustomMenuItem::new("delete_txt".to_string(), "Delete isolated txt"); 153 | let tools = Submenu::new("Tools", Menu::new().add_item(delete_txt)); 154 | 155 | Menu::new().add_submenu(file).add_submenu(edit).add_submenu(view).add_submenu(tools) 156 | } 157 | 158 | fn handle_menu(event: WindowMenuEvent) { 159 | match event.menu_item_id() { 160 | "quit" => { event.window().emit("menu", "quit").unwrap(); } 161 | "open" => { event.window().emit("menu", "open").unwrap(); } 162 | "save" => { event.window().emit("menu", "save").unwrap(); } 163 | "reload" => { event.window().emit("menu", "reload").unwrap(); } 164 | "undo" => { event.window().emit("menu", "undo").unwrap(); } 165 | "redo" => { event.window().emit("menu", "redo").unwrap(); } 166 | "settings" => { event.window().emit("menu", "settings").unwrap(); } 167 | "delete_txt" => { event.window().emit("menu", "delete_txt").unwrap(); } 168 | "translate" => { 169 | let state: State = event.window().state(); 170 | let tr = !*state.translate_enabled.lock().unwrap(); 171 | *state.translate_enabled.lock().unwrap() = tr; 172 | let menu = event.window().menu_handle().get_item("translate"); 173 | menu.set_selected(tr).unwrap(); 174 | event.window().emit("translate", tr).unwrap(); 175 | } 176 | "preview" => { 177 | let state: State = event.window().state(); 178 | let s = !*state.preview_enabled.lock().unwrap(); 179 | *state.preview_enabled.lock().unwrap() = s; 180 | let menu = event.window().menu_handle().get_item("preview"); 181 | menu.set_selected(s).unwrap(); 182 | event.window().emit("preview", s).unwrap(); 183 | } 184 | _ => {} 185 | } 186 | } 187 | 188 | fn main() { 189 | let menu = window_menu(); 190 | tauri::Builder::default() 191 | .manage(CmdState::default()) 192 | .invoke_handler(tauri::generate_handler![ 193 | listdir, save_tags, parse_tags, translate_tag, query_tag, load_tags_db, 194 | load_config, save_config, refresh_cache, list_isolated_txt 195 | ]) 196 | .menu(menu) 197 | .on_menu_event(handle_menu) 198 | .setup(|app| { 199 | let main_window = app.get_window("main").unwrap(); 200 | let title = "sd-tagtool v".to_string() + app.config().package.version.as_ref().unwrap().as_str(); 201 | main_window.set_title(title.as_str()).unwrap(); 202 | Ok(()) 203 | }) 204 | .run(tauri::generate_context!()) 205 | .expect("error while running tauri application"); 206 | } 207 | -------------------------------------------------------------------------------- /src-tauri/src/tagutils.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::fs; 3 | use std::fs::{File}; 4 | use std::path::Path; 5 | use std::path::PathBuf; 6 | use std::collections::{HashMap, HashSet}; 7 | use std::ffi::OsStr; 8 | use simsearch::{SimSearch, SearchOptions}; 9 | 10 | pub enum TagHint { 11 | Just(u32), 12 | Alias(String), 13 | } 14 | 15 | pub struct TagHintDB { 16 | pub database: HashMap, 17 | pub search: SimSearch, 18 | } 19 | 20 | #[derive(serde::Serialize)] 21 | pub struct TagData { 22 | pub name: String, 23 | pub tags: Vec, 24 | } 25 | 26 | #[derive(serde::Serialize)] 27 | pub struct QueryTag { 28 | pub tag: String, 29 | pub suggest: Option, 30 | pub usage_count: Option, 31 | } 32 | 33 | impl Default for TagHintDB { 34 | fn default() -> TagHintDB { TagHintDB::new() } 35 | } 36 | 37 | fn is_image_file(ext: &OsStr) -> bool { 38 | let ext = ext.to_ascii_lowercase(); 39 | ext == "png" || ext == "jpg" || ext == "jpeg" || ext == "webp" 40 | } 41 | 42 | pub fn read_tags + ToString>(path: P, base: impl AsRef) -> Option { 43 | PathBuf::from(path.as_ref()).extension().and_then(|ext| { 44 | is_image_file(ext).then_some(TagData { 45 | name: path.to_string(), 46 | tags: get_tags(&mut base.as_ref().join(path)), 47 | }) 48 | }) 49 | } 50 | 51 | pub fn listdir_files>(path: P) -> Vec { 52 | fs::read_dir(&path).unwrap() 53 | .filter_map(|e| e.ok().map(|e| e.path())) 54 | .filter_map(|e| 55 | if e.is_dir() { 56 | Some(listdir_files(&e)) 57 | } else if e.is_file() { 58 | Some(vec![e]) 59 | } else { 60 | None 61 | } 62 | ) 63 | .flatten() 64 | .collect::>() 65 | } 66 | 67 | pub fn listdir_images>(path: P) -> Vec { 68 | listdir_files(&path).iter() 69 | .filter_map(|e| 70 | e.extension().and_then(|ext| { 71 | is_image_file(&ext).then(|| ()).and( 72 | e.strip_prefix(&path).ok().and_then(|e| 73 | e.to_str().map(|e| e.to_string()) 74 | )) 75 | }) 76 | ) 77 | .collect::>() 78 | } 79 | 80 | fn is_image_exist(images: &HashSet, path: &PathBuf) -> bool { 81 | path.file_stem().and_then(|e| 82 | images.contains(e.to_str().unwrap()).then_some(()) 83 | ).is_some() 84 | } 85 | 86 | pub fn is_isolated_txt>(images: &HashSet, path: P) -> bool { 87 | let path_buf = PathBuf::from(path.as_ref()); 88 | path_buf.extension().and_then(|ext| 89 | (ext == "txt" && !is_image_exist(images, &path_buf)).then_some(()) 90 | ).is_some() 91 | } 92 | 93 | fn get_tags(path: &mut PathBuf) -> Vec { 94 | path.set_extension("txt"); 95 | fs::read_to_string(path).map_or(Vec::new(), |e| parse_tags(e.as_str())) 96 | } 97 | 98 | pub fn parse_tags(txt: &str) -> Vec { 99 | #[derive(Default)] 100 | enum M { #[default] White, Normal, Escape } 101 | #[derive(Default)] 102 | struct S { 103 | c: M, 104 | s: String, 105 | v: Vec, 106 | } 107 | let mut s = S::default(); 108 | impl S { 109 | fn read_tag(&mut self) { 110 | if !self.s.is_empty() { 111 | self.v.push(self.s.clone()); 112 | self.s.clear(); 113 | } 114 | self.c = M::White; 115 | } 116 | fn read_char(&mut self, c: char, m: M) { 117 | self.s.push(c); 118 | self.c = m; 119 | } 120 | } 121 | for c in txt.chars() { 122 | match s.c { 123 | M::Escape => { 124 | s.s.push(c); 125 | s.c = M::Normal; 126 | } 127 | M::White => match c { 128 | ' ' | '_' => {} 129 | ',' => s.read_tag(), 130 | _ => s.read_char(c, M::Normal) 131 | }, 132 | M::Normal => match c { 133 | ' ' | '_' => s.read_char(' ', M::White), 134 | '\\' => s.read_char(c, M::Escape), 135 | ',' => s.read_tag(), 136 | _ => s.s.push(c), 137 | }, 138 | } 139 | } 140 | s.read_tag(); 141 | s.v 142 | } 143 | 144 | fn try_parse_tag(tag: &str) -> String { 145 | parse_tags(tag).get(0).map_or(tag.to_string(), |e| e.clone()) 146 | } 147 | 148 | fn read_tag_csv>(db: &mut TagHintDB, path: P) -> io::Result<()> { 149 | let mut rdr = csv::ReaderBuilder::new() 150 | .has_headers(false).from_reader(File::open(path)?); 151 | for result in rdr.records() { 152 | let record = result?; 153 | if record.len() < 1 { continue; } 154 | let tag = try_parse_tag(record.get(0).unwrap()); 155 | match record.len() { 156 | 1 | 2 => { db.database.insert(tag, TagHint::Just(0)); } 157 | 3 => { 158 | db.database.insert(tag, TagHint::Just( 159 | record.get(2).unwrap().parse().unwrap_or(0))); 160 | } 161 | 4 => { 162 | db.database.insert(tag.clone(), TagHint::Just( 163 | record.get(2).unwrap().parse().unwrap_or(0))); 164 | for a in parse_tags(record.get(3).unwrap()) { 165 | db.database.insert(a, TagHint::Alias(tag.clone())); 166 | } 167 | } 168 | _ => {} 169 | }; 170 | } 171 | for rec in db.database.keys() { 172 | db.search.insert(rec.clone(), rec.as_str()); 173 | } 174 | Ok(()) 175 | } 176 | 177 | fn list_csv>(path: P) -> io::Result> { 178 | let dir = fs::read_dir(path)?; 179 | Ok(dir.filter_map(|e| e.ok().and_then(|e| { 180 | let p = e.path(); 181 | if p.extension().map_or(false, |x| x == "csv") { 182 | Some(p) 183 | } else { 184 | None 185 | } 186 | })).collect::>()) 187 | } 188 | 189 | impl TagHintDB { 190 | pub fn new() -> Self { 191 | TagHintDB { 192 | database: HashMap::new(), 193 | search: SimSearch::new_with( 194 | SearchOptions::new().stop_whitespace(false)), 195 | } 196 | } 197 | pub fn read_db>(&mut self, db_path: P) { 198 | let csv_list = list_csv(db_path).unwrap_or(Vec::new()); 199 | for p in csv_list { 200 | println!("load tags.db: {}", p.to_str().unwrap()); 201 | read_tag_csv(self, p).unwrap_or(()); 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src-tauri/src/translate.rs: -------------------------------------------------------------------------------- 1 | use futures::lock::Mutex; 2 | use std::collections::HashMap; 3 | use lazy_static::lazy_static; 4 | use regex::Regex; 5 | use reqwest::{Client, Error}; 6 | 7 | pub type TranslateCache = Mutex>; 8 | 9 | pub async fn translate(cache: &TranslateCache, tl: &str, q: &str) -> String { 10 | let cv = cache.lock().await.get(q).map(|e| e.clone()); 11 | match cv { 12 | Some(x) => x.clone(), 13 | None => { 14 | let x = translate_request(tl, q).await.unwrap_or(String::new()); 15 | cache.lock().await.insert(q.to_string(), x.clone()); 16 | x 17 | } 18 | } 19 | } 20 | 21 | async fn translate_request(tl: &str, q: &str) -> Result { 22 | let resp = Client::builder() 23 | .user_agent("Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.5563.116 Mobile Safari/537.36") 24 | .timeout(std::time::Duration::new(5, 0)) 25 | .build().unwrap() 26 | .get("https://translate.google.com/m?") 27 | .query(&[("sl", "en"), ("tl", tl), ("q", q)]) 28 | .send().await? 29 | .text().await?; 30 | lazy_static! { 31 | static ref RE: Regex = Regex::new(r#""result-container">(.*?)"#).unwrap(); 32 | } 33 | let caps = RE.captures(resp.as_str()); 34 | Ok(caps.and_then(|e| 35 | e.get(1).map(|e| e.as_str().to_string())).unwrap_or(String::new()) 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "beforeDevCommand": "pnpm dev", 4 | "beforeBuildCommand": "pnpm build", 5 | "devPath": "http://localhost:1420", 6 | "distDir": "../dist", 7 | "withGlobalTauri": false 8 | }, 9 | "package": { 10 | "productName": "sd-tagtool", 11 | "version": "1.2.4" 12 | }, 13 | "tauri": { 14 | "allowlist": { 15 | "all": false, 16 | "shell": { 17 | "all": false, 18 | "open": true 19 | }, 20 | "protocol": { 21 | "asset": true, 22 | "assetScope": [ 23 | "**" 24 | ] 25 | }, 26 | "process": { 27 | "exit": true 28 | }, 29 | "os": { 30 | "all": true 31 | }, 32 | "fs": { 33 | "all": true, 34 | "scope": [ 35 | "**" 36 | ] 37 | }, 38 | "path": { 39 | "all": true 40 | }, 41 | "dialog": { 42 | "all": true 43 | }, 44 | "window": { 45 | "setTitle": true, 46 | "close": true 47 | } 48 | }, 49 | "bundle": { 50 | "active": true, 51 | "targets": "all", 52 | "identifier": "sd-tagtool", 53 | "icon": [ 54 | "icons/32x32.png", 55 | "icons/128x128.png", 56 | "icons/128x128@2x.png", 57 | "icons/icon.icns", 58 | "icons/icon.ico" 59 | ], 60 | "resources": [ 61 | "shared/*" 62 | ] 63 | }, 64 | "security": { 65 | "csp": null 66 | }, 67 | "windows": [ 68 | { 69 | "fullscreen": false, 70 | "resizable": true, 71 | "title": "sd-tagtool", 72 | "width": 800, 73 | "height": 600, 74 | "fileDropEnabled": false 75 | } 76 | ] 77 | } 78 | } -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | 13 | -------------------------------------------------------------------------------- /src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/DeleteIsolatedTxt.ts: -------------------------------------------------------------------------------- 1 | import {ref} from 'vue' 2 | 3 | import {invoke} from '@tauri-apps/api/tauri' 4 | 5 | export const showDialog = ref(false) 6 | export const deleteList = ref([]) 7 | export let deleteDir: string = '' 8 | 9 | export async function deleteIsolatedTxt(path: string) { 10 | deleteDir = path 11 | deleteList.value = await invoke('list_isolated_txt', {path: path}) 12 | showDialog.value = true 13 | } 14 | -------------------------------------------------------------------------------- /src/components/DeleteIsolatedTxt.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 46 | 47 | -------------------------------------------------------------------------------- /src/components/Home.vue: -------------------------------------------------------------------------------- 1 | 225 | 226 | 262 | 263 | 283 | -------------------------------------------------------------------------------- /src/components/ImageFilter.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 47 | 48 | -------------------------------------------------------------------------------- /src/components/ImageList.vue: -------------------------------------------------------------------------------- 1 | 125 | 126 | 144 | 145 | 218 | 219 | 224 | -------------------------------------------------------------------------------- /src/components/Settings.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 55 | 56 | 100 | -------------------------------------------------------------------------------- /src/components/Tag.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 63 | 64 | 105 | -------------------------------------------------------------------------------- /src/components/TagEditor.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 58 | 59 | 102 | -------------------------------------------------------------------------------- /src/components/TagInput.vue: -------------------------------------------------------------------------------- 1 | 95 | 96 | 110 | 111 | -------------------------------------------------------------------------------- /src/components/TagList.vue: -------------------------------------------------------------------------------- 1 | 163 | 164 | 189 | 190 | 210 | 211 | 236 | -------------------------------------------------------------------------------- /src/lib/state.ts: -------------------------------------------------------------------------------- 1 | import {ref} from 'vue' 2 | import {invoke} from '@tauri-apps/api/tauri' 3 | 4 | export const translate = ref(false) 5 | 6 | export const tagPalette = ref>(new Map) 7 | 8 | const defaultConfig = { 9 | translate: { 10 | language: 'zh-CN' 11 | }, 12 | imageList: { 13 | width: 100 14 | } 15 | } 16 | 17 | export const config = ref(defaultConfig) 18 | 19 | invoke('load_config').then(e => { 20 | config.value = {...defaultConfig, ...e as (typeof defaultConfig)} 21 | }) 22 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | export interface TagData { 2 | key: number, 3 | name: string, 4 | url: string, 5 | tags: string[] 6 | } 7 | 8 | export interface EditAction { 9 | index: number, 10 | tags: string[] 11 | } 12 | 13 | export enum FilterMode { 14 | IncludeAny, 15 | IncludeAll, 16 | Exclude 17 | } 18 | 19 | export class EditorHistory { 20 | constructor(dataset: TagData[] = []) { 21 | this.dataset = dataset 22 | } 23 | 24 | public edit(actions: EditAction[]): TagData[] | undefined { 25 | if (actions.length) { 26 | let undo: EditAction[] = [] 27 | actions.forEach(x => { 28 | undo.push({index: x.index, tags: this.dataset[x.index].tags}) 29 | this.dataset[x.index].tags = x.tags 30 | }) 31 | this.undoStack.push(undo) 32 | this.redoStack = [] 33 | return this.dataset 34 | } 35 | return undefined 36 | } 37 | 38 | public undo(): TagData[] | undefined { 39 | if (this.undoStack.length) { 40 | let e: EditAction[] = [] 41 | this.undoStack.pop()?.forEach(x => { 42 | e.push({index: x.index, tags: [...this.dataset[x.index].tags]}) 43 | this.dataset[x.index].tags = x.tags 44 | }) 45 | this.redoStack.push(e) 46 | return this.dataset 47 | } 48 | return undefined 49 | } 50 | 51 | public redo(): TagData[] | undefined { 52 | if (this.redoStack.length) { 53 | let e: EditAction[] = [] 54 | this.redoStack.pop()?.forEach(x => { 55 | e.push({index: x.index, tags: [...this.dataset[x.index].tags]}) 56 | this.dataset[x.index].tags = x.tags 57 | }) 58 | this.undoStack.push(e) 59 | return this.dataset 60 | } 61 | return undefined 62 | } 63 | 64 | public state(): EditAction[] | undefined { 65 | if (this.undoStack.length) 66 | return this.undoStack[this.undoStack.length - 1] 67 | return undefined 68 | } 69 | 70 | public dataset: TagData[] 71 | private undoStack: EditAction[][] = [] 72 | private redoStack: EditAction[][] = [] 73 | } 74 | 75 | function removeDuplicates(x: T[]): T[] { 76 | let ts: Set = new Set 77 | return x.filter(x => x && !ts.has(x) && ts.add(x)) 78 | } 79 | 80 | export function collectTags(dataset: TagData[]): string[] { 81 | return removeDuplicates(dataset.flatMap(x => x.tags)) 82 | } 83 | 84 | export function deleteTags(dataset: TagData[], tags: string[]): EditAction[] { 85 | let del: Set = new Set(tags) 86 | let edited: EditAction[] = [] 87 | dataset.forEach(x => { 88 | const deleted = x.tags.filter(x => !del.has(x)) 89 | if (deleted.length < x.tags.length) { 90 | edited.push({ 91 | index: x.key, 92 | tags: deleted 93 | }) 94 | } 95 | }) 96 | return edited 97 | } 98 | 99 | export function insertTags(dataset: TagData[], 100 | tags: string[], 101 | position: number | undefined): EditAction[] { 102 | tags = removeDuplicates(tags) 103 | if (!tags.length) 104 | return [] 105 | if (typeof (position) != 'number') { // auto mode 106 | return dataset.map(x => { 107 | const ts: Set = new Set(x.tags) 108 | return {index: x.key, tags: x.tags.concat(tags.filter(a => !ts.has(a)))} 109 | }) 110 | } 111 | const ts: Set = new Set(tags) 112 | return dataset.map(x => { 113 | let s1: string[] = x.tags.filter(a => !ts.has(a)) 114 | let s2: string[] = [] 115 | if (position >= 0) 116 | s2 = s1.splice(position, s1.length) 117 | else 118 | s2 = s1.splice(s1.length + position, s1.length) 119 | return {index: x.key, tags: s1.concat(tags).concat(s2)} 120 | }) 121 | } 122 | 123 | export function replaceTags(dataset: TagData[], from: string[], to: string[]): EditAction[] { 124 | from = removeDuplicates(from) 125 | to = removeDuplicates(to) 126 | if (!from.length) 127 | return [] // do nothing 128 | if (!to.length) 129 | return deleteTags(dataset, from) 130 | let repl: Map = new Map 131 | let n = Math.min(from.length, to.length) 132 | for (let i = 0; i < n; i++) 133 | repl.set(from[i], [to[i]]) 134 | for (; n < from.length; n++) 135 | repl.set(from[n], []) 136 | if (to.length > from.length) 137 | repl.set(from[from.length - 1], to.slice(from.length - 1)) 138 | let edited: EditAction[] = [] 139 | dataset.forEach(x => { 140 | let replaced: string[] = [] 141 | let edit = false 142 | x.tags.forEach(x => { 143 | const r = repl.get(x) 144 | if (r != undefined) { 145 | edit = true 146 | replaced = replaced.concat(r) 147 | } else 148 | replaced.push(x) 149 | }) 150 | if (edit) { 151 | replaced = removeDuplicates(replaced) 152 | edited.push({ 153 | index: x.key, 154 | tags: replaced 155 | }) 156 | } 157 | }) 158 | return edited 159 | } 160 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import {createApp} from 'vue' 2 | import {createRouter, createWebHistory} from 'vue-router' 3 | import App from './App.vue' 4 | import Home from './components/Home.vue' 5 | import Settings from './components/Settings.vue' 6 | import PrimeVue from 'primevue/config' 7 | import ToastService from 'primevue/toastservice' 8 | import "primevue/resources/themes/lara-light-indigo/theme.css" 9 | import "primevue/resources/primevue.min.css" 10 | import "./styles.css" 11 | 12 | const router = createRouter({ 13 | history: createWebHistory(), 14 | routes: [ 15 | {path: '/', component: Home}, 16 | {path: '/settings', component: Settings} 17 | ] 18 | }) 19 | 20 | const app = createApp(App) 21 | app.use(router) 22 | app.use(PrimeVue) 23 | app.use(ToastService) 24 | app.mount("#app") 25 | 26 | // disable content menu 27 | document.addEventListener('contextmenu', event => event.preventDefault()); 28 | 29 | // prevent default key events 30 | const modifyKeys = new Set(["KeyR", "KeyP", "KeyF"]) 31 | document.addEventListener('keydown', event => { 32 | const modifier = event.ctrlKey || event.metaKey 33 | if (modifier && modifyKeys.has(event.code) || event.code == 'F5') 34 | event.preventDefault() 35 | }) 36 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 14px; 4 | line-height: 18px; 5 | font-weight: 400; 6 | 7 | color: #0f0f0f; 8 | background-color: #f4f4f4; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | :focus-visible { 18 | outline: initial; 19 | } 20 | 21 | body { 22 | margin: 8px; 23 | } 24 | 25 | #app { 26 | height: calc(100vh - 16px); /* without body margin */ 27 | display: flex; 28 | flex-direction: column; 29 | } 30 | 31 | .p-inputtext, .p-button { 32 | padding: 0.25em; 33 | } 34 | 35 | .p-button { 36 | padding-left: 0.5em; 37 | padding-right: 0.5em; 38 | } 39 | 40 | .p-autocomplete-panel .p-autocomplete-items .p-autocomplete-item { 41 | padding: 0.5em 0.75em; 42 | } 43 | 44 | .p-autocomplete .p-autocomplete-multiple-container { 45 | padding: 0.15em 0.15em; 46 | flex-grow: 1; 47 | } 48 | 49 | .p-autocomplete .p-autocomplete-multiple-container .p-autocomplete-token { 50 | padding: 0.1em 0.2em; 51 | border-radius: 0.4em; 52 | } 53 | 54 | .p-autocomplete .p-autocomplete-multiple-container .p-autocomplete-input-token input { 55 | padding: 0.1em 0; 56 | } 57 | 58 | .p-autocomplete .p-autocomplete-multiple-container .p-autocomplete-input-token { 59 | padding: 0; 60 | } 61 | 62 | .p-autocomplete .p-autocomplete-multiple-container .p-autocomplete-token .p-autocomplete-token-icon { 63 | margin-left: 0.2em; 64 | } 65 | 66 | .p-dropdown .p-dropdown-trigger { 67 | width: 1.5rem; 68 | } 69 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module "*.vue" { 4 | import type {DefineComponent} from "vue"; 5 | const component: DefineComponent<{}, {}, any>; 6 | export default component; 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "lib": ["ESNext", "DOM"], 14 | "skipLibCheck": true 15 | }, 16 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 17 | "references": [{ "path": "./tsconfig.node.json" }] 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import vue from "@vitejs/plugin-vue"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig(async () => ({ 6 | plugins: [vue()], 7 | 8 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` 9 | // prevent vite from obscuring rust errors 10 | clearScreen: false, 11 | // tauri expects a fixed port, fail if that port is not available 12 | server: { 13 | port: 1420, 14 | strictPort: false, 15 | }, 16 | // to make use of `TAURI_DEBUG` and other env variables 17 | // https://tauri.studio/v1/api/config#buildconfig.beforedevcommand 18 | envPrefix: ["VITE_", "TAURI_"], 19 | build: { 20 | // Tauri supports es2021 21 | target: process.env.TAURI_PLATFORM == "windows" ? "chrome105" : "safari13", 22 | // don't minify for debug builds 23 | minify: !process.env.TAURI_DEBUG ? "esbuild" : false, 24 | // produce sourcemaps for debug builds 25 | sourcemap: !!process.env.TAURI_DEBUG, 26 | }, 27 | })); 28 | --------------------------------------------------------------------------------