├── .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 | 
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 | 
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 |
6 |
7 |
8 |
9 |
10 |
11 |
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 |
27 |
28 |
29 | Select the files you want to clean
30 |
31 |
32 |
33 |
34 | {{ node.file }}
35 |
36 |
37 |
38 | There are no files to clean up.
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/components/Home.vue:
--------------------------------------------------------------------------------
1 |
225 |
226 |
227 |
228 |
229 |
231 |
232 |
233 |
234 |
235 |
238 |
244 |
248 |
249 |
250 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
283 |
--------------------------------------------------------------------------------
/src/components/ImageFilter.vue:
--------------------------------------------------------------------------------
1 |
34 |
35 |
36 |
37 |
39 |
42 |
43 | Filter
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/src/components/ImageList.vue:
--------------------------------------------------------------------------------
1 |
125 |
126 |
127 |
129 |
132 |
134 |
135 |
There are no images to show.
136 |
137 |
138 |
139 |
141 |
142 |
143 |
144 |
145 |
218 |
219 |
224 |
--------------------------------------------------------------------------------
/src/components/Settings.vue:
--------------------------------------------------------------------------------
1 |
25 |
26 |
27 |
28 |
34 |
35 |
36 |
37 | Target language
38 |
40 |
41 |
42 |
43 |
44 | Image width
45 |
46 | {{config.imageList.width}} px
47 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
100 |
--------------------------------------------------------------------------------
/src/components/Tag.vue:
--------------------------------------------------------------------------------
1 |
38 |
39 |
40 |
41 |
42 | •
43 | {{ label }}
44 | {{ trLabel }}
45 |
46 |
49 |
50 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
105 |
--------------------------------------------------------------------------------
/src/components/TagEditor.vue:
--------------------------------------------------------------------------------
1 |
32 |
33 |
34 |
35 |
36 |
38 |
39 | position
40 |
42 |
43 | Insert
44 |
45 | edit all tags
46 |
47 |
48 |
49 | replace with
50 |
51 |
53 | Replace
54 |
55 |
56 |
57 |
58 |
59 |
102 |
--------------------------------------------------------------------------------
/src/components/TagInput.vue:
--------------------------------------------------------------------------------
1 |
95 |
96 |
97 |
102 |
103 | {{ option.tag }}
104 | → {{ option.suggest }}
105 | ({{ readableNumber(option.usage_count) }})
106 | {{ option.translate }}
107 |
108 |
109 |
110 |
111 |
--------------------------------------------------------------------------------
/src/components/TagList.vue:
--------------------------------------------------------------------------------
1 |
163 |
164 |
165 |
166 |
174 |
175 |
183 |
184 |
185 |
186 |
187 |
188 |
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 |
--------------------------------------------------------------------------------