diff --git a/.env.example b/.env.example deleted file mode 100644 index 097c3e1..0000000 --- a/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -# API Key is saved through the app settings UI, not in this file. -DEEPSEEK_BASE_URL=https://api.deepseek.com -DEEPSEEK_MODEL=deepseek-v4-flash -FTB_TRANSLATER_BATCH_SIZE=auto -FTB_TRANSLATER_CONCURRENCY=auto diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b8fde1b..b56cfe0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,23 +3,11 @@ name: CI/CD on: pull_request: push: - branches: - - main - - master - tags: - - "v*" + branches: [main, master] + tags: ["v*"] + release: + types: [published] workflow_dispatch: - inputs: - run_live_e2e: - description: "Run live CurseForge/DeepSeek e2e tests" - required: false - default: false - type: boolean - live_deepseek_entries: - description: "Sample size for live DeepSeek e2e" - required: false - default: "12" - type: string concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -28,180 +16,78 @@ concurrency: permissions: contents: read -env: - PYTHON_VERSION: "3.11" - UV_LOCKED: "1" - UV_SYSTEM_PYTHON: "1" - jobs: test: name: Test (${{ matrix.os }}) - runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - os: - - ubuntu-latest - - windows-latest - - macos-latest - + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Install uv - uses: astral-sh/setup-uv@v5 + node-version: 22 + cache: npm + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 with: - enable-cache: true - - - name: Sync dependencies - run: uv sync --dev --locked - - - name: Run unit tests - run: uv run python -m tests.run_groups unit + workspaces: src-tauri + - name: Linux system dependencies + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + - run: npm ci + - run: npm run build + - run: cargo test --manifest-path src-tauri/Cargo.toml package: - name: Package (${{ matrix.suffix }}) + name: Package (${{ matrix.os }}) needs: test - runs-on: ${{ matrix.os }} - if: github.event_name != 'pull_request' + if: >- + github.event_name == 'release' || + github.event_name == 'workflow_dispatch' || + startsWith(github.ref, 'refs/tags/v') strategy: fail-fast: false matrix: - include: - - os: windows-latest - suffix: windows - - os: macos-latest - suffix: macos - + os: [windows-latest, macos-latest] + runs-on: ${{ matrix.os }} steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Install uv - uses: astral-sh/setup-uv@v5 + node-version: 22 + cache: npm + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 with: - enable-cache: true - - - name: Sync dependencies - run: uv sync --dev --locked - - - name: Build Windows exe - if: runner.os == 'Windows' - run: > - uv run python -m PyInstaller - --noconfirm - --clean - --onefile - --windowed - --name FTB-Translater - --collect-data customtkinter - main.py - - - name: Prepare Windows artifact - if: runner.os == 'Windows' - shell: pwsh - run: | - New-Item -ItemType Directory -Force artifacts | Out-Null - Copy-Item "dist/FTB-Translater.exe" "artifacts/FTB-Translater-windows.exe" -Force - Get-Item "artifacts/FTB-Translater-windows.exe" - - - name: Build macOS app - if: runner.os == 'macOS' - run: > - uv run python -m PyInstaller - --noconfirm - --clean - --windowed - --name FTB-Translater - --collect-data customtkinter - main.py - - - name: Prepare macOS dmg - if: runner.os == 'macOS' - run: | - mkdir -p artifacts - hdiutil create \ - -volname FTB-Translater \ - -srcfolder dist/FTB-Translater.app \ - -ov \ - -format UDZO \ - artifacts/FTB-Translater-macos.dmg - ls -lh artifacts/FTB-Translater-macos.dmg - - - name: Upload package artifact - uses: actions/upload-artifact@v4 + workspaces: src-tauri + - run: npm ci + - run: npm run tauri -- build + - uses: actions/upload-artifact@v4 with: - name: FTB-Translater-${{ matrix.suffix }} - path: artifacts/* + name: FTB-Translater-${{ runner.os }} + path: | + src-tauri/target/release/bundle/dmg/*.dmg + src-tauri/target/release/bundle/nsis/*.exe + src-tauri/target/release/bundle/msi/*.msi if-no-files-found: error - retention-days: 14 - - live-e2e: - name: Live e2e - needs: test - runs-on: ubuntu-latest - if: github.event_name == 'workflow_dispatch' && inputs.run_live_e2e - env: - DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} - FTB_TRANSLATER_CURSEFORGE_URL: ${{ secrets.FTB_TRANSLATER_CURSEFORGE_URL }} - FTB_TRANSLATER_LIVE_DEEPSEEK_ENTRIES: ${{ inputs.live_deepseek_entries }} - - steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Install uv - uses: astral-sh/setup-uv@v5 - with: - enable-cache: true - - - name: Sync dependencies - run: uv sync --dev --locked - - - name: Run full e2e tests - run: uv run python -m tests.run_groups e2e - - - name: Upload e2e output - if: always() - uses: actions/upload-artifact@v4 - with: - name: live-e2e-output - path: .ftb-translater/e2e-runs/ - if-no-files-found: ignore - retention-days: 7 release: name: Publish GitHub release needs: package runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/v') + if: github.event_name == 'release' || startsWith(github.ref, 'refs/tags/v') permissions: contents: write - steps: - - name: Download package artifacts - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v4 with: path: release-artifacts merge-multiple: true - - - name: Publish release assets - uses: softprops/action-gh-release@v2 + - uses: softprops/action-gh-release@v2 with: - files: release-artifacts/* + tag_name: ${{ github.event.release.tag_name || github.ref_name }} + files: release-artifacts/**/* + fail_on_unmatched_files: true diff --git a/.gitignore b/.gitignore index 590a13b..602b8e8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,9 @@ -.env -.venv/ .idea/ +.venv/ __pycache__/ *.py[cod] -.pytest_cache/ .ftb-translater/ history.sqlite3 -*.egg-info/ -build/ dist/ node_modules/ src-tauri/target/ -*.spec diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2efe8e7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,128 @@ +# FTB Translater 维护约束 + +本文档面向后续维护者和自动化代理。README 面向用户,必须保留项目原理、翻译模式、真实性能与准确度数据、运行方法和使用流程;实现约束集中维护在这里,避免在多份设计文档中复制后失去同步。 + +## 项目结构 + +- 当前桌面实现基于 Rust、Tauri 2、React 和 TypeScript,运行时不依赖 Python sidecar。 +- 前端入口:`src/main.tsx`。 +- 翻译流程与格式保护:`src-tauri/src/core.rs`。 +- 提供商请求:`src-tauri/src/providers.rs`。 +- 设置、钥匙串与历史:`src-tauri/src/storage.rs`。 +- 本地词表实现:`src-tauri/src/glossary.rs`。 +- 内置词表模板:`src-tauri/resources/minecraft_glossary.json`。 +- 翻译准确度审计:`docs/translation-accuracy-audit.md`。 + +## 翻译提供商能力 + +默认提供商是 `google_web`。已有有效用户配置必须继续按保存值加载,不能因为默认值变化而被强制迁移。 + +| 设置区域 | `google_web` | `deepl_web` | `deepl` | `openai_compatible` | +|---|:---:|:---:|:---:|:---:| +| 提供商选择 | 显示 | 显示 | 显示 | 显示 | +| 服务凭证 | 隐藏 | 隐藏 | DeepL Authentication Key | API Key | +| 接口地址 | 隐藏 | 隐藏 | 显示 | 显示 | +| 模型名称 | 隐藏 | 隐藏 | 隐藏 | 显示 | +| 翻译要求 | 隐藏 | 隐藏 | 隐藏 | 显示 | +| Minecraft/模组词表 | 隐藏 | 隐藏 | 显示,默认关闭 | 显示,默认关闭 | +| 每批条目与并发 | 隐藏 | 隐藏 | 显示 | 显示 | + +保存按钮始终可见。不要退回到仅使用 `needsKey` 判断所有卡片的设计;需要 Key 不代表提供商支持相同的配置。 + +前端 `providerOptions` 中每个提供商必须显式声明: + +- `credentialLabel`:是否需要以及如何命名凭证; +- `supportsGlossary`:是否显示词表; +- `supportsTaskParameters`:是否显示批大小和并发数; +- `configuration`:使用 `none`、`deepl` 或 `openai` 专属配置。 + +### 网页模式 + +- `google_web` 使用 `https://translate.googleapis.com`,单次尽量装入约 4,500 字符。 +- `deepl_web` 使用 `https://oneshot-free.www.deepl.com`,按约 1,500 字符限制装批。 +- 两者都不需要 Key,不显示其他配置卡片,有效并发固定上限为 1。 +- 切换到网页模式时,前端关闭词表,并把 `batch_size`、`concurrency` 恢复为 `auto`。 +- `storage.rs` 保存设置和 `core.rs` 启动任务时必须再次执行相同规范化,不能只依赖界面隐藏。 + +### DeepL 官方 API + +- 默认 Free 地址为 `https://api-free.deepl.com`,必须允许 Pro 用户改为 `https://api.deepl.com`。 +- 显示 Authentication Key、接口地址、本地词表、批大小和并发。 +- 不显示 OpenAI 模型或翻译提示词。 +- 词表是应用本地占位符保护层,不是 DeepL 官方 Glossary API。 + +### DeepSeek / OpenAI 兼容 + +- 默认地址为 `https://api.deepseek.com`,默认模型为 `deepseek-chat`。 +- 显示 API Key、接口地址、模型、翻译要求、本地词表、批大小和并发。 +- 必须继续允许用户填写其他 OpenAI 兼容地址和模型。 + +API 模式下,`batch_size=auto` 使用 25,`concurrency=auto` 使用 6,并发硬上限为 12。 + +## 提供商切换与保存 + +- 切换提供商时应用目标提供商的默认地址和模型。 +- 清空表单中的 Key 和本次 Key 编辑状态,避免将一个服务的凭证带到另一个服务。 +- 当前 `settings.json` 只保存正在使用的一套非敏感配置,不分别保存四套接口、模型和任务参数。 +- API Key 按提供商分别保存在系统凭证管理器中,不能写入 `settings.json`、项目文件、报告或历史数据库。 + +## 钥匙串访问 + +以下操作不得访问钥匙串: + +- 应用启动、加载普通设置或打开设置页; +- 切换提供商; +- 保存未修改 Key 的普通设置; +- Google/DeepL 网页翻译。 + +仅在以下情况按需访问钥匙串: + +- 用户点击眼睛按钮明确查看已保存的 Key; +- 用户保存新 Key 或删除原 Key; +- API 模式实际开始翻译且当前会话没有对应 Key。 + +成功读取的 Key 必须在当前应用会话中复用,不能按批次重复读取并反复触发系统验证。 + +## Minecraft/模组词表 + +- 词表只在 `deepl` 和 `openai_compatible` 模式可见并生效,默认关闭。 +- 首次运行把内置模板复制到应用数据目录,已有用户文件不得被后续启动覆盖。 +- 用户可以编辑默认 JSON、输入自定义路径、选择其他文件或恢复默认路径。 +- 保存设置和开始任务时都要校验 JSON、空条目与重复术语。 +- 词表内容的 SHA-256 指纹参与缓存键;修改内容后不能误用旧翻译缓存。 +- 不确定的模组专名优先保留英文,避免通用翻译引擎错误直译。 + +## README 与数据口径 + +README 必须持续包含: + +- 四种提供商的用户可见配置矩阵; +- 从源码运行、实际使用流程和构建命令; +- 批处理与并发原理; +- StoneBlock 4 Google 网页翻译的真实耗时、吞吐、接口成功和格式守卫数据; +- 明确区分接口成功率、格式守卫通过率和语义准确率。 + +不得把 99.44% 格式守卫通过率描述成翻译准确率。当前独立审计估算严格可发布准确率约为 45%–55%,详细证据保存在 `docs/translation-accuracy-audit.md`。更新基准数据时,需要记录日期、输入规模、缓存状态、提供商、批大小、有效并发、耗时、失败数和准确度口径。 + +## 修改后的最低验证 + +Rust 或设置逻辑修改: + +```bash +cargo fmt --manifest-path src-tauri/Cargo.toml -- --check +cargo test --manifest-path src-tauri/Cargo.toml +``` + +React、TypeScript 或设置页修改: + +```bash +npm run build +``` + +文档和所有改动: + +```bash +git diff --check +``` + +匿名网页接口的 live smoke test 默认忽略,因为它依赖网络与第三方端点。只有在明确需要验证真实服务时再单独运行,不能把第三方端点暂时限流误判为本地单元测试失败。 diff --git a/README.md b/README.md index 977ddc7..28bcbad 100644 --- a/README.md +++ b/README.md @@ -1,199 +1,225 @@ # FTB Translater -FTB Translater 是一个用于汉化现代 FTB Quests 任务文本的桌面工具,支持 OpenAI 兼容接口、DeepL 官方 API,以及无需 API Key 的实验性网页翻译接口。目前固定支持 `en_us -> zh_cn`。 +用于汉化现代 FTB Quests 任务文本的桌面工具,基于 Rust + Tauri 构建,支持 OpenAI 兼容接口、DeepL 官方 API,以及无需 API Key 的 Google/DeepL 网页翻译。固定翻译方向:`en_us → zh_cn`。 -## 功能 +默认翻译服务为免 API Key 的 Google 网页翻译。DeepSeek / OpenAI 兼容接口、DeepL 官方 API 和 DeepL 网页翻译仍可在设置中切换;内置 Minecraft/模组词表是 API 模式的可选增强项,默认关闭。 -- 支持新版语言文件:`config/ftbquests/quests/lang/en_us.snbt` -- 支持章节式任务文件:`config/ftbquests/quests/chapters/*.snbt` -- `lang` 模式写入或覆盖 `lang/zh_cn.snbt` -- `chapters` 模式原地改写 `chapters/*.snbt` 中可翻译文本字段 -- 写入前自动备份 `lang` 或 `chapters` 目录 -- 生成翻译缓存、报告、备份和可导出的翻译历史 +## 翻译模式与配置 -## 安装 +设置页不会向所有提供商展示同一套表单,而是按提供商能力组合配置: -需要 Python 3.11 或更高版本。 +| 配置项 | Google 网页 | DeepL 网页 | DeepL 官方 API | DeepSeek / OpenAI 兼容 | +|---|:---:|:---:|:---:|:---:| +| API Key | — | — | Authentication Key | API Key | +| 接口地址 | — | — | Free / Pro 地址 | 可配置兼容地址 | +| 模型与翻译要求 | — | — | — | 可配置 | +| Minecraft/模组词表 | — | — | 可选 | 可选 | +| 批大小与并发 | 内置安全策略 | 内置安全策略 | 可配置 | 可配置 | -```powershell -python -m pip install -e . -``` +Google 网页翻译是默认模式,无需任何 Key。DeepL 网页模式同样免 Key,但属于实验性匿名接口。DeepL 官方 API 默认使用 Free 地址,Pro 用户可以改成 `https://api.deepl.com`。DeepSeek/OpenAI 兼容模式默认指向 DeepSeek,也可以填写其他兼容服务。 -也可以使用 uv: +## 运行方法 -```powershell -uv sync --dev -``` +### 从源码启动 -## 运行 +需要 Node.js 20+、Rust stable 和 [Tauri 2 对应的系统依赖](https://tauri.app/start/prerequisites/)。 -```powershell -python main.py +```bash +npm install +npm run tauri dev ``` -安装为可执行脚本后也可以运行: +默认 Google 网页模式不需要额外环境变量或 API Key。若使用 DeepL 官方 API 或 DeepSeek/OpenAI 兼容模式,请在应用的“服务设置”中填写凭证,不要把 Key 写入项目文件。 + +### 实际使用流程 + +1. 打开“服务设置”,选择翻译提供商并保存。首次运行可以直接保留默认的 Google 网页翻译。 +2. 回到“翻译工作台”,选择整合包根目录,或直接选择 `config/ftbquests/quests`、`lang`、`chapters` 目录。 +3. 点击“扫描任务书”,确认识别出的格式、文件数和待翻译条目数。 +4. 点击“开始汉化”。程序会先创建完整备份,再翻译并写回内容。 +5. 完成后检查格式告警与人工修正列表。网页翻译只能作为机器初译,发布前应进行术语和语义审校。 -```powershell -ftb-translater +### 构建安装包 + +```bash +npm run tauri -- build ``` -启动后选择整合包目录即可。也可以直接选择它下面的 `config`、`config/ftbquests`、`config/ftbquests/quests`、`lang` 或 `chapters` 目录,程序会自动定位 FTB Quests 任务目录。 +构建产物位于 `src-tauri/target/release/bundle/`。 -## 配置 +## 功能 -右上角“设置”里可以选择翻译提供商、填写对应 API Key 和翻译参数。 +- 支持两种模式:语言文件(`lang/en_us.snbt`)和章节文件(`chapters/*.snbt`) +- 自动识别整合包目录结构,无需手动定位文件 +- 翻译前自动备份原始文件 +- 官方 API 支持批量并发,网页翻译以大批次、低并发方式减少匿名请求 +- 可选的版本化 Minecraft/模组词表,默认关闭;切换到 API 模式后可按需启用 +- 翻译缓存、JSON 报告、SQLite 历史与 ZIP 导出 +- 格式安全保护 + 人工修正页(见下方原理) +- API Key 存入系统密钥管理器;应用启动、切换服务和修改普通设置不会读取钥匙串,只有明确查看/修改 Key 或实际翻译需要 Key 时才按需读取一次,并在当前应用会话中复用 +- 浅色/深色主题,响应式桌面布局 +- 纯 Rust,运行时不依赖 Python 或任何 sidecar -API Key 优先保存到系统凭证管理器: +## 工作原理 -- macOS:钥匙串 -- Windows:凭据管理器 -- Linux:Secret Service +### 1. 文件解析 -如果系统凭证后端不可用,程序会回退到本地受限文件,避免把 API Key 明文写入 `.env`。旧版本 `.env` 里的 `DEEPSEEK_API_KEY` 会在启动时尝试迁移到新存储。 +工具支持两种文件格式,分别对应 FTB Quests 的两种任务书结构: -设置面板还可以配置: +- **lang 模式**:解析 `lang/en_us.snbt`,这是一个类 JSON 的 SNBT 格式文件,值可以是字符串或字符串数组(多行描述)。工具自己实现了 SNBT 解析器(`snbt.rs`),保留键的原始顺序,写出时也生成合法 SNBT。 +- **chapters 模式**:遍历 `chapters/*.snbt`,从每个章节文件中提取可翻译的字符串字段(任务标题、描述等)。 -- 翻译提供商:`FTB_TRANSLATER_PROVIDER`,可选 `openai_compatible`、`deepl`、`google_web` 或 `deepl_web` -- API 地址:`DEEPSEEK_BASE_URL`(沿用旧配置名以保持兼容) -- 模型名:`DEEPSEEK_MODEL`(DeepL 模式保持 `deepl` 即可) -- 翻译风格:`FTB_TRANSLATER_STYLE` -- 批大小:`FTB_TRANSLATER_BATCH_SIZE`,填 `auto` 使用自动策略 -- 并发数:`FTB_TRANSLATER_CONCURRENCY`,填 `auto` 使用自动策略 +### 2. Token 保护 -手动指定并发数也可以使用环境变量: +翻译前,每条原文会经过一道**占位符替换**流程(`core.rs::protect`): -```powershell -$env:FTB_TRANSLATER_CONCURRENCY=6 -``` +用正则匹配以下模式,将它们替换为 `⟨P_0⟩`、`⟨P_1⟩`…… 形式的不透明占位符: -恢复自动调节: +| 类型 | 示例 | +|------|------| +| Minecraft 颜色/格式码 | `&e`、`§6`、`§k` | +| printf 格式占位符 | `%s`、`%1$d` | +| 尖括号标签 | `` | +| 花括号宏 | `{@player}`、`{amount}` | +| 资源/路径标识符 | `assets/mod/textures/a.png`、`kubejs:items/foo` | +| 转义序列 | `\n`、`\t`、`\\` | +| URL | `https://...` | +| 十六进制颜色 | `#FF5733` | -```powershell -$env:FTB_TRANSLATER_CONCURRENCY=auto +保护后的文本只包含自然语言和占位符,例如: + +``` +Use &eGold Ingot&r on +→ Use ⟨P_0⟩Gold Ingot⟨P_1⟩ on ⟨P_2⟩ ``` -## 翻译流程 +被保护的 token 列表与原文一起保存,翻译后用于恢复。 -1. 选择整合包或 FTB Quests 目录。 -2. 点击扫描,确认程序识别到 `lang` 或 `chapters` 模式。 -3. 点击开始汉化。 -4. 程序会先弹窗确认覆盖写入,然后创建备份。 -5. 翻译完成后可以查看日志、报告和历史记录。 +### 3. 批量并发翻译 -程序会自动切分翻译请求,并根据任务规模选择保守的并发数。翻译时日志区域会显示 API 调用、批次进度、备份创建和覆盖写入目标。 +待翻译条目按 `batch_size`(默认 25)分批。OpenAI 兼容接口和 DeepL 官方 API 可使用 `concurrency` 并发请求(默认 6,上限 12);匿名网页接口固定低并发,通过增大单次请求减少 HTTP 调用: -OpenAI 兼容模式默认使用 DeepSeek,也可以填写 OpenAI、OpenRouter、硅基流动或中转服务的 Base URL 与模型名。如果服务不支持 `response_format=json_object`,程序会自动回退到仅通过提示词约束 JSON,并兼容 Markdown 代码块包裹的 JSON 返回值。 +- Google 网页翻译:使用不可翻译批次标记,一次 POST 尽量装入约 4500 字符,返回后按标记拆回原条目。 +- DeepL 网页翻译:一次请求使用文本数组装入约 1500 字符,符合匿名端点限制。 +- 超长单条文本会在标点或空白附近拆分,并避免切断 `⟨P_N⟩` 占位符。 -DeepL 模式默认使用 Free API 地址 `https://api-free.deepl.com`;Pro 账号可改为 `https://api.deepl.com`。DeepL 不使用翻译风格和模型参数。 +OpenAI 兼容模式下,每批以 JSON 对象形式发送,键是条目 ID,值是保护后的文本。模型被要求: +- 保持键集合不变 +- 不修改任何 `⟨P_N⟩` 占位符 +- 返回同结构的 JSON 对象 -Google 和 DeepL 网页翻译模式不需要 API Key。它们调用的是网站或浏览器扩展使用的匿名接口,不属于官方稳定 API,因此程序会强制使用低并发、失败重试和本地缓存。Google 会用不可翻译的批次标记在一次 POST 中装入约 4500 字符,DeepL 会按匿名端点限制在一次请求中装入约 1500 字符。服务端限流、鉴权规则或接口格式随时可能变化;调用失败时程序会保留原文,不应将网页模式视为有可用性保证的服务。 +所有提供商请求失败时最多重试 3 次,间隔递增。整批失败时,该批所有条目回退为原文。网页接口不是官方稳定 API,服务端限流或接口变化都可能导致暂时不可用。 -如果翻译 API 返回的译文丢失受保护格式,该条译文会被丢弃并保留原文。当前保护内容包括: +### Google 网页翻译全量实测 -- FTB / Minecraft 格式码,例如 `&e`、`&r`、`§a` -- 占位符,例如 `%s`、`%1$s` -- 物品或标签 token,例如 ``、`#forge:ingots/iron` -- 字面转义序列,例如 `\n`、`\t` -- 实际换行和制表符数量 +以下数据来自一次真实的端到端运行,不是理论估算。测试于 2026-07-13 使用 StoneBlock 4 `1.15.3` 的完整 `lang/en_us.snbt` 进行,初始缓存为空,全程使用免 API Key 的 Google 网页翻译,未使用 DeepSeek。 -## 输出 +| 指标 | 实测结果 | +|------|----------| +| 原始语言文件 | 440,833 字节、7,145 行 | +| 解析后的翻译条目 | 2,515 条 | +| Core 批大小 | 250 条/批 | +| Google 单次请求上限 | 尽量装满约 4,500 字符 | +| 配置并发数 | 8 | +| 网页提供商有效并发数 | **1**(安全上限强制收敛) | +| 总耗时 | **209.09 秒**(约 3 分 29 秒) | +| 平均处理速度 | **12.03 条/秒** | +| 接口级成功 | **2,515 / 2,515(100%)** | +| 接口级失败 | **0 / 2,515(0%)** | +| 格式守卫拦截并回退 | **14 / 2,515(0.56%)** | +| 格式守卫通过 | **2,501 / 2,515(99.44%)** | +| 输出文件 | 427,139 字节、7,457 行,SNBT 重新解析通过 | -翻译会写入或更新: +这里的“接口级成功”只表示接口为条目返回了结果;“格式守卫通过”只表示当前规则没有发现换行、格式码、占位符、资源标识或 JSON 结构异常。两者都**不代表翻译语义准确,也不代表译文适合直接发布**。14 条被当前守卫发现的异常译文会自动回退英文原文,并写入人工修正报告。 -- `config/ftbquests/quests/lang/zh_cn.snbt`,或 `config/ftbquests/quests/chapters/*.snbt` -- `config/ftbquests/quests/.ftb-translater/cache.json` -- `config/ftbquests/quests/.ftb-translater/report-latest.json` -- `config/ftbquests/quests/.ftb-translater/backups/YYYYMMDD-HHMMSS/` -- 当前运行目录下的 `history.sqlite3` +配置中的并发数为 8,但匿名 Google/DeepL 网页端点在程序内固定限制为有效并发 1。全量实测表明,在单并发下通过约 4,500 字符的大请求批处理,已经能在约三分半内处理 2,515 个真实条目。更高并发容易触发匿名服务限流,也会放大批次标记被改写或响应不完整的风险。实际耗时仍会随网络、文本长度和服务端状态变化;此数据应视为一次可复现的参考基准,而不是稳定性承诺。 -`report-latest.json` 包含本次翻译摘要、失败项、格式告警和中英映射。完整输出文件内容保存在历史数据库中,用于后续导出。 +#### 翻译准确度审计 -## 翻译历史 +在上述全量运行后,又对 2,515 个 key、5,789 个文本片段进行了独立质量审计。所有条目均经过程序化风险扫描,并人工复核了固定随机样本、全部富文本组件、全部原文未变化片段、全部同源异译组和高风险术语项,合计约 350 个不同片段。以下准确度比例是基于全量扫描与人工复核的**估算值**,合理误差约为 ±5–7 个百分点: -右上角“历史”入口会列出已保存的翻译记录。每条记录包含整合包路径、模式、模型、条目数、失败数、告警数和缓存命中数。 +| 准确度等级 | 估算比例 | 含义 | +|------------|----------|------| +| A | 约 48% | 语义正确,术语和表达基本可直接使用 | +| B | 约 29% | 大意正确,但存在明显机翻腔、术语或一致性问题 | +| C | 约 18% | 关键术语、信息关系或动作对象错误,需要重译 | +| D | 约 5% | 未翻译、内容损坏、严重幻觉或富文本无法解析 | +| 严格可发布准确率 | **约 45%–55%** | 未经进一步审校时可直接公开发布的估算范围 | -历史数据库保存在当前运行目录的 `history.sqlite3`。从源码目录执行 `python main.py` 时,数据库会出现在项目根目录;从其他目录启动时,数据库会出现在对应的当前工作目录。 +全量扫描确认的主要问题包括: -历史页面支持导出 ZIP: +- 44 条 JSON 富文本组件中有 19 条目标内容无法作为 JSON 解析,占 **43.2%**;多数没有被现有格式守卫拦截。 +- 52 个有意义的英文片段完全未译,涉及 26 个 key;其中至少 12 个 key 未被报告标记。 +- 相同英文原文出现不同译法,共 30 组。 +- `item/items` 被误译为“项目”至少 71 处,能量语境中的 `power` 被译为“电源”至少 32 处,普通方块 `block/blocks` 被译为“区块”至少 27 处。 +- `enchanting` 被译为“迷人”至少 11 处,`vanilla` 被译为“香草”至少 8 处。 +- `Mekanism`、`StoneBlock 4`、`Draconic Evolution` 等模组或整合包名称被按普通英语直译,且同一术语存在多套译名。 -- `lang` 模式导出 `lang/zh_cn.snbt` -- `chapters` 模式导出 `chapters/*.snbt` -- ZIP 内附带 `manifest.json` +因此,这次 Google 网页翻译结果应定位为**机器初译草稿,不适合直接发布**。当前 99.44% 数据只能用于衡量现有格式守卫的通过情况,不能作为翻译准确率。正式发布前至少需要加入 Minecraft/模组术语表、从模组 `zh_cn.json` 复用官方译名、对 JSON 富文本只翻译允许的展示字段,并进行第二阶段语义审校。完整方法、统计和具体错译案例见 [`docs/translation-accuracy-audit.md`](docs/translation-accuracy-audit.md)。 -## 测试 +### 4. Token 恢复与校验 -普通测试组: +API 返回后,每条译文经过两步处理: -```powershell -python -m tests.run_groups unit -``` +**恢复**:将 `⟨P_N⟩` 替换回对应的原始 token。 -或直接运行完整普通测试发现: +**校验**(`core.rs::warnings`):对恢复后的译文与原文做以下比较: +- 换行、回车、制表符数量是否一致 +- 保护 token 集合(排序后)是否完全一致——即没有缺失、没有多余 +- 如果原文是 JSON 文本组件,校验译文的 JSON 结构(除 `"text"` 字段外的键和类型)是否不变 -```powershell -uv run python -m unittest discover -s tests -p "test_*.py" -``` +**校验失败**:该条译文**不写入**,原文被保留,条目进入「人工修正」列表。校验通过的译文才写入文件并存入缓存。 -完整流程测试组: +### 5. 翻译缓存 -```powershell -python -m tests.run_groups e2e -``` +每条成功通过校验的翻译以 SHA-256 散列为键存入 `cache.json`。散列输入包含原文、提供商标识、模型/接口和风格提示,保证不同服务之间缓存不复用;原有 OpenAI/DeepSeek 缓存保持兼容。下次翻译同一整合包时,命中缓存的条目直接跳过请求。 -完整流程测试会启用真实 live 测试开关。它会下载真实 CurseForge 整合包 zip,先用假翻译器跑一遍真实下载处理流程,再抽样调用真实 DeepSeek API 跑一遍付费端到端流程。 +### 6. 可选 Minecraft/模组词表 -真实 DeepSeek 测试的输出目录: +切换到 DeepL 官方 API 或 DeepSeek/OpenAI 兼容模式后,设置页可以手动开启词表。词表默认关闭;Google/DeepL 网页模式不显示该设置,后端也会强制关闭词表。 -```text -.ftb-translater/e2e-runs/YYYYMMDD-HHMMSS/ -``` +开启后,工具会先保护颜色码、URL、资源路径等格式 token,再按最长词优先和英文单词边界匹配术语,将命中的词替换为 `⟨G_N⟩` 占位符。翻译服务只处理剩余自然语言,返回后工具把占位符恢复成词表中的统一中文。这个机制适用于 DeepSeek、OpenAI 兼容接口和 DeepL 官方 API;它是应用本地的术语保护层,不是 DeepL 官方 Glossary 功能,也不依赖模型提示词。 -其中 `summary.txt` 会列出 `zh_cn.snbt`、`report-latest.json`、`cache.json` 和备份目录的完整路径。 +首次运行时,程序会把 [`src-tauri/resources/minecraft_glossary.json`](src-tauri/resources/minecraft_glossary.json) 作为初始模板复制到应用数据目录。设置页会显示这份可编辑 JSON 的完整路径,用户可以直接修改文件、手动输入其他路径、通过文件选择器切换自定义词表,或恢复默认路径。已有的用户词表不会被后续启动覆盖。 -指定测试用整合包: +初始词表覆盖 600+ 个条目,面向常见整合包而不是单个整合包定制。除 Minecraft 通用术语外,还覆盖新旧版本常见的技术与自动化、存储与物流、魔法、冒险与维度、农业与食物、建筑和任务辅助模组,并收录这些生态中容易被通用翻译引擎误译的机器与机制短语。译名优先采用中文社区通行名称;没有稳定中文名的专名保留英文。 -```powershell -$env:FTB_TRANSLATER_CURSEFORGE_URL="https://edge.forgecdn.net/files/1234/567/your-pack.zip" -$env:FTB_TRANSLATER_LIVE_MAX_MB=500 -``` +对于 `Create`、`Carry On`、`Controlling`、`Artifacts`、`Spectrum` 等同时也是普通英文词的模组名,词表只收录带 `mod` 的明确写法或更完整的模组内术语,避免把普通句子中的动词、形容词和名词误替换成模组名称。 -调整真实 DeepSeek 测试抽样条目数: +旧版 220 条词表曾在 StoneBlock 4 `1.15.3` 中命中 103 条、保护 1,295 处术语;该结果仅作为历史基线。当前词表已改为跨整合包覆盖,实际命中率应针对目标整合包重新扫描,且命中率本身不代表语义准确率。 -```powershell -$env:FTB_TRANSLATER_LIVE_DEEPSEEK_ENTRIES=20 -``` +词表开关和所选 JSON 文件的 SHA-256 内容指纹都会参与缓存键计算,因此启用/禁用词表、修改文件或切换路径都不会误用旧译文缓存。保存设置和开始翻译时会校验 JSON 结构、空条目和重复术语。词表是用于兜底术语一致性的增强层,不能替代语义审校。 -安装为可执行脚本后也可以使用: +### 7. 写回与备份 -```powershell -ftb-test -ftb-test-e2e -``` +写入前先将原始 `lang` 或 `chapters` 目录整体备份到 `.ftb-translater/backups/<时间戳>/`。 -## CI/CD +- lang 模式:写出 `lang/zh_cn.snbt`,写前再次解析验证格式合法。 +- chapters 模式:按原文件路径就地修改各章节文件中的对应字段。 -GitHub Actions 工作流在 `.github/workflows/build.yml`: +--- -- PR:在 Ubuntu、Windows、macOS 上运行本地单元测试。 -- 推送到 `main` / `master`:先运行测试,再构建 Windows exe 和 macOS dmg,并上传为 workflow artifacts。 -- 推送 `v*` tag:构建成功后自动把安装包发布到 GitHub Release。 -- 手动触发:可以勾选 `run_live_e2e` 运行真实 CurseForge 下载和 DeepSeek 端到端测试。 +## 数据位置 -发布版本示例: +应用设置、可编辑的默认 `minecraft_glossary.json` 和历史数据库保存到系统应用数据目录(`AppData`/`Application Support`/`~/.local/share`)。 -```powershell -git tag v0.1.1 -git push origin v0.1.1 +每个任务书的运行数据保存在整合包目录内: + +``` +config/ftbquests/quests/.ftb-translater/ +├── cache.json # 翻译缓存(以 SHA-256 为键) +├── report-latest.json # 最近一次运行的完整报告 +└── backups/YYYYMMDD-HHMMSS/ # 翻译前的自动备份 ``` -手动 live e2e 需要在仓库 Secrets 中配置: +--- -- `DEEPSEEK_API_KEY`:真实 DeepSeek API Key。未配置时 DeepSeek 付费测试会跳过。 -- `FTB_TRANSLATER_CURSEFORGE_URL`:可选,直接指向 CurseForge / ForgeCDN `.zip` 文件;未配置时使用测试默认整合包。 +## 开发验证 -本地 CI 同款检查: +```bash +# Rust 单元测试 +cargo test --manifest-path src-tauri/Cargo.toml -```powershell -uv sync --dev --locked -uv run python -m tests.run_groups unit +# TypeScript 检查与前端构建 +npm run build ``` diff --git a/docs/translation-accuracy-audit.md b/docs/translation-accuracy-audit.md new file mode 100644 index 0000000..42b2967 --- /dev/null +++ b/docs/translation-accuracy-audit.md @@ -0,0 +1,109 @@ +# StoneBlock 4 Google 网页翻译准确度审计 + +- 审计日期:2026-07-13 +- 测试对象:StoneBlock 4 `1.15.3` 完整 `lang/en_us.snbt` +- 翻译提供商:免 API Key 的 Google 网页翻译 +结论:当前结果适合作为机器初译草稿,**不适合直接发布**。 + +## 覆盖范围与方法 + +程序化扫描覆盖全部 2,515 个 key、5,789 个文本片段,其中 4,183 个是非空且非纯占位文本。扫描项目包括: + +- 原文与译文完全相同 +- 中英文残留和异常长度 +- 数字、否定词和限定词风险 +- 相同原文翻译不一致 +- Minecraft 与模组高频术语 +- 专有名词误译 +- JSON 文本组件能否解析及结构是否保持 +- `item → 项目`、`block → 区块`、`tick → 蜱虫` 等常见机器翻译错误 + +人工复核包括固定随机种子抽样 200 条、全部 44 条 JSON 文本组件、全部 121 条原文未变化片段、全部 30 组同源异译,以及高频术语和严重异常项,合计约 350 个不同片段。 + +“全量覆盖”表示所有条目均经过规则扫描;准确度比例仍是基于抽样和高风险复核的估算,并非逐条人工定级,合理误差约为 ±5–7 个百分点。 + +## 准确度估算 + +| 等级 | 估算比例 | 含义 | +|------|----------|------| +| A | 约 48% | 语义正确,术语和表达基本可直接使用 | +| B | 约 29% | 大意正确,但存在明显机翻腔、术语或一致性问题 | +| C | 约 18% | 关键术语、信息关系或动作对象错误,需要重译 | +| D | 约 5% | 未翻译、内容损坏、严重幻觉或富文本无法解析 | + +严格按可直接公开发布的标准,当前准确率估计约为 **45%–55%**。README 中的 99.44% 是现有格式守卫通过率,不是翻译准确率。 + +## 确认的问题统计 + +- 44 条 JSON 文本组件中有 19 条目标内容无法解析,占 43.2%。 +- 52 个有意义的英文片段完全未译,涉及 26 个 key。 +- 其中 14 个 key 是格式守卫主动回退,至少 12 个未被报告标记。 +- 相同英文原文出现不同译法,共 30 组。 +- `item/items` 被译成“项目”至少 71 处。 +- 能量语境中的 `power` 被译成“电源”至少 32 处。 +- 普通方块 `block/blocks` 被误译成“区块”至少 27 处。 +- `enchanting` 被译成“迷人”至少 11 处。 +- `vanilla` 被译成“香草”至少 8 处。 +- `Mekanism` 被译成“机制”“机构”“机械”等,并与保留英文混用。 +- Echo-Location、Wyvern、Chaotic、Cobble、Mycelial、Eclipse 等术语存在多套译名。 + +## 代表性问题 + +| 严重度 | key | 英文 | 当前中文 | 建议译文 | +|--------|-----|------|----------|----------| +| 高 | `chapter.0C812DF8D14584AB.title` | Mekanism | 机制 | 通用机械 / Mekanism | +| 高 | `file.0000000000000001.title` | Stoneblock 4 | 石块 4 | StoneBlock 4 | +| 高 | `chapter.3BF272F9FDD1D8F9.title` | Draconic Evolution | 龙族进化 | 龙之进化 | +| 高 | `chapter.3B99F85218D37371.title` | Useful Items & Tips | 有用的项目&提示 | 实用物品与技巧 | +| 中 | `chapter.391B65CD04A90459.title` | Power | 电源 | 能源 / 电力 | +| 高 | `quest.009FE1E1B3017486.title` | Mob Slaughter Factory | 暴徒屠宰场 | 生物屠宰工厂 | +| 高 | `quest.11C58E38DE9488BE.title` | Mob Mashing | 暴徒捣乱 | 生物碾压 | +| 中 | `quest.007CD364F96C2912.quest_desc` | prevent all fall damage | 防止所有坠落损坏 | 免疫所有摔落伤害 | +| 高 | `quest.00A7DEFCF07BC63A.quest_desc` | Red Katar | 红色拳头 | 红物质拳剑,或采用 ProjectE 官方译名 | +| 高 | `quest.007F4A173D5CA51F.quest_desc` | souls you need for this craft | 这门手艺所需的所有灵魂 | 本次合成所需的全部灵魂 | +| 高 | `quest.00A7C1FE54F86429.quest_desc` | enchanting setup | 迷人设置 | 附魔设施 | +| 高 | `quest.00A7C1FE54F86429.quest_desc` | enchanting table | 迷人桌子 | 附魔台 | +| 高 | `quest.3382AC7FEFE0B76E.title` | Vanilla Netherite Weapons | 香草下界合金武器 | 原版下界合金武器 | +| 高 | `quest.38E2176886386133.quest_desc` | electrum | 金金 | 琥珀金 | +| 高 | `quest.0BEF15E1352A5F72.title` | Matter Replication | 事务复制 | 物质复制 | +| 高 | `task.496455D7AB00E7B8.title` | Constructors | 构造函数 | 构造器 | +| 高 | `quest.77074CE5719763BB.quest_desc` | These charges replenish | 这些费用会补充 | 这些充能次数会逐渐恢复 | +| 高 | `quest.456030ECDFA9CE3B.quest_desc` | Each tick | 每个蜱虫 | 每游戏刻 | +| 高 | `quest.13C8B2286F5184C7.quest_desc` | direction you are looking, up to 100 blocks | 您正在寻找的方向,最远100个街区 | 朝视线方向传送,最远 100 格 | +| 高 | `quest.5BE257CE3FD1FDAC.quest_desc` | upgrade your Infusion setup | 升级您的输液设置 | 升级注入合成设施 | +| 中 | `quest.091F82E2770F3011.title` | Ancient Check 1 | 古代支票 1 | 远古检查 1 | +| 高 | `quest.6BB9FD515DFD8FF1.title` | Applied Energistics 2 | 应用能量学2 | 应用能源 2 | +| 高 | `quest.1069575C4B5BCE39.title` | Refined Storage 2 | 精炼存储2 | 精致存储 2 | +| 高 | `quest.276D2B384F21AC10.title` | Just Dire's Weaponry | 只是可怕的武器 | Just Dire Things 武器 | +| 高 | `quest.7CA687749E953A19.quest_desc` | Right Click with a bullet | 用项目符号右键单击 | 手持法术子弹右键单击 | +| 高 | `quest.66BC4434409F959B.quest_desc` | hurting for Uranium | 伤害铀 | 缺少铀 | +| 高 | `quest.45CB3F325010A5AD.quest_desc` | Experience Obelisk | 体验方尖碑 | 经验方尖碑 | +| 高 | `quest.4C52D3DE2F7701D0.quest_desc` | Crafting the brush | 制作画笔 | 制作刷子 | +| 严重 | `quest.137EC884018376A1.quest_desc` | Ancient Debris rich-text link | 文本顺序被插入 `hoverEvent.action` | 重建 JSON,只翻译展示字段 | +| 严重 | `quest.05CEF45909A11F60.quest_desc` | backpack keybind JSON | JSON 引号变成中文全角引号 | 保持 JSON 语法,只翻译显示文本 | +| 严重 | `quest.4CE414ADD47790B2.quest_desc` | Replicators can be sped up... | 整句保持英文 | 复制器可通过安装复制器外壳来加速…… | +| 高 | `quest.4D754E91D203A89A.quest_desc` | Echo of Guidance | 指导回声 | 指引之回响,并全局统一 | +| 高 | `quest.5D2936B7F8D0F8FA.title` | Block of Ignitium Trade | Ignitium 贸易区块 | Ignitium 块兑换 | +| 高 | `quest.6755CACD4957A4CB.quest_desc` | essential part of your CAD | CAD CAD 的重要组成部分 | CAD 的核心组件 | +| 严重 | `quest.580CDFD2B7675485.quest_desc` | the craft should start | 飞船就会启动 | 合成过程就会启动 | +| 高 | `quest.427682661459BB4F.quest_desc` | either +100% or -100% power | +100% o或-100%力量 | 能量加成会在 +100% 与 -100% 之间变化 | +| 高 | `quest.0089BFB911BD5E5B.quest_desc` | 1 block per scale module / chunk claims | 1个区块 / 块声明 | 每个范围模块增加 1 格 / 区块认领 | +| 高 | `quest.5A04F7A95366D15F.quest_desc` | two blocks away | 两个街区 | 相距两个方块 | +| 高 | `quest.70D5BEA5CC437AB6.quest_desc` | Tome of Scrapping | 刮痧之书 | 拆解之书,或采用模组官方译名 | +| 高 | `task.3A831AEACF765BB4.title` | Quanta Items | 广达项目 | Quanta 物品 / 量子物品,需按 Apotheosis 术语表 | +| 高 | `quest.31C83D9253FFF685.title` | Mekanism Turbine | 机构涡轮机 | 通用机械:涡轮机 | + +## 发布判断与改进方向 + +当前译文不适合直接发布。主要问题不是少数错别字,而是富文本损坏、缺少模组术语库、玩法关键概念错译、同源异译、未译内容,以及部分会直接误导玩家操作的语义错误。 + +建议按以下顺序改进: + +1. 分开展示 API 请求成功率、格式守卫通过率和人工抽检准确率。 +2. 建立 Minecraft、模组、物品、方块、机器、Boss、NPC 和整合包叙事术语表。 +3. 从模组自带 `zh_cn.json` 提取注册名,优先复用官方本地化。 +4. 先解析 JSON 富文本,只翻译允许的 `text` 和展示型 `contents` 字段,再重新序列化和验证。 +5. 增加 `item → 物品`、`block → 方块`、`chunk → 区块`、`tick → 游戏刻`、`vanilla → 原版` 等语义规则。 +6. 标题和任务说明使用不同的翻译提示与质量标准。 +7. 对重复原文使用翻译记忆,禁止同源异译。 +8. 增加第二阶段术语修正与语义审校,重点检查否定、数字、方向、单位、快捷键和操作动词。 diff --git a/ftb_translater/__init__.py b/ftb_translater/__init__.py deleted file mode 100644 index 6bb53b6..0000000 --- a/ftb_translater/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""FTB Translater desktop tool.""" - -__version__ = "0.1.1" diff --git a/ftb_translater/app.py b/ftb_translater/app.py deleted file mode 100644 index bc9c3c8..0000000 --- a/ftb_translater/app.py +++ /dev/null @@ -1,1603 +0,0 @@ -from __future__ import annotations - -import queue -import threading -from collections import OrderedDict -from pathlib import Path -from tkinter import filedialog, messagebox -from typing import Literal, TypeAlias, TypedDict, cast - -import customtkinter as ctk - -from ftb_translater import credential_store -from ftb_translater.config import ( - BASE_URL_KEY, - BATCH_SIZE_KEY, - CONCURRENCY_KEY, - MODEL_KEY, - PROVIDER_KEY, - STYLE_KEY, - load_config_values, - migrate_api_key_from_env, - save_config_values, -) -from ftb_translater.deepseek_client import DEFAULT_BASE_URL, DEFAULT_MODEL, DEFAULT_STYLE -from ftb_translater.format_guard import protect_text, repair_translation_format, restore_text, preserved_token_warnings -from ftb_translater.history_db import FileRecord, HistoryDB, RunSummary -from ftb_translater.report import TranslationReport -from ftb_translater.chapters import count_chapter_segments, replace_chapter_segments -from ftb_translater.logger import get_logger, setup_logging -from ftb_translater.paths import detect_source_mode, resolve_quests_dir, source_lang_path -from ftb_translater.providers import ( - DEFAULT_PROVIDER, - PROVIDER_LABELS, - create_translator, - normalize_provider, - provider_defaults, - provider_requires_api_key, -) -from ftb_translater.snbt import load_lang_snbt, write_lang_snbt -from ftb_translater.translator import AUTO_BATCH_MAX_ENTRIES, AUTO_MAX_WORKERS, estimate_batches, translate_quests_auto - -_log = get_logger(__name__) - - -class _RunSettings(TypedDict): - api_key: str - provider: str - batch_size: int | None - model: str - style: str - base_url: str - max_workers: int | None - - -class _ReviewEntryData(TypedDict): - source: str - failed: str - textbox: ctk.CTkTextbox - status_label: ctk.CTkLabel - retrans_btn: ctk.CTkButton - frame: ctk.CTkFrame - - -_ProgressPayload: TypeAlias = tuple[str, int, int] -_AppQueueItem: TypeAlias = ( - tuple[Literal["progress"], _ProgressPayload] - | tuple[Literal["log"], str] - | tuple[Literal["done"], TranslationReport] - | tuple[Literal["error"], Exception] - | tuple[Literal["history_saved"], int] - | tuple[Literal["history_save_failed"], str] -) - - -class FtbTranslaterApp(ctk.CTk): - def __init__(self): - super().__init__() - setup_logging() - _log.info("FTB Translater starting up") - self.title("FTB Translater") - self.geometry("1120x780") - self.minsize(980, 700) - - migrate_api_key_from_env() - config_values = load_config_values() - self.selected_dir = ctk.StringVar() - configured_provider = normalize_provider(config_values.get(PROVIDER_KEY) or DEFAULT_PROVIDER) - configured_base_url, configured_model = provider_defaults(configured_provider) - self.api_key = ctk.StringVar(value=credential_store.load_api_key(configured_provider)) - self.provider_label = ctk.StringVar(value=PROVIDER_LABELS[configured_provider]) - self.base_url = ctk.StringVar(value=config_values.get(BASE_URL_KEY) or configured_base_url) - self.model = ctk.StringVar(value=config_values.get(MODEL_KEY) or configured_model) - self.style = ctk.StringVar(value=config_values.get(STYLE_KEY) or DEFAULT_STYLE) - self.batch_size = ctk.StringVar(value=config_values.get(BATCH_SIZE_KEY) or "auto") - self.max_workers = ctk.StringVar(value=config_values.get(CONCURRENCY_KEY) or "auto") - self.status = ctk.StringVar(value="请选择整合包目录,或它下面的 config/ftbquests/quests/lang/chapters 任一目录。") - self.summary = ctk.StringVar(value="未扫描") - self.stage = ctk.StringVar(value="准备就绪") - self.progress_text = ctk.StringVar(value="等待扫描") - self.settings_status = ctk.StringVar( - value=f"API Key 通过 {credential_store.storage_backend_label()} 保存。" - ) - self._quests_dir: Path | None = None - self._queue: queue.Queue[_AppQueueItem] = queue.Queue() - self._key_visible = False - self._step_labels: dict[str, ctk.CTkLabel] = {} - self._nav_buttons: dict[str, ctk.CTkButton] = {} - self._run_settings: _RunSettings | None = None - self._review_report: TranslationReport | None = None - self._review_data: dict[str, _ReviewEntryData] = {} - self.history_db = HistoryDB() - self._history_cards: dict[int, ctk.CTkFrame] = {} - self._build_ui() - self._sync_api_key_state() - self._set_stage("idle") - self.after(150, self._drain_queue) - - def _build_ui(self) -> None: - ctk.set_appearance_mode("System") - ctk.set_default_color_theme("blue") - - self.configure(fg_color=("#F8FAFB", "#0D1117")) - self.grid_columnconfigure(0, weight=1) - self.grid_rowconfigure(1, weight=1) - - topbar = ctk.CTkFrame( - self, - height=56, - corner_radius=0, - fg_color=("#FFFFFF", "#161B22"), - border_width=0, - ) - topbar.grid(row=0, column=0, sticky="ew") - topbar.grid_columnconfigure(1, weight=1) - topbar.grid_rowconfigure(0, weight=1) - topbar.grid_propagate(False) - - brand = ctk.CTkFrame(topbar, fg_color="transparent") - brand.grid(row=0, column=0, sticky="w", padx=24) - ctk.CTkLabel( - brand, - text="FTB Translater", - anchor="w", - font=ctk.CTkFont(size=18, weight="bold"), - text_color=("#1F2328", "#E6EDF3"), - ).grid(row=0, column=0, sticky="w") - ctk.CTkLabel( - brand, - text="任务书汉化", - anchor="w", - text_color=("#656D76", "#8B949E"), - font=ctk.CTkFont(size=12), - ).grid(row=0, column=1, sticky="w", padx=(10, 0)) - - right_bar = ctk.CTkFrame(topbar, fg_color="transparent") - right_bar.grid(row=0, column=2, sticky="e", padx=24) - self._nav_buttons["history"] = ctk.CTkButton( - right_bar, - text="📚 历史", - width=72, - height=32, - corner_radius=8, - command=lambda: self._show_view("history"), - font=ctk.CTkFont(size=13), - fg_color=("#F3F4F6", "#21262D"), - hover_color=("#E5E7EB", "#30363D"), - text_color=("#374151", "#C9D1D9"), - ) - self._nav_buttons["history"].grid(row=0, column=0, padx=(0, 8)) - self._nav_buttons["settings"] = ctk.CTkButton( - right_bar, - text="⚙ 设置", - width=72, - height=32, - corner_radius=8, - command=lambda: self._show_view("settings"), - font=ctk.CTkFont(size=13), - fg_color=("#F3F4F6", "#21262D"), - hover_color=("#E5E7EB", "#30363D"), - text_color=("#374151", "#C9D1D9"), - ) - self._nav_buttons["settings"].grid(row=0, column=1, padx=(0, 10)) - self.appearance_segment = ctk.CTkSegmentedButton( - right_bar, - values=["系统", "浅色", "深色"], - command=self._change_appearance, - selected_color="#2563EB", - selected_hover_color="#1D4ED8", - height=30, - font=ctk.CTkFont(size=12), - ) - self.appearance_segment.grid(row=0, column=2) - self.appearance_segment.set("系统") - - main = ctk.CTkFrame(self, corner_radius=0, fg_color="transparent") - main.grid(row=1, column=0, sticky="nsew", padx=32, pady=(20, 24)) - main.grid_columnconfigure(0, weight=1) - main.grid_rowconfigure(4, weight=1) - self.workbench_frame = main - - header = ctk.CTkFrame(main, fg_color="transparent") - header.grid(row=0, column=0, sticky="ew", pady=(0, 20)) - header.grid_columnconfigure(0, weight=1) - - title_row = ctk.CTkFrame(header, fg_color="transparent") - title_row.grid(row=0, column=0, sticky="ew") - title_row.grid_columnconfigure(0, weight=1) - ctk.CTkLabel( - title_row, - text="工作台", - anchor="w", - font=ctk.CTkFont(size=24, weight="bold"), - text_color=("#1F2328", "#E6EDF3"), - ).grid(row=0, column=0, sticky="w") - self.stage_badge = ctk.CTkLabel( - title_row, - textvariable=self.stage, - height=28, - corner_radius=14, - padx=14, - font=ctk.CTkFont(size=12, weight="bold"), - ) - self.stage_badge.grid(row=0, column=1, sticky="e") - - ctk.CTkLabel( - header, - textvariable=self.status, - anchor="w", - text_color=("#656D76", "#8B949E"), - font=ctk.CTkFont(size=13), - ).grid(row=1, column=0, sticky="ew", pady=(6, 0)) - - stepper = ctk.CTkFrame(header, fg_color="transparent", height=36) - stepper.grid(row=2, column=0, sticky="ew", pady=(12, 0)) - for col in range(4): - stepper.grid_columnconfigure(col, weight=0) - stepper.grid_columnconfigure(4, weight=1) - for col, (key, text) in enumerate( - [ - ("idle", "① 选择"), - ("scanned", "② 扫描"), - ("running", "③ 翻译"), - ("done", "④ 完成"), - ] - ): - self._step_labels[key] = ctk.CTkLabel( - stepper, - text=text, - height=28, - corner_radius=6, - padx=10, - font=ctk.CTkFont(size=12), - text_color=("#656D76", "#8B949E"), - fg_color=("#F3F4F6", "#1C2128"), - ) - self._step_labels[key].grid(row=0, column=col, padx=(0, 6)) - - source_panel = self._panel(main, "选择整合包", "选择整合包根目录,或直接选择 quests / lang / chapters 目录") - source_panel.grid(row=1, column=0, sticky="ew", pady=(0, 10)) - source_panel.grid_columnconfigure(0, weight=1) - ctk.CTkEntry( - source_panel, - textvariable=self.selected_dir, - height=36, - corner_radius=8, - placeholder_text="选择整合包目录...", - border_width=1, - border_color=("#D0D7DE", "#30363D"), - ).grid(row=2, column=0, sticky="ew", padx=16, pady=(0, 14)) - ctk.CTkButton( - source_panel, text="选择目录", width=90, height=36, - corner_radius=8, command=self._choose_dir, - fg_color=("#2563EB", "#2563EB"), hover_color=("#1D4ED8", "#1D4ED8"), - ).grid(row=2, column=1, padx=(0, 8), pady=(0, 14)) - self.scan_button = ctk.CTkButton( - source_panel, text="扫描", width=64, height=36, - corner_radius=8, command=self._scan, - fg_color=("#F3F4F6", "#21262D"), hover_color=("#E5E7EB", "#30363D"), - text_color=("#374151", "#C9D1D9"), - ) - self.scan_button.grid(row=2, column=2, padx=(0, 16), pady=(0, 14)) - - summary_panel = self._panel(main, "扫描结果", "扫描后显示目录、模式、条目数和预计批次") - summary_panel.grid(row=2, column=0, sticky="ew", pady=(0, 10)) - summary_panel.grid_columnconfigure(0, weight=1) - ctk.CTkLabel( - summary_panel, - textvariable=self.summary, - anchor="w", - justify="left", - wraplength=700, - font=ctk.CTkFont(size=13), - text_color=("#374151", "#C9D1D9"), - ).grid(row=2, column=0, sticky="ew", padx=16, pady=(0, 14)) - - run_panel = ctk.CTkFrame( - main, - corner_radius=12, - border_width=1, - fg_color=("#FFFFFF", "#161B22"), - border_color=("#D0D7DE", "#30363D"), - ) - run_panel.grid(row=3, column=0, sticky="ew", pady=(0, 10)) - run_panel.grid_columnconfigure(0, weight=1) - self.progress = ctk.CTkProgressBar( - run_panel, height=8, corner_radius=4, - progress_color="#2563EB", - ) - self.progress.grid(row=0, column=0, sticky="ew", padx=16, pady=(16, 6)) - self.progress.set(0) - ctk.CTkLabel( - run_panel, textvariable=self.progress_text, anchor="w", - text_color=("#656D76", "#8B949E"), font=ctk.CTkFont(size=12), - ).grid(row=1, column=0, sticky="ew", padx=16, pady=(0, 16)) - self.translate_button = ctk.CTkButton( - run_panel, - text="开始汉化", - width=110, - height=36, - corner_radius=8, - command=self._start_translate, - state="disabled", - font=ctk.CTkFont(size=14, weight="bold"), - fg_color=("#16A34A", "#238636"), - hover_color=("#15803D", "#2EA043"), - ) - self.translate_button.grid(row=0, column=1, rowspan=2, sticky="e", padx=16, pady=16) - - bottom = ctk.CTkFrame(main, corner_radius=0, fg_color="transparent") - bottom.grid(row=4, column=0, sticky="nsew") - bottom.grid_columnconfigure(0, weight=1) - bottom.grid_rowconfigure(0, weight=1) - - log_panel = self._panel(bottom, "日志", "扫描、备份、翻译和写入的实时输出") - log_panel.grid(row=0, column=0, sticky="nsew") - log_panel.grid_columnconfigure(0, weight=1) - log_panel.grid_rowconfigure(2, weight=1) - self.log = ctk.CTkTextbox( - log_panel, height=140, corner_radius=8, - font=ctk.CTkFont(size=12, family="Menlo"), - fg_color=("#F6F8FA", "#0D1117"), - text_color=("#1F2328", "#C9D1D9"), - ) - self.log.grid(row=2, column=0, sticky="nsew", padx=16, pady=(0, 14)) - - review_panel = ctk.CTkFrame( - bottom, corner_radius=12, border_width=1, - fg_color=("#FFFBEB", "#1C1917"), - border_color=("#F59E0B", "#92400E"), - ) - review_panel.grid(row=1, column=0, sticky="nsew", pady=(10, 0)) - review_panel.grid_columnconfigure(0, weight=1) - review_panel.grid_columnconfigure(1, weight=0) - review_panel.grid_rowconfigure(3, weight=1) - review_panel.grid_remove() - self.review_panel = review_panel - - self._review_title = ctk.StringVar(value="人工处理") - ctk.CTkLabel( - review_panel, textvariable=self._review_title, anchor="w", - font=ctk.CTkFont(size=15, weight="bold"), - text_color=("#92400E", "#FCD34D"), - ).grid(row=0, column=0, sticky="ew", padx=16, pady=(14, 2)) - - self._review_badge = ctk.CTkLabel( - review_panel, text="", height=24, corner_radius=12, padx=10, - fg_color=("#F59E0B", "#B45309"), text_color="#FFFFFF", - font=ctk.CTkFont(size=11, weight="bold"), - ) - self._review_badge.grid(row=0, column=1, sticky="e", padx=(0, 16), pady=(14, 2)) - - self._review_subtitle = ctk.CTkLabel( - review_panel, text="", anchor="w", - text_color=("#78716C", "#A8A29E"), - wraplength=760, font=ctk.CTkFont(size=12), - ) - self._review_subtitle.grid(row=1, column=0, columnspan=2, sticky="ew", padx=16, pady=(0, 6)) - - action_bar = ctk.CTkFrame(review_panel, fg_color="transparent") - action_bar.grid(row=2, column=0, columnspan=2, sticky="ew", padx=16, pady=(0, 6)) - action_bar.grid_columnconfigure(0, weight=1) - self._retranslate_all_btn = ctk.CTkButton( - action_bar, text="全部重新翻译", width=120, height=30, - corner_radius=8, command=self._retranslate_all_review, - fg_color=("#F59E0B", "#B45309"), hover_color=("#D97706", "#92400E"), - ) - self._retranslate_all_btn.grid(row=0, column=1, padx=(0, 6), sticky="e") - self._ignore_all_btn = ctk.CTkButton( - action_bar, text="全部忽略", width=90, height=30, - corner_radius=8, - fg_color=("#6B7280", "#4B5563"), hover_color=("#4B5563", "#374151"), - command=self._ignore_all_review, - ) - self._ignore_all_btn.grid(row=0, column=2, sticky="e") - - self._review_scroll = ctk.CTkScrollableFrame( - review_panel, corner_radius=8, border_width=1, - fg_color=("#FFFFFF", "#0D1117"), - border_color=("#E5E7EB", "#30363D"), - ) - self._review_scroll.grid(row=3, column=0, columnspan=2, sticky="nsew", padx=16, pady=(0, 14)) - self._review_scroll.grid_columnconfigure(0, weight=1) - - self._build_settings_view() - self._build_history_view() - self._show_view("workbench") - - def _build_settings_view(self) -> None: - settings = ctk.CTkFrame(self, corner_radius=0, fg_color="transparent") - settings.grid(row=1, column=0, sticky="nsew", padx=32, pady=(20, 24)) - settings.grid_columnconfigure(0, weight=1) - settings.grid_rowconfigure(1, weight=1) - self.settings_frame = settings - - header = ctk.CTkFrame(settings, fg_color="transparent") - header.grid(row=0, column=0, sticky="ew", pady=(0, 16)) - header.grid_columnconfigure(0, weight=1) - ctk.CTkLabel( - header, - text="设置", - anchor="w", - font=ctk.CTkFont(size=24, weight="bold"), - text_color=("#1F2328", "#E6EDF3"), - ).grid(row=0, column=0, sticky="ew") - ctk.CTkButton( - header, text="← 返回工作台", width=110, height=32, - corner_radius=8, command=lambda: self._show_view("workbench"), - fg_color=("#F3F4F6", "#21262D"), hover_color=("#E5E7EB", "#30363D"), - text_color=("#374151", "#C9D1D9"), - ).grid(row=0, column=1, sticky="e") - ctk.CTkLabel( - header, - text="修改后会影响之后的扫描和汉化任务", - anchor="w", - text_color=("#656D76", "#8B949E"), - font=ctk.CTkFont(size=13), - ).grid(row=1, column=0, columnspan=2, sticky="ew", pady=(6, 0)) - - content = ctk.CTkScrollableFrame(settings, corner_radius=0, fg_color="transparent") - content.grid(row=1, column=0, sticky="nsew") - content.grid_columnconfigure(0, weight=1) - - api_panel = self._panel( - content, - "API Key", - f"调用翻译接口的凭证,会保存到{credential_store.storage_backend_label()}", - ) - api_panel.grid(row=0, column=0, sticky="ew", pady=(0, 10)) - api_panel.grid_columnconfigure(0, weight=1) - self.key_entry = ctk.CTkEntry( - api_panel, textvariable=self.api_key, show="*", height=36, - corner_radius=8, border_width=1, border_color=("#D0D7DE", "#30363D"), - ) - self.key_entry.grid(row=2, column=0, sticky="ew", padx=16, pady=(0, 10)) - self.toggle_key_button = ctk.CTkButton( - api_panel, text="显示", width=60, height=36, corner_radius=8, - command=self._toggle_key_visibility, - fg_color=("#F3F4F6", "#21262D"), hover_color=("#E5E7EB", "#30363D"), - text_color=("#374151", "#C9D1D9"), - ) - self.toggle_key_button.grid(row=2, column=1, padx=(0, 8), pady=(0, 10)) - ctk.CTkButton( - api_panel, text="保存全部", width=80, height=36, corner_radius=8, - command=self._save_settings, - fg_color=("#2563EB", "#2563EB"), hover_color=("#1D4ED8", "#1D4ED8"), - ).grid(row=2, column=2, padx=(0, 16), pady=(0, 10)) - ctk.CTkLabel( - api_panel, - textvariable=self.settings_status, - anchor="w", - justify="left", - wraplength=660, - text_color=("#656D76", "#8B949E"), - font=ctk.CTkFont(size=12), - ).grid(row=3, column=0, columnspan=3, sticky="ew", padx=16, pady=(0, 14)) - - service_panel = self._panel(content, "翻译服务", "支持 OpenAI 兼容接口和 DeepL 翻译 API") - service_panel.grid(row=1, column=0, sticky="ew", pady=(0, 10)) - service_panel.grid_columnconfigure(0, weight=1) - ctk.CTkLabel( - service_panel, text="提供商", anchor="w", - font=ctk.CTkFont(size=12), text_color=("#374151", "#C9D1D9"), - ).grid(row=2, column=0, sticky="ew", padx=16, pady=(0, 4)) - ctk.CTkOptionMenu( - service_panel, - variable=self.provider_label, - values=list(PROVIDER_LABELS.values()), - command=self._change_provider, - height=36, - corner_radius=8, - ).grid(row=3, column=0, columnspan=2, sticky="ew", padx=16, pady=(0, 12)) - ctk.CTkLabel( - service_panel, text="API 地址", anchor="w", - font=ctk.CTkFont(size=12), text_color=("#374151", "#C9D1D9"), - ).grid(row=4, column=0, sticky="ew", padx=16, pady=(0, 4)) - ctk.CTkEntry( - service_panel, textvariable=self.base_url, height=36, - corner_radius=8, border_width=1, border_color=("#D0D7DE", "#30363D"), - ).grid(row=5, column=0, columnspan=2, sticky="ew", padx=16, pady=(0, 12)) - ctk.CTkLabel( - service_panel, text="模型(DeepL 可保持 deepl)", anchor="w", - font=ctk.CTkFont(size=12), text_color=("#374151", "#C9D1D9"), - ).grid(row=6, column=0, sticky="ew", padx=16, pady=(0, 4)) - ctk.CTkEntry( - service_panel, textvariable=self.model, height=36, - corner_radius=8, border_width=1, border_color=("#D0D7DE", "#30363D"), - ).grid(row=7, column=0, sticky="ew", padx=16, pady=(0, 14)) - ctk.CTkButton( - service_panel, text="恢复默认", width=80, height=32, corner_radius=8, - command=self._reset_service_settings, - fg_color=("#F3F4F6", "#21262D"), hover_color=("#E5E7EB", "#30363D"), - text_color=("#374151", "#C9D1D9"), - ).grid(row=7, column=1, sticky="e", padx=(0, 16), pady=(0, 14)) - - translate_panel = self._panel(content, "翻译参数", "批大小和并发可填 auto 使用自动策略,或填正整数手动控制") - translate_panel.grid(row=2, column=0, sticky="ew", pady=(0, 10)) - translate_panel.grid_columnconfigure(0, weight=1) - translate_panel.grid_columnconfigure(1, weight=1) - ctk.CTkLabel( - translate_panel, text="翻译风格", anchor="w", - font=ctk.CTkFont(size=12), text_color=("#374151", "#C9D1D9"), - ).grid(row=2, column=0, columnspan=2, sticky="ew", padx=16, pady=(0, 4)) - self.style_text = ctk.CTkTextbox( - translate_panel, height=68, corner_radius=8, - font=ctk.CTkFont(size=13), - fg_color=("#F6F8FA", "#0D1117"), - border_width=1, border_color=("#D0D7DE", "#30363D"), - ) - self.style_text.grid(row=3, column=0, columnspan=2, sticky="ew", padx=16, pady=(0, 12)) - self.style_text.insert("1.0", self.style.get()) - ctk.CTkLabel( - translate_panel, text="批大小", anchor="w", - font=ctk.CTkFont(size=12), text_color=("#374151", "#C9D1D9"), - ).grid(row=4, column=0, sticky="ew", padx=16, pady=(0, 4)) - ctk.CTkLabel( - translate_panel, text="并发数", anchor="w", - font=ctk.CTkFont(size=12), text_color=("#374151", "#C9D1D9"), - ).grid(row=4, column=1, sticky="ew", padx=(0, 16), pady=(0, 4)) - ctk.CTkEntry( - translate_panel, textvariable=self.batch_size, height=36, - corner_radius=8, placeholder_text="auto", - border_width=1, border_color=("#D0D7DE", "#30363D"), - ).grid(row=5, column=0, sticky="ew", padx=16, pady=(0, 14)) - ctk.CTkEntry( - translate_panel, textvariable=self.max_workers, height=36, - corner_radius=8, placeholder_text="auto", - border_width=1, border_color=("#D0D7DE", "#30363D"), - ).grid(row=5, column=1, sticky="ew", padx=(0, 16), pady=(0, 14)) - ctk.CTkLabel( - translate_panel, - text=f"auto: 最多 {AUTO_BATCH_MAX_ENTRIES} 条/批,{AUTO_MAX_WORKERS} 并发", - anchor="w", - text_color=("#656D76", "#8B949E"), - font=ctk.CTkFont(size=11), - ).grid(row=6, column=0, columnspan=2, sticky="ew", padx=16, pady=(0, 14)) - - def _build_history_view(self) -> None: - history = ctk.CTkFrame(self, corner_radius=0, fg_color="transparent") - history.grid(row=1, column=0, sticky="nsew", padx=32, pady=(20, 24)) - history.grid_columnconfigure(0, weight=1) - history.grid_rowconfigure(1, weight=1) - self.history_frame = history - - header = ctk.CTkFrame(history, fg_color="transparent") - header.grid(row=0, column=0, sticky="ew", pady=(0, 16)) - header.grid_columnconfigure(0, weight=1) - ctk.CTkLabel( - header, text="翻译历史", anchor="w", - font=ctk.CTkFont(size=24, weight="bold"), - text_color=("#1F2328", "#E6EDF3"), - ).grid(row=0, column=0, sticky="ew") - - button_row = ctk.CTkFrame(header, fg_color="transparent") - button_row.grid(row=0, column=1, sticky="e") - ctk.CTkButton( - button_row, text="🔄 刷新", width=72, height=32, corner_radius=8, - command=self._refresh_history_list, - fg_color=("#F3F4F6", "#21262D"), hover_color=("#E5E7EB", "#30363D"), - text_color=("#374151", "#C9D1D9"), - ).grid(row=0, column=0, padx=(0, 8)) - ctk.CTkButton( - button_row, text="← 返回工作台", width=110, height=32, corner_radius=8, - command=lambda: self._show_view("workbench"), - fg_color=("#F3F4F6", "#21262D"), hover_color=("#E5E7EB", "#30363D"), - text_color=("#374151", "#C9D1D9"), - ).grid(row=0, column=1) - - ctk.CTkLabel( - header, - text="保存的汉化记录,可以导出 ZIP 分享给其他玩家,或者删除清理空间", - anchor="w", - text_color=("#656D76", "#8B949E"), - font=ctk.CTkFont(size=13), - ).grid(row=1, column=0, columnspan=2, sticky="ew", pady=(6, 0)) - - self._history_list_frame = ctk.CTkScrollableFrame( - history, corner_radius=0, fg_color="transparent", - ) - self._history_list_frame.grid(row=1, column=0, sticky="nsew") - self._history_list_frame.grid_columnconfigure(0, weight=1) - - self._history_empty_label = ctk.CTkLabel( - self._history_list_frame, - text="暂无翻译历史。完成一次汉化后会自动出现在这里。", - text_color=("#656D76", "#8B949E"), - font=ctk.CTkFont(size=14), - ) - - def _refresh_history_list(self) -> None: - for widget in self._history_list_frame.winfo_children(): - widget.destroy() - self._history_cards.clear() - runs = self.history_db.list_runs() - if not runs: - self._history_empty_label = ctk.CTkLabel( - self._history_list_frame, - text="暂无翻译历史。完成一次汉化后会自动出现在这里。", - text_color=("#656D76", "#8B949E"), - font=ctk.CTkFont(size=14), - ) - self._history_empty_label.grid(row=0, column=0, pady=60) - return - for idx, summary in enumerate(runs): - self._build_history_card(idx, summary) - - def _build_history_card(self, idx: int, summary: RunSummary) -> None: - card = ctk.CTkFrame( - self._history_list_frame, corner_radius=12, border_width=1, - fg_color=("#FFFFFF", "#161B22"), - border_color=("#D0D7DE", "#30363D"), - ) - card.grid(row=idx, column=0, sticky="ew", pady=(0, 10)) - card.grid_columnconfigure(0, weight=1) - self._history_cards[summary.id] = card - - top_row = ctk.CTkFrame(card, fg_color="transparent") - top_row.grid(row=0, column=0, sticky="ew", padx=16, pady=(14, 4)) - top_row.grid_columnconfigure(0, weight=1) - ctk.CTkLabel( - top_row, - text=f"{summary.pack_name or '未命名'} · #{summary.id}", - anchor="w", - font=ctk.CTkFont(size=15, weight="bold"), - text_color=("#1F2328", "#E6EDF3"), - ).grid(row=0, column=0, sticky="w") - ctk.CTkLabel( - top_row, text=summary.created_at, anchor="e", - text_color=("#656D76", "#8B949E"), - font=ctk.CTkFont(size=12), - ).grid(row=0, column=1, sticky="e") - - mode_text = "lang 模式" if summary.mode == "lang" else "chapters 模式" - meta_text = ( - f"{mode_text} · {summary.total_entries} 条 · {summary.model}" - ) - ctk.CTkLabel( - card, text=meta_text, anchor="w", - text_color=("#374151", "#C9D1D9"), - font=ctk.CTkFont(size=12), - ).grid(row=1, column=0, sticky="ew", padx=16, pady=(0, 2)) - - stats_text = ( - f"成功 {summary.translated_entries} · " - f"失败 {summary.failed_count} · " - f"告警 {summary.warning_count} · " - f"缓存命中 {summary.cache_hits}" - ) - ctk.CTkLabel( - card, text=stats_text, anchor="w", - text_color=("#656D76", "#8B949E"), - font=ctk.CTkFont(size=11), - ).grid(row=2, column=0, sticky="ew", padx=16, pady=(0, 2)) - - path_text = summary.quests_dir - if len(path_text) > 90: - path_text = "..." + path_text[-87:] - ctk.CTkLabel( - card, text=path_text, anchor="w", - text_color=("#94A3B8", "#6E7681"), - font=ctk.CTkFont(size=11, family="Menlo"), - ).grid(row=3, column=0, sticky="ew", padx=16, pady=(0, 8)) - - btn_row = ctk.CTkFrame(card, fg_color="transparent") - btn_row.grid(row=4, column=0, sticky="ew", padx=16, pady=(0, 12)) - btn_row.grid_columnconfigure(0, weight=1) - ctk.CTkButton( - btn_row, text="导出 ZIP", width=90, height=30, corner_radius=6, - fg_color=("#2563EB", "#2563EB"), hover_color=("#1D4ED8", "#1D4ED8"), - command=lambda sid=summary.id: self._export_history_zip(sid), - ).grid(row=0, column=1, padx=(0, 6), sticky="e") - ctk.CTkButton( - btn_row, text="查看", width=70, height=30, corner_radius=6, - fg_color=("#F3F4F6", "#21262D"), hover_color=("#E5E7EB", "#30363D"), - text_color=("#374151", "#C9D1D9"), - command=lambda sid=summary.id: self._show_history_detail(sid), - ).grid(row=0, column=2, padx=(0, 6), sticky="e") - ctk.CTkButton( - btn_row, text="删除", width=70, height=30, corner_radius=6, - fg_color=("#DC2626", "#991B1B"), hover_color=("#B91C1C", "#7F1D1D"), - command=lambda sid=summary.id: self._delete_history_run(sid), - ).grid(row=0, column=3, sticky="e") - - def _export_history_zip(self, run_id: int) -> None: - summary = self.history_db.get_run(run_id) - if summary is None: - messagebox.showerror("导出失败", f"记录 #{run_id} 不存在。") - return - default_name = f"{summary.pack_name or 'ftb-translation'}-{summary.id}.zip" - dest = filedialog.asksaveasfilename( - title="导出翻译 ZIP", - defaultextension=".zip", - initialfile=default_name, - filetypes=[("ZIP 文件", "*.zip")], - ) - if not dest: - return - try: - written = self.history_db.export_zip(run_id, Path(dest)) - messagebox.showinfo("导出成功", f"已写入:\n{written}") - self._log(f"已导出历史 #{run_id} -> {written}") - except Exception as exc: # noqa: BLE001 - messagebox.showerror("导出失败", str(exc)) - _log.error("Export failed: %s", exc, exc_info=True) - - def _delete_history_run(self, run_id: int) -> None: - if not messagebox.askyesno("确认删除", f"确定要删除历史记录 #{run_id} 吗?此操作不可撤销。"): - return - try: - self.history_db.delete_run(run_id) - self._log(f"已删除历史 #{run_id}") - self._refresh_history_list() - except Exception as exc: # noqa: BLE001 - messagebox.showerror("删除失败", str(exc)) - - def _show_history_detail(self, run_id: int) -> None: - summary = self.history_db.get_run(run_id) - if summary is None: - messagebox.showerror("查看失败", f"记录 #{run_id} 不存在。") - return - files = self.history_db.get_files(run_id) - - window = ctk.CTkToplevel(self) - window.title(f"历史 #{run_id} - {summary.pack_name or '未命名'}") - window.geometry("760x560") - window.grab_set() - window.grid_columnconfigure(0, weight=1) - window.grid_rowconfigure(1, weight=1) - - meta_text = ( - f"模式: {summary.mode} 模型: {summary.model} 创建于: {summary.created_at}\n" - f"总条目: {summary.total_entries} 成功: {summary.translated_entries} " - f"失败: {summary.failed_count} 告警: {summary.warning_count}\n" - f"风格: {summary.style}\n" - f"路径: {summary.quests_dir}" - ) - ctk.CTkLabel( - window, text=meta_text, anchor="w", justify="left", - font=ctk.CTkFont(size=12), - text_color=("#374151", "#C9D1D9"), - ).grid(row=0, column=0, sticky="ew", padx=16, pady=(14, 8)) - - textbox = ctk.CTkTextbox( - window, corner_radius=8, - font=ctk.CTkFont(size=12, family="Menlo"), - fg_color=("#F6F8FA", "#0D1117"), - ) - textbox.grid(row=1, column=0, sticky="nsew", padx=16, pady=(0, 12)) - - for record in files: - textbox.insert("end", f"=== {record.filename} ({len(record.mapping)} 条) ===\n") - for idx, (key, pair) in enumerate(list(record.mapping.items())[:20]): - en = pair.get("en", "").replace("\n", " ⏎ ") - zh = pair.get("zh", "").replace("\n", " ⏎ ") - textbox.insert("end", f" [{key}]\n EN: {en[:120]}\n ZH: {zh[:120]}\n") - if len(record.mapping) > 20: - textbox.insert("end", f" ... 还有 {len(record.mapping) - 20} 条\n") - textbox.insert("end", "\n") - textbox.configure(state="disabled") - - ctk.CTkButton( - window, text="关闭", width=80, height=32, corner_radius=8, - command=window.destroy, - ).grid(row=2, column=0, pady=(0, 14)) - - def _show_review_entries(self) -> None: - for widget in self._review_scroll.winfo_children(): - widget.destroy() - self._review_data.clear() - - report = self._review_report - if report is None: - self._hide_review_panel() - return - - entries = self._review_entries(report) - if not entries: - self._hide_review_panel() - return - - self.review_panel.grid() - self.review_panel.master.grid_rowconfigure(1, weight=1) - self._review_title.set(f"人工处理 — {len(entries)} 条需要确认的映射") - self._review_badge.configure(text=f"待处理 {len(entries)}") - - self._review_subtitle.configure( - text="这些条目因为 API 失败或格式保护被保留为原文。可以逐条编辑后保存,也可以重新翻译或忽略。" - ) - - for idx, (key, warning_list) in enumerate(entries): - ft = report.failed_translations.get(key, {}) - source = ft.get("source", key) - failed = ft.get("failed", "") - - card = ctk.CTkFrame( - self._review_scroll, corner_radius=10, - fg_color=("#FFFFFF", "#161B22"), - border_width=1, border_color=("#D0D7DE", "#30363D"), - ) - card.grid(row=idx, column=0, sticky="ew", pady=(0, 8)) - card.grid_columnconfigure(0, weight=1) - card.grid_columnconfigure(1, weight=1) - - key_row = ctk.CTkFrame(card, fg_color="transparent") - key_row.grid(row=0, column=0, columnspan=2, sticky="ew", padx=12, pady=(10, 6)) - key_row.grid_columnconfigure(0, weight=1) - ctk.CTkLabel( - key_row, text=key, anchor="w", - font=ctk.CTkFont(size=11, family="Menlo"), - text_color=("#656D76", "#8B949E"), - ).grid(row=0, column=0, sticky="ew") - ctk.CTkLabel( - key_row, text="待确认", height=20, corner_radius=10, padx=8, - fg_color=("#FEF3C7", "#451A03"), text_color=("#92400E", "#FCD34D"), - font=ctk.CTkFont(size=10, weight="bold"), - ).grid(row=0, column=1, sticky="e") - - ctk.CTkLabel( - card, text="原文", anchor="w", - font=ctk.CTkFont(size=12, weight="bold"), - text_color=("#374151", "#C9D1D9"), - ).grid(row=1, column=0, sticky="w", padx=12, pady=(0, 2)) - ctk.CTkLabel( - card, text="翻译(可编辑)", anchor="w", - font=ctk.CTkFont(size=12, weight="bold"), - text_color=("#374151", "#C9D1D9"), - ).grid(row=1, column=1, sticky="w", padx=12, pady=(0, 2)) - - src_tb = ctk.CTkTextbox( - card, height=90, corner_radius=6, - fg_color=("#F6F8FA", "#0D1117"), - border_width=1, border_color=("#D0D7DE", "#30363D"), - font=ctk.CTkFont(size=12), - ) - src_tb.grid(row=2, column=0, sticky="nsew", padx=(12, 6), pady=(0, 4)) - src_tb.insert("1.0", source) - src_tb.configure(state="disabled") - - trans_tb = ctk.CTkTextbox( - card, height=90, corner_radius=6, - border_width=1, border_color=("#2563EB", "#2563EB"), - font=ctk.CTkFont(size=12), - ) - trans_tb.grid(row=2, column=1, sticky="nsew", padx=(6, 12), pady=(0, 4)) - trans_tb.insert("1.0", failed or source) - - warn_text = "\n".join(f"\u26a0 {w}" for w in warning_list) - warn_box = ctk.CTkFrame(card, corner_radius=6, fg_color=("#FEF2F2", "#2D1B1B")) - warn_box.grid(row=3, column=0, columnspan=2, sticky="ew", padx=12, pady=(4, 4)) - warn_box.grid_columnconfigure(0, weight=1) - ctk.CTkLabel( - warn_box, text=warn_text, anchor="w", wraplength=700, - text_color=("#DC2626", "#FCA5A5"), font=ctk.CTkFont(size=11), - justify="left", - ).grid(row=0, column=0, sticky="ew", padx=8, pady=6) - - status_lbl = ctk.CTkLabel( - card, text="", anchor="w", - text_color=("#656D76", "#8B949E"), font=ctk.CTkFont(size=11), - ) - status_lbl.grid(row=4, column=0, columnspan=2, sticky="ew", padx=12, pady=(2, 4)) - - btn_frame = ctk.CTkFrame(card, fg_color="transparent") - btn_frame.grid(row=5, column=0, columnspan=2, sticky="ew", padx=12, pady=(0, 8)) - btn_frame.grid_columnconfigure(0, weight=1) - - ctk.CTkButton( - btn_frame, text="保存", width=60, height=28, corner_radius=6, - fg_color=("#2563EB", "#2563EB"), hover_color=("#1D4ED8", "#1D4ED8"), - command=lambda k=key, tb=trans_tb, sl=status_lbl: - self._save_review_entry(k, tb, sl), - ).grid(row=0, column=1, padx=(0, 6), sticky="e") - - retrans_btn = ctk.CTkButton( - btn_frame, text="重新翻译", width=80, height=28, corner_radius=6, - fg_color=("#F59E0B", "#B45309"), hover_color=("#D97706", "#92400E"), - ) - retrans_btn.configure( - command=lambda rb=retrans_btn, k=key, s=source, tb=trans_tb, sl=status_lbl: - self._retranslate_single(k, s, tb, sl, rb), - ) - retrans_btn.grid(row=0, column=2, padx=(0, 6), sticky="e") - - ctk.CTkButton( - btn_frame, text="忽略", width=60, height=28, corner_radius=6, - fg_color=("#6B7280", "#4B5563"), hover_color=("#4B5563", "#374151"), - command=lambda k=key, c=card: self._ignore_review_entry(k, c), - ).grid(row=0, column=3, sticky="e") - - self._review_data[key] = { - "source": source, - "failed": failed, - "textbox": trans_tb, - "status_label": status_lbl, - "retrans_btn": retrans_btn, - "frame": card, - } - - def _review_entries(self, report: TranslationReport) -> list[tuple[str, list[str]]]: - entries: OrderedDict[str, list[str]] = OrderedDict() - for key, warning_list in report.warnings.items(): - entries[key] = list(warning_list) - for failed_entry in report.failed_entries: - key, _, error = failed_entry.partition(":") - key = key.strip() - if not key: - continue - if key in entries: - continue - message = error.strip() or failed_entry - entries.setdefault(key, []).append(f"API 调用失败:{message}") - return list(entries.items()) - - def _hide_review_panel(self) -> None: - for widget in self._review_scroll.winfo_children(): - widget.destroy() - self._review_data.clear() - self.review_panel.grid_remove() - self.review_panel.master.grid_rowconfigure(1, weight=0) - - def _save_review_entry(self, key: str, textbox: ctk.CTkTextbox, status_label: ctk.CTkLabel) -> None: - report = self._review_report - if report is None: - status_label.configure(text="错误:无翻译报告。") - return - new_text = textbox.get("1.0", "end").strip() - if not new_text: - status_label.configure(text="错误:翻译内容为空。") - return - try: - target = Path(report.target_file) - if not target.exists(): - status_label.configure(text=f"错误:目标文件不存在 {target}") - return - if target.suffix == ".snbt": - values = load_lang_snbt(target) - values[key] = new_text - write_lang_snbt(target, values) - elif target.is_dir(): - self._save_chapter_review_entry(target, key, new_text) - else: - status_label.configure(text=f"错误:无法识别目标文件 {target}") - return - status_label.configure(text="已保存 \u2713") - except Exception as exc: - status_label.configure(text=f"保存失败:{exc}") - - def _save_chapter_review_entry(self, chapters_dir: Path, key: str, new_text: str) -> None: - parts = key.split(":", 2) - if len(parts) != 3: - raise ValueError(f"章节映射 key 格式无效:{key}") - filename, index_text, _segment_key = parts - try: - segment_index = int(index_text) - except ValueError as exc: - raise ValueError(f"章节映射序号无效:{key}") from exc - chapter_path = chapters_dir / filename - if not chapter_path.exists(): - raise FileNotFoundError(f"章节文件不存在:{chapter_path}") - replaced = replace_chapter_segments(chapter_path, {segment_index: new_text}) - if replaced != 1: - raise ValueError(f"未能定位章节文本段:{key}") - - def _retranslate_single( - self, key: str, source: str, - textbox: ctk.CTkTextbox, status_label: ctk.CTkLabel, - button: ctk.CTkButton | None, - ) -> None: - api_key = self.api_key.get().strip() - if not api_key: - status_label.configure(text="错误:请先配置 API Key。") - return - if button: - button.configure(state="disabled", text="翻译中...") - - def worker(): - try: - selected_api_key, provider, model, style, base_url = self._current_translator_settings(api_key) - translator = create_translator( - provider=provider, - api_key=selected_api_key, - model=model, - base_url=base_url, - ) - protected_source, protections = protect_text(source) - batch = OrderedDict([(key, protected_source)]) - result = translator.translate_batch(batch, style=style) - translated = result.get(key, protected_source) - restored = restore_text(translated, protections) - restored = repair_translation_format(source, restored) - warnings = preserved_token_warnings(source, restored) - if warnings: - self.after(0, lambda t=restored: self._on_retranslate_result( - textbox, status_label, button, t, - f"仍有 {len(warnings)} 个告警,可手动编辑后保存。", - )) - else: - self.after(0, lambda: self._on_retranslate_result( - textbox, status_label, button, restored, "翻译成功 \u2713", - )) - except Exception as exc: - self.after(0, lambda e=exc: self._on_retranslate_result( - textbox, status_label, button, "", - f"翻译失败:{e}", - )) - - thread = threading.Thread(target=worker, daemon=True) - thread.start() - - def _on_retranslate_result( - self, - textbox: ctk.CTkTextbox, - status_label: ctk.CTkLabel, - button: ctk.CTkButton | None, - text: str, - status: str, - ) -> None: - if text: - textbox.delete("1.0", "end") - textbox.insert("1.0", text) - status_label.configure(text=status) - if button: - button.configure(state="normal", text="重新翻译") - - def _retranslate_all_review(self) -> None: - if not self._review_data: - return - api_key = self.api_key.get().strip() - if not api_key: - self._review_subtitle.configure(text="错误:请先配置 API Key。") - return - self._retranslate_all_btn.configure(state="disabled", text="翻译中...") - pending: list[tuple[str, str]] = [] - for key, data in self._review_data.items(): - pending.append((key, data["source"])) - total = len(pending) - self._review_subtitle.configure(text=f"正在逐条重新翻译 0/{total}。每条会独立处理,不会互相影响。") - - def worker(): - try: - selected_api_key, provider, model, style, base_url = self._current_translator_settings(api_key) - translator = create_translator( - provider=provider, - api_key=selected_api_key, - model=model, - base_url=base_url, - ) - except Exception as exc: - self.after(0, lambda e=exc: self._on_retranslate_all_error(e)) - return - - ok = 0 - warning_count = 0 - failed = 0 - for index, (key, source) in enumerate(pending, start=1): - self.after(0, lambda k=key: self._set_review_entry_status(k, "正在重新翻译...")) - try: - protected_source, protections = protect_text(source) - batch = OrderedDict([(key, protected_source)]) - result = translator.translate_batch(batch, style=style) - translated = result.get(key, protected_source) - restored = restore_text(translated, protections) - restored = repair_translation_format(source, restored) - warnings = preserved_token_warnings(source, restored) - if warnings: - warning_count += 1 - self.after(0, lambda k=key, t=restored, w=warnings: - self._on_batch_retranslate(k, t, w)) - else: - ok += 1 - self.after(0, lambda k=key, r=restored: - self._on_batch_retranslate(k, r, [])) - except Exception as exc: - failed += 1 - self.after(0, lambda k=key, e=exc: self._on_batch_retranslate_error(k, e)) - self.after(0, lambda i=index, o=ok, w=warning_count, f=failed: - self._update_retranslate_all_progress(i, total, o, w, f)) - self.after(0, lambda o=ok, w=warning_count, f=failed: - self._on_retranslate_all_done(o, w, f)) - - thread = threading.Thread(target=worker, daemon=True) - thread.start() - - def _set_review_entry_status(self, key: str, text: str) -> None: - data = self._review_data.get(key) - if data is None: - return - label = data.get("status_label") - if label: - label.configure(text=text) - - def _on_batch_retranslate(self, key: str, text: str, warnings: list[str]) -> None: - data = self._review_data.get(key) - if data is None: - return - tb = data.get("textbox") - sl = data.get("status_label") - if tb: - tb.delete("1.0", "end") - tb.insert("1.0", text) - if sl: - if warnings: - sl.configure(text=f"API 已返回,但仍有 {len(warnings)} 个格式告警,可手动编辑后保存。") - else: - sl.configure(text="重新翻译成功,确认无误后点击保存。") - - def _on_batch_retranslate_error(self, key: str, exc: Exception) -> None: - self._set_review_entry_status(key, f"API 重试失败:{exc}") - - def _update_retranslate_all_progress(self, done: int, total: int, ok: int, warning_count: int, failed: int) -> None: - self._review_subtitle.configure( - text=f"正在逐条重新翻译 {done}/{total}。成功 {ok},仍有格式告警 {warning_count},API 失败 {failed}。" - ) - - def _on_retranslate_all_done(self, ok: int, warning_count: int, failed: int) -> None: - self._retranslate_all_btn.configure(state="normal", text="全部重新翻译") - self._review_subtitle.configure( - text=f"重新翻译完成:成功 {ok} 条,仍需人工处理 {warning_count} 条,API 失败 {failed} 条。成功项仍需确认后点击保存。" - ) - - def _on_retranslate_all_error(self, exc: Exception) -> None: - self._retranslate_all_btn.configure(state="normal", text="全部重新翻译") - self._review_subtitle.configure(text=f"无法开始全部重新翻译:{exc}") - - def _ignore_review_entry(self, key: str, card: ctk.CTkFrame) -> None: - card.grid_remove() - self._review_data.pop(key, None) - if not self._review_data: - self._review_subtitle.configure(text="所有条目已处理。") - self._review_badge.configure(text="待处理 0") - else: - self._review_badge.configure(text=f"待处理 {len(self._review_data)}") - self._review_title.set(f"人工处理 — {len(self._review_data)} 条需要确认的映射") - self._refresh_review_layout() - - def _refresh_review_layout(self) -> None: - for idx, data in enumerate(self._review_data.values()): - frame = data.get("frame") - if frame and frame.winfo_exists(): - frame.grid(row=idx, column=0, sticky="ew", pady=(0, 12)) - - def _ignore_all_review(self) -> None: - for widget in self._review_scroll.winfo_children(): - widget.destroy() - self._review_data.clear() - self._review_subtitle.configure(text="已忽略所有条目。") - self._review_badge.configure(text="待处理 0") - - def _panel( - self, - parent: ctk.CTkFrame | ctk.CTkScrollableFrame, - title: str, - description: str, - ) -> ctk.CTkFrame: - panel = ctk.CTkFrame( - parent, - corner_radius=12, - border_width=1, - fg_color=("#FFFFFF", "#161B22"), - border_color=("#D0D7DE", "#30363D"), - ) - ctk.CTkLabel( - panel, - text=title, - anchor="w", - font=ctk.CTkFont(size=14, weight="bold"), - text_color=("#1F2328", "#E6EDF3"), - ).grid( - row=0, column=0, columnspan=3, sticky="ew", padx=16, pady=(14, 2) - ) - ctk.CTkLabel( - panel, text=description, anchor="w", - text_color=("#656D76", "#8B949E"), - font=ctk.CTkFont(size=12), - ).grid( - row=1, column=0, columnspan=3, sticky="ew", padx=16, pady=(0, 10) - ) - return panel - - def _show_view(self, view: str) -> None: - self.workbench_frame.grid_remove() - self.settings_frame.grid_remove() - self.history_frame.grid_remove() - if view == "settings": - self.settings_frame.grid() - elif view == "history": - self.history_frame.grid() - self._refresh_history_list() - else: - self.workbench_frame.grid() - view = "workbench" - self._set_nav_state(view) - - def _set_nav_state(self, active_view: str) -> None: - for view, button in self._nav_buttons.items(): - if view == active_view: - button.configure(fg_color="#2563EB", hover_color="#1D4ED8", text_color="#FFFFFF") - else: - button.configure( - fg_color=("#F3F4F6", "#21262D"), - hover_color=("#E5E7EB", "#30363D"), - text_color=("#374151", "#C9D1D9"), - ) - - def _change_appearance(self, value: str) -> None: - modes = {"系统": "System", "浅色": "Light", "深色": "Dark"} - ctk.set_appearance_mode(modes.get(value, "System")) - - def _toggle_key_visibility(self) -> None: - self._key_visible = not self._key_visible - self.key_entry.configure(show="" if self._key_visible else "*") - self.toggle_key_button.configure(text="隐藏" if self._key_visible else "显示") - - def _reset_service_settings(self) -> None: - base_url, model = provider_defaults(self._current_provider()) - self.base_url.set(base_url) - self.model.set(model) - self.settings_status.set("已恢复当前提供商的默认参数,点击保存全部后生效。") - - def _current_provider(self) -> str: - selected_label = self.provider_label.get() - for provider, label in PROVIDER_LABELS.items(): - if label == selected_label: - return provider - return DEFAULT_PROVIDER - - def _change_provider(self, _selected_label: str) -> None: - provider = self._current_provider() - base_url, model = provider_defaults(provider) - self.api_key.set(credential_store.load_api_key(provider)) - self._sync_api_key_state() - self.base_url.set(base_url) - self.model.set(model) - if provider_requires_api_key(provider): - message = "请填写对应 API Key 后保存。" - else: - message = "该实验性网页接口不需要 API Key,并会自动限制并发。" - self.settings_status.set(f"已切换到 {PROVIDER_LABELS[provider]},{message}") - - def _sync_api_key_state(self) -> None: - state = "normal" if provider_requires_api_key(self._current_provider()) else "disabled" - self.key_entry.configure(state=state) - self.toggle_key_button.configure(state=state) - - def _current_style(self) -> str: - text = self.style_text.get("1.0", "end").strip() - return text or DEFAULT_STYLE - - def _current_translator_settings(self, fallback_api_key: str) -> tuple[str, str, str, str, str]: - settings = self._run_settings - if settings is not None: - return ( - settings["api_key"] or fallback_api_key, - settings["provider"], - settings["model"], - settings["style"], - settings["base_url"], - ) - default_base_url, default_model = provider_defaults(self._current_provider()) - return ( - fallback_api_key, - self._current_provider(), - self.model.get() or default_model, - self._current_style(), - self.base_url.get() or default_base_url, - ) - - def _parse_optional_positive_int(self, value: str, label: str) -> int | None: - stripped = value.strip() - if not stripped or stripped.lower() == "auto": - return None - try: - parsed = int(stripped) - except ValueError as exc: - raise ValueError(f"{label} 必须填写 auto 或正整数。") from exc - if parsed <= 0: - raise ValueError(f"{label} 必须大于 0。") - return parsed - - def _effective_batch_size(self) -> int | None: - return self._parse_optional_positive_int(self.batch_size.get(), "批大小") - - def _effective_max_workers(self) -> int | None: - return self._parse_optional_positive_int(self.max_workers.get(), "并发数") - - def _save_settings(self) -> None: - try: - self._effective_batch_size() - self._effective_max_workers() - except ValueError as exc: - self.settings_status.set(str(exc)) - messagebox.showerror("配置无效", str(exc)) - return - - self.style.set(self._current_style()) - provider = self._current_provider() - default_base_url, default_model = provider_defaults(provider) - credential_backend = credential_store.save_api_key(self.api_key.get(), provider) - save_config_values( - { - PROVIDER_KEY: provider, - BASE_URL_KEY: self.base_url.get() or default_base_url, - MODEL_KEY: self.model.get() or default_model, - STYLE_KEY: self.style.get() or DEFAULT_STYLE, - BATCH_SIZE_KEY: self.batch_size.get() or "auto", - CONCURRENCY_KEY: self.max_workers.get() or "auto", - } - ) - self.settings_status.set( - f"已保存全部设置。API Key 已存入 {credential_store.storage_backend_label(credential_backend)}。" - ) - _log.info("Saved app settings") - self._log("已保存全部设置。") - - def _set_stage(self, stage: str) -> None: - labels = { - "idle": ("准备就绪", ("#DBEAFE", "#1E3A5F"), ("#1D4ED8", "#93C5FD")), - "scanned": ("已完成扫描", ("#DCFCE7", "#14532D"), ("#16A34A", "#86EFAC")), - "running": ("正在汉化", ("#FEF3C7", "#451A03"), ("#D97706", "#FCD34D")), - "done": ("汉化完成", ("#DCFCE7", "#14532D"), ("#16A34A", "#86EFAC")), - "error": ("需要处理", ("#FEE2E2", "#450A0A"), ("#DC2626", "#FCA5A5")), - } - text, bg_color, text_color = labels.get(stage, labels["idle"]) - self.stage.set(text) - self.stage_badge.configure(fg_color=bg_color, text_color=text_color) - - active_order = ["idle", "scanned", "running", "done"] - active_index = active_order.index(stage) if stage in active_order else 0 - for index, key in enumerate(active_order): - label = self._step_labels.get(key) - if label is None: - continue - if index <= active_index and stage != "error": - label.configure( - fg_color=("#2563EB", "#2563EB"), - text_color="#FFFFFF", - ) - else: - label.configure( - fg_color=("#F3F4F6", "#1C2128"), - text_color=("#656D76", "#8B949E"), - ) - - def _choose_dir(self) -> None: - directory = filedialog.askdirectory() - if directory: - self.selected_dir.set(directory) - _log.info("User selected directory: %s", directory) - self._scan() - - def _scan(self) -> None: - self._review_report = None - self._hide_review_panel() - try: - _log.info("Starting scan for: %s", self.selected_dir.get()) - batch_size = self._effective_batch_size() - quests_dir = resolve_quests_dir(Path(self.selected_dir.get())) - mode = detect_source_mode(quests_dir) - if mode == "lang": - values = load_lang_snbt(source_lang_path(quests_dir)) - entry_count = len(values) - source_label = str(source_lang_path(quests_dir)) - mode_label = "新版 lang/en_us.snbt" - _log.info("Scan result: lang mode, %d entries from %s", entry_count, source_label) - else: - file_count, entry_count = count_chapter_segments(quests_dir) - source_label = f"{quests_dir / 'chapters'}({file_count} 个章节文件)" - mode_label = "章节式 chapters/*.snbt" - _log.info("Scan result: chapters mode, %d files, %d entries", file_count, entry_count) - effective_batch_size = batch_size or AUTO_BATCH_MAX_ENTRIES - batches = estimate_batches(entry_count, effective_batch_size) - except Exception as exc: # noqa: BLE001 - _log.error("Scan failed: %s", exc, exc_info=True) - self._quests_dir = None - self.translate_button.configure(state="disabled") - self.summary.set("扫描失败") - self.status.set(str(exc)) - self.progress_text.set("扫描失败") - self._set_stage("error") - self._log(f"扫描失败:{exc}") - return - - self._quests_dir = quests_dir - self.translate_button.configure(state="normal") - self.summary.set( - f"任务书目录:{quests_dir}\n" - f"模式:{mode_label}\n" - f"源:{source_label}\n" - f"可翻译条目数:{entry_count},预计 {batches} 批,模型:{self.model.get() or DEFAULT_MODEL}" - ) - self.status.set("扫描完成,可以开始汉化。") - self.progress_text.set(f"扫描完成:{entry_count} 条,预计 {batches} 批") - self._set_stage("scanned") - self._log("扫描完成。") - - def _start_translate(self) -> None: - _log.info("Translation requested by user") - if self._quests_dir is None: - self._scan() - if self._quests_dir is None: - return - try: - batch_size = self._effective_batch_size() - max_workers = self._effective_max_workers() - except ValueError as exc: - self._show_view("settings") - self.settings_status.set(str(exc)) - messagebox.showerror("配置无效", str(exc)) - return - provider = self._current_provider() - if provider_requires_api_key(provider) and not self.api_key.get().strip(): - self._show_view("settings") - self.settings_status.set("请先填写 API Key,然后点击保存全部。") - messagebox.showerror("缺少 API Key", "请填写当前翻译提供商的 API Key。") - return - mode = detect_source_mode(self._quests_dir) - target = "lang/zh_cn.snbt" if mode == "lang" else "chapters/*.snbt" - if not messagebox.askyesno( - "确认覆盖写入", - f"将先创建备份,然后覆盖写入 {target}。\n\n任务书目录:{self._quests_dir}\n\n是否继续?", - ): - self._log("已取消:未执行覆盖写入。") - _log.info("User cancelled the overwrite confirmation") - return - self._save_settings() - default_base_url, default_model = provider_defaults(provider) - self._run_settings = { - "api_key": self.api_key.get(), - "provider": provider, - "batch_size": batch_size, - "model": self.model.get() or default_model, - "style": self._current_style(), - "base_url": self.base_url.get() or default_base_url, - "max_workers": max_workers, - } - self.scan_button.configure(state="disabled") - self.translate_button.configure(state="disabled") - self._review_report = None - self._hide_review_panel() - self.progress.set(0) - self.status.set("正在汉化...") - self.progress_text.set("正在准备翻译任务...") - self._set_stage("running") - _log.info("Starting translate worker thread (quests_dir=%s, mode=%s)", self._quests_dir, mode) - thread = threading.Thread(target=self._translate_worker, daemon=True) - thread.start() - - def _translate_worker(self) -> None: - assert self._quests_dir is not None - settings = self._run_settings - if settings is None: - self._queue.put(("error", RuntimeError("翻译配置未初始化。"))) - return - - def progress(stage: str, done: int, total: int) -> None: - self._queue.put(("progress", (stage, done, total))) - - def logger(message: str) -> None: - self._queue.put(("log", message)) - - try: - _log.info("Calling translate_quests_auto with quests_dir=%s", self._quests_dir) - report = translate_quests_auto( - quests_dir=self._quests_dir, - api_key=settings["api_key"], - batch_size=settings["batch_size"], - model=settings["model"], - style=settings["style"], - base_url=settings["base_url"], - provider=settings["provider"], - max_workers=settings["max_workers"], - progress=progress, - logger=logger, - ) - _log.info("Translation completed successfully: target=%s", report.target_file) - self._queue.put(("done", report)) - except Exception as exc: # noqa: BLE001 - _log.error("Translation worker failed: %s", exc, exc_info=True) - self._queue.put(("error", exc)) - - def _drain_queue(self) -> None: - try: - while True: - kind, payload = self._queue.get_nowait() - if kind == "progress": - stage, done, total = cast(_ProgressPayload, payload) - ratio = 1 if total == 0 else min(1, done / total) - self.progress.set(ratio) - self.status.set(f"{stage}: {done}/{total}") - self.progress_text.set(f"{stage}:{done}/{total}") - elif kind == "done": - report = cast(TranslationReport, payload) - self.progress.set(1) - self.status.set("汉化完成。") - self.progress_text.set("汉化完成,可以查看日志和输出文件。") - self._set_stage("done") - self._log(f"完成:写入 {report.target_file}") - self._log(f"备份:{report.backup_dir}") - self._log(f"缓存命中:{report.cache_hits},失败:{len(report.failed_entries)}") - self._review_report = report - self._save_history(report) - if report.warnings or report.failed_entries: - self._log(f"告警:{len(report.warnings)} 条翻译存在格式 token 问题。") - if report.failed_entries: - self._log(f"失败映射:{len(report.failed_entries)} 条需要人工处理。") - self._show_review_entries() - else: - self._hide_review_panel() - self.scan_button.configure(state="normal") - self.translate_button.configure(state="normal") - elif kind == "log": - self._log(str(payload)) - elif kind == "history_saved": - run_id = cast(int, payload) - self._log(f"已保存到历史记录 #{run_id}") - if getattr(self, "_history_list_frame", None) is not None \ - and self.history_frame.winfo_ismapped(): - self._refresh_history_list() - elif kind == "history_save_failed": - self._log(f"历史记录保存失败:{payload}") - elif kind == "error": - exc = cast(Exception, payload) - self.status.set(str(exc)) - self.progress_text.set("汉化失败,请查看日志。") - self._set_stage("error") - self._log(f"汉化失败:{exc}") - self.scan_button.configure(state="normal") - self.translate_button.configure(state="normal") - except queue.Empty: - pass - self.after(150, self._drain_queue) - - def _save_history(self, report: TranslationReport) -> None: - """在后台线程把翻译结果写入 SQLite,避免阻塞 GUI。""" - if not report.output_files: - return - settings = self._run_settings - quests_dir = self._quests_dir - if quests_dir is None or settings is None: - return - try: - mode = detect_source_mode(quests_dir) - except Exception: # noqa: BLE001 - mode = "lang" if report.target_file.endswith(".snbt") else "chapters" - - files: list[FileRecord] = [] - for filename, content in report.output_files.items(): - mapping = report.mapping.get(filename, {}) - files.append(FileRecord(filename=filename, mapping=mapping, output_content=content)) - - payload = { - "quests_dir": str(quests_dir), - "mode": mode, - "model": settings["model"], - "style": settings["style"], - "base_url": settings["base_url"], - "total_entries": report.total_entries, - "translated_entries": report.translated_entries, - "cache_hits": report.cache_hits, - "failed_count": len(report.failed_entries), - "warning_count": len(report.warnings), - "files": files, - } - - def worker() -> None: - try: - run_id = self.history_db.insert_run(**payload) - self._queue.put(("history_saved", run_id)) - except Exception as exc: # noqa: BLE001 - _log.error("Failed to save history: %s", exc, exc_info=True) - self._queue.put(("history_save_failed", str(exc))) - - threading.Thread(target=worker, daemon=True).start() - - def _log(self, text: str) -> None: - """Log to both the GUI textbox and the file logger.""" - self.log.insert("end", text + "\n") - self.log.see("end") - _log.info("[GUI] %s", text) - - -def main() -> None: - app = FtbTranslaterApp() - app.mainloop() diff --git a/ftb_translater/backup.py b/ftb_translater/backup.py deleted file mode 100644 index 08ce51f..0000000 --- a/ftb_translater/backup.py +++ /dev/null @@ -1,30 +0,0 @@ -from __future__ import annotations - -import shutil -from datetime import datetime -from pathlib import Path - -from ftb_translater.logger import get_logger - -_log = get_logger(__name__) - - -def create_backup(quests_dir: Path, directories: tuple[str, ...] = ("lang",)) -> Path: - timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") - backup_root = quests_dir / ".ftb-translater" / "backups" / timestamp - copied = False - for directory in directories: - source = quests_dir / directory - if not source.is_dir(): - _log.warning("Backup source not found, skipping: %s", source) - continue - destination = backup_root / directory - destination.parent.mkdir(parents=True, exist_ok=True) - shutil.copytree(source, destination) - _log.info("Backed up %s -> %s", source, destination) - copied = True - if not copied: - names = ", ".join(directories) - _log.error("No backup source directories found under %s: %s", quests_dir, names) - raise FileNotFoundError(f"Missing backup source directories under {quests_dir}: {names}") - return backup_root diff --git a/ftb_translater/cache.py b/ftb_translater/cache.py deleted file mode 100644 index e7d9180..0000000 --- a/ftb_translater/cache.py +++ /dev/null @@ -1,56 +0,0 @@ -from __future__ import annotations - -import hashlib -import json -from pathlib import Path -from typing import Any - -from ftb_translater.logger import get_logger - -_log = get_logger(__name__) - - -class TranslationCache: - def __init__(self, path: Path): - self.path = path - self._data: dict[str, str] = {} - - def load(self) -> None: - if not self.path.exists(): - _log.debug("Cache file not found, starting empty: %s", self.path) - self._data = {} - return - _log.debug("Loading cache from %s", self.path) - with self.path.open("r", encoding="utf-8") as file: - raw = json.load(file) - if not isinstance(raw, dict): - _log.error("Invalid cache file (not a JSON object): %s", self.path) - raise ValueError(f"Invalid cache file: {self.path}") - self._data = {str(key): str(value) for key, value in raw.items()} - _log.debug("Cache loaded: %d entries", len(self._data)) - - def save(self) -> None: - self.path.parent.mkdir(parents=True, exist_ok=True) - _log.debug("Saving cache to %s (%d entries)", self.path, len(self._data)) - with self.path.open("w", encoding="utf-8") as file: - json.dump(self._data, file, ensure_ascii=False, indent=2, sort_keys=True) - - def get(self, source_text: str, model: str, target_locale: str, style: str) -> str | None: - result = self._data.get(self._key(source_text, model, target_locale, style)) - if result is not None: - _log.debug("Cache hit for text (len=%d)", len(source_text)) - return result - - def set(self, source_text: str, model: str, target_locale: str, style: str, translation: str) -> None: - self._data[self._key(source_text, model, target_locale, style)] = translation - - @staticmethod - def _key(source_text: str, model: str, target_locale: str, style: str) -> str: - payload: dict[str, Any] = { - "source_text": source_text, - "model": model, - "target_locale": target_locale, - "style": style, - } - encoded = json.dumps(payload, ensure_ascii=False, sort_keys=True).encode("utf-8") - return hashlib.sha256(encoded).hexdigest() diff --git a/ftb_translater/chapters.py b/ftb_translater/chapters.py deleted file mode 100644 index 68625ac..0000000 --- a/ftb_translater/chapters.py +++ /dev/null @@ -1,345 +0,0 @@ -from __future__ import annotations - -import re -from dataclasses import dataclass -from pathlib import Path - -from ftb_translater.logger import get_logger - -_log = get_logger(__name__) - -INLINE_TRANSLATABLE_KEYS = frozenset({"title", "subtitle", "description", "text", "name"}) -TRANSLATION_TABLE_KEYS = frozenset({"title", "quest_subtitle", "quest_desc", "chapter_subtitle"}) -REFERENCE_VALUE_PATTERN = re.compile(r"^[a-z0-9_.+-]+(?::[a-z0-9_./+-]+)?$") - - -@dataclass(frozen=True) -class ChapterTextSegment: - path: Path - key: str - source_text: str - start: int - end: int - quote: str - index: int - - @property - def cache_id(self) -> str: - return f"{self.path.name}:{self.index}:{self.key}" - - -def chapter_files(quests_dir: Path) -> list[Path]: - chapters_dir = quests_dir / "chapters" - if not chapters_dir.is_dir(): - _log.debug("No chapters directory at %s", chapters_dir) - return [] - files = sorted(chapters_dir.glob("*.snbt")) - _log.debug("Found %d chapter files in %s", len(files), chapters_dir) - return files - - -def extract_chapter_segments(path: Path) -> list[ChapterTextSegment]: - _log.debug("Extracting segments from %s", path) - text = path.read_text(encoding="utf-8-sig") - segments = _ChapterSnbtWalker(text, path).extract() - _log.debug("Extracted %d translatable segments from %s", len(segments), path) - return segments - - -def replace_chapter_segments(path: Path, translations: dict[int, str]) -> int: - _log.debug("Replacing %d segments in %s", len(translations), path) - text = path.read_text(encoding="utf-8-sig") - segments = extract_chapter_segments(path) - replacements = [segment for segment in segments if segment.index in translations] - for segment in sorted(replacements, key=lambda item: item.start, reverse=True): - literal = _quote_string(translations[segment.index], segment.quote) - text = text[: segment.start] + literal + text[segment.end :] - path.write_text(text, encoding="utf-8") - _log.debug("Replaced %d segments in %s", len(replacements), path) - return len(replacements) - - -def count_chapter_segments(quests_dir: Path) -> tuple[int, int]: - files = chapter_files(quests_dir) - counts = {path: len(extract_chapter_segments(path)) for path in files} - total = sum(counts.values()) - _log.debug("Chapter segments count: %d files, %d segments total", len(files), total) - return len(files), total - - -class _ChapterSnbtWalker: - def __init__(self, text: str, path: Path): - self.text = text - self.path = path - self.index = 0 - self.segments: list[ChapterTextSegment] = [] - - def extract(self) -> list[ChapterTextSegment]: - self._skip_ws_and_comments() - if self._peek() == "{": - self._parse_compound() - else: - self._parse_entries_until("") - return self.segments - - def _parse_entries_until(self, end_char: str) -> None: - while self.index < len(self.text): - self._skip_ws_and_comments() - if end_char and self._peek() == end_char: - self.index += 1 - return - before = self.index - if not self._parse_pair(): - self.index = before - self._skip_unknown_value() - self._consume_optional_separator() - if self.index == before: - self.index += 1 - - def _parse_compound(self) -> None: - if self._peek() != "{": - self._skip_unknown_value() - return - self.index += 1 - self._parse_entries_until("}") - - def _parse_pair(self) -> bool: - key = self._parse_key() - if key is None: - return False - self._skip_ws_and_comments() - if self._peek() != ":": - return False - self.index += 1 - active_key = _translatable_key(key) - self._parse_value(active_key) - return True - - def _parse_key(self) -> str | None: - self._skip_ws_and_comments() - if self._peek() in {'"', "'"}: - value, end, _quote = _parse_string_literal(self.text, self.index) - self.index = end - return value - if not _is_key_start(self._peek()): - return None - start = self.index - while self.index < len(self.text) and _is_key_part(self.text[self.index]): - self.index += 1 - return self.text[start:self.index] - - def _parse_value(self, active_key: str | None) -> None: - self._skip_ws_and_comments() - char = self._peek() - if not char: - return - if char in {'"', "'"}: - self._parse_string_value(active_key) - return - if char == "{": - self._parse_compound() - return - if char == "[": - self._parse_list(active_key) - return - self._skip_atom() - - def _parse_string_value(self, active_key: str | None) -> None: - start = self.index - value, end, quote = _parse_string_literal(self.text, start) - self.index = end - if active_key is None or not _should_translate(value): - return - self.segments.append( - ChapterTextSegment( - self.path, - active_key, - value, - start, - end, - quote, - len(self.segments), - ) - ) - - def _parse_list(self, active_key: str | None) -> None: - if self._peek() != "[": - return - self.index += 1 - if self._consume_typed_array_prefix(): - self._skip_until_matching_list_end() - return - while self.index < len(self.text): - self._skip_ws_and_comments() - if self._peek() == "]": - self.index += 1 - return - before = self.index - self._parse_value(active_key) - self._consume_optional_separator() - if self.index == before: - self.index += 1 - - def _consume_typed_array_prefix(self) -> bool: - checkpoint = self.index - self._skip_ws_and_comments() - if self._peek() not in {"B", "I", "L"}: - self.index = checkpoint - return False - array_type_end = self.index + 1 - self.index = array_type_end - self._skip_ws_and_comments() - if self._peek() != ";": - self.index = checkpoint - return False - self.index += 1 - return True - - def _skip_until_matching_list_end(self) -> None: - depth = 1 - while self.index < len(self.text) and depth > 0: - self._skip_ws_and_comments() - char = self._peek() - if not char: - return - if char in {'"', "'"}: - _, end, _ = _parse_string_literal(self.text, self.index) - self.index = end - continue - if char == "[": - depth += 1 - elif char == "]": - depth -= 1 - self.index += 1 - - def _skip_unknown_value(self) -> None: - self._skip_ws_and_comments() - char = self._peek() - if not char: - return - if char in {'"', "'"}: - _, end, _ = _parse_string_literal(self.text, self.index) - self.index = end - return - if char == "{": - self._parse_compound() - return - if char == "[": - self._parse_list(None) - return - self._skip_atom() - - def _skip_atom(self) -> None: - while self.index < len(self.text): - char = self.text[self.index] - if char.isspace() or char in ",]}{": - return - if self.text.startswith("//", self.index): - return - if char == "#" and _is_hash_comment(self.text, self.index): - return - self.index += 1 - - def _consume_optional_separator(self) -> None: - self._skip_ws_and_comments() - if self._peek() in {",", ";"}: - self.index += 1 - - def _skip_ws_and_comments(self) -> None: - while self.index < len(self.text): - char = self.text[self.index] - if char.isspace(): - self.index += 1 - continue - if self.text.startswith("//", self.index): - self.index = _skip_line_comment(self.text, self.index) - continue - if char == "#" and _is_hash_comment(self.text, self.index): - self.index = _skip_line_comment(self.text, self.index) - continue - return - - def _peek(self) -> str: - if self.index >= len(self.text): - return "" - return self.text[self.index] - - -def _parse_string_literal(text: str, start: int) -> tuple[str, int, str]: - quote = text[start] - i = start + 1 - chars: list[str] = [] - while i < len(text): - char = text[i] - i += 1 - if char == quote: - return "".join(chars), i, quote - if char == "\\": - if i >= len(text): - chars.append("\\") - break - esc = text[i] - i += 1 - chars.append(_decode_escape(esc)) - else: - chars.append(char) - raise ValueError(f"Unterminated string in {start}") - - -def _quote_string(value: str, quote: str) -> str: - escaped = ( - value.replace("\\", "\\\\") - .replace(quote, f"\\{quote}") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t") - ) - return f"{quote}{escaped}{quote}" - - -def _decode_escape(esc: str) -> str: - mapping = { - "n": "\n", - "r": "\r", - "t": "\t", - "\\": "\\", - '"': '"', - "'": "'", - } - return mapping.get(esc, esc) - - -def _skip_line_comment(text: str, start: int) -> int: - end = text.find("\n", start) - return len(text) if end == -1 else end + 1 - - -def _is_hash_comment(text: str, position: int) -> bool: - line_start = text.rfind("\n", 0, position) + 1 - return text[line_start:position].strip() == "" - - -def _is_key_start(char: str) -> bool: - return char.isalpha() or char == "_" - - -def _is_key_part(char: str) -> bool: - return char.isalnum() or char in "_-.+" - - -def _translatable_key(key: str) -> str | None: - if key in INLINE_TRANSLATABLE_KEYS or key in TRANSLATION_TABLE_KEYS: - return key - suffix = key.rsplit(".", 1)[-1] - if suffix in TRANSLATION_TABLE_KEYS: - return key - return None - - -def _should_translate(value: str) -> bool: - stripped = value.strip() - if not stripped: - return False - if REFERENCE_VALUE_PATTERN.fullmatch(stripped): - return False - return any("A" <= char <= "Z" or "a" <= char <= "z" for char in stripped) diff --git a/ftb_translater/config.py b/ftb_translater/config.py deleted file mode 100644 index 876b7a4..0000000 --- a/ftb_translater/config.py +++ /dev/null @@ -1,170 +0,0 @@ -from __future__ import annotations - -from pathlib import Path - -from ftb_translater.logger import get_logger -from ftb_translater.user_paths import user_config_dir - -try: - from dotenv import dotenv_values -except ImportError: # pragma: no cover - dependency is declared, fallback keeps imports readable. - dotenv_values = None - - -_log = get_logger(__name__) - -ENV_KEY = "DEEPSEEK_API_KEY" -BASE_URL_KEY = "DEEPSEEK_BASE_URL" -MODEL_KEY = "DEEPSEEK_MODEL" -PROVIDER_KEY = "FTB_TRANSLATER_PROVIDER" -STYLE_KEY = "FTB_TRANSLATER_STYLE" -BATCH_SIZE_KEY = "FTB_TRANSLATER_BATCH_SIZE" -CONCURRENCY_KEY = "FTB_TRANSLATER_CONCURRENCY" - -# API Key 现在通过 credential_store(keyring)管理,不再写入 .env -APP_CONFIG_KEYS = ( - PROVIDER_KEY, - BASE_URL_KEY, - MODEL_KEY, - STYLE_KEY, - BATCH_SIZE_KEY, - CONCURRENCY_KEY, -) - - -def env_path(base_dir: Path | None = None) -> Path: - if base_dir is not None: - return base_dir / ".env" - return user_config_dir() / ".env" - - -def load_api_key(base_dir: Path | None = None) -> str: - """已废弃:仅用于从旧 .env 文件迁移读取。新代码应使用 credential_store。""" - path = env_path(base_dir) - _log.debug("Loading API key from %s", path) - if dotenv_values is not None and path.exists(): - value = dotenv_values(path).get(ENV_KEY) - if value: - _log.debug("API key loaded via python-dotenv") - return str(value) - if path.exists(): - value = _read_env_file(path).get(ENV_KEY) - if value: - _log.debug("API key loaded from raw app settings file") - return value - return "" - - -def load_config_values(base_dir: Path | None = None) -> dict[str, str]: - path = env_path(base_dir) - values: dict[str, str] = {} - if dotenv_values is not None and path.exists(): - values.update({key: str(value) for key, value in dotenv_values(path).items() if value is not None}) - elif path.exists(): - values.update(_read_env_file(path)) - - for key in APP_CONFIG_KEYS: - if not values.get(key): - values[key] = "" - return values - - -def save_config_values(values: dict[str, str], base_dir: Path | None = None) -> None: - path = env_path(base_dir) - path.parent.mkdir(parents=True, exist_ok=True) - _log.info("Saving app config to %s", path) - - lines: list[str] = [] - if path.exists(): - lines = path.read_text(encoding="utf-8").splitlines() - - pending = {key: value.strip() for key, value in values.items() if key in APP_CONFIG_KEYS} - found: set[str] = set() - next_lines: list[str] = [] - for line in lines: - stripped = line.strip() - if "=" not in stripped or stripped.startswith("#"): - next_lines.append(line) - continue - key, _value = stripped.split("=", 1) - key = key.strip() - if key in pending: - next_lines.append(f"{key}={pending[key]}") - found.add(key) - else: - next_lines.append(line) - - for key in APP_CONFIG_KEYS: - if key in pending and key not in found: - next_lines.append(f"{key}={pending[key]}") - - path.write_text("\n".join(next_lines).rstrip() + "\n", encoding="utf-8") - _log.debug("App config saved successfully") - - -def save_api_key(api_key: str, base_dir: Path | None = None) -> str: - """转发到 credential_store。base_dir 参数仅为向后兼容保留,会被忽略。""" - from ftb_translater import credential_store - - backend = credential_store.save_api_key(api_key) - _log.debug("API key saved via credential_store") - return backend - - -def migrate_api_key_from_env(base_dir: Path | None = None) -> bool: - """把 .env 里的旧 API Key 迁移到 credential_store,迁移成功后从 .env 删除。 - - Returns True 如果完成了迁移。 - """ - from ftb_translater import credential_store - - legacy_key = load_api_key(base_dir) - if not legacy_key: - return False - - stored_key = credential_store.load_api_key() - if stored_key: - if stored_key == legacy_key: - _strip_api_key_from_env(base_dir) - _log.info("Removed duplicate API key from .env after credential migration") - else: - _log.warning( - "Legacy .env API key differs from credential store; keeping .env value for manual review" - ) - return False - - credential_store.save_api_key(legacy_key) - _strip_api_key_from_env(base_dir) - _log.info("Migrated API key from .env to credential store") - return True - - -def _strip_api_key_from_env(base_dir: Path | None = None) -> None: - path = env_path(base_dir) - if not path.exists(): - return - lines = path.read_text(encoding="utf-8").splitlines() - kept = [] - changed = False - for line in lines: - stripped = line.strip() - if "=" in stripped and not stripped.startswith("#"): - key = stripped.split("=", 1)[0].strip() - if key == ENV_KEY: - changed = True - continue - kept.append(line) - if changed: - path.write_text("\n".join(kept).rstrip() + "\n", encoding="utf-8") - - -def _read_env_file(path: Path) -> dict[str, str]: - values: dict[str, str] = {} - for raw_line in path.read_text(encoding="utf-8").splitlines(): - line = raw_line.strip() - if not line or line.startswith("#") or "=" not in line: - continue - key, value = line.split("=", 1) - value = value.strip().strip('"').strip("'") - values[key.strip()] = value - return values diff --git a/ftb_translater/credential_store.py b/ftb_translater/credential_store.py deleted file mode 100644 index 9fde12f..0000000 --- a/ftb_translater/credential_store.py +++ /dev/null @@ -1,169 +0,0 @@ -"""API Key 凭证存储。 - -优先用系统 keyring(macOS Keychain / Windows Credential Manager / Linux Secret Service), -这才是"加密存储"——密钥由系统级安全组件管理。 - -如果 keyring 后端不可用(例如 headless Linux 没装 Secret Service),会写入一个混淆过的 -本地文件作为兜底。这只是为了避免明文落盘,密钥派生自机器特征,本机能读这个文件的攻击 -者通常也能推导密钥——因此这不是真正的加密,只是让 API Key 不直接 grep 到。强烈建议 -配置可用的 keyring 后端来获得真正的凭证安全。 -""" -from __future__ import annotations - -import base64 -import hashlib -import os -import platform -import uuid -from pathlib import Path - -from ftb_translater.logger import get_logger -from ftb_translater.user_paths import user_config_dir - -_log = get_logger(__name__) - -SERVICE_NAME = "ftb-translater" -ACCOUNT_NAME = "deepseek_api_key" - - -def _credential_account(provider: str | None = None) -> str: - if not provider or provider == "openai_compatible": - return ACCOUNT_NAME - safe_provider = "".join(char for char in provider.lower() if char.isalnum() or char in {"-", "_"}) - return f"{safe_provider or 'translation'}_api_key" - - -def _fallback_path(provider: str | None = None) -> Path: - suffix = "" if not provider or provider == "openai_compatible" else f"-{_credential_account(provider)}" - return user_config_dir() / f".credential-fallback{suffix}" - - -def _machine_key() -> bytes: - """从机器特征派生 Fernet 密钥。同一台机器多次启动稳定,跨机器不同。""" - seed_parts = [platform.node(), platform.machine(), str(uuid.getnode())] - seed = "::".join(seed_parts).encode("utf-8") - digest = hashlib.sha256(seed).digest() - return base64.urlsafe_b64encode(digest) - - -def _keyring_available() -> bool: - try: - import keyring - from keyring.errors import NoKeyringError - - backend = keyring.get_keyring() - if backend is None: - return False - if "fail" in type(backend).__name__.lower(): - return False - return True - except (ImportError, Exception) as exc: # noqa: BLE001 - _log.debug("Keyring unavailable: %s", exc) - return False - - -def save_api_key(api_key: str, provider: str | None = None) -> str: - """Save the API key and return the backend actually used.""" - api_key = api_key.strip() - if _keyring_available(): - try: - import keyring - - if not api_key: - try: - keyring.delete_password(SERVICE_NAME, _credential_account(provider)) - except Exception: # noqa: BLE001 - pass - else: - keyring.set_password(SERVICE_NAME, _credential_account(provider), api_key) - _log.info("API key saved to system keyring") - _clear_fallback(provider) - return "keyring" - except Exception as exc: # noqa: BLE001 - _log.warning("Keyring save failed, falling back to encrypted file: %s", exc) - _save_fallback(api_key, provider) - return "fallback" - - -def load_api_key(provider: str | None = None) -> str: - if _keyring_available(): - try: - import keyring - - value = keyring.get_password(SERVICE_NAME, _credential_account(provider)) - if value: - return value - except Exception as exc: # noqa: BLE001 - _log.warning("Keyring load failed, trying fallback: %s", exc) - return _load_fallback(provider) - - -def delete_api_key(provider: str | None = None) -> None: - if _keyring_available(): - try: - import keyring - - keyring.delete_password(SERVICE_NAME, _credential_account(provider)) - except Exception: # noqa: BLE001 - pass - _clear_fallback(provider) - - -def has_api_key(provider: str | None = None) -> bool: - return bool(load_api_key(provider)) - - -def storage_backend_label(backend: str | None = None) -> str: - """返回当前使用的存储后端友好名,用于 UI 文案。""" - if backend == "fallback": - return "本地受限文件(非加密,仅做混淆)" - if backend == "keyring" or _keyring_available(): - system = platform.system() - if system == "Darwin": - return "macOS 钥匙串" - if system == "Windows": - return "Windows 凭据管理器" - if system == "Linux": - return "系统 Secret Service" - return "系统凭证管理器" - return "本地受限文件(非加密,仅做混淆)" - - -def _save_fallback(api_key: str, provider: str | None = None) -> None: - from cryptography.fernet import Fernet - - path = _fallback_path(provider) - path.parent.mkdir(parents=True, exist_ok=True) - if not api_key: - _clear_fallback(provider) - return - token = Fernet(_machine_key()).encrypt(api_key.encode("utf-8")) - path.write_bytes(token) - try: - os.chmod(path, 0o600) - except OSError: - pass - _log.info("API key saved to encrypted fallback file") - - -def _load_fallback(provider: str | None = None) -> str: - from cryptography.fernet import Fernet, InvalidToken - - path = _fallback_path(provider) - if not path.exists(): - return "" - try: - decoded = Fernet(_machine_key()).decrypt(path.read_bytes()) - return decoded.decode("utf-8") - except (InvalidToken, ValueError) as exc: - _log.error("Fallback credential corrupt or from another machine: %s", exc) - return "" - - -def _clear_fallback(provider: str | None = None) -> None: - path = _fallback_path(provider) - if path.exists(): - try: - path.unlink() - except OSError as exc: - _log.warning("Could not delete fallback credential: %s", exc) diff --git a/ftb_translater/deepl_client.py b/ftb_translater/deepl_client.py deleted file mode 100644 index 93d3069..0000000 --- a/ftb_translater/deepl_client.py +++ /dev/null @@ -1,105 +0,0 @@ -from __future__ import annotations - -import json -import time -from collections.abc import Callable, Mapping -from urllib.error import HTTPError, URLError -from urllib.request import Request, urlopen - -from ftb_translater.logger import get_logger - - -DEFAULT_DEEPL_BASE_URL = "https://api-free.deepl.com" -DEFAULT_DEEPL_MODEL = "deepl" - -_log = get_logger(__name__) - - -class DeepLTranslationError(RuntimeError): - pass - - -class DeepLTranslator: - """Adapter for DeepL's text translation API. - - Keys are preserved locally: only values are sent to DeepL, then results are - paired back to their original keys by position. - """ - - def __init__( - self, - api_key: str, - model: str = DEFAULT_DEEPL_MODEL, - base_url: str = DEFAULT_DEEPL_BASE_URL, - retries: int = 2, - timeout: float = 60, - logger: Callable[[str], None] | None = None, - ): - if not api_key.strip(): - raise ValueError("DeepL API Key is required.") - self.api_key = api_key.strip() - self.model = model or DEFAULT_DEEPL_MODEL - self.base_url = base_url.rstrip("/") - self.retries = retries - self.timeout = timeout - self.logger = logger - - def translate_batch(self, entries: Mapping[str, str], style: str = "") -> dict[str, str]: - if not entries: - return {} - keys = list(entries) - texts = [entries[key] for key in keys] - last_error: Exception | None = None - for attempt in range(self.retries + 1): - try: - msg = f"Calling DeepL: {len(texts)} entries, attempt {attempt + 1}." - _log.info(msg) - self._log(msg) - translated = self._request(texts) - return dict(zip(keys, translated, strict=True)) - except Exception as exc: # noqa: BLE001 - last_error = exc - msg = f"DeepL batch attempt {attempt + 1} failed: {exc}" - _log.warning(msg) - self._log(msg) - if attempt < self.retries: - time.sleep(0.8 * (attempt + 1)) - raise DeepLTranslationError(f"DeepL translation failed: {last_error}") from last_error - - def _request(self, texts: list[str]) -> list[str]: - payload = json.dumps( - {"text": texts, "source_lang": "EN", "target_lang": "ZH-HANS"}, - ensure_ascii=False, - ).encode("utf-8") - request = Request( - f"{self.base_url}/v2/translate", - data=payload, - headers={ - "Authorization": f"DeepL-Auth-Key {self.api_key}", - "Content-Type": "application/json", - "User-Agent": "FTB-Translater/0.1", - }, - method="POST", - ) - try: - with urlopen(request, timeout=self.timeout) as response: # noqa: S310 - URL is user-configurable by design. - raw = json.loads(response.read().decode("utf-8")) - except HTTPError as exc: - detail = exc.read().decode("utf-8", errors="replace")[:500] - raise DeepLTranslationError(f"DeepL HTTP {exc.code}: {detail}") from exc - except (URLError, TimeoutError, json.JSONDecodeError) as exc: - raise DeepLTranslationError(f"DeepL request failed: {exc}") from exc - - translations = raw.get("translations") if isinstance(raw, dict) else None - if not isinstance(translations, list) or len(translations) != len(texts): - raise DeepLTranslationError("DeepL returned an unexpected number of translations.") - result: list[str] = [] - for item in translations: - if not isinstance(item, dict) or not isinstance(item.get("text"), str): - raise DeepLTranslationError("DeepL returned an invalid translation item.") - result.append(item["text"]) - return result - - def _log(self, message: str) -> None: - if self.logger: - self.logger(message) diff --git a/ftb_translater/deepseek_client.py b/ftb_translater/deepseek_client.py deleted file mode 100644 index a53e4a8..0000000 --- a/ftb_translater/deepseek_client.py +++ /dev/null @@ -1,210 +0,0 @@ -from __future__ import annotations - -import json -import time -from collections.abc import Callable, Mapping -from typing import Any - -from ftb_translater.logger import get_logger - -DEFAULT_BASE_URL = "https://api.deepseek.com" -DEFAULT_MODEL = "deepseek-v4-flash" -DEFAULT_STYLE = "自然玩家向简体中文汉化" - -_log = get_logger(__name__) - - -class DeepSeekTranslationError(RuntimeError): - pass - - -class OpenAICompatibleTranslator: - def __init__( - self, - api_key: str, - model: str = DEFAULT_MODEL, - base_url: str = DEFAULT_BASE_URL, - client: Any | None = None, - retries: int = 2, - logger: Callable[[str], None] | None = None, - ): - if not api_key.strip() and client is None: - raise ValueError("DeepSeek API Key is required.") - self.model = model - self.retries = retries - self.logger = logger - self.client = client or self._create_client(api_key=api_key, base_url=base_url) - - def translate_batch(self, entries: Mapping[str, str], style: str = DEFAULT_STYLE) -> dict[str, str]: - if not entries: - return {} - prompt = self._build_prompt(entries, style) - last_error: Exception | None = None - for attempt in range(self.retries + 1): - try: - msg = f"Calling OpenAI-compatible API {self.model}: {len(entries)} entries, attempt {attempt + 1}." - _log.info(msg) - self._log(msg) - return self._request_json(prompt, expected_keys=set(entries)) - except Exception as exc: # noqa: BLE001 - last_error = exc - msg = f"OpenAI-compatible batch attempt {attempt + 1} failed: {exc}" - _log.warning(msg) - self._log(msg) - if attempt < self.retries: - time.sleep(0.8 * (attempt + 1)) - - recovered: dict[str, str] = {} - failures: list[str] = [] - for key, value in entries.items(): - try: - msg = f"Retrying OpenAI-compatible API as single entry: {key}" - _log.info(msg) - self._log(msg) - recovered[key] = self._request_json( - self._build_prompt({key: value}, style), - expected_keys={key}, - )[key] - except Exception as exc: # noqa: BLE001 - _log.error("Single-entry retry failed for key %r: %s", key, exc) - failures.append(f"{key}: {exc}") - if failures: - message = "; ".join(failures) - if last_error: - message = f"batch failed with {last_error}; single-item failures: {message}" - raise DeepSeekTranslationError(message) - return recovered - - def _request_json(self, prompt: str, expected_keys: set[str]) -> dict[str, str]: - _log.debug("Sending request to DeepSeek, expected keys: %s", sorted(expected_keys)) - response = self._request_json_response(prompt) - - content = response.choices[0].message.content - if not content: - raise DeepSeekTranslationError("DeepSeek returned an empty response.") - _log.debug("DeepSeek raw response length: %d chars", len(content)) - try: - raw = self._parse_json_object(content) - except json.JSONDecodeError as exc: - _log.error("DeepSeek returned invalid JSON: %s\nRaw content: %s", exc, content[:500]) - raise DeepSeekTranslationError(f"DeepSeek returned invalid JSON: {exc}") from exc - - if not isinstance(raw, dict): - _log.error("DeepSeek JSON response is not a dict, got %s: %s", type(raw).__name__, str(raw)[:200]) - raise DeepSeekTranslationError("DeepSeek JSON response must be an object.") - - missing = expected_keys - set(raw) - if missing: - _log.error("DeepSeek response missed keys: %s. Got keys: %s", sorted(missing), sorted(raw.keys())) - raise DeepSeekTranslationError(f"DeepSeek response missed keys: {sorted(missing)}") - - extra = set(raw) - expected_keys - if extra: - _log.warning("DeepSeek response returned extra keys that will be ignored: %s", sorted(extra)) - - _log.debug("DeepSeek response OK: %d keys returned", len(expected_keys)) - return {key: str(raw[key]) for key in expected_keys} - - def _request_json_response(self, prompt: str) -> Any: - request = { - "model": self.model, - "messages": [ - { - "role": "system", - "content": ( - "You are a Minecraft modpack localization assistant. 你是 Minecraft 整合包任务书汉化助手。 " - "Translate only user-facing English quest text into natural Simplified Chinese. " - "只翻译玩家可见英文为自然简体中文。 " - "Never alter, remove, merge, or invent opaque placeholders such as ⟨P_0⟩. " - "绝不能修改、删除、合并或新增 ⟨P_0⟩ 这类占位符。" - ), - }, - {"role": "user", "content": prompt}, - ], - "temperature": 0.2, - } - try: - return self.client.chat.completions.create( - **request, - response_format={"type": "json_object"}, - ) - except Exception as exc: # noqa: BLE001 - if not self._is_response_format_unsupported(exc): - raise - msg = "API does not support response_format=json_object; retrying with prompt-only JSON mode." - _log.info(msg) - self._log(msg) - return self.client.chat.completions.create(**request) - - @staticmethod - def _parse_json_object(content: str) -> Any: - text = content.strip() - if text.startswith("```"): - lines = text.splitlines() - if lines and lines[0].strip().lower() in {"```", "```json"}: - lines = lines[1:] - if lines and lines[-1].strip() == "```": - lines = lines[:-1] - text = "\n".join(lines).strip() - try: - return json.loads(text) - except json.JSONDecodeError: - start = text.find("{") - end = text.rfind("}") - if start >= 0 and end > start: - return json.loads(text[start : end + 1]) - raise - - @staticmethod - def _is_response_format_unsupported(exc: Exception) -> bool: - message = str(exc).lower() - status_code = getattr(exc, "status_code", None) - mentions_format = "response_format" in message or "json_object" in message - return mentions_format and (status_code in {400, 404, 422} or "unsupported" in message or "unknown" in message) - - @staticmethod - def _build_prompt(entries: Mapping[str, str], style: str) -> str: - payload = json.dumps(entries, ensure_ascii=False, indent=2) - return ( - "Task / 任务:Translate this FTB Quests language map to Simplified Chinese.\n" - f"Style / 风格:{style}.\n" - "Return one JSON object with exactly the same keys and translated string values.\n" - "返回一个 JSON 对象,key 必须与输入完全一致,value 是翻译后的字符串。\n\n" - "Hard rules / 硬性规则:\n" - "1. Translate English player-facing text into Simplified Chinese. 将玩家可见英文翻译为简体中文。\n" - "2. Do not translate JSON keys. 不要翻译 JSON key。\n" - "3. Opaque placeholders like ⟨P_0⟩, ⟨P_1⟩ are formatting/resource tokens, not words. 占位符是格式或资源标记,不是单词。\n" - "4. Every placeholder from the input value must appear in the output value exactly once. 输入 value 中每个占位符在输出 value 中必须出现且只出现一次。\n" - "5. Keep each placeholder byte-for-byte unchanged. Do not remove, rename, duplicate, merge, or reorder characters inside it. 占位符本身必须逐字完全不变。\n" - "6. Placeholders may wrap a word, e.g. ⟨P_0⟩Nether⟨P_1⟩. Translate the word but keep both wrappers: ⟨P_0⟩下界⟨P_1⟩. 占位符包住的英文也必须翻译,不能因为被包住就保留英文。\n" - "7. If Chinese word order moves a highlighted phrase, move the whole placeholder-wrapped phrase together. 中文语序变化时,移动整段被占位符包住的短语。\n" - "8. Preserve item IDs, tags, markdown links, line breaks, escape sequences, numbers, and units. 保留物品 ID、标签、链接、换行、转义、数字和单位。\n\n" - "Examples / 示例:\n" - "Input text: Defeat ⟨P_0⟩Ignis⟨P_1⟩ in the ⟨P_2⟩Burning Arena⟨P_3⟩.\n" - "Good: 在⟨P_2⟩燃烧竞技场⟨P_3⟩中击败⟨P_0⟩伊格尼斯⟨P_1⟩。\n" - "Bad: 在燃烧竞技场中击败⟨P_0⟩伊格尼斯⟨P_1⟩。 (lost ⟨P_2⟩ and ⟨P_3⟩)\n" - "Bad: 在⟨P_2⟩燃烧竞技场中击败⟨P_0⟩伊格尼斯⟨P_1⟩。 (lost closing wrapper ⟨P_3⟩)\n" - "Input text: Found in ⟨P_0⟩Nether⟨P_1⟩.\n" - "Good: 可在⟨P_0⟩下界⟨P_1⟩找到。\n" - "Bad: 可在下界找到。 (lost wrappers ⟨P_0⟩ and ⟨P_1⟩)\n\n" - f"{payload}" - ) - - @staticmethod - def _create_client(api_key: str, base_url: str) -> Any: - try: - from openai import OpenAI - except ImportError as exc: - raise DeepSeekTranslationError( - "Missing dependency 'openai'. Run `python -m pip install -e .` before using DeepSeek translation." - ) from exc - _log.debug("Creating OpenAI client with base_url=%s", base_url) - return OpenAI(api_key=api_key.strip(), base_url=base_url) - - def _log(self, message: str) -> None: - if self.logger: - self.logger(message) - - -# Backward-compatible name for callers that imported the original DeepSeek-only client. -DeepSeekTranslator = OpenAICompatibleTranslator diff --git a/ftb_translater/format_guard.py b/ftb_translater/format_guard.py deleted file mode 100644 index 45c1116..0000000 --- a/ftb_translater/format_guard.py +++ /dev/null @@ -1,470 +0,0 @@ -from __future__ import annotations - -import json -import re -import threading -from collections import Counter - -from ftb_translater.logger import get_logger - -_log = get_logger(__name__) - -# ---- All protected token patterns (MUST be exactly preserved) ---- - -# Minecraft colour / formatting codes (& and § variants, including rainbow &z) -_COLOR_PATTERN = re.compile(r"[&§][0-9a-fk-orz]", re.IGNORECASE) - -# printf-style format specifiers e.g. %s, %d, %1$s, %.2f -# The trailing (?!\w) avoids false matches on natural text like "50% faster". -_FORMAT_PATTERN = re.compile( - r"%(?:\d+\$)?[+#\- 0,(]*\d*(?:\.\d+)?[bcdeEufFgGosxX](?!\w)", -) - -# Angle-bracket IDs e.g. , -_ANGLE_PATTERN = re.compile(r"<[^<>\n]+>") - -# FTB/quest text macros and embeds e.g. {@pagebreak}, {image:ftb:textures/...} -_BRACE_MACRO_PATTERN = re.compile(r"\{[@A-Za-z][^{}\n]*\}") - -# Resource identifiers / file paths that can appear outside brace macros. -# -# Keep this intentionally narrow. A broad "word/word" matcher treats normal -# player-facing text such as "and/or", "input/output", "RF/t", and "Up/Down" -# as hard format tokens, which causes safe translations to be discarded. Real -# resource paths usually have a namespace, a known asset/config prefix, or a -# filename extension. -_RESOURCE_PATH_PATTERN = re.compile( - r"\b(?:" - r"[a-z0-9_.-]+:[a-z0-9_.-]+(?:/[a-z0-9_.-]+)+(?:\.[a-z0-9]+)?" - r"|(?:assets|config|data|kubejs|models|recipes|textures|ftbquests|chapters|lang|scripts|shaderpacks)" - r"/[a-z0-9_.-]+(?:/[a-z0-9_.-]+)*(?:\.[a-z0-9]+)?" - r"|[a-z0-9_.-]+(?:/[a-z0-9_.-]+)+\.[a-z0-9]+" - r")\b", - re.IGNORECASE, -) - -# Escape sequences \n \r \t \" \' \\ -_ESCAPE_PATTERN = re.compile(r"\\[nrt\"'\\]") - -# URLs -_URL_PATTERN = re.compile(r"https?://[^\s\"')\]]+") - -# Hex colour codes #RRGGBB (not SNBT comments — those start at line begin) -_HEX_PATTERN = re.compile(r"#[0-9a-fA-F]{6}\b") - -# All token patterns in priority order (JSON handled separately first) -_FLAT_PATTERNS: list[re.Pattern[str]] = [ - _COLOR_PATTERN, - _FORMAT_PATTERN, - _ANGLE_PATTERN, - _BRACE_MACRO_PATTERN, - _RESOURCE_PATH_PATTERN, - _ESCAPE_PATTERN, - _URL_PATTERN, - _HEX_PATTERN, -] - -# JSON-like object / array pattern (used for detection only) -_JSON_OBJECT_PATTERN = re.compile(r"^\s*[\[{].*[\]}]\s*$", re.DOTALL) - - -Protection = tuple[str, str] # (placeholder, original_token) - - -def protect_text(text: str) -> tuple[str, list[Protection]]: - """Replace all protected format tokens with numbered placeholders. - - For JSON text components ({...} or [...]), uses structural protection: - only the human-readable text inside ``"text"`` fields is left exposed - for translation; everything else is replaced with placeholders. - - Returns (protected_text, [(placeholder, original_token), ...]). - """ - stripped = text.strip() - if _looks_like_json(stripped): - return _protect_json_text(text) - return _protect_flat_text(text) - - -def restore_text(text: str, protections: list[Protection]) -> str: - """Replace ``{P_N}`` placeholders with their original tokens.""" - result = text - for placeholder, original in protections: - result = result.replace(placeholder, original) - return result - - -def preserved_token_warnings(source: str, translated: str) -> list[str]: - """Verify that all protected tokens survived translation unchanged. - - Runs a full protect → restore round-trip on the source, then makes - sure every original token appears in the same order in the translated - output. This catches cases where the model ignored placeholders. - """ - warnings: list[str] = [] - - # 1. Control characters — strict count - for char, name in (("\n", "newline"), ("\r", "carriage return"), ("\t", "tab")): - if source.count(char) != translated.count(char): - warnings.append(f"Control character count mismatch for {name}") - - # 2. Token round-trip check. Colour/style codes are allowed to move as - # complete segments because Chinese word order often moves highlighted - # phrases. Non-colour tokens still require strict relative order. - _, src_protections = protect_text(source) - tgt_restored = restore_text(translated, src_protections) - src_tokens = _extract_protected_tokens(source) - tgt_tokens = _extract_protected_tokens(tgt_restored) - - src_fixed = [token for token in src_tokens if not _is_movable_style_token(token)] - tgt_fixed = [token for token in tgt_tokens if not _is_movable_style_token(token)] - if len(src_fixed) != len(tgt_fixed): - warnings.append( - f"Non-colour token count mismatch: {len(src_fixed)} source vs " - f"{len(tgt_fixed)} translated", - ) - else: - for source_token, translated_token in zip(src_fixed, tgt_fixed): - if source_token != translated_token: - warnings.append( - f"Non-colour token mismatch: source={source_token!r} translated={translated_token!r}", - ) - - src_style = Counter(token for token in src_tokens if _is_movable_style_token(token)) - tgt_style = Counter(token for token in tgt_tokens if _is_movable_style_token(token)) - if src_style != tgt_style: - warnings.append(f"Colour/style token count mismatch: source={dict(src_style)} translated={dict(tgt_style)}") - elif src_style: - warnings.extend(_colour_ast_warnings(source, tgt_restored)) - - # 3. Strict occurrence check catches dropped duplicate tokens as well as - # completely missing tokens. - missing = Counter(src_tokens) - Counter(tgt_tokens) - for original, count in sorted(missing.items()): - if count == 1: - warnings.append(f"Missing token in translation: {original!r}") - else: - warnings.append(f"Missing token in translation: {original!r} x{count}") - - if warnings: - _log.warning( - "Format token issues in translation (%d): source=%.50r -> target=%.50r", - len(warnings), source, translated, - ) - return warnings - - -def repair_translation_format(source: str, translated: str) -> str: - """Apply conservative local repairs before rejecting a translation. - - This intentionally handles only edits that are mechanically safe. The main - case is an LLM duplicating colour/style codes to "close" a highlighted - phrase, e.g. ``&f与&d军需官的回响&f对话`` when the source only contained one - ``&f`` and one ``&d``. Extra colour codes change styling but do not carry - content or IDs, so removing extras is safer than discarding the whole - translation. Missing or changed required codes are still rejected. - """ - return _remove_extra_style_tokens(source, translated) - - -# --------------------------------------------------------------------------- -# Internal helpers -# --------------------------------------------------------------------------- - -_PLACEHOLDER_COUNTER = 0 -_PLACEHOLDER_LOCK = threading.Lock() - - -def _next_placeholder() -> str: - """Return a distinctive placeholder the model won't translate. - - Uses Unicode angle brackets (U+27E8 / U+27E9) around ``P_N`` so the - model clearly sees an opaque token to preserve verbatim. - """ - global _PLACEHOLDER_COUNTER - with _PLACEHOLDER_LOCK: - ph = f"\u27e8P_{_PLACEHOLDER_COUNTER}\u27e9" - _PLACEHOLDER_COUNTER += 1 - return ph - - -def _reset_counter() -> None: - global _PLACEHOLDER_COUNTER - with _PLACEHOLDER_LOCK: - _PLACEHOLDER_COUNTER = 0 - - -def _looks_like_json(text: str) -> bool: - """Quick heuristic: does the string look like a JSON object or array?""" - return bool(_JSON_OBJECT_PATTERN.match(text)) - - -def _protect_json_text(text: str) -> tuple[str, list[Protection]]: - """Structural protection for JSON text components. - - Parses the JSON and walks every node. Only ``"text"`` field values and - bare string array elements are processed for MC token protection; all - other keys (color, clickEvent, hoverEvent, …) and their values are left - entirely untouched. This guarantees the JSON structure is never altered. - """ - try: - obj = json.loads(text) - except json.JSONDecodeError: - return _protect_flat_text(text) - - protections: list[Protection] = [] - - def _walk(node: object) -> object: - if isinstance(node, dict): - new: dict[str, object] = {} - for key, value in node.items(): - if key == "text" and isinstance(value, str): - ptext, psub = _protect_flat_text(value) - protections.extend(psub) - new[key] = ptext - elif isinstance(value, (dict, list)): - new[key] = _walk(value) - else: - new[key] = value - return new - if isinstance(node, list): - return [_walk(item) for item in node] - if isinstance(node, str): - ptext, psub = _protect_flat_text(node) - protections.extend(psub) - return ptext - return node - - obj = _walk(obj) - result = json.dumps(obj, ensure_ascii=False) - return result, protections - - -def _protect_flat_text(text: str) -> tuple[str, list[Protection]]: - """Flat regex-based protection for plain text strings. - - Every match of every known token pattern is replaced with a - ``\\x00P_N\\x00`` placeholder. - """ - protections: list[Protection] = [] - result = text - for pattern in _FLAT_PATTERNS: - # Work backwards through matches to preserve positions - for match in reversed(list(pattern.finditer(result))): - original = match.group() - ph = _next_placeholder() - protections.insert(0, (ph, original)) - result = result[:match.start()] + ph + result[match.end():] - return result, protections - - -def _extract_protected_tokens(text: str) -> list[str]: - """Extract protected tokens in human-readable text order.""" - stripped = text.strip() - if _looks_like_json(stripped): - try: - obj = json.loads(text) - except json.JSONDecodeError: - return _extract_flat_tokens(text) - - tokens: list[str] = [] - - def _walk(node: object) -> None: - if isinstance(node, dict): - for key, value in node.items(): - if key == "text" and isinstance(value, str): - tokens.extend(_extract_flat_tokens(value)) - elif isinstance(value, (dict, list)): - _walk(value) - elif isinstance(node, list): - for item in node: - if isinstance(item, str): - tokens.extend(_extract_flat_tokens(item)) - else: - _walk(item) - - _walk(obj) - return tokens - return _extract_flat_tokens(text) - - -def _extract_flat_tokens(text: str) -> list[str]: - matches: list[tuple[int, int, int, str]] = [] - for priority, pattern in enumerate(_FLAT_PATTERNS): - for match in pattern.finditer(text): - matches.append((match.start(), match.end(), priority, match.group())) - - tokens: list[str] = [] - occupied: list[tuple[int, int]] = [] - for start, end, _priority, token in sorted(matches, key=lambda item: (item[0], item[2], item[1])): - if any(start < used_end and end > used_start for used_start, used_end in occupied): - continue - occupied.append((start, end)) - tokens.append(token) - return tokens - - -def _is_movable_style_token(token: str) -> bool: - return bool(_COLOR_PATTERN.fullmatch(token)) - - -def _colour_ast_warnings(source: str, translated: str) -> list[str]: - """Validate legacy colour/style codes with a lightweight style AST. - - Counter checks intentionally allow complete styled phrases to move. This - second pass catches sequences that have the same tokens but impossible or - semantically different style structure, such as ``&rtext&c`` or changing - ``&c&l`` into ``&l&c``. - """ - source_spans, source_anomalies = _extract_colour_ast(source) - translated_spans, translated_anomalies = _extract_colour_ast(translated) - warnings: list[str] = [] - - source_span_counts = Counter(source_spans) - translated_span_counts = Counter(translated_spans) - if source_span_counts != translated_span_counts: - warnings.append( - "Colour/style AST mismatch: " - f"source={_format_style_counter(source_span_counts)} " - f"translated={_format_style_counter(translated_span_counts)}", - ) - - extra_anomalies = Counter(translated_anomalies) - Counter(source_anomalies) - if extra_anomalies: - warnings.append( - "Colour/style AST issue: " - f"translated has {_format_anomaly_counter(extra_anomalies)}", - ) - - return warnings - - -def _extract_colour_ast(text: str) -> tuple[list[tuple[str, ...]], list[str]]: - """Return styled text span signatures and syntax anomalies. - - The "AST" here is intentionally small: each node is a styled text span - represented by the active legacy codes that apply to that text. - """ - stripped = text.strip() - if _looks_like_json(stripped): - try: - obj = json.loads(text) - except json.JSONDecodeError: - return _extract_flat_colour_ast(text) - - spans: list[tuple[str, ...]] = [] - anomalies: list[str] = [] - - def _walk(node: object) -> None: - if isinstance(node, dict): - for key, value in node.items(): - if key == "text" and isinstance(value, str): - node_spans, node_anomalies = _extract_flat_colour_ast(value) - spans.extend(node_spans) - anomalies.extend(node_anomalies) - elif isinstance(value, (dict, list)): - _walk(value) - elif isinstance(node, list): - for item in node: - if isinstance(item, str): - node_spans, node_anomalies = _extract_flat_colour_ast(item) - spans.extend(node_spans) - anomalies.extend(node_anomalies) - else: - _walk(item) - - _walk(obj) - return spans, anomalies - return _extract_flat_colour_ast(text) - - -def _extract_flat_colour_ast(text: str) -> tuple[list[tuple[str, ...]], list[str]]: - spans: list[tuple[str, ...]] = [] - anomalies: list[str] = [] - active: list[str] = [] - pos = 0 - last_style_without_text: str | None = None - - for match in _COLOR_PATTERN.finditer(text): - chunk = text[pos:match.start()] - if chunk and active: - spans.append(tuple(active)) - last_style_without_text = None - elif chunk: - last_style_without_text = None - - token = match.group() - code = token[1].lower() - if code == "r": - if not active: - anomalies.append("reset without active style") - active = [] - last_style_without_text = None - elif _is_colour_code(code): - active = [token] - last_style_without_text = token - else: - if token not in active: - active.append(token) - last_style_without_text = token - pos = match.end() - - tail = text[pos:] - if tail and active: - spans.append(tuple(active)) - elif tail: - last_style_without_text = None - - if last_style_without_text is not None: - anomalies.append("style code without styled text") - - return spans, anomalies - - -def _is_colour_code(code: str) -> bool: - return code in "0123456789abcdefz" - - -def _format_style_counter(counter: Counter[tuple[str, ...]]) -> dict[str, int]: - return {"+".join(style): count for style, count in sorted(counter.items())} - - -def _format_anomaly_counter(counter: Counter[str]) -> dict[str, int]: - return {anomaly: count for anomaly, count in sorted(counter.items())} - - -def _remove_extra_style_tokens(source: str, translated: str) -> str: - """Remove colour/style tokens that appear only in the translation. - - The repair is deliberately one-way: it only removes surplus style tokens. - It never invents a missing source token and never edits JSON component - strings, where changing raw text can accidentally alter structure. - """ - if _looks_like_json(source.strip()) or _looks_like_json(translated.strip()): - return translated - - source_style = Counter(match.group() for match in _COLOR_PATTERN.finditer(source)) - target_matches = list(_COLOR_PATTERN.finditer(translated)) - target_style = Counter(match.group() for match in target_matches) - - if not target_matches or source_style == target_style: - return translated - if source_style - target_style: - return translated - - remaining = Counter(source_style) - remove_spans: list[tuple[int, int]] = [] - for match in target_matches: - token = match.group() - if remaining[token] > 0: - remaining[token] -= 1 - else: - remove_spans.append(match.span()) - - if not remove_spans: - return translated - - result = translated - for start, end in reversed(remove_spans): - result = result[:start] + result[end:] - return result diff --git a/ftb_translater/history_db.py b/ftb_translater/history_db.py deleted file mode 100644 index a4ea150..0000000 --- a/ftb_translater/history_db.py +++ /dev/null @@ -1,303 +0,0 @@ -"""翻译历史 SQLite 存储。 - -每次翻译完成后保存一条 run + 对应文件的完整中英映射 + 输出文件原文。 -支持列表查询、删除、ZIP 导出,后续可用于词表挖掘。 -""" -from __future__ import annotations - -import json -import sqlite3 -import zipfile -from contextlib import contextmanager -from dataclasses import dataclass, field -from datetime import datetime -from pathlib import Path, PurePosixPath -from typing import Iterator - -from ftb_translater.logger import get_logger -_log = get_logger(__name__) -DEFAULT_HISTORY_DB_NAME = "history.sqlite3" - - -_SCHEMA = """ -CREATE TABLE IF NOT EXISTS translation_runs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - quests_dir TEXT NOT NULL, - pack_name TEXT, - mode TEXT NOT NULL, - model TEXT NOT NULL, - style TEXT NOT NULL, - base_url TEXT, - total_entries INTEGER NOT NULL, - translated_entries INTEGER NOT NULL, - cache_hits INTEGER NOT NULL, - failed_count INTEGER NOT NULL, - warning_count INTEGER NOT NULL, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS translation_files ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - run_id INTEGER NOT NULL, - filename TEXT NOT NULL, - source_hash TEXT, - mapping TEXT NOT NULL, - output_content TEXT NOT NULL, - FOREIGN KEY(run_id) REFERENCES translation_runs(id) ON DELETE CASCADE, - UNIQUE(run_id, filename) -); - -CREATE INDEX IF NOT EXISTS idx_runs_created ON translation_runs(created_at DESC); -CREATE INDEX IF NOT EXISTS idx_files_run ON translation_files(run_id); -""" - - -@dataclass -class FileRecord: - filename: str - mapping: dict[str, dict[str, str]] - output_content: str - source_hash: str = "" - - -@dataclass -class RunSummary: - id: int - pack_name: str - quests_dir: str - mode: str - model: str - style: str - total_entries: int - translated_entries: int - cache_hits: int - failed_count: int - warning_count: int - created_at: str - - -@dataclass -class RunDetail: - summary: RunSummary - files: list[FileRecord] = field(default_factory=list) - - -class HistoryDB: - def __init__(self, path: Path | None = None): - self.path = path or (Path.cwd() / DEFAULT_HISTORY_DB_NAME) - self.path.parent.mkdir(parents=True, exist_ok=True) - self.init_schema() - - @contextmanager - def _connect(self) -> Iterator[sqlite3.Connection]: - conn = sqlite3.connect(self.path) - conn.row_factory = sqlite3.Row - try: - conn.execute("PRAGMA foreign_keys = ON") - yield conn - conn.commit() - except Exception: - conn.rollback() - raise - finally: - conn.close() - - def init_schema(self) -> None: - with self._connect() as conn: - conn.executescript(_SCHEMA) - - def insert_run( - self, - *, - quests_dir: str, - mode: str, - model: str, - style: str, - base_url: str, - total_entries: int, - translated_entries: int, - cache_hits: int, - failed_count: int, - warning_count: int, - files: list[FileRecord], - pack_name: str | None = None, - created_at: str | None = None, - ) -> int: - pack_name = pack_name or _derive_pack_name(quests_dir) - created_at = created_at or datetime.now().isoformat(timespec="seconds") - with self._connect() as conn: - cursor = conn.execute( - """ - INSERT INTO translation_runs( - quests_dir, pack_name, mode, model, style, base_url, - total_entries, translated_entries, cache_hits, - failed_count, warning_count, created_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - quests_dir, pack_name, mode, model, style, base_url, - total_entries, translated_entries, cache_hits, - failed_count, warning_count, created_at, - ), - ) - run_id = cursor.lastrowid - assert run_id is not None - for record in files: - conn.execute( - """ - INSERT INTO translation_files( - run_id, filename, source_hash, mapping, output_content - ) VALUES (?, ?, ?, ?, ?) - """, - ( - run_id, - record.filename, - record.source_hash, - json.dumps(record.mapping, ensure_ascii=False), - record.output_content, - ), - ) - _log.info("Inserted history run #%d with %d file(s)", run_id, len(files)) - return run_id - - def list_runs(self, limit: int = 100) -> list[RunSummary]: - with self._connect() as conn: - rows = conn.execute( - """ - SELECT id, pack_name, quests_dir, mode, model, style, - total_entries, translated_entries, cache_hits, - failed_count, warning_count, created_at - FROM translation_runs - ORDER BY created_at DESC, id DESC - LIMIT ? - """, - (limit,), - ).fetchall() - return [_row_to_summary(row) for row in rows] - - def get_run(self, run_id: int) -> RunSummary | None: - with self._connect() as conn: - row = conn.execute( - """ - SELECT id, pack_name, quests_dir, mode, model, style, - total_entries, translated_entries, cache_hits, - failed_count, warning_count, created_at - FROM translation_runs WHERE id = ? - """, - (run_id,), - ).fetchone() - return _row_to_summary(row) if row else None - - def get_files(self, run_id: int) -> list[FileRecord]: - with self._connect() as conn: - rows = conn.execute( - """ - SELECT filename, source_hash, mapping, output_content - FROM translation_files WHERE run_id = ? - ORDER BY filename - """, - (run_id,), - ).fetchall() - return [ - FileRecord( - filename=row["filename"], - source_hash=row["source_hash"] or "", - mapping=json.loads(row["mapping"]), - output_content=row["output_content"], - ) - for row in rows - ] - - def delete_run(self, run_id: int) -> None: - with self._connect() as conn: - conn.execute("DELETE FROM translation_files WHERE run_id = ?", (run_id,)) - conn.execute("DELETE FROM translation_runs WHERE id = ?", (run_id,)) - _log.info("Deleted history run #%d", run_id) - - def export_zip(self, run_id: int, dest: Path) -> Path: - summary = self.get_run(run_id) - if summary is None: - raise ValueError(f"Run #{run_id} not found") - files = self.get_files(run_id) - if not files: - raise ValueError(f"Run #{run_id} has no files") - - dest = Path(dest) - dest.parent.mkdir(parents=True, exist_ok=True) - archive_files = [_archive_name(record.filename, summary.mode) for record in files] - manifest = { - "run_id": summary.id, - "pack_name": summary.pack_name, - "quests_dir": summary.quests_dir, - "mode": summary.mode, - "model": summary.model, - "style": summary.style, - "total_entries": summary.total_entries, - "translated_entries": summary.translated_entries, - "failed_count": summary.failed_count, - "warning_count": summary.warning_count, - "created_at": summary.created_at, - "files": archive_files, - } - with zipfile.ZipFile(dest, "w", zipfile.ZIP_DEFLATED) as zf: - zf.writestr("manifest.json", json.dumps(manifest, ensure_ascii=False, indent=2)) - for record, archive_name in zip(files, archive_files, strict=True): - zf.writestr(archive_name, record.output_content) - _log.info("Exported run #%d to %s", run_id, dest) - return dest - - -def _row_to_summary(row: sqlite3.Row) -> RunSummary: - return RunSummary( - id=row["id"], - pack_name=row["pack_name"] or "", - quests_dir=row["quests_dir"], - mode=row["mode"], - model=row["model"], - style=row["style"], - total_entries=row["total_entries"], - translated_entries=row["translated_entries"], - cache_hits=row["cache_hits"], - failed_count=row["failed_count"], - warning_count=row["warning_count"], - created_at=row["created_at"], - ) - - -def _derive_pack_name(quests_dir: str) -> str: - path = Path(quests_dir) - parts = path.parts - for marker in ("config", "ftbquests"): - if marker in parts: - idx = parts.index(marker) - if idx > 0: - return parts[idx - 1] - return path.name or "unknown" - - -def _archive_name(filename: str, mode: str) -> str: - normalized = filename.replace("\\", "/").strip("/") - if mode == "lang" and normalized == "zh_cn.snbt": - normalized = "lang/zh_cn.snbt" - - parts = PurePosixPath(normalized).parts - if ( - not normalized - or PurePosixPath(normalized).is_absolute() - or ".." in parts - or (parts and parts[0].endswith(":")) - or normalized.startswith("/") - ): - raise ValueError(f"Unsafe history export path: {filename!r}") - - if mode == "lang": - allowed = normalized == "lang/zh_cn.snbt" - else: - allowed = ( - normalized.startswith("chapters/") - and len(parts) == 2 - and parts[1].endswith(".snbt") - ) - if not allowed: - raise ValueError(f"Unexpected history export path for {mode} mode: {filename!r}") - return normalized diff --git a/ftb_translater/logger.py b/ftb_translater/logger.py deleted file mode 100644 index 26dc2d3..0000000 --- a/ftb_translater/logger.py +++ /dev/null @@ -1,56 +0,0 @@ -from __future__ import annotations - -import logging -import logging.handlers -from pathlib import Path - -from ftb_translater.user_paths import user_config_dir - - -LOG_FORMAT = "%(asctime)s [%(levelname)s] %(name)s: %(message)s" -DATE_FORMAT = "%Y-%m-%d %H:%M:%S" - -_initialized = False - - -def setup_logging(log_dir: Path | None = None, level: int = logging.DEBUG) -> Path: - """Configure file + console logging. Call once at startup. Returns the log file path.""" - global _initialized - if _initialized: - return _get_log_path(log_dir) - - log_path = _get_log_path(log_dir) - log_path.parent.mkdir(parents=True, exist_ok=True) - - root = logging.getLogger() - root.setLevel(level) - - # Rotating file handler — keep 5 files of 1 MB each - file_handler = logging.handlers.RotatingFileHandler( - log_path, - maxBytes=1024 * 1024, - backupCount=5, - encoding="utf-8", - ) - file_handler.setLevel(logging.DEBUG) - file_handler.setFormatter(logging.Formatter(LOG_FORMAT, DATE_FORMAT)) - root.addHandler(file_handler) - - # Console handler shows INFO and above - console_handler = logging.StreamHandler() - console_handler.setLevel(logging.INFO) - console_handler.setFormatter(logging.Formatter(LOG_FORMAT, DATE_FORMAT)) - root.addHandler(console_handler) - - _initialized = True - logging.getLogger(__name__).debug("Logging initialised. Log file: %s", log_path) - return log_path - - -def get_logger(name: str) -> logging.Logger: - return logging.getLogger(name) - - -def _get_log_path(log_dir: Path | None) -> Path: - base = log_dir or user_config_dir() / "logs" - return base / "ftb_translater.log" diff --git a/ftb_translater/paths.py b/ftb_translater/paths.py deleted file mode 100644 index 8ffb283..0000000 --- a/ftb_translater/paths.py +++ /dev/null @@ -1,104 +0,0 @@ -from __future__ import annotations - -from pathlib import Path - -from ftb_translater.logger import get_logger - -_log = get_logger(__name__) - -MAX_SEARCH_DEPTH = 5 - - -def has_lang_source(quests_dir: Path) -> bool: - return (quests_dir / "lang" / "en_us.snbt").is_file() - - -def has_chapters_source(quests_dir: Path) -> bool: - chapters_dir = quests_dir / "chapters" - return chapters_dir.is_dir() and any(chapters_dir.glob("*.snbt")) - - -def resolve_quests_dir(selected_dir: Path) -> Path: - selected_dir = selected_dir.expanduser().resolve() - _log.debug("resolve_quests_dir: starting from %s", selected_dir) - candidates = _candidate_quests_dirs(selected_dir) - _log.debug("Trying %d candidate directories", len(candidates)) - for candidate in candidates: - if has_lang_source(candidate): - _log.info("Found lang source at: %s", candidate) - return candidate - if has_chapters_source(candidate): - _log.info("Found chapters source at: %s", candidate) - return candidate - _log.error("No FTB quests directory found under %s. Tried: %s", selected_dir, candidates) - raise FileNotFoundError( - "Could not find FTB Quests lang/en_us.snbt or chapters/*.snbt. " - "Select a modpack root, config folder, ftbquests folder, quests folder, lang folder, or chapters folder." - ) - - -def detect_source_mode(quests_dir: Path) -> str: - if has_lang_source(quests_dir): - _log.debug("Source mode: lang (%s)", quests_dir) - return "lang" - if has_chapters_source(quests_dir): - _log.debug("Source mode: chapters (%s)", quests_dir) - return "chapters" - _log.error("detect_source_mode: no lang or chapters source found at %s", quests_dir) - raise FileNotFoundError("Could not find lang/en_us.snbt or chapters/*.snbt.") - - -def source_lang_path(quests_dir: Path) -> Path: - return quests_dir / "lang" / "en_us.snbt" - - -def target_lang_path(quests_dir: Path) -> Path: - return quests_dir / "lang" / "zh_cn.snbt" - - -def _candidate_quests_dirs(selected_dir: Path) -> list[Path]: - candidates: list[Path] = [] - - def add(path: Path) -> None: - resolved = path.resolve() - if resolved not in candidates: - candidates.append(resolved) - - add(selected_dir) - if selected_dir.name.lower() in {"chapters", "lang"}: - add(selected_dir.parent) - - for parent in [selected_dir, *selected_dir.parents]: - name = parent.name.lower() - if name == "quests": - add(parent) - if name == "ftbquests": - add(parent / "quests") - if name == "config": - add(parent / "ftbquests" / "quests") - - direct_patterns = [ - selected_dir / "config" / "ftbquests" / "quests", - selected_dir / "ftbquests" / "quests", - selected_dir / "quests", - ] - for pattern in direct_patterns: - add(pattern) - - if selected_dir.is_dir(): - for path in selected_dir.rglob("ftbquests"): - if _relative_depth(selected_dir, path) > MAX_SEARCH_DEPTH: - continue - add(path / "quests") - for path in selected_dir.rglob("quests"): - if _relative_depth(selected_dir, path) > MAX_SEARCH_DEPTH: - continue - add(path) - return candidates - - -def _relative_depth(root: Path, child: Path) -> int: - try: - return len(child.relative_to(root).parts) - except ValueError: - return MAX_SEARCH_DEPTH + 1 diff --git a/ftb_translater/providers.py b/ftb_translater/providers.py deleted file mode 100644 index 05c1f9d..0000000 --- a/ftb_translater/providers.py +++ /dev/null @@ -1,80 +0,0 @@ -from __future__ import annotations - -from collections.abc import Callable - -from ftb_translater.deepl_client import DEFAULT_DEEPL_BASE_URL, DEFAULT_DEEPL_MODEL, DeepLTranslator -from ftb_translater.deepseek_client import DEFAULT_BASE_URL, DEFAULT_MODEL, OpenAICompatibleTranslator -from ftb_translater.web_translation_clients import ( - DEEPL_WEB_MODEL, - GOOGLE_WEB_MODEL, - DEFAULT_DEEPL_WEB_BASE_URL, - DEFAULT_GOOGLE_WEB_BASE_URL, - DeepLWebTranslator, - GoogleWebTranslator, -) - - -OPENAI_COMPATIBLE = "openai_compatible" -DEEPL = "deepl" -GOOGLE_WEB = "google_web" -DEEPL_WEB = "deepl_web" -DEFAULT_PROVIDER = OPENAI_COMPATIBLE -PROVIDER_LABELS = { - OPENAI_COMPATIBLE: "OpenAI 兼容接口", - DEEPL: "DeepL 翻译 API", - GOOGLE_WEB: "Google 网页翻译(实验性)", - DEEPL_WEB: "DeepL 网页翻译(实验性)", -} - - -def normalize_provider(provider: str | None) -> str: - value = (provider or DEFAULT_PROVIDER).strip().lower() - if value not in PROVIDER_LABELS: - raise ValueError(f"不支持的翻译提供商:{provider}") - return value - - -def provider_defaults(provider: str) -> tuple[str, str]: - normalized = normalize_provider(provider) - if normalized == DEEPL: - return DEFAULT_DEEPL_BASE_URL, DEFAULT_DEEPL_MODEL - if normalized == GOOGLE_WEB: - return DEFAULT_GOOGLE_WEB_BASE_URL, GOOGLE_WEB_MODEL - if normalized == DEEPL_WEB: - return DEFAULT_DEEPL_WEB_BASE_URL, DEEPL_WEB_MODEL - return DEFAULT_BASE_URL, DEFAULT_MODEL - - -def provider_cache_id(provider: str, model: str, base_url: str) -> str: - normalized = normalize_provider(provider) - if normalized == OPENAI_COMPATIBLE: - # Keep existing DeepSeek cache entries usable after the migration. - return model - return f"{normalized}:{model}:{base_url.rstrip('/')}" - - -def create_translator( - provider: str, - api_key: str, - model: str, - base_url: str, - logger: Callable[[str], None] | None = None, -): - normalized = normalize_provider(provider) - if normalized == DEEPL: - return DeepLTranslator(api_key=api_key, model=model, base_url=base_url, logger=logger) - if normalized == GOOGLE_WEB: - return GoogleWebTranslator(model=model, base_url=base_url, logger=logger) - if normalized == DEEPL_WEB: - return DeepLWebTranslator(model=model, base_url=base_url, logger=logger) - return OpenAICompatibleTranslator(api_key=api_key, model=model, base_url=base_url, logger=logger) - - -def provider_requires_api_key(provider: str) -> bool: - return normalize_provider(provider) not in {GOOGLE_WEB, DEEPL_WEB} - - -def provider_max_workers(provider: str) -> int | None: - if normalize_provider(provider) in {GOOGLE_WEB, DEEPL_WEB}: - return 1 - return None diff --git a/ftb_translater/report.py b/ftb_translater/report.py deleted file mode 100644 index 40dcf0c..0000000 --- a/ftb_translater/report.py +++ /dev/null @@ -1,44 +0,0 @@ -from __future__ import annotations - -import json -from dataclasses import dataclass, field -from pathlib import Path -from typing import Any - - -@dataclass -class TranslationReport: - source_file: str - target_file: str - backup_dir: str - total_entries: int - translated_entries: int - cache_hits: int - failed_entries: list[str] = field(default_factory=list) - warnings: dict[str, list[str]] = field(default_factory=dict) - failed_translations: dict[str, dict[str, str]] = field(default_factory=dict) - # Stored for in-app history. output_files intentionally stays out of report-latest.json - # because it can duplicate every translated chapter file. - mapping: dict[str, dict[str, dict[str, str]]] = field(default_factory=dict) - output_files: dict[str, str] = field(default_factory=dict) - - def save(self, quests_dir: Path) -> Path: - path = quests_dir / ".ftb-translater" / "report-latest.json" - path.parent.mkdir(parents=True, exist_ok=True) - with path.open("w", encoding="utf-8") as file: - json.dump(self.to_dict(), file, ensure_ascii=False, indent=2) - return path - - def to_dict(self) -> dict[str, Any]: - return { - "source_file": self.source_file, - "target_file": self.target_file, - "backup_dir": self.backup_dir, - "total_entries": self.total_entries, - "translated_entries": self.translated_entries, - "cache_hits": self.cache_hits, - "failed_entries": self.failed_entries, - "warnings": self.warnings, - "failed_translations": self.failed_translations, - "mapping": self.mapping, - } diff --git a/ftb_translater/snbt.py b/ftb_translater/snbt.py deleted file mode 100644 index cb4c8b3..0000000 --- a/ftb_translater/snbt.py +++ /dev/null @@ -1,209 +0,0 @@ -from __future__ import annotations - -from collections import OrderedDict -from pathlib import Path -from typing import TypeAlias - -from ftb_translater.logger import get_logger - -LangValue: TypeAlias = "str | list[str]" -LangMap: TypeAlias = "OrderedDict[str, LangValue]" - -_log = get_logger(__name__) - - -class SnbtParseError(ValueError): - pass - - -class _Parser: - def __init__(self, text: str): - self.text = text - self.index = 0 - - def parse(self) -> LangMap: - self._skip_ws_and_comments() - self._expect("{") - values: LangMap = OrderedDict() - while True: - self._skip_ws_and_comments() - if self._peek() == "}": - self.index += 1 - break - key = self._parse_key() - self._skip_ws_and_comments() - self._expect(":") - self._skip_ws_and_comments() - value = self._parse_value() - values[key] = value - self._skip_ws_and_comments() - if self._peek() == ",": - self.index += 1 - continue - if self._peek() == "}": - continue - if self._peek(): - continue - raise self._error("Expected key or '}'") - self._skip_ws_and_comments() - if self.index != len(self.text): - raise self._error("Unexpected trailing content") - return values - - def _parse_value(self) -> LangValue: - if self._peek() in {'"', "'"}: - return self._parse_string() - if self._peek() == "[": - return self._parse_string_list() - raise self._error("Expected quoted string or string list value") - - def _parse_key(self) -> str: - if self._peek() in {'"', "'"}: - return self._parse_string() - start = self.index - while self.index < len(self.text) and self.text[self.index] not in ":\r\n\t ": - self.index += 1 - key = self.text[start:self.index].strip() - if not key: - raise self._error("Expected key") - return key - - def _parse_string(self) -> str: - quote = self._peek() - if quote not in {'"', "'"}: - raise self._error("Expected quoted string value") - self.index += 1 - chars: list[str] = [] - while self.index < len(self.text): - char = self.text[self.index] - self.index += 1 - if char == quote: - return "".join(chars) - if char == "\\": - if self.index >= len(self.text): - raise self._error("Unfinished escape sequence") - esc = self.text[self.index] - self.index += 1 - chars.append(_decode_escape(esc)) - else: - chars.append(char) - raise self._error("Unterminated string") - - def _parse_string_list(self) -> list[str]: - self._expect("[") - values: list[str] = [] - while True: - self._skip_ws_and_comments() - if self._peek() == "]": - self.index += 1 - return values - values.append(self._parse_string()) - self._skip_ws_and_comments() - if self._peek() == ",": - self.index += 1 - continue - if self._peek() == "]": - continue - if self._peek(): - continue - raise self._error("Expected string or ']'") - - def _skip_ws_and_comments(self) -> None: - while self.index < len(self.text): - char = self.text[self.index] - if char.isspace(): - self.index += 1 - continue - if self.text.startswith("//", self.index): - self.index = self.text.find("\n", self.index) - if self.index == -1: - self.index = len(self.text) - continue - if char == "#": - self.index = self.text.find("\n", self.index) - if self.index == -1: - self.index = len(self.text) - continue - break - - def _expect(self, char: str) -> None: - if self._peek() != char: - raise self._error(f"Expected '{char}'") - self.index += 1 - - def _peek(self) -> str: - if self.index >= len(self.text): - return "" - return self.text[self.index] - - def _error(self, message: str) -> SnbtParseError: - return SnbtParseError(f"{message} at offset {self.index}") - - -def parse_lang_snbt(text: str) -> LangMap: - return _Parser(text).parse() - - -def load_lang_snbt(path: Path) -> LangMap: - _log.debug("Loading lang SNBT: %s", path) - try: - result = parse_lang_snbt(path.read_text(encoding="utf-8-sig")) - _log.debug("Loaded %d entries from %s", len(result), path) - return result - except OSError as exc: - _log.error("Could not read %s: %s", path, exc) - raise SnbtParseError(f"Could not read {path}: {exc}") from exc - except SnbtParseError as exc: - _log.error("Parse error in %s: %s", path, exc) - raise - - -def dump_lang_snbt(values: LangMap | dict[str, LangValue]) -> str: - lines = ["{"] - items = list(values.items()) - for index, (key, value) in enumerate(items): - suffix = "," if index < len(items) - 1 else "" - if isinstance(value, list): - lines.append(f' "{_escape(key)}": [') - for item_index, item in enumerate(value): - item_suffix = "," if item_index < len(value) - 1 else "" - lines.append(f' "{_escape(item)}"{item_suffix}') - lines.append(f" ]{suffix}") - else: - lines.append(f' "{_escape(key)}": "{_escape(value)}"{suffix}') - lines.append("}") - return "\n".join(lines) + "\n" - - -def write_lang_snbt(path: Path, values: LangMap | dict[str, LangValue]) -> None: - _log.debug("Writing lang SNBT: %s (%d entries)", path, len(values)) - text = dump_lang_snbt(values) - parsed = parse_lang_snbt(text) - if list(parsed.keys()) != list(values.keys()): - _log.error("SNBT round-trip key mismatch when writing %s", path) - raise SnbtParseError("Written SNBT key set did not validate.") - path.write_text(text, encoding="utf-8") - _log.debug("Wrote %s OK", path) - - -def _decode_escape(esc: str) -> str: - mapping = { - "n": "\n", - "r": "\r", - "t": "\t", - "\\": "\\", - '"': '"', - "'": "'", - } - return mapping.get(esc, esc) - - -def _escape(value: str) -> str: - return ( - str(value) - .replace("\\", "\\\\") - .replace('"', '\\"') - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t") - ) diff --git a/ftb_translater/translator.py b/ftb_translater/translator.py deleted file mode 100644 index 4c829ec..0000000 --- a/ftb_translater/translator.py +++ /dev/null @@ -1,603 +0,0 @@ -from __future__ import annotations - -from concurrent.futures import ThreadPoolExecutor, as_completed -from collections import OrderedDict -import json -from collections.abc import Callable, Mapping -import os -from pathlib import Path -import threading -from dataclasses import dataclass -from typing import Protocol, cast - -from ftb_translater.backup import create_backup -from ftb_translater.cache import TranslationCache -from ftb_translater.chapters import chapter_files, extract_chapter_segments, replace_chapter_segments -from ftb_translater.deepseek_client import DEFAULT_BASE_URL, DEFAULT_MODEL, DEFAULT_STYLE -from ftb_translater.format_guard import preserved_token_warnings, protect_text, repair_translation_format, restore_text -from ftb_translater.logger import get_logger -from ftb_translater.paths import detect_source_mode, source_lang_path, target_lang_path -from ftb_translater.providers import DEFAULT_PROVIDER, create_translator, provider_cache_id, provider_max_workers -from ftb_translater.report import TranslationReport -from ftb_translater.snbt import LangValue, load_lang_snbt, write_lang_snbt - -_log = get_logger(__name__) - - -ProgressCallback = Callable[[str, int, int], None] -LogCallback = Callable[[str], None] -AUTO_BATCH_MAX_ENTRIES = 25 -AUTO_BATCH_MAX_CHARS = 6000 -AUTO_MAX_WORKERS = 6 -MAX_WORKERS_ENV = "FTB_TRANSLATER_CONCURRENCY" - - -class TranslatorClient(Protocol): - def translate_batch(self, entries: Mapping[str, str], style: str) -> dict[str, str]: ... - - -@dataclass(frozen=True) -class _BatchResult: - batch_index: int - batch: OrderedDict[str, str] - result: dict[str, str] - error: Exception | None = None - - -def estimate_batches(total_entries: int, batch_size: int) -> int: - if batch_size <= 0: - raise ValueError("Batch size must be greater than zero.") - return (total_entries + batch_size - 1) // batch_size - - -def build_translation_batches( - entries: Mapping[str, str], - batch_size: int | None = None, - max_chars: int = AUTO_BATCH_MAX_CHARS, -) -> list[OrderedDict[str, str]]: - if batch_size is not None and batch_size <= 0: - raise ValueError("Batch size must be greater than zero.") - max_entries = batch_size or AUTO_BATCH_MAX_ENTRIES - batches: list[OrderedDict[str, str]] = [] - current: OrderedDict[str, str] = OrderedDict() - current_chars = 0 - - for key, value in entries.items(): - estimated_chars = len(json.dumps({key: value}, ensure_ascii=False)) - if current and (len(current) >= max_entries or current_chars + estimated_chars > max_chars): - batches.append(current) - current = OrderedDict() - current_chars = 0 - current[key] = value - current_chars += estimated_chars - - if current: - batches.append(current) - return batches - - -def translate_quests_lang( - quests_dir: Path, - api_key: str, - batch_size: int | None = None, - model: str = DEFAULT_MODEL, - style: str = DEFAULT_STYLE, - progress: ProgressCallback | None = None, - logger: LogCallback | None = None, - translator: TranslatorClient | None = None, - max_workers: int | None = None, - base_url: str = DEFAULT_BASE_URL, - provider: str = DEFAULT_PROVIDER, -) -> TranslationReport: - if batch_size is not None and batch_size <= 0: - raise ValueError("Batch size must be greater than zero.") - - source_path = source_lang_path(quests_dir) - target_path = target_lang_path(quests_dir) - _log.info("translate_quests_lang: quests_dir=%s source=%s", quests_dir, source_path) - - source_values = load_lang_snbt(source_path) - _log.debug("Loaded source lang: %d entries", len(source_values)) - - cache = TranslationCache(quests_dir / ".ftb-translater" / "cache.json") - cache.load() - - translated_values: OrderedDict[str, LangValue] = OrderedDict() - pending: OrderedDict[str, str] = OrderedDict() - pending_sources: OrderedDict[str, str] = OrderedDict() - protections_by_key: dict[str, list[tuple[str, str]]] = {} - cache_hits = 0 - warnings: dict[str, list[str]] = {} - failed_translations: dict[str, dict[str, str]] = {} - - cache_model = provider_cache_id(provider, model, base_url) - for key, value in source_values.items(): - source_text = _lang_value_to_text(value) - cached = cache.get(source_text, cache_model, "zh_cn", style) - if cached is not None: - translated_values[key] = _text_to_lang_value(cached, value) - cache_hits += 1 - else: - protected_text, protections = protect_text(source_text) - pending[key] = protected_text - pending_sources[key] = source_text - protections_by_key[key] = protections - - _log.info("Cache hits: %d, pending translation: %d", cache_hits, len(pending)) - - total_pending = len(pending) - failed_entries: list[str] = [] - batches = build_translation_batches(pending, batch_size=batch_size) - _log.info("Built %d batches for %d pending entries", len(batches), total_pending) - - batch_results = _translate_batches( - batches=batches, - api_key=api_key, - model=model, - style=style, - base_url=base_url, - provider=provider, - logger=logger, - progress=progress, - progress_total=total_pending, - label="lang entries", - translator=translator, - max_workers=max_workers, - ) - for batch_result in sorted(batch_results, key=lambda item: item.batch_index): - batch = batch_result.batch - result = batch_result.result - if batch_result.error is not None: - for key in batch: - error_text = f"API batch failed: {batch_result.error}" - failed_entries.append(f"{key}: {batch_result.error}") - warnings[key] = [error_text] - failed_translations[key] = {"source": pending_sources[key], "failed": "", "error": error_text} - for key, protected_text in batch.items(): - source_text = pending_sources[key] - translated_text = result.get(key, protected_text) - model_raw = translated_text - translated_text, token_warnings = _guard_translation(source_text, translated_text, protections_by_key[key]) - translated_values[key] = _text_to_lang_value(translated_text, source_values[key]) - if translated_text != source_text: - cache.set(source_text, cache_model, "zh_cn", style, translated_text) - if token_warnings: - _log.warning("Format token mismatch for key %r: %s", key, token_warnings) - warnings[key] = token_warnings - failed_translations[key] = {"source": source_text, "failed": restore_text(model_raw, protections_by_key[key])} - - ordered_output: OrderedDict[str, LangValue] = OrderedDict() - for key in source_values: - ordered_output[key] = translated_values[key] - - msg = "Creating backup for lang directory before overwrite." - _log.info(msg) - if logger: - logger(msg) - backup_dir = create_backup(quests_dir, directories=("lang",)) - _log.info("Backup created at: %s", backup_dir) - - msg = f"Overwriting target lang file: {target_path}" - _log.info(msg) - if logger: - logger(msg) - write_lang_snbt(target_path, ordered_output) - - parsed_target = load_lang_snbt(target_path) - if list(parsed_target.keys()) != list(source_values.keys()): - _log.error( - "Key mismatch after write! source keys=%d written keys=%d", - len(source_values), len(parsed_target), - ) - raise ValueError("Written zh_cn.snbt does not contain the same keys as en_us.snbt.") - - cache.save() - _log.info( - "Lang translation done: total=%d translated=%d cache_hits=%d failed=%d warnings=%d", - len(source_values), len(source_values) - len(failed_entries), - cache_hits, len(failed_entries), len(warnings), - ) - if failed_entries: - _log.warning("Failed entries:\n%s", "\n".join(failed_entries)) - - mapping_data: dict[str, dict[str, str]] = {} - for key in source_values: - src_text = _lang_value_to_text(source_values[key]) - tgt_text = _lang_value_to_text(translated_values[key]) - mapping_data[key] = {"en": src_text, "zh": tgt_text} - # 用 lang/zh_cn.snbt 而不是 zh_cn.snbt,这样导出的 ZIP 解压后能直接覆盖整合包目录 - relative_target = f"lang/{target_path.name}" - - report = TranslationReport( - source_file=str(source_path), - target_file=str(target_path), - backup_dir=str(backup_dir), - total_entries=len(source_values), - translated_entries=len(source_values) - len(failed_entries), - cache_hits=cache_hits, - failed_entries=failed_entries, - warnings=warnings, - failed_translations=failed_translations, - mapping={relative_target: mapping_data}, - output_files={relative_target: target_path.read_text(encoding="utf-8")}, - ) - report.save(quests_dir) - if progress: - progress("done", total_pending, total_pending) - return report - - -def translate_quests_chapters( - quests_dir: Path, - api_key: str, - batch_size: int | None = None, - model: str = DEFAULT_MODEL, - style: str = DEFAULT_STYLE, - progress: ProgressCallback | None = None, - logger: LogCallback | None = None, - translator: TranslatorClient | None = None, - max_workers: int | None = None, - base_url: str = DEFAULT_BASE_URL, - provider: str = DEFAULT_PROVIDER, -) -> TranslationReport: - if batch_size is not None and batch_size <= 0: - raise ValueError("Batch size must be greater than zero.") - - files = chapter_files(quests_dir) - if not files: - _log.error("No chapter SNBT files found under %s", quests_dir / "chapters") - raise FileNotFoundError(f"No chapter SNBT files found under {quests_dir / 'chapters'}") - - _log.info("translate_quests_chapters: quests_dir=%s, files=%d", quests_dir, len(files)) - - cache = TranslationCache(quests_dir / ".ftb-translater" / "cache.json") - cache.load() - segments_by_file = {path: extract_chapter_segments(path) for path in files} - all_segments = [segment for segments in segments_by_file.values() for segment in segments] - _log.debug("Extracted %d total text segments from %d chapter files", len(all_segments), len(files)) - - pending: OrderedDict[str, str] = OrderedDict() - pending_sources: OrderedDict[str, str] = OrderedDict() - protections_by_key: dict[str, list[tuple[str, str]]] = {} - translations_by_file: dict[Path, dict[int, str]] = {path: {} for path in files} - cache_hits = 0 - warnings: dict[str, list[str]] = {} - failed_translations: dict[str, dict[str, str]] = {} - - cache_model = provider_cache_id(provider, model, base_url) - for segment in all_segments: - cached = cache.get(segment.source_text, cache_model, "zh_cn", style) - if cached is not None: - translations_by_file[segment.path][segment.index] = cached - cache_hits += 1 - else: - protected_text, protections = protect_text(segment.source_text) - pending[segment.cache_id] = protected_text - pending_sources[segment.cache_id] = segment.source_text - protections_by_key[segment.cache_id] = protections - - _log.info("Cache hits: %d, pending translation: %d", cache_hits, len(pending)) - - segment_by_id = {segment.cache_id: segment for segment in all_segments} - batches = build_translation_batches(pending, batch_size=batch_size) - _log.info("Built %d batches for %d pending segments", len(batches), len(pending)) - failed_entries: list[str] = [] - - batch_results = _translate_batches( - batches=batches, - api_key=api_key, - model=model, - style=style, - base_url=base_url, - provider=provider, - logger=logger, - progress=progress, - progress_total=len(pending), - label="chapter text entries", - translator=translator, - max_workers=max_workers, - ) - for batch_result in sorted(batch_results, key=lambda item: item.batch_index): - batch = batch_result.batch - result = batch_result.result - if batch_result.error is not None: - for key in batch: - error_text = f"API batch failed: {batch_result.error}" - failed_entries.append(f"{key}: {batch_result.error}") - warnings[key] = [error_text] - failed_translations[key] = {"source": pending_sources[key], "failed": "", "error": error_text} - for cache_id, protected_text in batch.items(): - segment = segment_by_id[cache_id] - source_text = pending_sources[cache_id] - translated_text = result.get(cache_id, protected_text) - model_raw = translated_text - translated_text, token_warnings = _guard_translation(source_text, translated_text, protections_by_key[cache_id]) - translations_by_file[segment.path][segment.index] = translated_text - if translated_text != source_text: - cache.set(source_text, cache_model, "zh_cn", style, translated_text) - if token_warnings: - _log.warning("Format token mismatch for segment %r: %s", cache_id, token_warnings) - warnings[cache_id] = token_warnings - failed_translations[cache_id] = {"source": source_text, "failed": restore_text(model_raw, protections_by_key[cache_id])} - - msg = "Creating backup for chapters directory before overwrite." - _log.info(msg) - if logger: - logger(msg) - backup_dir = create_backup(quests_dir, directories=("chapters",)) - _log.info("Backup created at: %s", backup_dir) - - replaced_count = 0 - for path, replacements in translations_by_file.items(): - if replacements: - msg = f"Overwriting chapter file: {path} ({len(replacements)} text segments)." - _log.info(msg) - if logger: - logger(msg) - replaced_count += replace_chapter_segments(path, replacements) - - cache.save() - _log.info( - "Chapters translation done: total=%d replaced=%d cache_hits=%d failed=%d warnings=%d", - len(all_segments), replaced_count, cache_hits, len(failed_entries), len(warnings), - ) - if failed_entries: - _log.warning("Failed entries:\n%s", "\n".join(failed_entries)) - - mapping_by_file: dict[str, dict[str, dict[str, str]]] = {} - output_by_file: dict[str, str] = {} - for path, replacements in translations_by_file.items(): - rel_name = f"chapters/{path.name}" - file_map: dict[str, dict[str, str]] = {} - for segment in segments_by_file[path]: - translated = replacements.get(segment.index, segment.source_text) - file_map[segment.cache_id] = {"en": segment.source_text, "zh": translated} - mapping_by_file[rel_name] = file_map - try: - output_by_file[rel_name] = path.read_text(encoding="utf-8") - except OSError as exc: - _log.warning("Could not read chapter file %s for history: %s", path, exc) - - report = TranslationReport( - source_file=str(quests_dir / "chapters"), - target_file=str(quests_dir / "chapters"), - backup_dir=str(backup_dir), - total_entries=len(all_segments), - translated_entries=replaced_count - len(failed_entries), - cache_hits=cache_hits, - failed_entries=failed_entries, - warnings=warnings, - failed_translations=failed_translations, - mapping=mapping_by_file, - output_files=output_by_file, - ) - report.save(quests_dir) - if progress: - progress("done", len(pending), len(pending)) - return report - - -def translate_quests_auto( - quests_dir: Path, - api_key: str, - batch_size: int | None = None, - model: str = DEFAULT_MODEL, - style: str = DEFAULT_STYLE, - progress: ProgressCallback | None = None, - logger: LogCallback | None = None, - translator: TranslatorClient | None = None, - max_workers: int | None = None, - base_url: str = DEFAULT_BASE_URL, - provider: str = DEFAULT_PROVIDER, -) -> TranslationReport: - mode = detect_source_mode(quests_dir) - _log.info("translate_quests_auto: mode=%s quests_dir=%s", mode, quests_dir) - if mode == "lang": - return translate_quests_lang( - quests_dir=quests_dir, - api_key=api_key, - batch_size=batch_size, - model=model, - style=style, - base_url=base_url, - provider=provider, - progress=progress, - logger=logger, - translator=translator, - max_workers=max_workers, - ) - return translate_quests_chapters( - quests_dir=quests_dir, - api_key=api_key, - batch_size=batch_size, - model=model, - style=style, - base_url=base_url, - provider=provider, - progress=progress, - logger=logger, - translator=translator, - max_workers=max_workers, - ) - - -def _translate_batches( - batches: list[OrderedDict[str, str]], - api_key: str, - model: str, - style: str, - base_url: str, - provider: str, - logger: LogCallback | None, - progress: ProgressCallback | None, - progress_total: int, - label: str, - translator: TranslatorClient | None, - max_workers: int | None, -) -> list[_BatchResult]: - worker_count = _resolve_max_workers(max_workers, len(batches), progress_total) - worker_limit = provider_max_workers(provider) - if worker_limit is not None: - worker_count = min(worker_count, worker_limit) - if not batches: - if progress: - progress("done", 0, progress_total) - return [] - - if logger: - logger(f"Translation API concurrency: {worker_count} worker(s), {len(batches)} batches.") - _log.info("Translation API concurrency: %d worker(s), %d batches", worker_count, len(batches)) - - completed_entries = 0 - results: list[_BatchResult] = [] - - if worker_count == 1: - for batch_index, batch in enumerate(batches, start=1): - if progress: - progress("translating", completed_entries, progress_total) - batch_result = _translate_one_batch( - batch_index=batch_index, - batch_count=len(batches), - batch=batch, - api_key=api_key, - model=model, - style=style, - base_url=base_url, - provider=provider, - logger=logger, - label=label, - translator=translator, - ) - results.append(batch_result) - completed_entries += len(batch) - if progress: - progress("translating", completed_entries, progress_total) - return results - - thread_state = threading.local() - - def worker(batch_index: int, batch: OrderedDict[str, str]) -> _BatchResult: - worker_translator = translator - if worker_translator is None: - worker_translator = cast(TranslatorClient | None, getattr(thread_state, "translator", None)) - if worker_translator is None: - worker_translator = create_translator( - provider=provider, api_key=api_key, model=model, base_url=base_url, logger=logger - ) - thread_state.translator = worker_translator - return _translate_one_batch( - batch_index=batch_index, - batch_count=len(batches), - batch=batch, - api_key=api_key, - model=model, - style=style, - base_url=base_url, - provider=provider, - logger=logger, - label=label, - translator=worker_translator, - ) - - with ThreadPoolExecutor(max_workers=worker_count) as executor: - futures = { - executor.submit(worker, batch_index, batch): batch - for batch_index, batch in enumerate(batches, start=1) - } - for future in as_completed(futures): - batch_result = future.result() - results.append(batch_result) - completed_entries += len(batch_result.batch) - if progress: - progress("translating", completed_entries, progress_total) - return results - - -def _translate_one_batch( - batch_index: int, - batch_count: int, - batch: OrderedDict[str, str], - api_key: str, - model: str, - style: str, - base_url: str, - provider: str, - logger: LogCallback | None, - label: str, - translator: TranslatorClient | None, -) -> _BatchResult: - client = translator or create_translator( - provider=provider, api_key=api_key, model=model, base_url=base_url, logger=logger - ) - msg = f"Translation API batch {batch_index}/{batch_count}: {len(batch)} {label}." - _log.info(msg) - if logger: - logger(msg) - try: - return _BatchResult(batch_index=batch_index, batch=batch, result=client.translate_batch(batch, style=style)) - except Exception as exc: # noqa: BLE001 - msg = f"Batch {batch_index} failed, preserving source text for this batch: {exc}" - _log.error(msg) - if logger: - logger(msg) - return _BatchResult(batch_index=batch_index, batch=batch, result=dict(batch), error=exc) - - -def _resolve_max_workers(max_workers: int | None, batch_count: int, entry_count: int = 0) -> int: - if batch_count <= 0: - return 1 - value = max_workers - if value is None: - raw_value = os.getenv(MAX_WORKERS_ENV) - if raw_value: - if raw_value.strip().lower() == "auto": - return _auto_max_workers(batch_count, entry_count) - try: - value = int(raw_value) - except ValueError: - _log.warning("%s must be an integer or 'auto', falling back to automatic concurrency", MAX_WORKERS_ENV) - return _auto_max_workers(batch_count, entry_count) - else: - return _auto_max_workers(batch_count, entry_count) - if value <= 0: - raise ValueError("max_workers must be greater than zero.") - return min(value, batch_count) - - -def _auto_max_workers(batch_count: int, entry_count: int) -> int: - if batch_count <= 1: - return 1 - if entry_count <= 25: - return min(2, batch_count) - if entry_count <= 150: - return min(3, batch_count) - if entry_count <= 800: - return min(4, batch_count) - return min(AUTO_MAX_WORKERS, batch_count) - - -def _guard_translation( - source_text: str, - translated_text: str, - protections: list[tuple[str, str]], -) -> tuple[str, list[str]]: - # Restore the exact tokens that were removed before sending text to the model. - restored = restore_text(translated_text, protections) - restored = repair_translation_format(source_text, restored) - - token_warnings = preserved_token_warnings(source_text, restored) - if token_warnings: - return source_text, [*token_warnings, "Unsafe translation discarded; source text preserved."] - return restored, [] - - -def _lang_value_to_text(value: LangValue) -> str: - if isinstance(value, list): - return "\n".join(value) - return value - - -def _text_to_lang_value(text: str, template: LangValue) -> LangValue: - if isinstance(template, list): - return text.split("\n") - return text diff --git a/ftb_translater/user_paths.py b/ftb_translater/user_paths.py deleted file mode 100644 index 933c140..0000000 --- a/ftb_translater/user_paths.py +++ /dev/null @@ -1,24 +0,0 @@ -from __future__ import annotations - -import os -from pathlib import Path - - -APP_DIR_NAME = "FTB-Translater" - - -def user_config_dir() -> Path: - override = os.getenv("FTB_TRANSLATER_CONFIG_DIR") - if override: - return Path(override).expanduser() - - if os.name == "nt": - base = os.getenv("LOCALAPPDATA") or os.getenv("APPDATA") - if base: - return Path(base) / APP_DIR_NAME - - xdg_config_home = os.getenv("XDG_CONFIG_HOME") - if xdg_config_home: - return Path(xdg_config_home) / "ftb-translater" - - return Path.home() / ".config" / "ftb-translater" diff --git a/ftb_translater/web_translation_clients.py b/ftb_translater/web_translation_clients.py deleted file mode 100644 index d319aba..0000000 --- a/ftb_translater/web_translation_clients.py +++ /dev/null @@ -1,265 +0,0 @@ -from __future__ import annotations - -import json -import time -import uuid -from collections.abc import Callable, Mapping -from urllib.error import HTTPError, URLError -from urllib.parse import urlencode -from urllib.request import Request, urlopen - -from ftb_translater.logger import get_logger - - -DEFAULT_GOOGLE_WEB_BASE_URL = "https://translate.googleapis.com" -DEFAULT_DEEPL_WEB_BASE_URL = "https://oneshot-free.www.deepl.com" -GOOGLE_WEB_MODEL = "google-web" -DEEPL_WEB_MODEL = "deepl-web" -DEEPL_WEB_MAX_CHARS = 1500 -GOOGLE_WEB_MAX_CHARS = 4500 - -_log = get_logger(__name__) - - -class WebTranslationError(RuntimeError): - pass - - -class GoogleWebTranslator: - """Experimental adapter for Google Translate's undocumented web endpoint.""" - - def __init__( - self, - api_key: str = "", - model: str = GOOGLE_WEB_MODEL, - base_url: str = DEFAULT_GOOGLE_WEB_BASE_URL, - retries: int = 2, - timeout: float = 30, - logger: Callable[[str], None] | None = None, - ): - self.model = model or GOOGLE_WEB_MODEL - self.base_url = base_url.rstrip("/") - self.retries = retries - self.timeout = timeout - self.logger = logger - - def translate_batch(self, entries: Mapping[str, str], style: str = "") -> dict[str, str]: - if not entries: - return {} - units: list[tuple[str, str]] = [] - piece_limit = GOOGLE_WEB_MAX_CHARS - 100 - for key, text in entries.items(): - units.extend((key, piece) for piece in _split_text(text, piece_limit)) - - result = {key: "" for key in entries} - chunk: list[tuple[str, str]] = [] - chunk_chars = 0 - for unit in units: - estimated_chars = len(unit[1]) + 40 - if chunk and chunk_chars + estimated_chars > GOOGLE_WEB_MAX_CHARS: - self._append_google_results(result, chunk) - chunk = [] - chunk_chars = 0 - chunk.append(unit) - chunk_chars += estimated_chars - if chunk: - self._append_google_results(result, chunk) - return result - - def _append_google_results(self, result: dict[str, str], units: list[tuple[str, str]]) -> None: - translated = self._translate_chunk([text for _key, text in units]) - for (key, _source), text in zip(units, translated, strict=True): - result[key] += text - - def _translate_chunk(self, texts: list[str]) -> list[str]: - markers = [f"⟪FTB_TRANSLATER_BATCH_{index}⟫" for index in range(len(texts))] - if any(marker in text for marker in markers for text in texts): - raise WebTranslationError("Source text contains a reserved Google batch marker.") - combined = "\n".join(marker + text for marker, text in zip(markers, texts, strict=True)) - body = urlencode( - {"client": "gtx", "sl": "en", "tl": "zh-CN", "dt": "t", "q": combined} - ).encode("utf-8") - request = Request( - f"{self.base_url}/translate_a/single", - data=body, - headers={ - "Content-Type": "application/x-www-form-urlencoded", - "User-Agent": _USER_AGENT, - }, - method="POST", - ) - raw = self._request_with_retry(request) - try: - payload = json.loads(raw) - segments = payload[0] - translated = "".join(segment[0] for segment in segments if segment and isinstance(segment[0], str)) - except (json.JSONDecodeError, IndexError, TypeError) as exc: - raise WebTranslationError(f"Google web translation returned invalid JSON: {exc}") from exc - if not translated: - raise WebTranslationError("Google web translation returned empty text.") - positions = [translated.find(marker) for marker in markers] - if any(position < 0 for position in positions) or positions != sorted(positions): - raise WebTranslationError("Google web translation did not preserve batch markers.") - results: list[str] = [] - for index, marker in enumerate(markers): - start = positions[index] + len(marker) - end = positions[index + 1] if index + 1 < len(markers) else len(translated) - value = translated[start:end] - if index + 1 < len(markers) and value.endswith("\n"): - value = value[:-1] - results.append(value) - return results - - def _request_with_retry(self, request: Request) -> str: - last_error: Exception | None = None - for attempt in range(self.retries + 1): - try: - with urlopen(request, timeout=self.timeout) as response: # noqa: S310 - endpoint is configurable. - return response.read().decode("utf-8") - except (HTTPError, URLError, TimeoutError) as exc: - last_error = exc - self._log(f"Google web request attempt {attempt + 1} failed: {exc}") - if attempt < self.retries: - time.sleep(1.2 * (attempt + 1)) - raise WebTranslationError(f"Google web translation failed: {last_error}") from last_error - - def _log(self, message: str) -> None: - _log.warning(message) - if self.logger: - self.logger(message) - - -class DeepLWebTranslator: - """Experimental adapter matching DeepL's anonymous browser-extension request.""" - - def __init__( - self, - api_key: str = "", - model: str = DEEPL_WEB_MODEL, - base_url: str = DEFAULT_DEEPL_WEB_BASE_URL, - retries: int = 2, - timeout: float = 30, - logger: Callable[[str], None] | None = None, - ): - self.model = model or DEEPL_WEB_MODEL - self.base_url = base_url.rstrip("/") - self.retries = retries - self.timeout = timeout - self.logger = logger - self.instance_id = str(uuid.uuid4()) - - def translate_batch(self, entries: Mapping[str, str], style: str = "") -> dict[str, str]: - if not entries: - return {} - result: dict[str, str] = {} - chunk: list[tuple[str, str]] = [] - chunk_chars = 0 - for key, text in entries.items(): - text_chars = len(text) - if text_chars > DEEPL_WEB_MAX_CHARS: - if chunk: - result.update(self._translate_chunk(chunk)) - chunk = [] - chunk_chars = 0 - result[key] = "".join(self._translate_texts(_split_text(text, DEEPL_WEB_MAX_CHARS))) - continue - if chunk and chunk_chars + text_chars > DEEPL_WEB_MAX_CHARS: - result.update(self._translate_chunk(chunk)) - chunk = [] - chunk_chars = 0 - chunk.append((key, text)) - chunk_chars += text_chars - if chunk: - result.update(self._translate_chunk(chunk)) - return result - - def _translate_chunk(self, items: list[tuple[str, str]]) -> dict[str, str]: - values = self._translate_texts([text for _key, text in items]) - return {key: value for (key, _text), value in zip(items, values, strict=True)} - - def _translate_texts(self, texts: list[str]) -> list[str]: - payload = { - "text": texts, - "target_lang": "zh-Hans", - "source_lang": "en", - "usage_type": "Translate", - "app_information": { - "os": "brex_macOS", - "os_version": "brex_chrome_120.0.0.0", - "app_version": "1.86.0", - "app_build": "chrome_web_store", - "instance_id": self.instance_id, - }, - } - request = Request( - f"{self.base_url}/v1/translate", - data=json.dumps(payload, ensure_ascii=False).encode("utf-8"), - headers={ - "Content-Type": "application/json", - "Accept": "*/*", - "Authorization": "None", - "Origin": "chrome-extension://cofdbpoegempjloogbagkncekinflcnj", - "Sec-Fetch-Site": "cross-site", - "Sec-Fetch-Mode": "cors", - "Sec-Fetch-Dest": "empty", - "User-Agent": _USER_AGENT, - }, - method="POST", - ) - raw = self._request_with_retry(request) - try: - response = json.loads(raw) - translations = response["translations"] - values = [item["text"] for item in translations] - except (json.JSONDecodeError, KeyError, TypeError) as exc: - raise WebTranslationError(f"DeepL web translation returned invalid JSON: {exc}") from exc - if len(values) != len(texts) or not all(isinstance(value, str) for value in values): - raise WebTranslationError("DeepL web translation returned an unexpected number of results.") - return values - - def _request_with_retry(self, request: Request) -> str: - last_error: Exception | None = None - for attempt in range(self.retries + 1): - try: - with urlopen(request, timeout=self.timeout) as response: # noqa: S310 - endpoint is configurable. - return response.read().decode("utf-8") - except (HTTPError, URLError, TimeoutError) as exc: - last_error = exc - self._log(f"DeepL web request attempt {attempt + 1} failed: {exc}") - if attempt < self.retries: - time.sleep(1.5 * (attempt + 1)) - raise WebTranslationError(f"DeepL web translation failed: {last_error}") from last_error - - def _log(self, message: str) -> None: - _log.warning(message) - if self.logger: - self.logger(message) - - -_USER_AGENT = ( - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " - "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" -) - - -def _split_text(text: str, max_chars: int) -> list[str]: - """Split long text without cutting an opaque ⟨P_n⟩ placeholder.""" - chunks: list[str] = [] - start = 0 - while len(text) - start > max_chars: - end = start + max_chars - window = text[start:end] - cut = max((window.rfind(char) + 1 for char in "\n.!?。!?;; " ), default=0) - if cut < max_chars // 2: - cut = max_chars - open_pos = window.rfind("⟨") - close_pos = window.rfind("⟩") - if open_pos > close_pos and open_pos < cut: - cut = open_pos - if cut <= 0: - cut = max_chars - chunks.append(text[start : start + cut]) - start += cut - if start < len(text): - chunks.append(text[start:]) - return chunks diff --git a/index.html b/index.html new file mode 100644 index 0000000..4360098 --- /dev/null +++ b/index.html @@ -0,0 +1,5 @@ + + + FTB Translater +
+ diff --git a/main.py b/main.py deleted file mode 100644 index c3f4652..0000000 --- a/main.py +++ /dev/null @@ -1,5 +0,0 @@ -from ftb_translater.app import main - - -if __name__ == "__main__": - main() diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3202ee7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2072 @@ +{ + "name": "ftb-translater-desktop", + "version": "0.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ftb-translater-desktop", + "version": "0.2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0", + "@tauri-apps/plugin-dialog": "^2.4.0", + "lucide-react": "^0.468.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.8.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.0", + "typescript": "^5.7.0", + "vite": "^6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tauri-apps/api": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz", + "integrity": "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.4.tgz", + "integrity": "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.11.4", + "@tauri-apps/cli-darwin-x64": "2.11.4", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", + "@tauri-apps/cli-linux-arm64-musl": "2.11.4", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-musl": "2.11.4", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", + "@tauri-apps/cli-win32-x64-msvc": "2.11.4" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.4.tgz", + "integrity": "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.4.tgz", + "integrity": "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.4.tgz", + "integrity": "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.4.tgz", + "integrity": "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.4.tgz", + "integrity": "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.4.tgz", + "integrity": "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.4.tgz", + "integrity": "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.4.tgz", + "integrity": "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.4.tgz", + "integrity": "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.4.tgz", + "integrity": "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz", + "integrity": "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/plugin-dialog": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.1.tgz", + "integrity": "sha512-OK1UBXYt+ojcmxMktzzuyonYIFta8CmAASpX+CA+DTGK24KlHjhYI6x2iOJ/TjZF4N7/ACK1oFmEOjIY9IhzOQ==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.43", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", + "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", + "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001803", + "electron-to-chromium": "^1.5.389", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001805", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001805.tgz", + "integrity": "sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.389", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", + "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.468.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz", + "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.17.tgz", + "integrity": "sha512-J7EF+8X+CzRPaJPOv9Ck2wNWJvGnnl3PcNPAdGg6GTLjyVpyQ0yATMSXRFRV01BviT/9Gwuc3rjEyJbDJG9a4w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..8e7a709 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "ftb-translater-desktop", + "private": true, + "version": "0.2.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "tauri": "tauri" + }, + "dependencies": { + "@tauri-apps/api": "^2.8.0", + "@tauri-apps/plugin-dialog": "^2.4.0", + "lucide-react": "^0.468.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.8.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.0", + "typescript": "^5.7.0", + "vite": "^6.0.0" + } +} diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 86efd36..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,21 +0,0 @@ -[project] -name = "ftb-translater" -version = "0.1.1" -requires-python = ">=3.11" -dependencies = [ - "cryptography>=49.0.0", - "customtkinter>=5.2.2", - "keyring>=25.7.0", - "openai>=1.0.0", - "python-dotenv>=1.0.0", -] - -[project.scripts] -ftb-translater = "ftb_translater.app:main" -ftb-test = "tests.run_groups:main" -ftb-test-e2e = "tests.run_groups:main" - -[dependency-groups] -dev = [ - "pyinstaller>=6.21.0", -] diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock new file mode 100644 index 0000000..f315ed2 --- /dev/null +++ b/src-tauri/Cargo.lock @@ -0,0 +1,5137 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.118", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "dbus-secret-service" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6" +dependencies = [ + "dbus", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.118", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "ftb-translater" +version = "0.2.0" +dependencies = [ + "chrono", + "futures", + "hex", + "keyring", + "regex", + "reqwest 0.12.28", + "rusqlite", + "serde", + "serde_json", + "sha2", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tempfile", + "tokio", + "walkdir", + "zip", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core", + "wasm-bindgen", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "byteorder", + "dbus-secret-service", + "log", + "security-framework 2.11.1", + "security-framework 3.7.0", + "windows-sys 0.60.2", + "zeroize", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +dependencies = [ + "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.0", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.0", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "regex" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags 2.13.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.118", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.0", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.0", + "block2", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest 0.13.4", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.118", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.2+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "time" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.0", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.118", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webpki-roots" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap 2.14.0", + "memchr", + "thiserror 2.0.18", + "zopfli", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..66ec2de --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "ftb-translater" +version = "0.2.0" +description = "FTB Quests translation workbench" +authors = ["FTB Translater contributors"] +edition = "2021" + +[lib] +name = "ftb_translater_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tauri = { version = "2", features = [] } +tauri-plugin-dialog = "2" +chrono = "0.4" +futures = "0.3" +hex = "0.4" +keyring = { version = "3", features = ["apple-native", "windows-native", "sync-secret-service"] } +regex = "1" +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +rusqlite = { version = "0.32", features = ["bundled"] } +sha2 = "0.10" +tokio = { version = "1", features = ["rt-multi-thread", "time"] } +walkdir = "2" +zip = { version = "2", default-features = false, features = ["deflate"] } + +[dev-dependencies] +tempfile = "3" diff --git a/src-tauri/app-icon.svg b/src-tauri/app-icon.svg new file mode 100644 index 0000000..878f603 --- /dev/null +++ b/src-tauri/app-icon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100644 index 0000000..e875c0f --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,7 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Default desktop permissions", + "windows": ["main"], + "permissions": ["core:default", "dialog:default", "dialog:allow-open", "dialog:allow-save"] +} diff --git a/src-tauri/gen/schemas/acl-manifests.json b/src-tauri/gen/schemas/acl-manifests.json new file mode 100644 index 0000000..f2f210a --- /dev/null +++ b/src-tauri/gen/schemas/acl-manifests.json @@ -0,0 +1 @@ +{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener","allow-supports-multiple-windows"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-supports-multiple-windows":{"identifier":"allow-supports-multiple-windows","description":"Enables the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":["supports_multiple_windows"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-supports-multiple-windows":{"identifier":"deny-supports-multiple-windows","description":"Denies the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["supports_multiple_windows"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-icon-with-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-icon-with-as-template":{"identifier":"allow-set-icon-with-as-template","description":"Enables the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_with_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-icon-with-as-template":{"identifier":"deny-set-icon-with-as-template","description":"Denies the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_with_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-activity-name","allow-scene-identifier","allow-internal-toggle-maximize"]},"permissions":{"allow-activity-name":{"identifier":"allow-activity-name","description":"Enables the activity_name command without any pre-configured scope.","commands":{"allow":["activity_name"],"deny":[]}},"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-scene-identifier":{"identifier":"allow-scene-identifier","description":"Enables the scene_identifier command without any pre-configured scope.","commands":{"allow":["scene_identifier"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-activity-name":{"identifier":"deny-activity-name","description":"Denies the activity_name command without any pre-configured scope.","commands":{"allow":[],"deny":["activity_name"]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-scene-identifier":{"identifier":"deny-scene-identifier","description":"Denies the scene_identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["scene_identifier"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)","commands":{"allow":["message"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)","commands":{"allow":["message"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)","commands":{"allow":[],"deny":["message"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)","commands":{"allow":[],"deny":["message"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file diff --git a/src-tauri/gen/schemas/capabilities.json b/src-tauri/gen/schemas/capabilities.json new file mode 100644 index 0000000..a023285 --- /dev/null +++ b/src-tauri/gen/schemas/capabilities.json @@ -0,0 +1 @@ +{"default":{"identifier":"default","description":"Default desktop permissions","local":true,"windows":["main"],"permissions":["core:default","dialog:default","dialog:allow-open","dialog:allow-save"]}} \ No newline at end of file diff --git a/src-tauri/gen/schemas/desktop-schema.json b/src-tauri/gen/schemas/desktop-schema.json new file mode 100644 index 0000000..24c9001 --- /dev/null +++ b/src-tauri/gen/schemas/desktop-schema.json @@ -0,0 +1,2358 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CapabilityFile", + "description": "Capability formats accepted in a capability file.", + "anyOf": [ + { + "description": "A single capability.", + "allOf": [ + { + "$ref": "#/definitions/Capability" + } + ] + }, + { + "description": "A list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + }, + { + "description": "A list of capabilities.", + "type": "object", + "required": [ + "capabilities" + ], + "properties": { + "capabilities": { + "description": "The list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + } + } + } + ], + "definitions": { + "Capability": { + "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", + "type": "object", + "required": [ + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", + "type": "string" + }, + "description": { + "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", + "default": "", + "type": "string" + }, + "remote": { + "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", + "anyOf": [ + { + "$ref": "#/definitions/CapabilityRemote" + }, + { + "type": "null" + } + ] + }, + "local": { + "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", + "default": true, + "type": "boolean" + }, + "windows": { + "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "webviews": { + "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "permissions": { + "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionEntry" + }, + "uniqueItems": true + }, + "platforms": { + "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "CapabilityRemote": { + "description": "Configuration for remote URLs that are associated with the capability.", + "type": "object", + "required": [ + "urls" + ], + "properties": { + "urls": { + "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionEntry": { + "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", + "anyOf": [ + { + "description": "Reference a permission or permission set by identifier.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + { + "description": "Reference a permission or permission set by identifier and extends its scope.", + "type": "object", + "allOf": [ + { + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + } + ], + "required": [ + "identifier" + ] + } + ] + }, + "Identifier": { + "description": "Permission identifier", + "oneOf": [ + { + "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", + "type": "string", + "const": "core:default", + "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`", + "type": "string", + "const": "core:app:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`" + }, + { + "description": "Enables the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-hide", + "markdownDescription": "Enables the app_hide command without any pre-configured scope." + }, + { + "description": "Enables the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-show", + "markdownDescription": "Enables the app_show command without any pre-configured scope." + }, + { + "description": "Enables the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-bundle-type", + "markdownDescription": "Enables the bundle_type command without any pre-configured scope." + }, + { + "description": "Enables the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-default-window-icon", + "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." + }, + { + "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-fetch-data-store-identifiers", + "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Enables the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-identifier", + "markdownDescription": "Enables the identifier command without any pre-configured scope." + }, + { + "description": "Enables the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-name", + "markdownDescription": "Enables the name command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-data-store", + "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." + }, + { + "description": "Enables the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-listener", + "markdownDescription": "Enables the remove_listener command without any pre-configured scope." + }, + { + "description": "Enables the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-app-theme", + "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-dock-visibility", + "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Enables the supports_multiple_windows command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-supports-multiple-windows", + "markdownDescription": "Enables the supports_multiple_windows command without any pre-configured scope." + }, + { + "description": "Enables the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-tauri-version", + "markdownDescription": "Enables the tauri_version command without any pre-configured scope." + }, + { + "description": "Enables the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-version", + "markdownDescription": "Enables the version command without any pre-configured scope." + }, + { + "description": "Denies the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-hide", + "markdownDescription": "Denies the app_hide command without any pre-configured scope." + }, + { + "description": "Denies the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-show", + "markdownDescription": "Denies the app_show command without any pre-configured scope." + }, + { + "description": "Denies the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-bundle-type", + "markdownDescription": "Denies the bundle_type command without any pre-configured scope." + }, + { + "description": "Denies the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-default-window-icon", + "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." + }, + { + "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-fetch-data-store-identifiers", + "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Denies the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-identifier", + "markdownDescription": "Denies the identifier command without any pre-configured scope." + }, + { + "description": "Denies the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-name", + "markdownDescription": "Denies the name command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-data-store", + "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." + }, + { + "description": "Denies the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-listener", + "markdownDescription": "Denies the remove_listener command without any pre-configured scope." + }, + { + "description": "Denies the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-app-theme", + "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-dock-visibility", + "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Denies the supports_multiple_windows command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-supports-multiple-windows", + "markdownDescription": "Denies the supports_multiple_windows command without any pre-configured scope." + }, + { + "description": "Denies the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-tauri-version", + "markdownDescription": "Denies the tauri_version command without any pre-configured scope." + }, + { + "description": "Denies the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-version", + "markdownDescription": "Denies the version command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", + "type": "string", + "const": "core:event:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" + }, + { + "description": "Enables the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit", + "markdownDescription": "Enables the emit command without any pre-configured scope." + }, + { + "description": "Enables the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit-to", + "markdownDescription": "Enables the emit_to command without any pre-configured scope." + }, + { + "description": "Enables the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-listen", + "markdownDescription": "Enables the listen command without any pre-configured scope." + }, + { + "description": "Enables the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-unlisten", + "markdownDescription": "Enables the unlisten command without any pre-configured scope." + }, + { + "description": "Denies the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit", + "markdownDescription": "Denies the emit command without any pre-configured scope." + }, + { + "description": "Denies the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit-to", + "markdownDescription": "Denies the emit_to command without any pre-configured scope." + }, + { + "description": "Denies the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-listen", + "markdownDescription": "Denies the listen command without any pre-configured scope." + }, + { + "description": "Denies the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-unlisten", + "markdownDescription": "Denies the unlisten command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", + "type": "string", + "const": "core:image:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" + }, + { + "description": "Enables the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-bytes", + "markdownDescription": "Enables the from_bytes command without any pre-configured scope." + }, + { + "description": "Enables the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-path", + "markdownDescription": "Enables the from_path command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-rgba", + "markdownDescription": "Enables the rgba command without any pre-configured scope." + }, + { + "description": "Enables the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-size", + "markdownDescription": "Enables the size command without any pre-configured scope." + }, + { + "description": "Denies the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-bytes", + "markdownDescription": "Denies the from_bytes command without any pre-configured scope." + }, + { + "description": "Denies the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-path", + "markdownDescription": "Denies the from_path command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-rgba", + "markdownDescription": "Denies the rgba command without any pre-configured scope." + }, + { + "description": "Denies the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-size", + "markdownDescription": "Denies the size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", + "type": "string", + "const": "core:menu:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" + }, + { + "description": "Enables the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-append", + "markdownDescription": "Enables the append command without any pre-configured scope." + }, + { + "description": "Enables the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-create-default", + "markdownDescription": "Enables the create_default command without any pre-configured scope." + }, + { + "description": "Enables the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-get", + "markdownDescription": "Enables the get command without any pre-configured scope." + }, + { + "description": "Enables the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-insert", + "markdownDescription": "Enables the insert command without any pre-configured scope." + }, + { + "description": "Enables the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-checked", + "markdownDescription": "Enables the is_checked command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-items", + "markdownDescription": "Enables the items command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-popup", + "markdownDescription": "Enables the popup command without any pre-configured scope." + }, + { + "description": "Enables the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-prepend", + "markdownDescription": "Enables the prepend command without any pre-configured scope." + }, + { + "description": "Enables the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove", + "markdownDescription": "Enables the remove command without any pre-configured scope." + }, + { + "description": "Enables the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove-at", + "markdownDescription": "Enables the remove_at command without any pre-configured scope." + }, + { + "description": "Enables the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-accelerator", + "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." + }, + { + "description": "Enables the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-app-menu", + "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-help-menu-for-nsapp", + "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-window-menu", + "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-windows-menu-for-nsapp", + "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-checked", + "markdownDescription": "Enables the set_checked command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-text", + "markdownDescription": "Enables the set_text command without any pre-configured scope." + }, + { + "description": "Enables the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-text", + "markdownDescription": "Enables the text command without any pre-configured scope." + }, + { + "description": "Denies the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-append", + "markdownDescription": "Denies the append command without any pre-configured scope." + }, + { + "description": "Denies the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-create-default", + "markdownDescription": "Denies the create_default command without any pre-configured scope." + }, + { + "description": "Denies the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-get", + "markdownDescription": "Denies the get command without any pre-configured scope." + }, + { + "description": "Denies the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-insert", + "markdownDescription": "Denies the insert command without any pre-configured scope." + }, + { + "description": "Denies the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-checked", + "markdownDescription": "Denies the is_checked command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-items", + "markdownDescription": "Denies the items command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-popup", + "markdownDescription": "Denies the popup command without any pre-configured scope." + }, + { + "description": "Denies the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-prepend", + "markdownDescription": "Denies the prepend command without any pre-configured scope." + }, + { + "description": "Denies the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove", + "markdownDescription": "Denies the remove command without any pre-configured scope." + }, + { + "description": "Denies the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove-at", + "markdownDescription": "Denies the remove_at command without any pre-configured scope." + }, + { + "description": "Denies the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-accelerator", + "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." + }, + { + "description": "Denies the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-app-menu", + "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-help-menu-for-nsapp", + "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-window-menu", + "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-windows-menu-for-nsapp", + "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-checked", + "markdownDescription": "Denies the set_checked command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-text", + "markdownDescription": "Denies the set_text command without any pre-configured scope." + }, + { + "description": "Denies the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-text", + "markdownDescription": "Denies the text command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", + "type": "string", + "const": "core:path:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" + }, + { + "description": "Enables the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-basename", + "markdownDescription": "Enables the basename command without any pre-configured scope." + }, + { + "description": "Enables the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-dirname", + "markdownDescription": "Enables the dirname command without any pre-configured scope." + }, + { + "description": "Enables the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-extname", + "markdownDescription": "Enables the extname command without any pre-configured scope." + }, + { + "description": "Enables the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-is-absolute", + "markdownDescription": "Enables the is_absolute command without any pre-configured scope." + }, + { + "description": "Enables the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-join", + "markdownDescription": "Enables the join command without any pre-configured scope." + }, + { + "description": "Enables the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-normalize", + "markdownDescription": "Enables the normalize command without any pre-configured scope." + }, + { + "description": "Enables the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve", + "markdownDescription": "Enables the resolve command without any pre-configured scope." + }, + { + "description": "Enables the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve-directory", + "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." + }, + { + "description": "Denies the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-basename", + "markdownDescription": "Denies the basename command without any pre-configured scope." + }, + { + "description": "Denies the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-dirname", + "markdownDescription": "Denies the dirname command without any pre-configured scope." + }, + { + "description": "Denies the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-extname", + "markdownDescription": "Denies the extname command without any pre-configured scope." + }, + { + "description": "Denies the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-is-absolute", + "markdownDescription": "Denies the is_absolute command without any pre-configured scope." + }, + { + "description": "Denies the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-join", + "markdownDescription": "Denies the join command without any pre-configured scope." + }, + { + "description": "Denies the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-normalize", + "markdownDescription": "Denies the normalize command without any pre-configured scope." + }, + { + "description": "Denies the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve", + "markdownDescription": "Denies the resolve command without any pre-configured scope." + }, + { + "description": "Denies the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve-directory", + "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", + "type": "string", + "const": "core:resources:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`", + "type": "string", + "const": "core:tray:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`" + }, + { + "description": "Enables the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-get-by-id", + "markdownDescription": "Enables the get_by_id command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-remove-by-id", + "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-as-template", + "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_with_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-with-as-template", + "markdownDescription": "Enables the set_icon_with_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-menu", + "markdownDescription": "Enables the set_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-show-menu-on-left-click", + "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Enables the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-temp-dir-path", + "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-tooltip", + "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." + }, + { + "description": "Enables the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-visible", + "markdownDescription": "Enables the set_visible command without any pre-configured scope." + }, + { + "description": "Denies the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-get-by-id", + "markdownDescription": "Denies the get_by_id command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-remove-by-id", + "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-as-template", + "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_with_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-with-as-template", + "markdownDescription": "Denies the set_icon_with_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-menu", + "markdownDescription": "Denies the set_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-show-menu-on-left-click", + "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Denies the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-temp-dir-path", + "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-tooltip", + "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." + }, + { + "description": "Denies the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-visible", + "markdownDescription": "Denies the set_visible command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", + "type": "string", + "const": "core:webview:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" + }, + { + "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-clear-all-browsing-data", + "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Enables the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview", + "markdownDescription": "Enables the create_webview command without any pre-configured scope." + }, + { + "description": "Enables the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview-window", + "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." + }, + { + "description": "Enables the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-get-all-webviews", + "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-internal-toggle-devtools", + "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Enables the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-print", + "markdownDescription": "Enables the print command without any pre-configured scope." + }, + { + "description": "Enables the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-reparent", + "markdownDescription": "Enables the reparent command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-auto-resize", + "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-background-color", + "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-focus", + "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-position", + "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-size", + "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-zoom", + "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Enables the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-close", + "markdownDescription": "Enables the webview_close command without any pre-configured scope." + }, + { + "description": "Enables the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-hide", + "markdownDescription": "Enables the webview_hide command without any pre-configured scope." + }, + { + "description": "Enables the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-position", + "markdownDescription": "Enables the webview_position command without any pre-configured scope." + }, + { + "description": "Enables the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-show", + "markdownDescription": "Enables the webview_show command without any pre-configured scope." + }, + { + "description": "Enables the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-size", + "markdownDescription": "Enables the webview_size command without any pre-configured scope." + }, + { + "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-clear-all-browsing-data", + "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Denies the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview", + "markdownDescription": "Denies the create_webview command without any pre-configured scope." + }, + { + "description": "Denies the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview-window", + "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." + }, + { + "description": "Denies the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-get-all-webviews", + "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-internal-toggle-devtools", + "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Denies the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-print", + "markdownDescription": "Denies the print command without any pre-configured scope." + }, + { + "description": "Denies the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-reparent", + "markdownDescription": "Denies the reparent command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-auto-resize", + "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-background-color", + "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-focus", + "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-position", + "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-size", + "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-zoom", + "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Denies the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-close", + "markdownDescription": "Denies the webview_close command without any pre-configured scope." + }, + { + "description": "Denies the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-hide", + "markdownDescription": "Denies the webview_hide command without any pre-configured scope." + }, + { + "description": "Denies the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-position", + "markdownDescription": "Denies the webview_position command without any pre-configured scope." + }, + { + "description": "Denies the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-show", + "markdownDescription": "Denies the webview_show command without any pre-configured scope." + }, + { + "description": "Denies the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-size", + "markdownDescription": "Denies the webview_size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`", + "type": "string", + "const": "core:window:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`" + }, + { + "description": "Enables the activity_name command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-activity-name", + "markdownDescription": "Enables the activity_name command without any pre-configured scope." + }, + { + "description": "Enables the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-available-monitors", + "markdownDescription": "Enables the available_monitors command without any pre-configured scope." + }, + { + "description": "Enables the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-center", + "markdownDescription": "Enables the center command without any pre-configured scope." + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Enables the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-create", + "markdownDescription": "Enables the create command without any pre-configured scope." + }, + { + "description": "Enables the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-current-monitor", + "markdownDescription": "Enables the current_monitor command without any pre-configured scope." + }, + { + "description": "Enables the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-cursor-position", + "markdownDescription": "Enables the cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-destroy", + "markdownDescription": "Enables the destroy command without any pre-configured scope." + }, + { + "description": "Enables the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-get-all-windows", + "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." + }, + { + "description": "Enables the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-hide", + "markdownDescription": "Enables the hide command without any pre-configured scope." + }, + { + "description": "Enables the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-position", + "markdownDescription": "Enables the inner_position command without any pre-configured scope." + }, + { + "description": "Enables the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-size", + "markdownDescription": "Enables the inner_size command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-internal-toggle-maximize", + "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-always-on-top", + "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-closable", + "markdownDescription": "Enables the is_closable command without any pre-configured scope." + }, + { + "description": "Enables the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-decorated", + "markdownDescription": "Enables the is_decorated command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-focused", + "markdownDescription": "Enables the is_focused command without any pre-configured scope." + }, + { + "description": "Enables the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-fullscreen", + "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximizable", + "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximized", + "markdownDescription": "Enables the is_maximized command without any pre-configured scope." + }, + { + "description": "Enables the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimizable", + "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimized", + "markdownDescription": "Enables the is_minimized command without any pre-configured scope." + }, + { + "description": "Enables the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-resizable", + "markdownDescription": "Enables the is_resizable command without any pre-configured scope." + }, + { + "description": "Enables the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-visible", + "markdownDescription": "Enables the is_visible command without any pre-configured scope." + }, + { + "description": "Enables the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-maximize", + "markdownDescription": "Enables the maximize command without any pre-configured scope." + }, + { + "description": "Enables the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-minimize", + "markdownDescription": "Enables the minimize command without any pre-configured scope." + }, + { + "description": "Enables the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-monitor-from-point", + "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Enables the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-position", + "markdownDescription": "Enables the outer_position command without any pre-configured scope." + }, + { + "description": "Enables the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-size", + "markdownDescription": "Enables the outer_size command without any pre-configured scope." + }, + { + "description": "Enables the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-primary-monitor", + "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." + }, + { + "description": "Enables the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-request-user-attention", + "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." + }, + { + "description": "Enables the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scale-factor", + "markdownDescription": "Enables the scale_factor command without any pre-configured scope." + }, + { + "description": "Enables the scene_identifier command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scene-identifier", + "markdownDescription": "Enables the scene_identifier command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-bottom", + "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-top", + "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-background-color", + "markdownDescription": "Enables the set_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-count", + "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-label", + "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." + }, + { + "description": "Enables the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-closable", + "markdownDescription": "Enables the set_closable command without any pre-configured scope." + }, + { + "description": "Enables the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-content-protected", + "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-grab", + "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-icon", + "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-position", + "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-visible", + "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Enables the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-decorations", + "markdownDescription": "Enables the set_decorations command without any pre-configured scope." + }, + { + "description": "Enables the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-effects", + "markdownDescription": "Enables the set_effects command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focus", + "markdownDescription": "Enables the set_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focusable", + "markdownDescription": "Enables the set_focusable command without any pre-configured scope." + }, + { + "description": "Enables the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-fullscreen", + "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-ignore-cursor-events", + "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Enables the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-max-size", + "markdownDescription": "Enables the set_max_size command without any pre-configured scope." + }, + { + "description": "Enables the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-maximizable", + "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-min-size", + "markdownDescription": "Enables the set_min_size command without any pre-configured scope." + }, + { + "description": "Enables the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-minimizable", + "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-overlay-icon", + "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-position", + "markdownDescription": "Enables the set_position command without any pre-configured scope." + }, + { + "description": "Enables the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-progress-bar", + "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Enables the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-resizable", + "markdownDescription": "Enables the set_resizable command without any pre-configured scope." + }, + { + "description": "Enables the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-shadow", + "markdownDescription": "Enables the set_shadow command without any pre-configured scope." + }, + { + "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-simple-fullscreen", + "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size", + "markdownDescription": "Enables the set_size command without any pre-configured scope." + }, + { + "description": "Enables the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size-constraints", + "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Enables the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-skip-taskbar", + "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Enables the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-theme", + "markdownDescription": "Enables the set_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title-bar-style", + "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-visible-on-all-workspaces", + "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Enables the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-dragging", + "markdownDescription": "Enables the start_dragging command without any pre-configured scope." + }, + { + "description": "Enables the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-resize-dragging", + "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Enables the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-theme", + "markdownDescription": "Enables the theme command without any pre-configured scope." + }, + { + "description": "Enables the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-title", + "markdownDescription": "Enables the title command without any pre-configured scope." + }, + { + "description": "Enables the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-toggle-maximize", + "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unmaximize", + "markdownDescription": "Enables the unmaximize command without any pre-configured scope." + }, + { + "description": "Enables the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unminimize", + "markdownDescription": "Enables the unminimize command without any pre-configured scope." + }, + { + "description": "Denies the activity_name command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-activity-name", + "markdownDescription": "Denies the activity_name command without any pre-configured scope." + }, + { + "description": "Denies the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-available-monitors", + "markdownDescription": "Denies the available_monitors command without any pre-configured scope." + }, + { + "description": "Denies the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-center", + "markdownDescription": "Denies the center command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Denies the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-create", + "markdownDescription": "Denies the create command without any pre-configured scope." + }, + { + "description": "Denies the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-current-monitor", + "markdownDescription": "Denies the current_monitor command without any pre-configured scope." + }, + { + "description": "Denies the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-cursor-position", + "markdownDescription": "Denies the cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-destroy", + "markdownDescription": "Denies the destroy command without any pre-configured scope." + }, + { + "description": "Denies the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-get-all-windows", + "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." + }, + { + "description": "Denies the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-hide", + "markdownDescription": "Denies the hide command without any pre-configured scope." + }, + { + "description": "Denies the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-position", + "markdownDescription": "Denies the inner_position command without any pre-configured scope." + }, + { + "description": "Denies the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-size", + "markdownDescription": "Denies the inner_size command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-internal-toggle-maximize", + "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-always-on-top", + "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-closable", + "markdownDescription": "Denies the is_closable command without any pre-configured scope." + }, + { + "description": "Denies the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-decorated", + "markdownDescription": "Denies the is_decorated command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-focused", + "markdownDescription": "Denies the is_focused command without any pre-configured scope." + }, + { + "description": "Denies the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-fullscreen", + "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximizable", + "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximized", + "markdownDescription": "Denies the is_maximized command without any pre-configured scope." + }, + { + "description": "Denies the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimizable", + "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimized", + "markdownDescription": "Denies the is_minimized command without any pre-configured scope." + }, + { + "description": "Denies the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-resizable", + "markdownDescription": "Denies the is_resizable command without any pre-configured scope." + }, + { + "description": "Denies the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-visible", + "markdownDescription": "Denies the is_visible command without any pre-configured scope." + }, + { + "description": "Denies the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-maximize", + "markdownDescription": "Denies the maximize command without any pre-configured scope." + }, + { + "description": "Denies the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-minimize", + "markdownDescription": "Denies the minimize command without any pre-configured scope." + }, + { + "description": "Denies the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-monitor-from-point", + "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Denies the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-position", + "markdownDescription": "Denies the outer_position command without any pre-configured scope." + }, + { + "description": "Denies the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-size", + "markdownDescription": "Denies the outer_size command without any pre-configured scope." + }, + { + "description": "Denies the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-primary-monitor", + "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." + }, + { + "description": "Denies the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-request-user-attention", + "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." + }, + { + "description": "Denies the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scale-factor", + "markdownDescription": "Denies the scale_factor command without any pre-configured scope." + }, + { + "description": "Denies the scene_identifier command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scene-identifier", + "markdownDescription": "Denies the scene_identifier command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-bottom", + "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-top", + "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-background-color", + "markdownDescription": "Denies the set_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-count", + "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-label", + "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." + }, + { + "description": "Denies the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-closable", + "markdownDescription": "Denies the set_closable command without any pre-configured scope." + }, + { + "description": "Denies the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-content-protected", + "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-grab", + "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-icon", + "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-position", + "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-visible", + "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Denies the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-decorations", + "markdownDescription": "Denies the set_decorations command without any pre-configured scope." + }, + { + "description": "Denies the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-effects", + "markdownDescription": "Denies the set_effects command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focus", + "markdownDescription": "Denies the set_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focusable", + "markdownDescription": "Denies the set_focusable command without any pre-configured scope." + }, + { + "description": "Denies the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-fullscreen", + "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-ignore-cursor-events", + "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Denies the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-max-size", + "markdownDescription": "Denies the set_max_size command without any pre-configured scope." + }, + { + "description": "Denies the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-maximizable", + "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-min-size", + "markdownDescription": "Denies the set_min_size command without any pre-configured scope." + }, + { + "description": "Denies the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-minimizable", + "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-overlay-icon", + "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-position", + "markdownDescription": "Denies the set_position command without any pre-configured scope." + }, + { + "description": "Denies the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-progress-bar", + "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Denies the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-resizable", + "markdownDescription": "Denies the set_resizable command without any pre-configured scope." + }, + { + "description": "Denies the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-shadow", + "markdownDescription": "Denies the set_shadow command without any pre-configured scope." + }, + { + "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-simple-fullscreen", + "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size", + "markdownDescription": "Denies the set_size command without any pre-configured scope." + }, + { + "description": "Denies the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size-constraints", + "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Denies the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-skip-taskbar", + "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Denies the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-theme", + "markdownDescription": "Denies the set_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title-bar-style", + "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-visible-on-all-workspaces", + "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "Denies the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-dragging", + "markdownDescription": "Denies the start_dragging command without any pre-configured scope." + }, + { + "description": "Denies the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-resize-dragging", + "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Denies the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-theme", + "markdownDescription": "Denies the theme command without any pre-configured scope." + }, + { + "description": "Denies the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-title", + "markdownDescription": "Denies the title command without any pre-configured scope." + }, + { + "description": "Denies the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-toggle-maximize", + "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unmaximize", + "markdownDescription": "Denies the unmaximize command without any pre-configured scope." + }, + { + "description": "Denies the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unminimize", + "markdownDescription": "Denies the unminimize command without any pre-configured scope." + }, + { + "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`", + "type": "string", + "const": "dialog:default", + "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`" + }, + { + "description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", + "type": "string", + "const": "dialog:allow-ask", + "markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + }, + { + "description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", + "type": "string", + "const": "dialog:allow-confirm", + "markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + }, + { + "description": "Enables the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-message", + "markdownDescription": "Enables the message command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-save", + "markdownDescription": "Enables the save command without any pre-configured scope." + }, + { + "description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", + "type": "string", + "const": "dialog:deny-ask", + "markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + }, + { + "description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", + "type": "string", + "const": "dialog:deny-confirm", + "markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + }, + { + "description": "Denies the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-message", + "markdownDescription": "Denies the message command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-save", + "markdownDescription": "Denies the save command without any pre-configured scope." + } + ] + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/src-tauri/gen/schemas/macOS-schema.json b/src-tauri/gen/schemas/macOS-schema.json new file mode 100644 index 0000000..24c9001 --- /dev/null +++ b/src-tauri/gen/schemas/macOS-schema.json @@ -0,0 +1,2358 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CapabilityFile", + "description": "Capability formats accepted in a capability file.", + "anyOf": [ + { + "description": "A single capability.", + "allOf": [ + { + "$ref": "#/definitions/Capability" + } + ] + }, + { + "description": "A list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + }, + { + "description": "A list of capabilities.", + "type": "object", + "required": [ + "capabilities" + ], + "properties": { + "capabilities": { + "description": "The list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + } + } + } + ], + "definitions": { + "Capability": { + "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", + "type": "object", + "required": [ + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", + "type": "string" + }, + "description": { + "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", + "default": "", + "type": "string" + }, + "remote": { + "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", + "anyOf": [ + { + "$ref": "#/definitions/CapabilityRemote" + }, + { + "type": "null" + } + ] + }, + "local": { + "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", + "default": true, + "type": "boolean" + }, + "windows": { + "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "webviews": { + "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "permissions": { + "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionEntry" + }, + "uniqueItems": true + }, + "platforms": { + "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "CapabilityRemote": { + "description": "Configuration for remote URLs that are associated with the capability.", + "type": "object", + "required": [ + "urls" + ], + "properties": { + "urls": { + "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionEntry": { + "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", + "anyOf": [ + { + "description": "Reference a permission or permission set by identifier.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + { + "description": "Reference a permission or permission set by identifier and extends its scope.", + "type": "object", + "allOf": [ + { + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + } + ], + "required": [ + "identifier" + ] + } + ] + }, + "Identifier": { + "description": "Permission identifier", + "oneOf": [ + { + "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", + "type": "string", + "const": "core:default", + "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`", + "type": "string", + "const": "core:app:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`" + }, + { + "description": "Enables the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-hide", + "markdownDescription": "Enables the app_hide command without any pre-configured scope." + }, + { + "description": "Enables the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-show", + "markdownDescription": "Enables the app_show command without any pre-configured scope." + }, + { + "description": "Enables the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-bundle-type", + "markdownDescription": "Enables the bundle_type command without any pre-configured scope." + }, + { + "description": "Enables the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-default-window-icon", + "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." + }, + { + "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-fetch-data-store-identifiers", + "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Enables the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-identifier", + "markdownDescription": "Enables the identifier command without any pre-configured scope." + }, + { + "description": "Enables the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-name", + "markdownDescription": "Enables the name command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-data-store", + "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." + }, + { + "description": "Enables the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-listener", + "markdownDescription": "Enables the remove_listener command without any pre-configured scope." + }, + { + "description": "Enables the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-app-theme", + "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-dock-visibility", + "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Enables the supports_multiple_windows command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-supports-multiple-windows", + "markdownDescription": "Enables the supports_multiple_windows command without any pre-configured scope." + }, + { + "description": "Enables the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-tauri-version", + "markdownDescription": "Enables the tauri_version command without any pre-configured scope." + }, + { + "description": "Enables the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-version", + "markdownDescription": "Enables the version command without any pre-configured scope." + }, + { + "description": "Denies the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-hide", + "markdownDescription": "Denies the app_hide command without any pre-configured scope." + }, + { + "description": "Denies the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-show", + "markdownDescription": "Denies the app_show command without any pre-configured scope." + }, + { + "description": "Denies the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-bundle-type", + "markdownDescription": "Denies the bundle_type command without any pre-configured scope." + }, + { + "description": "Denies the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-default-window-icon", + "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." + }, + { + "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-fetch-data-store-identifiers", + "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Denies the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-identifier", + "markdownDescription": "Denies the identifier command without any pre-configured scope." + }, + { + "description": "Denies the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-name", + "markdownDescription": "Denies the name command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-data-store", + "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." + }, + { + "description": "Denies the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-listener", + "markdownDescription": "Denies the remove_listener command without any pre-configured scope." + }, + { + "description": "Denies the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-app-theme", + "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-dock-visibility", + "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Denies the supports_multiple_windows command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-supports-multiple-windows", + "markdownDescription": "Denies the supports_multiple_windows command without any pre-configured scope." + }, + { + "description": "Denies the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-tauri-version", + "markdownDescription": "Denies the tauri_version command without any pre-configured scope." + }, + { + "description": "Denies the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-version", + "markdownDescription": "Denies the version command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", + "type": "string", + "const": "core:event:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" + }, + { + "description": "Enables the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit", + "markdownDescription": "Enables the emit command without any pre-configured scope." + }, + { + "description": "Enables the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit-to", + "markdownDescription": "Enables the emit_to command without any pre-configured scope." + }, + { + "description": "Enables the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-listen", + "markdownDescription": "Enables the listen command without any pre-configured scope." + }, + { + "description": "Enables the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-unlisten", + "markdownDescription": "Enables the unlisten command without any pre-configured scope." + }, + { + "description": "Denies the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit", + "markdownDescription": "Denies the emit command without any pre-configured scope." + }, + { + "description": "Denies the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit-to", + "markdownDescription": "Denies the emit_to command without any pre-configured scope." + }, + { + "description": "Denies the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-listen", + "markdownDescription": "Denies the listen command without any pre-configured scope." + }, + { + "description": "Denies the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-unlisten", + "markdownDescription": "Denies the unlisten command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", + "type": "string", + "const": "core:image:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" + }, + { + "description": "Enables the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-bytes", + "markdownDescription": "Enables the from_bytes command without any pre-configured scope." + }, + { + "description": "Enables the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-path", + "markdownDescription": "Enables the from_path command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-rgba", + "markdownDescription": "Enables the rgba command without any pre-configured scope." + }, + { + "description": "Enables the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-size", + "markdownDescription": "Enables the size command without any pre-configured scope." + }, + { + "description": "Denies the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-bytes", + "markdownDescription": "Denies the from_bytes command without any pre-configured scope." + }, + { + "description": "Denies the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-path", + "markdownDescription": "Denies the from_path command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-rgba", + "markdownDescription": "Denies the rgba command without any pre-configured scope." + }, + { + "description": "Denies the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-size", + "markdownDescription": "Denies the size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", + "type": "string", + "const": "core:menu:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" + }, + { + "description": "Enables the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-append", + "markdownDescription": "Enables the append command without any pre-configured scope." + }, + { + "description": "Enables the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-create-default", + "markdownDescription": "Enables the create_default command without any pre-configured scope." + }, + { + "description": "Enables the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-get", + "markdownDescription": "Enables the get command without any pre-configured scope." + }, + { + "description": "Enables the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-insert", + "markdownDescription": "Enables the insert command without any pre-configured scope." + }, + { + "description": "Enables the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-checked", + "markdownDescription": "Enables the is_checked command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-items", + "markdownDescription": "Enables the items command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-popup", + "markdownDescription": "Enables the popup command without any pre-configured scope." + }, + { + "description": "Enables the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-prepend", + "markdownDescription": "Enables the prepend command without any pre-configured scope." + }, + { + "description": "Enables the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove", + "markdownDescription": "Enables the remove command without any pre-configured scope." + }, + { + "description": "Enables the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove-at", + "markdownDescription": "Enables the remove_at command without any pre-configured scope." + }, + { + "description": "Enables the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-accelerator", + "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." + }, + { + "description": "Enables the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-app-menu", + "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-help-menu-for-nsapp", + "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-window-menu", + "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-windows-menu-for-nsapp", + "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-checked", + "markdownDescription": "Enables the set_checked command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-text", + "markdownDescription": "Enables the set_text command without any pre-configured scope." + }, + { + "description": "Enables the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-text", + "markdownDescription": "Enables the text command without any pre-configured scope." + }, + { + "description": "Denies the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-append", + "markdownDescription": "Denies the append command without any pre-configured scope." + }, + { + "description": "Denies the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-create-default", + "markdownDescription": "Denies the create_default command without any pre-configured scope." + }, + { + "description": "Denies the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-get", + "markdownDescription": "Denies the get command without any pre-configured scope." + }, + { + "description": "Denies the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-insert", + "markdownDescription": "Denies the insert command without any pre-configured scope." + }, + { + "description": "Denies the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-checked", + "markdownDescription": "Denies the is_checked command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-items", + "markdownDescription": "Denies the items command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-popup", + "markdownDescription": "Denies the popup command without any pre-configured scope." + }, + { + "description": "Denies the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-prepend", + "markdownDescription": "Denies the prepend command without any pre-configured scope." + }, + { + "description": "Denies the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove", + "markdownDescription": "Denies the remove command without any pre-configured scope." + }, + { + "description": "Denies the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove-at", + "markdownDescription": "Denies the remove_at command without any pre-configured scope." + }, + { + "description": "Denies the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-accelerator", + "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." + }, + { + "description": "Denies the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-app-menu", + "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-help-menu-for-nsapp", + "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-window-menu", + "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-windows-menu-for-nsapp", + "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-checked", + "markdownDescription": "Denies the set_checked command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-text", + "markdownDescription": "Denies the set_text command without any pre-configured scope." + }, + { + "description": "Denies the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-text", + "markdownDescription": "Denies the text command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", + "type": "string", + "const": "core:path:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" + }, + { + "description": "Enables the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-basename", + "markdownDescription": "Enables the basename command without any pre-configured scope." + }, + { + "description": "Enables the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-dirname", + "markdownDescription": "Enables the dirname command without any pre-configured scope." + }, + { + "description": "Enables the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-extname", + "markdownDescription": "Enables the extname command without any pre-configured scope." + }, + { + "description": "Enables the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-is-absolute", + "markdownDescription": "Enables the is_absolute command without any pre-configured scope." + }, + { + "description": "Enables the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-join", + "markdownDescription": "Enables the join command without any pre-configured scope." + }, + { + "description": "Enables the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-normalize", + "markdownDescription": "Enables the normalize command without any pre-configured scope." + }, + { + "description": "Enables the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve", + "markdownDescription": "Enables the resolve command without any pre-configured scope." + }, + { + "description": "Enables the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve-directory", + "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." + }, + { + "description": "Denies the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-basename", + "markdownDescription": "Denies the basename command without any pre-configured scope." + }, + { + "description": "Denies the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-dirname", + "markdownDescription": "Denies the dirname command without any pre-configured scope." + }, + { + "description": "Denies the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-extname", + "markdownDescription": "Denies the extname command without any pre-configured scope." + }, + { + "description": "Denies the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-is-absolute", + "markdownDescription": "Denies the is_absolute command without any pre-configured scope." + }, + { + "description": "Denies the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-join", + "markdownDescription": "Denies the join command without any pre-configured scope." + }, + { + "description": "Denies the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-normalize", + "markdownDescription": "Denies the normalize command without any pre-configured scope." + }, + { + "description": "Denies the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve", + "markdownDescription": "Denies the resolve command without any pre-configured scope." + }, + { + "description": "Denies the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve-directory", + "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", + "type": "string", + "const": "core:resources:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`", + "type": "string", + "const": "core:tray:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`" + }, + { + "description": "Enables the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-get-by-id", + "markdownDescription": "Enables the get_by_id command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-remove-by-id", + "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-as-template", + "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_with_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-with-as-template", + "markdownDescription": "Enables the set_icon_with_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-menu", + "markdownDescription": "Enables the set_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-show-menu-on-left-click", + "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Enables the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-temp-dir-path", + "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-tooltip", + "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." + }, + { + "description": "Enables the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-visible", + "markdownDescription": "Enables the set_visible command without any pre-configured scope." + }, + { + "description": "Denies the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-get-by-id", + "markdownDescription": "Denies the get_by_id command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-remove-by-id", + "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-as-template", + "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_with_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-with-as-template", + "markdownDescription": "Denies the set_icon_with_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-menu", + "markdownDescription": "Denies the set_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-show-menu-on-left-click", + "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Denies the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-temp-dir-path", + "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-tooltip", + "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." + }, + { + "description": "Denies the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-visible", + "markdownDescription": "Denies the set_visible command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", + "type": "string", + "const": "core:webview:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" + }, + { + "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-clear-all-browsing-data", + "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Enables the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview", + "markdownDescription": "Enables the create_webview command without any pre-configured scope." + }, + { + "description": "Enables the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview-window", + "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." + }, + { + "description": "Enables the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-get-all-webviews", + "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-internal-toggle-devtools", + "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Enables the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-print", + "markdownDescription": "Enables the print command without any pre-configured scope." + }, + { + "description": "Enables the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-reparent", + "markdownDescription": "Enables the reparent command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-auto-resize", + "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-background-color", + "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-focus", + "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-position", + "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-size", + "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-zoom", + "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Enables the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-close", + "markdownDescription": "Enables the webview_close command without any pre-configured scope." + }, + { + "description": "Enables the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-hide", + "markdownDescription": "Enables the webview_hide command without any pre-configured scope." + }, + { + "description": "Enables the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-position", + "markdownDescription": "Enables the webview_position command without any pre-configured scope." + }, + { + "description": "Enables the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-show", + "markdownDescription": "Enables the webview_show command without any pre-configured scope." + }, + { + "description": "Enables the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-size", + "markdownDescription": "Enables the webview_size command without any pre-configured scope." + }, + { + "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-clear-all-browsing-data", + "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Denies the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview", + "markdownDescription": "Denies the create_webview command without any pre-configured scope." + }, + { + "description": "Denies the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview-window", + "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." + }, + { + "description": "Denies the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-get-all-webviews", + "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-internal-toggle-devtools", + "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Denies the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-print", + "markdownDescription": "Denies the print command without any pre-configured scope." + }, + { + "description": "Denies the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-reparent", + "markdownDescription": "Denies the reparent command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-auto-resize", + "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-background-color", + "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-focus", + "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-position", + "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-size", + "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-zoom", + "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Denies the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-close", + "markdownDescription": "Denies the webview_close command without any pre-configured scope." + }, + { + "description": "Denies the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-hide", + "markdownDescription": "Denies the webview_hide command without any pre-configured scope." + }, + { + "description": "Denies the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-position", + "markdownDescription": "Denies the webview_position command without any pre-configured scope." + }, + { + "description": "Denies the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-show", + "markdownDescription": "Denies the webview_show command without any pre-configured scope." + }, + { + "description": "Denies the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-size", + "markdownDescription": "Denies the webview_size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`", + "type": "string", + "const": "core:window:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`" + }, + { + "description": "Enables the activity_name command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-activity-name", + "markdownDescription": "Enables the activity_name command without any pre-configured scope." + }, + { + "description": "Enables the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-available-monitors", + "markdownDescription": "Enables the available_monitors command without any pre-configured scope." + }, + { + "description": "Enables the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-center", + "markdownDescription": "Enables the center command without any pre-configured scope." + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Enables the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-create", + "markdownDescription": "Enables the create command without any pre-configured scope." + }, + { + "description": "Enables the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-current-monitor", + "markdownDescription": "Enables the current_monitor command without any pre-configured scope." + }, + { + "description": "Enables the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-cursor-position", + "markdownDescription": "Enables the cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-destroy", + "markdownDescription": "Enables the destroy command without any pre-configured scope." + }, + { + "description": "Enables the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-get-all-windows", + "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." + }, + { + "description": "Enables the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-hide", + "markdownDescription": "Enables the hide command without any pre-configured scope." + }, + { + "description": "Enables the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-position", + "markdownDescription": "Enables the inner_position command without any pre-configured scope." + }, + { + "description": "Enables the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-size", + "markdownDescription": "Enables the inner_size command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-internal-toggle-maximize", + "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-always-on-top", + "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-closable", + "markdownDescription": "Enables the is_closable command without any pre-configured scope." + }, + { + "description": "Enables the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-decorated", + "markdownDescription": "Enables the is_decorated command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-focused", + "markdownDescription": "Enables the is_focused command without any pre-configured scope." + }, + { + "description": "Enables the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-fullscreen", + "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximizable", + "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximized", + "markdownDescription": "Enables the is_maximized command without any pre-configured scope." + }, + { + "description": "Enables the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimizable", + "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimized", + "markdownDescription": "Enables the is_minimized command without any pre-configured scope." + }, + { + "description": "Enables the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-resizable", + "markdownDescription": "Enables the is_resizable command without any pre-configured scope." + }, + { + "description": "Enables the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-visible", + "markdownDescription": "Enables the is_visible command without any pre-configured scope." + }, + { + "description": "Enables the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-maximize", + "markdownDescription": "Enables the maximize command without any pre-configured scope." + }, + { + "description": "Enables the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-minimize", + "markdownDescription": "Enables the minimize command without any pre-configured scope." + }, + { + "description": "Enables the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-monitor-from-point", + "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Enables the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-position", + "markdownDescription": "Enables the outer_position command without any pre-configured scope." + }, + { + "description": "Enables the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-size", + "markdownDescription": "Enables the outer_size command without any pre-configured scope." + }, + { + "description": "Enables the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-primary-monitor", + "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." + }, + { + "description": "Enables the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-request-user-attention", + "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." + }, + { + "description": "Enables the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scale-factor", + "markdownDescription": "Enables the scale_factor command without any pre-configured scope." + }, + { + "description": "Enables the scene_identifier command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scene-identifier", + "markdownDescription": "Enables the scene_identifier command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-bottom", + "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-top", + "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-background-color", + "markdownDescription": "Enables the set_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-count", + "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-label", + "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." + }, + { + "description": "Enables the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-closable", + "markdownDescription": "Enables the set_closable command without any pre-configured scope." + }, + { + "description": "Enables the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-content-protected", + "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-grab", + "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-icon", + "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-position", + "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-visible", + "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Enables the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-decorations", + "markdownDescription": "Enables the set_decorations command without any pre-configured scope." + }, + { + "description": "Enables the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-effects", + "markdownDescription": "Enables the set_effects command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focus", + "markdownDescription": "Enables the set_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focusable", + "markdownDescription": "Enables the set_focusable command without any pre-configured scope." + }, + { + "description": "Enables the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-fullscreen", + "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-ignore-cursor-events", + "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Enables the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-max-size", + "markdownDescription": "Enables the set_max_size command without any pre-configured scope." + }, + { + "description": "Enables the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-maximizable", + "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-min-size", + "markdownDescription": "Enables the set_min_size command without any pre-configured scope." + }, + { + "description": "Enables the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-minimizable", + "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-overlay-icon", + "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-position", + "markdownDescription": "Enables the set_position command without any pre-configured scope." + }, + { + "description": "Enables the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-progress-bar", + "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Enables the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-resizable", + "markdownDescription": "Enables the set_resizable command without any pre-configured scope." + }, + { + "description": "Enables the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-shadow", + "markdownDescription": "Enables the set_shadow command without any pre-configured scope." + }, + { + "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-simple-fullscreen", + "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size", + "markdownDescription": "Enables the set_size command without any pre-configured scope." + }, + { + "description": "Enables the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size-constraints", + "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Enables the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-skip-taskbar", + "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Enables the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-theme", + "markdownDescription": "Enables the set_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title-bar-style", + "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-visible-on-all-workspaces", + "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Enables the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-dragging", + "markdownDescription": "Enables the start_dragging command without any pre-configured scope." + }, + { + "description": "Enables the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-resize-dragging", + "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Enables the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-theme", + "markdownDescription": "Enables the theme command without any pre-configured scope." + }, + { + "description": "Enables the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-title", + "markdownDescription": "Enables the title command without any pre-configured scope." + }, + { + "description": "Enables the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-toggle-maximize", + "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unmaximize", + "markdownDescription": "Enables the unmaximize command without any pre-configured scope." + }, + { + "description": "Enables the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unminimize", + "markdownDescription": "Enables the unminimize command without any pre-configured scope." + }, + { + "description": "Denies the activity_name command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-activity-name", + "markdownDescription": "Denies the activity_name command without any pre-configured scope." + }, + { + "description": "Denies the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-available-monitors", + "markdownDescription": "Denies the available_monitors command without any pre-configured scope." + }, + { + "description": "Denies the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-center", + "markdownDescription": "Denies the center command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Denies the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-create", + "markdownDescription": "Denies the create command without any pre-configured scope." + }, + { + "description": "Denies the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-current-monitor", + "markdownDescription": "Denies the current_monitor command without any pre-configured scope." + }, + { + "description": "Denies the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-cursor-position", + "markdownDescription": "Denies the cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-destroy", + "markdownDescription": "Denies the destroy command without any pre-configured scope." + }, + { + "description": "Denies the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-get-all-windows", + "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." + }, + { + "description": "Denies the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-hide", + "markdownDescription": "Denies the hide command without any pre-configured scope." + }, + { + "description": "Denies the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-position", + "markdownDescription": "Denies the inner_position command without any pre-configured scope." + }, + { + "description": "Denies the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-size", + "markdownDescription": "Denies the inner_size command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-internal-toggle-maximize", + "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-always-on-top", + "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-closable", + "markdownDescription": "Denies the is_closable command without any pre-configured scope." + }, + { + "description": "Denies the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-decorated", + "markdownDescription": "Denies the is_decorated command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-focused", + "markdownDescription": "Denies the is_focused command without any pre-configured scope." + }, + { + "description": "Denies the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-fullscreen", + "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximizable", + "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximized", + "markdownDescription": "Denies the is_maximized command without any pre-configured scope." + }, + { + "description": "Denies the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimizable", + "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimized", + "markdownDescription": "Denies the is_minimized command without any pre-configured scope." + }, + { + "description": "Denies the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-resizable", + "markdownDescription": "Denies the is_resizable command without any pre-configured scope." + }, + { + "description": "Denies the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-visible", + "markdownDescription": "Denies the is_visible command without any pre-configured scope." + }, + { + "description": "Denies the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-maximize", + "markdownDescription": "Denies the maximize command without any pre-configured scope." + }, + { + "description": "Denies the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-minimize", + "markdownDescription": "Denies the minimize command without any pre-configured scope." + }, + { + "description": "Denies the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-monitor-from-point", + "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Denies the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-position", + "markdownDescription": "Denies the outer_position command without any pre-configured scope." + }, + { + "description": "Denies the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-size", + "markdownDescription": "Denies the outer_size command without any pre-configured scope." + }, + { + "description": "Denies the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-primary-monitor", + "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." + }, + { + "description": "Denies the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-request-user-attention", + "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." + }, + { + "description": "Denies the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scale-factor", + "markdownDescription": "Denies the scale_factor command without any pre-configured scope." + }, + { + "description": "Denies the scene_identifier command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scene-identifier", + "markdownDescription": "Denies the scene_identifier command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-bottom", + "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-top", + "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-background-color", + "markdownDescription": "Denies the set_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-count", + "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-label", + "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." + }, + { + "description": "Denies the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-closable", + "markdownDescription": "Denies the set_closable command without any pre-configured scope." + }, + { + "description": "Denies the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-content-protected", + "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-grab", + "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-icon", + "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-position", + "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-visible", + "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Denies the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-decorations", + "markdownDescription": "Denies the set_decorations command without any pre-configured scope." + }, + { + "description": "Denies the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-effects", + "markdownDescription": "Denies the set_effects command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focus", + "markdownDescription": "Denies the set_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focusable", + "markdownDescription": "Denies the set_focusable command without any pre-configured scope." + }, + { + "description": "Denies the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-fullscreen", + "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-ignore-cursor-events", + "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Denies the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-max-size", + "markdownDescription": "Denies the set_max_size command without any pre-configured scope." + }, + { + "description": "Denies the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-maximizable", + "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-min-size", + "markdownDescription": "Denies the set_min_size command without any pre-configured scope." + }, + { + "description": "Denies the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-minimizable", + "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-overlay-icon", + "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-position", + "markdownDescription": "Denies the set_position command without any pre-configured scope." + }, + { + "description": "Denies the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-progress-bar", + "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Denies the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-resizable", + "markdownDescription": "Denies the set_resizable command without any pre-configured scope." + }, + { + "description": "Denies the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-shadow", + "markdownDescription": "Denies the set_shadow command without any pre-configured scope." + }, + { + "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-simple-fullscreen", + "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size", + "markdownDescription": "Denies the set_size command without any pre-configured scope." + }, + { + "description": "Denies the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size-constraints", + "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Denies the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-skip-taskbar", + "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Denies the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-theme", + "markdownDescription": "Denies the set_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title-bar-style", + "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-visible-on-all-workspaces", + "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "Denies the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-dragging", + "markdownDescription": "Denies the start_dragging command without any pre-configured scope." + }, + { + "description": "Denies the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-resize-dragging", + "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Denies the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-theme", + "markdownDescription": "Denies the theme command without any pre-configured scope." + }, + { + "description": "Denies the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-title", + "markdownDescription": "Denies the title command without any pre-configured scope." + }, + { + "description": "Denies the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-toggle-maximize", + "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unmaximize", + "markdownDescription": "Denies the unmaximize command without any pre-configured scope." + }, + { + "description": "Denies the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unminimize", + "markdownDescription": "Denies the unminimize command without any pre-configured scope." + }, + { + "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`", + "type": "string", + "const": "dialog:default", + "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`" + }, + { + "description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", + "type": "string", + "const": "dialog:allow-ask", + "markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + }, + { + "description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", + "type": "string", + "const": "dialog:allow-confirm", + "markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + }, + { + "description": "Enables the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-message", + "markdownDescription": "Enables the message command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-save", + "markdownDescription": "Enables the save command without any pre-configured scope." + }, + { + "description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", + "type": "string", + "const": "dialog:deny-ask", + "markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + }, + { + "description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", + "type": "string", + "const": "dialog:deny-confirm", + "markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + }, + { + "description": "Denies the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-message", + "markdownDescription": "Denies the message command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-save", + "markdownDescription": "Denies the save command without any pre-configured scope." + } + ] + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 0000000..feb8550 Binary files /dev/null and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000..6160144 Binary files /dev/null and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png new file mode 100644 index 0000000..d73ead4 Binary files /dev/null and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/64x64.png b/src-tauri/icons/64x64.png new file mode 100644 index 0000000..0504b8c Binary files /dev/null and b/src-tauri/icons/64x64.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000..2988e03 Binary files /dev/null and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000..4a2c7f3 Binary files /dev/null and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000..a79cc97 Binary files /dev/null and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000..3276a2d Binary files /dev/null and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000..b5b806e Binary files /dev/null and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000..90af09f Binary files /dev/null and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000..125fd45 Binary files /dev/null and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000..69372b5 Binary files /dev/null and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000..43e02b0 Binary files /dev/null and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000..ffed42c Binary files /dev/null and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml b/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..2ffbf24 --- /dev/null +++ b/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..07b2b66 Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..d2c093b Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..8462420 Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..10b3a4f Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..740ee15 Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..42c5079 Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..8ea5d0c Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..c6b3fea Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..2235afb Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..f9511a2 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..97ea145 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..00e803b Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..c07b8cc Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..3db3e34 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..040902b Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/values/ic_launcher_background.xml b/src-tauri/icons/android/values/ic_launcher_background.xml new file mode 100644 index 0000000..ea9c223 --- /dev/null +++ b/src-tauri/icons/android/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #fff + \ No newline at end of file diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns new file mode 100644 index 0000000..d9391ee Binary files /dev/null and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico new file mode 100644 index 0000000..c21d411 Binary files /dev/null and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png new file mode 100644 index 0000000..0d7e84b Binary files /dev/null and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@1x.png b/src-tauri/icons/ios/AppIcon-20x20@1x.png new file mode 100644 index 0000000..1b86aa3 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png new file mode 100644 index 0000000..f075cc6 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x.png b/src-tauri/icons/ios/AppIcon-20x20@2x.png new file mode 100644 index 0000000..f075cc6 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@3x.png b/src-tauri/icons/ios/AppIcon-20x20@3x.png new file mode 100644 index 0000000..34921b6 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@1x.png b/src-tauri/icons/ios/AppIcon-29x29@1x.png new file mode 100644 index 0000000..68a3c76 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png new file mode 100644 index 0000000..75f1599 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x.png b/src-tauri/icons/ios/AppIcon-29x29@2x.png new file mode 100644 index 0000000..75f1599 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@3x.png b/src-tauri/icons/ios/AppIcon-29x29@3x.png new file mode 100644 index 0000000..b4a6ffb Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@1x.png b/src-tauri/icons/ios/AppIcon-40x40@1x.png new file mode 100644 index 0000000..f075cc6 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png new file mode 100644 index 0000000..9816c85 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x.png b/src-tauri/icons/ios/AppIcon-40x40@2x.png new file mode 100644 index 0000000..9816c85 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@3x.png b/src-tauri/icons/ios/AppIcon-40x40@3x.png new file mode 100644 index 0000000..ceabbe9 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-512@2x.png b/src-tauri/icons/ios/AppIcon-512@2x.png new file mode 100644 index 0000000..8c8ef8c Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-512@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-60x60@2x.png b/src-tauri/icons/ios/AppIcon-60x60@2x.png new file mode 100644 index 0000000..ceabbe9 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-60x60@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-60x60@3x.png b/src-tauri/icons/ios/AppIcon-60x60@3x.png new file mode 100644 index 0000000..3e56a63 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-60x60@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-76x76@1x.png b/src-tauri/icons/ios/AppIcon-76x76@1x.png new file mode 100644 index 0000000..6e24d44 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-76x76@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-76x76@2x.png b/src-tauri/icons/ios/AppIcon-76x76@2x.png new file mode 100644 index 0000000..eae4735 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-76x76@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png new file mode 100644 index 0000000..aac624f Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png differ diff --git a/src-tauri/resources/minecraft_glossary.json b/src-tauri/resources/minecraft_glossary.json new file mode 100644 index 0000000..dae5639 --- /dev/null +++ b/src-tauri/resources/minecraft_glossary.json @@ -0,0 +1,636 @@ +{ + "version": 2, + "entries": [ + { "source": "FTB StoneBlock 4", "target": "FTB StoneBlock 4" }, + { "source": "StoneBlock 4", "target": "StoneBlock 4" }, + { "source": "Applied Energistics 2", "target": "应用能源 2" }, + { "source": "Refined Storage 2", "target": "精致存储 2" }, + { "source": "Refined Storage", "target": "精致存储" }, + { "source": "Draconic Evolution", "target": "龙之进化" }, + { "source": "Industrial Foregoing", "target": "工业先锋" }, + { "source": "Thermal Expansion", "target": "热力膨胀" }, + { "source": "Thermal Foundation", "target": "热力基本" }, + { "source": "Thermal Innovation", "target": "热力创新" }, + { "source": "Thermal Series", "target": "热力系列" }, + { "source": "Ars Nouveau", "target": "新生魔艺" }, + { "source": "The Twilight Forest", "target": "暮色森林" }, + { "source": "Twilight Forest", "target": "暮色森林" }, + { "source": "Blood Magic", "target": "血魔法" }, + { "source": "Botania", "target": "植物魔法" }, + { "source": "Occultism", "target": "神秘学" }, + { "source": "Apotheosis", "target": "神化" }, + { "source": "PneumaticCraft: Repressurized", "target": "气动工艺:重压" }, + { "source": "PneumaticCraft", "target": "气动工艺" }, + { "source": "Extreme Reactors", "target": "极限反应堆" }, + { "source": "Bigger Reactors", "target": "更大的反应堆" }, + { "source": "Integrated Dynamics", "target": "集成动力" }, + { "source": "Integrated Tunnels", "target": "集成隧道" }, + { "source": "Integrated Crafting", "target": "集成合成" }, + { "source": "Actually Additions", "target": "实用拓展" }, + { "source": "Sophisticated Backpacks", "target": "精妙背包" }, + { "source": "Sophisticated Storage", "target": "精妙存储" }, + { "source": "Functional Storage", "target": "功能存储" }, + { "source": "Storage Drawers", "target": "储物抽屉" }, + { "source": "Iron Chests", "target": "铁箱" }, + { "source": "FTB Quests", "target": "FTB 任务" }, + { "source": "FTB Chunks", "target": "FTB 区块" }, + { "source": "FTB Teams", "target": "FTB 队伍" }, + { "source": "Just Dire Things", "target": "Just Dire Things" }, + { "source": "Ender IO", "target": "末影接口" }, + { "source": "ProjectE", "target": "ProjectE" }, + { "source": "Mekanism", "target": "通用机械" }, + { "source": "Oritech", "target": "Oritech" }, + { "source": "Powah", "target": "Powah" }, + { "source": "RFTools", "target": "RF工具" }, + { "source": "XNet", "target": "XNet" }, + { "source": "KubeJS", "target": "KubeJS" }, + { "source": "JEI", "target": "JEI" }, + { "source": "EMI", "target": "EMI" }, + { "source": "AE2", "target": "AE2" }, + { "source": "RS", "target": "RS", "case_sensitive": true }, + { "source": "The End", "target": "末地", "case_sensitive": true }, + { "source": "Minecraft Forge", "target": "Minecraft Forge" }, + { "source": "NeoForge", "target": "NeoForge" }, + { "source": "Fabric Loader", "target": "Fabric Loader" }, + { "source": "Fabric API", "target": "Fabric API" }, + { "source": "Quilt Loader", "target": "Quilt Loader" }, + { "source": "Create Crafts & Additions", "target": "机械动力:创想附加" }, + { "source": "Create: Enchantment Industry", "target": "机械动力:附魔工业" }, + { "source": "Create: Steam 'n' Rails", "target": "机械动力:蒸汽与铁路" }, + { "source": "Create: New Age", "target": "机械动力:新时代" }, + { "source": "Create: Central Kitchen", "target": "机械动力:中央厨房" }, + { "source": "Create Deco", "target": "机械动力装饰" }, + { "source": "the Create mod", "target": "机械动力模组" }, + { "source": "Immersive Engineering", "target": "沉浸工程" }, + { "source": "Modern Industrialization", "target": "现代工业化" }, + { "source": "IndustrialCraft 2", "target": "工业时代 2" }, + { "source": "Industrial Craft 2", "target": "工业时代 2" }, + { "source": "IndustrialCraft²", "target": "工业时代 2" }, + { "source": "BuildCraft", "target": "建筑" }, + { "source": "GregTech Community Edition", "target": "格雷科技社区版" }, + { "source": "GregTech CEu", "target": "格雷科技社区版非官方版" }, + { "source": "GregTech", "target": "格雷科技" }, + { "source": "Tech Reborn", "target": "科技复兴" }, + { "source": "NuclearCraft: Overhauled", "target": "核电工艺:重制版" }, + { "source": "NuclearCraft", "target": "核电工艺" }, + { "source": "Advanced Rocketry", "target": "高级火箭" }, + { "source": "Ad Astra", "target": "Ad Astra" }, + { "source": "Galacticraft", "target": "星系" }, + { "source": "Beyond Earth", "target": "超越地球" }, + { "source": "Railcraft Reborn", "target": "铁路:重生" }, + { "source": "Railcraft", "target": "铁路" }, + { "source": "Steve's Carts Reborn", "target": "史蒂夫矿车:重生" }, + { "source": "Modular Routers", "target": "模块化路由器" }, + { "source": "Pretty Pipes", "target": "简易管道" }, + { "source": "Pipez", "target": "Pipez" }, + { "source": "LaserIO", "target": "LaserIO" }, + { "source": "Super Factory Manager", "target": "超级工厂管理器" }, + { "source": "Integrated Terminals", "target": "集成终端" }, + { "source": "Integrated Scripting", "target": "集成脚本" }, + { "source": "RFTools Base", "target": "RF工具:基础" }, + { "source": "RFTools Builder", "target": "RF工具:建造机" }, + { "source": "RFTools Utility", "target": "RF工具:实用设备" }, + { "source": "RFTools Storage", "target": "RF工具:存储" }, + { "source": "RFTools Power", "target": "RF工具:能量" }, + { "source": "CC: Tweaked", "target": "CC: Tweaked" }, + { "source": "ComputerCraft", "target": "电脑" }, + { "source": "OpenComputers", "target": "开放式电脑" }, + { "source": "Advanced Peripherals", "target": "高级外设" }, + { "source": "Flux Networks", "target": "通量网络" }, + { "source": "Solar Flux Reborn", "target": "太阳能通量:重生" }, + { "source": "Environmental Tech", "target": "环境科技" }, + { "source": "Extra Utilities 2", "target": "更多实用设备 2" }, + { "source": "Dark Utilities", "target": "黑暗实用设备" }, + { "source": "Cyclic", "target": "循环" }, + { "source": "Mob Grinding Utils", "target": "刷怪塔实用设备" }, + { "source": "Hostile Neural Networks", "target": "敌对神经网络" }, + { "source": "Mekanism Generators", "target": "通用机械:发电机" }, + { "source": "Mekanism Tools", "target": "通用机械:工具" }, + { "source": "Applied Mekanistics", "target": "应用能源:通用机械附属" }, + { "source": "Advanced AE", "target": "高级应用能源" }, + { "source": "ExtendedAE", "target": "应用能源 2 扩展" }, + { "source": "MEGA Cells", "target": "MEGA 存储元件" }, + { "source": "AE2 Wireless Terminals", "target": "AE2 无线终端" }, + { "source": "Refined Storage Addons", "target": "精致存储附加" }, + { "source": "Extra Disks", "target": "更多磁盘" }, + { "source": "ExtraStorage", "target": "更多存储" }, + { "source": "Simple Storage Network", "target": "简易存储网络" }, + { "source": "Tom's Simple Storage Mod", "target": "汤姆的简易存储" }, + { "source": "Colossal Chests", "target": "巨型箱子" }, + { "source": "Ender Storage", "target": "末影存储" }, + { "source": "EnderStorage", "target": "末影存储" }, + { "source": "Dank Storage", "target": "Dank 存储" }, + { "source": "DimStorage", "target": "维度存储" }, + { "source": "Quantum Storage", "target": "量子存储" }, + { "source": "Iron Furnaces", "target": "更多熔炉" }, + { "source": "Lootr", "target": "Lootr" }, + { "source": "Tinkers' Construct", "target": "匠魂" }, + { "source": "Tinkers Construct", "target": "匠魂" }, + { "source": "Tinkers' Tool Leveling", "target": "匠魂工具升级" }, + { "source": "Silent Gear", "target": "寂静装备" }, + { "source": "Silent's Gems", "target": "寂静宝石" }, + { "source": "Construct's Armory", "target": "匠魂盔甲" }, + { "source": "Tetra", "target": "Tetra" }, + { "source": "Tool Belt", "target": "工具皮带" }, + { "source": "Mystical Agriculture", "target": "神秘农业" }, + { "source": "Mystical Agradditions", "target": "神秘农业扩展" }, + { "source": "Productive Bees", "target": "资源蜜蜂" }, + { "source": "Resourceful Bees", "target": "资源蜜蜂" }, + { "source": "Botany Pots", "target": "植物盆栽" }, + { "source": "Botany Trees", "target": "植树盆栽" }, + { "source": "Ex Nihilo: Sequentia", "target": "无中生有:继承" }, + { "source": "Ex Nihilo Creatio", "target": "无中生有:创造" }, + { "source": "Ex Compressum", "target": "无中生有:压缩" }, + { "source": "Sky Resources 2", "target": "天空资源 2" }, + { "source": "Compact Machines", "target": "紧凑型机械" }, + { "source": "Compact Crafting", "target": "紧凑型合成" }, + { "source": "Deep Mob Learning", "target": "深度怪物学习" }, + { "source": "Woot", "target": "Woot" }, + { "source": "Chickens", "target": "鸡" }, + { "source": "Roost", "target": "鸡窝" }, + { "source": "Hatchery", "target": "孵化场" }, + { "source": "Resource Hogs", "target": "资源猪" }, + { "source": "Alchemistry", "target": "炼金化学" }, + { "source": "ChemLib", "target": "化学库" }, + { "source": "Thaumcraft", "target": "神秘时代" }, + { "source": "Thaumic Energistics", "target": "神秘能源" }, + { "source": "Astral Sorcery", "target": "星辉魔法" }, + { "source": "MythicBotany", "target": "神话植物学" }, + { "source": "Ars Elemental", "target": "元素魔艺" }, + { "source": "Ars Creo", "target": "Ars Creo" }, + { "source": "Iron's Spells 'n Spellbooks", "target": "Iron 的法术与魔法书" }, + { "source": "Nature's Aura", "target": "自然灵气" }, + { "source": "Forbidden and Arcanus", "target": "禁忌与奥秘" }, + { "source": "ElementalCraft", "target": "元素工艺" }, + { "source": "EvilCraft", "target": "邪恶工艺" }, + { "source": "Mahou Tsukai", "target": "魔法使" }, + { "source": "Hexerei", "target": "巫术学" }, + { "source": "Hex Casting", "target": "咒法学" }, + { "source": "Mana and Artifice", "target": "魔力与艺术" }, + { "source": "Psi", "target": "Psi" }, + { "source": "Roots Classic", "target": "根源魔法经典版" }, + { "source": "Reliquary Reincarnations", "target": "圣遗物" }, + { "source": "Reliquary", "target": "圣遗物" }, + { "source": "Relics", "target": "遗物" }, + { "source": "Theurgy", "target": "Theurgy" }, + { "source": "Eidolon: Repraised", "target": "Eidolon: Repraised" }, + { "source": "Witchery", "target": "巫术" }, + { "source": "Electroblob's Wizardry", "target": "Electroblob 的巫术" }, + { "source": "Embers Rekindled", "target": "余烬复兴" }, + { "source": "Avaritia", "target": "无尽贪婪" }, + { "source": "Extended Crafting", "target": "扩展合成" }, + { "source": "Equivalent Exchange 2", "target": "等价交换 2" }, + { "source": "The Aether", "target": "天境" }, + { "source": "Blue Skies", "target": "蔚蓝浩空" }, + { "source": "The Bumblezone", "target": "蜜蜂领域" }, + { "source": "The Undergarden", "target": "深暗之园" }, + { "source": "Deeper and Darker", "target": "幽邃黑暗" }, + { "source": "The Betweenlands", "target": "交错次元" }, + { "source": "Atum 2: Return to the Sands", "target": "阿图姆 2:重返沙漠" }, + { "source": "The Erebus", "target": "混沌之地" }, + { "source": "Tropicraft", "target": "热带工艺" }, + { "source": "DivineRPG", "target": "神圣RPG" }, + { "source": "Advent of Ascension", "target": "虚无世界" }, + { "source": "Ice and Fire: Dragons", "target": "冰火传说" }, + { "source": "Ice and Fire", "target": "冰火传说" }, + { "source": "L_Ender's Cataclysm", "target": "灾变" }, + { "source": "Alex's Mobs", "target": "Alex 的生物" }, + { "source": "Mowzie's Mobs", "target": "Mowzie 的生物" }, + { "source": "Lycanites Mobs", "target": "恐怖生物" }, + { "source": "Mutant Beasts", "target": "突变生物" }, + { "source": "Enderman Overhaul", "target": "末影人革新" }, + { "source": "Creeper Overhaul", "target": "苦力怕革新" }, + { "source": "Aquamirae", "target": "Aquamirae" }, + { "source": "Bosses of Mass Destruction", "target": "祸乱鬼魅" }, + { "source": "When Dungeons Arise", "target": "地牢浮现之时" }, + { "source": "Dungeon Crawl", "target": "Dungeon Crawl" }, + { "source": "Roguelike Dungeons", "target": "类Rogue地牢" }, + { "source": "YUNG's Better Dungeons", "target": "YUNG 的地牢优化" }, + { "source": "YUNG's Better Mineshafts", "target": "YUNG 的矿井优化" }, + { "source": "YUNG's Better Strongholds", "target": "YUNG 的要塞优化" }, + { "source": "YUNG's Better Desert Temples", "target": "YUNG 的沙漠神殿优化" }, + { "source": "YUNG's Better Ocean Monuments", "target": "YUNG 的海底神殿优化" }, + { "source": "YUNG's Better Nether Fortresses", "target": "YUNG 的下界要塞优化" }, + { "source": "YUNG's Better Witch Huts", "target": "YUNG 的沼泽小屋优化" }, + { "source": "Terralith", "target": "Terralith" }, + { "source": "Regions Unexplored", "target": "未至之地" }, + { "source": "Biomes O' Plenty", "target": "超多生物群系" }, + { "source": "Oh The Biomes We've Gone", "target": "我们走过的生物群系" }, + { "source": "Oh The Biomes You'll Go", "target": "你将去的生物群系" }, + { "source": "BetterNether", "target": "更好的下界" }, + { "source": "BetterEnd", "target": "更好的末地" }, + { "source": "Nullscape", "target": "空无之景" }, + { "source": "Incendium", "target": "Incendium" }, + { "source": "Farmer's Delight", "target": "农夫乐事" }, + { "source": "Pam's HarvestCraft 2", "target": "潘马斯农场 2" }, + { "source": "Pam's HarvestCraft", "target": "潘马斯农场" }, + { "source": "Cooking for Blockheads", "target": "懒人厨房" }, + { "source": "Aquaculture 2", "target": "水产业 2" }, + { "source": "Serene Seasons", "target": "静谧四季" }, + { "source": "Spice of Life: Carrot Edition", "target": "生活调味料:胡萝卜版" }, + { "source": "AppleSkin", "target": "苹果皮" }, + { "source": "Croptopia", "target": "作物盛景" }, + { "source": "Brewin' and Chewin'", "target": "饮酒作乐" }, + { "source": "Farming for Blockheads", "target": "农场贸易" }, + { "source": "MineColonies", "target": "模拟殖民地" }, + { "source": "Structurize", "target": "结构化" }, + { "source": "Domum Ornamentum", "target": "家园装饰" }, + { "source": "Quark", "target": "夸克" }, + { "source": "Supplementaries", "target": "锦致装饰" }, + { "source": "Chisel", "target": "凿子" }, + { "source": "Rechiseled", "target": "重凿" }, + { "source": "FramedBlocks", "target": "框架方块" }, + { "source": "Handcrafted", "target": "精巧手艺" }, + { "source": "Macaw's Furniture", "target": "Macaw 的家具" }, + { "source": "Macaw's Roofs", "target": "Macaw 的屋顶" }, + { "source": "Macaw's Doors", "target": "Macaw 的门" }, + { "source": "MrCrayfish's Furniture Mod", "target": "MrCrayfish 的家具" }, + { "source": "Just Enough Items", "target": "JEI 物品管理器" }, + { "source": "Roughly Enough Items", "target": "REI 物品管理器" }, + { "source": "Jade", "target": "玉" }, + { "source": "WTHIT", "target": "WTHIT" }, + { "source": "The One Probe", "target": "TOP 信息显示" }, + { "source": "JourneyMap", "target": "旅行地图" }, + { "source": "Xaero's Minimap", "target": "Xaero 的小地图" }, + { "source": "Xaero's World Map", "target": "Xaero 的世界地图" }, + { "source": "Waystones", "target": "传送石碑" }, + { "source": "Nature's Compass", "target": "自然罗盘" }, + { "source": "Explorer's Compass", "target": "探险者指南针" }, + { "source": "Mouse Tweaks", "target": "鼠标手势" }, + { "source": "Inventory Tweaks", "target": "R键整理" }, + { "source": "Inventory Tweaks Renewed", "target": "R键整理:重制" }, + { "source": "Controlling mod", "target": "键位冲突显示模组" }, + { "source": "Polymorph mod", "target": "多态合成模组" }, + { "source": "Corail Tombstone", "target": "Corail 的墓碑" }, + { "source": "Corail's Tombstone", "target": "Corail 的墓碑" }, + { "source": "Gravestone Mod", "target": "墓碑" }, + { "source": "FTB Ultimine", "target": "FTB 连锁破坏" }, + { "source": "VeinMiner", "target": "矿脉矿工" }, + { "source": "Ore Excavation", "target": "矿脉挖掘" }, + { "source": "Building Gadgets", "target": "建筑小帮手" }, + { "source": "Mining Gadgets", "target": "采矿小工具" }, + { "source": "Construction Wand", "target": "建筑权杖" }, + { "source": "Carry On mod", "target": "搬运模组" }, + { "source": "Akashic Tome", "target": "阿卡什宝典" }, + { "source": "Patchouli", "target": "帕秋莉手册" }, + { "source": "Curios API", "target": "Curios 饰品栏" }, + { "source": "Artifacts mod", "target": "奇异饰品模组" }, + { "source": "Baubley Heart Canisters", "target": "心之容器" }, + { "source": "Comforts mod", "target": "舒适用品模组" }, + { "source": "Torchmaster", "target": "火炬大师" }, + { "source": "SecurityCraft", "target": "安全工艺" }, + { "source": "Easy Villagers", "target": "简单村民" }, + { "source": "OpenBlocks Elevator", "target": "开放式电梯" }, + { "source": "FerriteCore", "target": "铁氧体磁芯" }, + { "source": "ModernFix", "target": "ModernFix" }, + { "source": "ImmediatelyFast", "target": "ImmediatelyFast" }, + { "source": "Iris Shaders", "target": "Iris 光影" }, + { "source": "OptiFine", "target": "OptiFine" }, + { "source": "Not Enough Items", "target": "NEI 物品管理器" }, + { "source": "CraftGuide", "target": "合成指南" }, + { "source": "Hwyla", "target": "Hwyla" }, + { "source": "Better Questing", "target": "更好的任务" }, + { "source": "Hardcore Questing Mode", "target": "极限任务模式" }, + { "source": "CraftTweaker", "target": "合成魔改器" }, + { "source": "ContentTweaker", "target": "内容魔改器" }, + { "source": "ModTweaker", "target": "模组魔改器" }, + { "source": "GameStages", "target": "游戏阶段" }, + { "source": "Forestry", "target": "林业" }, + { "source": "Gendustry", "target": "基因工业" }, + { "source": "Extra Bees", "target": "更多蜜蜂" }, + { "source": "Magic Bees", "target": "魔法蜜蜂" }, + { "source": "Binnie's Mods", "target": "Binnie 的模组" }, + { "source": "MineFactory Reloaded", "target": "我的工厂 2" }, + { "source": "Thermal Dynamics", "target": "热力动力" }, + { "source": "Thermal Cultivation", "target": "热力农业" }, + { "source": "Big Reactors", "target": "大型反应堆" }, + { "source": "RotaryCraft", "target": "旋转工艺" }, + { "source": "ReactorCraft", "target": "反应堆工艺" }, + { "source": "ChromatiCraft", "target": "彩色工艺" }, + { "source": "AbyssalCraft", "target": "深渊国度" }, + { "source": "Ancient Warfare 2", "target": "古代战争 2" }, + { "source": "Millenaire", "target": "千年村庄" }, + { "source": "Millénaire", "target": "千年村庄" }, + { "source": "Mo' Creatures", "target": "更多生物" }, + { "source": "Better Combat", "target": "更好的战斗" }, + { "source": "Simply Swords", "target": "简易刀剑" }, + { "source": "Spell Engine", "target": "法术引擎" }, + { "source": "Origins mod", "target": "起源模组" }, + { "source": "Trinkets", "target": "Trinkets 饰品栏" }, + { "source": "Spectrum mod", "target": "Spectrum 模组" }, + { "source": "Cobblemon", "target": "Cobblemon" }, + { "source": "Pixelmon", "target": "Pixelmon" }, + { "source": "Tough As Nails", "target": "意志坚定" }, + { "source": "First Aid", "target": "急救" }, + { "source": "Reskillable", "target": "技能升级" }, + { "source": "Quest Book", "target": "任务书" }, + { "source": "Quest Line", "target": "任务线" }, + { "source": "Optional Quest", "target": "可选任务" }, + { "source": "Repeatable Quest", "target": "可重复任务" }, + { "source": "Prerequisite Quest", "target": "前置任务" }, + { "source": "Claim Reward", "target": "领取奖励" }, + { "source": "Multiblock Structure", "target": "多方块结构" }, + { "source": "Machine Casing", "target": "机器外壳" }, + { "source": "Machine Frame", "target": "机器框架" }, + { "source": "Controller Block", "target": "控制器方块" }, + { "source": "Input Hatch", "target": "输入仓" }, + { "source": "Output Hatch", "target": "输出仓" }, + { "source": "Energy Port", "target": "能量端口" }, + { "source": "Maintenance Hatch", "target": "维护仓" }, + { "source": "Fluid Input", "target": "流体输入" }, + { "source": "Fluid Output", "target": "流体输出" }, + { "source": "Item Input", "target": "物品输入" }, + { "source": "Item Output", "target": "物品输出" }, + { "source": "Auto Input", "target": "自动输入" }, + { "source": "Auto Output", "target": "自动输出" }, + { "source": "Auto Eject", "target": "自动弹出" }, + { "source": "Side Configuration", "target": "侧面配置" }, + { "source": "Redstone Control", "target": "红石控制" }, + { "source": "Always Active", "target": "始终活动" }, + { "source": "Active with Signal", "target": "有信号时活动" }, + { "source": "Active without Signal", "target": "无信号时活动" }, + { "source": "Energy Capacity", "target": "能量容量" }, + { "source": "Energy Consumption", "target": "能量消耗" }, + { "source": "Energy Generation", "target": "能量生产" }, + { "source": "Energy Buffer", "target": "能量缓冲区" }, + { "source": "Transfer Rate", "target": "传输速率" }, + { "source": "Maximum Input", "target": "最大输入" }, + { "source": "Maximum Output", "target": "最大输出" }, + { "source": "Energy Cable", "target": "能量线缆" }, + { "source": "Fluid Pipe", "target": "流体管道" }, + { "source": "Item Pipe", "target": "物品管道" }, + { "source": "Gas Tube", "target": "气体管道" }, + { "source": "Universal Cable", "target": "通用线缆" }, + { "source": "Mechanical Pipe", "target": "机械管道" }, + { "source": "Pressurized Tube", "target": "加压管道" }, + { "source": "Logistical Transporter", "target": "物流运输器" }, + { "source": "Thermodynamic Conductor", "target": "热力导线" }, + { "source": "Ore Doubling", "target": "矿物二倍产线" }, + { "source": "Ore Tripling", "target": "矿物三倍产线" }, + { "source": "Ore Quadrupling", "target": "矿物四倍产线" }, + { "source": "Ore Quintupling", "target": "矿物五倍产线" }, + { "source": "Ore Processing Chain", "target": "矿物处理产线" }, + { "source": "Crushed Ore", "target": "粉碎矿石" }, + { "source": "Purified Ore", "target": "纯净矿石" }, + { "source": "Ore Slurry", "target": "矿石浆液" }, + { "source": "Clean Ore Slurry", "target": "清洁矿石浆液" }, + { "source": "Dirty Ore Slurry", "target": "污浊矿石浆液" }, + { "source": "Speed Upgrade", "target": "速度升级" }, + { "source": "Energy Upgrade", "target": "能量升级" }, + { "source": "Range Upgrade", "target": "范围升级" }, + { "source": "Stack Upgrade", "target": "堆叠升级" }, + { "source": "Void Upgrade", "target": "虚空升级" }, + { "source": "Filter Upgrade", "target": "过滤升级" }, + { "source": "Whitelist Mode", "target": "白名单模式" }, + { "source": "Blacklist Mode", "target": "黑名单模式" }, + { "source": "Round Robin", "target": "循环分配" }, + { "source": "Working Area", "target": "工作区域" }, + { "source": "Storage Network", "target": "存储网络" }, + { "source": "Wireless Grid", "target": "无线网格" }, + { "source": "Wireless Crafting Grid", "target": "无线合成网格" }, + { "source": "Network Transmitter", "target": "网络发射器" }, + { "source": "Network Receiver", "target": "网络接收器" }, + { "source": "Network Card", "target": "网络卡" }, + { "source": "Storage Disk", "target": "存储磁盘" }, + { "source": "Storage Cell", "target": "存储元件" }, + { "source": "Cell Workbench", "target": "元件工作台" }, + { "source": "Crafting CPU", "target": "合成 CPU" }, + { "source": "Crafting Storage", "target": "合成存储器" }, + { "source": "Crafting Co-Processing Unit", "target": "合成协处理单元" }, + { "source": "Pattern Encoding Terminal", "target": "样板编码终端" }, + { "source": "Blank Pattern", "target": "空白样板" }, + { "source": "Crafting Pattern", "target": "合成样板" }, + { "source": "Processing Pattern", "target": "处理样板" }, + { "source": "Autocrafting", "target": "自动合成" }, + { "source": "Auto-Crafting", "target": "自动合成" }, + { "source": "Subnetwork", "target": "子网络" }, + { "source": "P2P Tunnel", "target": "点对点通道" }, + { "source": "Mechanical Press", "target": "动力冲压机" }, + { "source": "Mechanical Mixer", "target": "动力搅拌器" }, + { "source": "Crushing Wheels", "target": "粉碎轮" }, + { "source": "Encased Fan", "target": "鼓风机" }, + { "source": "Blaze Burner", "target": "烈焰人燃烧室" }, + { "source": "Sequenced Assembly", "target": "序列组装" }, + { "source": "Kinetic Stress", "target": "动力应力" }, + { "source": "Stress Units", "target": "应力单位" }, + { "source": "Rotational Speed", "target": "转速" }, + { "source": "Large Cogwheel", "target": "大齿轮" }, + { "source": "Mechanical Belt", "target": "传送带" }, + { "source": "Mechanical Saw", "target": "动力锯" }, + { "source": "Mechanical Drill", "target": "动力钻头" }, + { "source": "Item Vault", "target": "物品保险库" }, + { "source": "Engineer's Hammer", "target": "工程师锤" }, + { "source": "Crescent Hammer", "target": "月牙锤" }, + { "source": "Yeta Wrench", "target": "Yeta 扳手" }, + { "source": "Wire Connector", "target": "线缆连接器" }, + { "source": "Wire Relay", "target": "线缆中继器" }, + { "source": "LV Capacitor", "target": "低压电容器" }, + { "source": "MV Capacitor", "target": "中压电容器" }, + { "source": "HV Capacitor", "target": "高压电容器" }, + { "source": "Coke Oven", "target": "焦炉" }, + { "source": "Blast Brick", "target": "高炉砖" }, + { "source": "Metal Press", "target": "金属冲压机" }, + { "source": "Crusher Multiblock", "target": "破碎机多方块结构" }, + { "source": "Arc Furnace", "target": "电弧炉" }, + { "source": "Excavator Multiblock", "target": "斗轮式挖掘机多方块结构" }, + { "source": "Mana Pool", "target": "魔力池" }, + { "source": "Mana Spreader", "target": "魔力发射器" }, + { "source": "Runic Altar", "target": "符文祭坛" }, + { "source": "Petal Apothecary", "target": "花药台" }, + { "source": "Pure Daisy", "target": "白雏菊" }, + { "source": "Livingrock", "target": "活石" }, + { "source": "Livingwood", "target": "活木" }, + { "source": "Terrasteel", "target": "泰拉钢" }, + { "source": "Blood Altar", "target": "鲜血祭坛" }, + { "source": "Blood Orb", "target": "血之宝珠" }, + { "source": "Source Jar", "target": "魔源罐" }, + { "source": "Enchanting Apparatus", "target": "附魔装置" }, + { "source": "Arcane Pedestal", "target": "奥术基座" }, + { "source": "Smeltery Controller", "target": "冶炼炉控制器" }, + { "source": "Seared Bricks", "target": "焦黑砖" }, + { "source": "Seared Tank", "target": "焦黑储罐" }, + { "source": "Casting Table", "target": "浇铸台" }, + { "source": "Casting Basin", "target": "浇铸盆" }, + { "source": "Part Builder", "target": "部件加工台" }, + { "source": "Tinker's Station", "target": "工匠站" }, + { "source": "Tool Station", "target": "工具装配台" }, + { "source": "Tool Forge", "target": "工具锻造台" }, + { "source": "Molten Metal", "target": "熔融金属" }, + { "source": "Modifier Slot", "target": "强化槽" }, + { "source": "Ability Slot", "target": "能力槽" }, + { "source": "Garden Cloche", "target": "园艺玻璃罩" }, + { "source": "Garden Cloches", "target": "园艺玻璃罩" }, + { "source": "Overworld", "target": "主世界" }, + { "source": "Nether", "target": "下界" }, + { "source": "Ender Dragon", "target": "末影龙" }, + { "source": "Wither Skeleton", "target": "凋灵骷髅" }, + { "source": "Wither Storm", "target": "凋灵风暴" }, + { "source": "Wither", "target": "凋灵" }, + { "source": "Enderman", "target": "末影人" }, + { "source": "Creeper", "target": "苦力怕" }, + { "source": "Zombie Villager", "target": "僵尸村民" }, + { "source": "Zombie", "target": "僵尸" }, + { "source": "Skeleton", "target": "骷髅" }, + { "source": "Villager", "target": "村民" }, + { "source": "Piglin Brute", "target": "猪灵蛮兵" }, + { "source": "Piglin", "target": "猪灵" }, + { "source": "Hoglin", "target": "疣猪兽" }, + { "source": "Warden", "target": "监守者" }, + { "source": "Ancient City", "target": "远古城市" }, + { "source": "Trial Chamber", "target": "试炼密室" }, + { "source": "Crafting Table", "target": "工作台" }, + { "source": "Enchanting Table", "target": "附魔台" }, + { "source": "Smithing Table", "target": "锻造台" }, + { "source": "Fletching Table", "target": "制箭台" }, + { "source": "Cartography Table", "target": "制图台" }, + { "source": "Stonecutter", "target": "切石机" }, + { "source": "Grindstone", "target": "砂轮" }, + { "source": "Anvil", "target": "铁砧" }, + { "source": "Blast Furnace", "target": "高炉" }, + { "source": "Smoker", "target": "烟熏炉" }, + { "source": "Furnace", "target": "熔炉" }, + { "source": "Ancient Debris", "target": "远古残骸" }, + { "source": "Netherite Ingot", "target": "下界合金锭" }, + { "source": "Netherite Scrap", "target": "下界合金碎片" }, + { "source": "Netherite", "target": "下界合金" }, + { "source": "Redstone Torch", "target": "红石火把" }, + { "source": "Redstone Comparator", "target": "红石比较器" }, + { "source": "Redstone Repeater", "target": "红石中继器" }, + { "source": "Redstone", "target": "红石" }, + { "source": "Glowstone", "target": "萤石" }, + { "source": "Cobblestone", "target": "圆石" }, + { "source": "Cobbled Deepslate", "target": "深板岩圆石" }, + { "source": "Deepslate", "target": "深板岩" }, + { "source": "Netherrack", "target": "下界岩" }, + { "source": "End Stone", "target": "末地石" }, + { "source": "Soul Sand", "target": "灵魂沙" }, + { "source": "Soul Soil", "target": "灵魂土" }, + { "source": "Obsidian", "target": "黑曜石" }, + { "source": "Crying Obsidian", "target": "哭泣的黑曜石" }, + { "source": "Amethyst Shard", "target": "紫水晶碎片" }, + { "source": "Echo Shard", "target": "回响碎片" }, + { "source": "Totem of Undying", "target": "不死图腾" }, + { "source": "Elytra", "target": "鞘翅" }, + { "source": "Shulker Box", "target": "潜影盒" }, + { "source": "Ender Chest", "target": "末影箱" }, + { "source": "Ender Pearl", "target": "末影珍珠" }, + { "source": "Eye of Ender", "target": "末影之眼" }, + { "source": "Experience Orb", "target": "经验球" }, + { "source": "Experience Bottle", "target": "附魔之瓶" }, + { "source": "Raw Ore", "target": "粗矿" }, + { "source": "Ore Processing", "target": "矿石处理" }, + { "source": "Silk Touch", "target": "精准采集" }, + { "source": "Fortune", "target": "时运" }, + { "source": "Looting", "target": "抢夺" }, + { "source": "Unbreaking", "target": "耐久" }, + { "source": "Mending", "target": "经验修补" }, + { "source": "Efficiency", "target": "效率" }, + { "source": "Enchanting", "target": "附魔" }, + { "source": "Enchantment", "target": "魔咒" }, + { "source": "Advancement", "target": "进度" }, + { "source": "Items", "target": "物品" }, + { "source": "Item", "target": "物品" }, + { "source": "Mobs", "target": "生物" }, + { "source": "Mob", "target": "生物" }, + { "source": "Chunks", "target": "区块" }, + { "source": "Chunk", "target": "区块" }, + { "source": "Game Ticks", "target": "游戏刻" }, + { "source": "Game Tick", "target": "游戏刻" }, + { "source": "Ticks", "target": "游戏刻" }, + { "source": "Tick", "target": "游戏刻" }, + { "source": "Blocks Away", "target": "格" }, + { "source": "Block Interaction", "target": "方块交互" }, + { "source": "Block Entity", "target": "方块实体" }, + { "source": "Power Generation", "target": "能源生产" }, + { "source": "Power Storage", "target": "能量存储" }, + { "source": "Power Input", "target": "能量输入" }, + { "source": "Power Output", "target": "能量输出" }, + { "source": "Crafting", "target": "合成" }, + { "source": "Crafted", "target": "合成" }, + { "source": "Craft", "target": "合成" }, + { "source": "Recipe", "target": "配方" }, + { "source": "Crafting Grid", "target": "合成网格" }, + { "source": "Crafting Terminal", "target": "合成终端" }, + { "source": "Pattern Provider", "target": "样板供应器" }, + { "source": "Molecular Assembler", "target": "分子装配室" }, + { "source": "ME Controller", "target": "ME 控制器" }, + { "source": "ME Drive", "target": "ME 驱动器" }, + { "source": "ME Interface", "target": "ME 接口" }, + { "source": "Storage Bus", "target": "存储总线" }, + { "source": "Import Bus", "target": "输入总线" }, + { "source": "Export Bus", "target": "输出总线" }, + { "source": "Fluix Crystal", "target": "福鲁伊克斯水晶" }, + { "source": "Certus Quartz", "target": "赛特斯石英" }, + { "source": "Digital Miner", "target": "数字型采矿机" }, + { "source": "Metallurgic Infuser", "target": "冶金灌注机" }, + { "source": "Enrichment Chamber", "target": "富集仓" }, + { "source": "Osmium Compressor", "target": "锇压缩机" }, + { "source": "Electrolytic Separator", "target": "电解分离器" }, + { "source": "Chemical Infuser", "target": "化学灌注机" }, + { "source": "Purification Chamber", "target": "净化仓" }, + { "source": "Chemical Injection Chamber", "target": "化学压射室" }, + { "source": "Precision Sawmill", "target": "精密锯木机" }, + { "source": "Formulaic Assemblicator", "target": "公式化装配机" }, + { "source": "Atomic Disassembler", "target": "原子分解机" }, + { "source": "Induction Matrix", "target": "感应矩阵" }, + { "source": "Fusion Reactor", "target": "聚变反应堆" }, + { "source": "Fission Reactor", "target": "裂变反应堆" }, + { "source": "Supercritical Phase Shifter", "target": "超临界移相器" }, + { "source": "Supercharged Coil", "target": "超充线圈" }, + { "source": "SPS Port", "target": "SPS 端口" }, + { "source": "Antimatter Pellet", "target": "反物质颗粒" }, + { "source": "Nuclear Waste", "target": "核废料" }, + { "source": "Antimatter", "target": "反物质" }, + { "source": "Polonium", "target": "钋" }, + { "source": "Plutonium", "target": "钚" }, + { "source": "Uranium", "target": "铀" }, + { "source": "Electrum", "target": "琥珀金" }, + { "source": "Experience Obelisk", "target": "经验方尖碑" }, + { "source": "Mob Slaughter Factory", "target": "生物屠宰工厂" }, + { "source": "Matter Replication", "target": "物质复制" }, + { "source": "Constructors", "target": "构造器" }, + { "source": "Constructor", "target": "构造器" }, + { "source": "Red Katar", "target": "红物质拳剑" }, + { "source": "Tome of Scrapping", "target": "拆解之书" }, + { "source": "Spell Bullet", "target": "法术子弹" }, + { "source": "Brush", "target": "刷子" }, + { "source": "Echo of Guidance", "target": "指引之回响" }, + { "source": "Free Runners", "target": "自由跑者" }, + { "source": "Warp Scroll", "target": "传送卷轴" }, + { "source": "Capacitor Bank", "target": "电容库" }, + { "source": "Energy Cell", "target": "能量单元" }, + { "source": "Flux Network", "target": "通量网络" }, + { "source": "Energy Storage", "target": "能量存储" }, + { "source": "Wireless Transmitter", "target": "无线发射器" }, + { "source": "Wireless Receiver", "target": "无线接收器" }, + { "source": "Chunk Loading", "target": "区块加载" }, + { "source": "Chunk Claim", "target": "区块认领" }, + { "source": "Right-click", "target": "右键单击" }, + { "source": "Right Click", "target": "右键单击" }, + { "source": "Left-click", "target": "左键单击" }, + { "source": "Left Click", "target": "左键单击" }, + { "source": "Shift-right-click", "target": "潜行并右键单击" }, + { "source": "Sneak + Right-click", "target": "潜行并右键单击" }, + { "source": "Sneak", "target": "潜行" }, + { "source": "Keybind", "target": "按键绑定" }, + { "source": "Cooldown", "target": "冷却时间" }, + { "source": "Durability", "target": "耐久度" }, + { "source": "Fall Damage", "target": "摔落伤害" }, + { "source": "Area of Effect", "target": "作用范围" }, + { "source": "Redstone Flux", "target": "红石通量" }, + { "source": "Forge Energy", "target": "Forge 能量" }, + { "source": "FE/t", "target": "FE/t" }, + { "source": "RF/t", "target": "RF/t" }, + { "source": "FE", "target": "FE", "case_sensitive": true }, + { "source": "RF", "target": "RF", "case_sensitive": true }, + { "source": "NBT", "target": "NBT", "case_sensitive": true }, + { "source": "JSON", "target": "JSON", "case_sensitive": true }, + { "source": "GUI", "target": "界面", "case_sensitive": true }, + { "source": "EMC", "target": "EMC", "case_sensitive": true }, + { "source": "CAD", "target": "CAD", "case_sensitive": true }, + { "source": "QIO", "target": "QIO", "case_sensitive": true }, + { "source": "Vanilla Minecraft", "target": "原版 Minecraft" }, + { "source": "Vanilla", "target": "原版" }, + { "source": "Minecraft", "target": "Minecraft" } + ] +} diff --git a/src-tauri/src/chapters.rs b/src-tauri/src/chapters.rs new file mode 100644 index 0000000..e37dafe --- /dev/null +++ b/src-tauri/src/chapters.rs @@ -0,0 +1,136 @@ +use regex::Regex; +use serde::{Deserialize, Serialize}; +use std::{ + fs, + path::{Path, PathBuf}, +}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Segment { + pub path: PathBuf, + pub key: String, + pub source: String, + pub start: usize, + pub end: usize, + pub quote: char, + pub index: usize, + pub cache_id: String, +} +pub fn files(q: &Path) -> Vec { + let mut v = fs::read_dir(q.join("chapters")) + .ok() + .into_iter() + .flatten() + .flatten() + .map(|e| e.path()) + .filter(|p| p.extension().is_some_and(|x| x == "snbt")) + .collect::>(); + v.sort(); + v +} +fn decode(s: &str) -> String { + let mut o = String::new(); + let mut it = s.chars(); + while let Some(c) = it.next() { + if c == '\\' { + o.push(match it.next().unwrap_or('\\') { + 'n' => '\n', + 'r' => '\r', + 't' => '\t', + x => x, + }) + } else { + o.push(c) + } + } + o +} +fn quote(s: &str, q: char) -> String { + format!( + "{q}{}{q}", + s.replace('\\', "\\\\") + .replace(q, &format!("\\{q}")) + .replace('\n', "\\n") + .replace('\r', "\\r") + .replace('\t', "\\t") + ) +} +pub fn extract(path: &Path) -> Result, String> { + let text = fs::read_to_string(path).map_err(|e| e.to_string())?; + let re=Regex::new(r#"(?s)(?:\b(title|subtitle|description|text|name)|["'](title|subtitle|description|text|name)["'])\s*:\s*(\[[^\]]*]|"(?:\\.|[^"])*"|'(?:\\.|[^'])*')"#).unwrap(); + let strings = Regex::new(r#""((?:\\.|[^"])*)"|'((?:\\.|[^'])*)'"#).unwrap(); + let mut out = vec![]; + for cap in re.captures_iter(&text) { + let match_start = cap.get(0).unwrap().start(); + let line_start = text[..match_start].rfind('\n').map_or(0, |i| i + 1); + let prefix = &text[line_start..match_start]; + if prefix.trim_start().starts_with('#') || prefix.contains("//") { + continue; + } + let key = cap.get(1).or(cap.get(2)).unwrap().as_str(); + let value = cap.get(3).unwrap(); + for sc in strings.captures_iter(value.as_str()) { + let m = sc.get(0).unwrap(); + let raw = sc.get(1).or(sc.get(2)).unwrap().as_str(); + let source = decode(raw); + if !source.chars().any(|c| c.is_ascii_alphabetic()) { + continue; + } + let start = value.start() + m.start(); + let idx = out.len(); + out.push(Segment { + path: path.to_path_buf(), + key: key.into(), + source, + start, + end: value.start() + m.end(), + quote: m.as_str().chars().next().unwrap(), + index: idx, + cache_id: format!( + "{}:{idx}:{key}", + path.file_name().unwrap().to_string_lossy() + ), + }); + } + } + Ok(out) +} +pub fn replace(path: &Path, replacements: &[(usize, String)]) -> Result { + let mut text = fs::read_to_string(path).map_err(|e| e.to_string())?; + let segs = extract(path)?; + let mut matches = segs + .into_iter() + .filter_map(|s| { + replacements + .iter() + .find(|x| x.0 == s.index) + .map(|x| (s, x.1.clone())) + }) + .collect::>(); + matches.sort_by_key(|x| std::cmp::Reverse(x.0.start)); + for (s, t) in &matches { + text.replace_range(s.start..s.end, "e(t, s.quote)); + } + fs::write(path, text).map_err(|e| e.to_string())?; + Ok(matches.len()) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + #[test] + fn extracts_and_replaces() { + let d = tempdir().unwrap(); + let p = d.path().join("a.snbt"); + fs::write( + &p, + "{\n // title: \"Comment\"\n title: \"Hello\", description: [\"Line one\", \"第二行\"]\n}", + ) + .unwrap(); + let s = extract(&p).unwrap(); + assert_eq!(s.len(), 2); + assert_eq!(replace(&p, &[(0, "你好".into())]).unwrap(), 1); + assert!(fs::read_to_string(p).unwrap().contains("你好")); + } +} diff --git a/src-tauri/src/core.rs b/src-tauri/src/core.rs new file mode 100644 index 0000000..5c31710 --- /dev/null +++ b/src-tauri/src/core.rs @@ -0,0 +1,653 @@ +use crate as crate_root; +use crate::{ + chapters, glossary, providers, + snbt::{self, LangValue}, + storage::{History, Settings}, +}; +use chrono::Local; +use futures::stream::{self, StreamExt}; +use regex::Regex; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use sha2::{Digest, Sha256}; +use std::{ + collections::{BTreeMap, HashMap}, + fs, + path::{Path, PathBuf}, +}; +use tauri::{AppHandle, Emitter}; +use walkdir::WalkDir; + +#[derive(Clone, Debug, Serialize)] +pub struct Scan { + quests_dir: String, + pack_name: String, + mode: String, + mode_label: String, + source: String, + entry_count: usize, + file_count: usize, + estimated_batches: usize, +} +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Report { + source_file: String, + target_file: String, + backup_dir: String, + total_entries: usize, + translated_entries: usize, + cache_hits: usize, + failed_entries: Vec, + warnings: BTreeMap>, + failed_translations: BTreeMap, +} +#[derive(Clone)] +struct Item { + id: String, + source: String, + protected: String, + tokens: Vec<(String, String)>, +} + +fn has_lang(p: &Path) -> bool { + p.join("lang/en_us.snbt").is_file() +} +fn has_chapters(p: &Path) -> bool { + !chapters::files(p).is_empty() +} +pub fn resolve(selected: &Path) -> Result { + let s = selected + .canonicalize() + .map_err(|e| format!("无法打开所选目录:{e}"))?; + let mut candidates = vec![s.clone()]; + if s.file_name() + .is_some_and(|x| x == "lang" || x == "chapters") + { + if let Some(p) = s.parent() { + candidates.push(p.into()) + } + } + for a in s.ancestors() { + if a.file_name().is_some_and(|x| x == "quests") { + candidates.push(a.into()) + } + if a.file_name().is_some_and(|x| x == "ftbquests") { + candidates.push(a.join("quests")) + } + if a.file_name().is_some_and(|x| x == "config") { + candidates.push(a.join("ftbquests/quests")) + } + } + candidates.extend([ + s.join("config/ftbquests/quests"), + s.join("ftbquests/quests"), + s.join("quests"), + ]); + for e in WalkDir::new(&s) + .max_depth(5) + .into_iter() + .filter_map(Result::ok) + .filter(|e| e.file_type().is_dir() && e.file_name() == "quests") + { + candidates.push(e.path().into()) + } + candidates + .into_iter() + .find(|p| has_lang(p) || has_chapters(p)) + .ok_or("没有找到 FTB Quests 的 lang/en_us.snbt 或 chapters/*.snbt。".into()) +} +fn mode(q: &Path) -> Result<&'static str, String> { + if has_lang(q) { + Ok("lang") + } else if has_chapters(q) { + Ok("chapters") + } else { + Err("任务书目录中没有可翻译内容".into()) + } +} +fn pack_name(q: &Path) -> String { + q.ancestors() + .find(|p| p.file_name().is_some_and(|n| n == "config")) + .and_then(Path::parent) + .and_then(Path::file_name) + .map(|x| x.to_string_lossy().into_owned()) + .unwrap_or_else(|| { + q.file_name() + .unwrap_or_default() + .to_string_lossy() + .into_owned() + }) +} +pub fn scan(payload: &Value) -> Result { + let q = resolve(Path::new(payload["path"].as_str().unwrap_or("")))?; + let m = mode(&q)?; + let (files, count, source) = if m == "lang" { + let p = q.join("lang/en_us.snbt"); + (1, snbt::load(&p)?.len(), p) + } else { + let fs = chapters::files(&q); + let count = fs + .iter() + .map(|p| chapters::extract(p).map(|x| x.len())) + .collect::, _>>()? + .iter() + .sum(); + (fs.len(), count, q.join("chapters")) + }; + let bs = parse_auto(payload["batch_size"].as_str().unwrap_or("auto"), 25)?; + Ok(serde_json::to_value(Scan { + quests_dir: q.display().to_string(), + pack_name: pack_name(&q), + mode: m.into(), + mode_label: if m == "lang" { + "语言文件" + } else { + "章节文件" + } + .into(), + source: source.display().to_string(), + entry_count: count, + file_count: files, + estimated_batches: count.div_ceil(bs), + }) + .unwrap()) +} +fn parse_auto(s: &str, default: usize) -> Result { + if s.trim().is_empty() || s.eq_ignore_ascii_case("auto") { + Ok(default) + } else { + s.parse::() + .ok() + .filter(|x| *x > 0) + .ok_or("批大小与并发数必须是 auto 或正整数".into()) + } +} +fn patterns() -> Vec { + [r"(?i)[&§][0-9a-fk-orz]",r"%(?:\d+\$)?[-+# 0,(]*\d*(?:\.\d+)?[bcdeEufFgGosxX]",r"<[^<>\n]+>",r"\{[@A-Za-z][^{}\n]*\}",r"(?i)\b(?:[a-z0-9_.-]+:[a-z0-9_.-]+(?:/[a-z0-9_.-]+)+|(?:assets|config|data|kubejs|models|recipes|textures|ftbquests|chapters|lang|scripts)/[a-z0-9_./-]+|[a-z0-9_.-]+(?:/[a-z0-9_.-]+)+\.[a-z0-9]+)\b",r#"\\[nrt\"'\\]"#,r#"https?://[^\s\"')\]]+"#,r"#[0-9a-fA-F]{6}\b"].iter().map(|x|Regex::new(x).unwrap()).collect() +} +fn protect(text: &str) -> (String, Vec<(String, String)>) { + let mut found = vec![]; + for re in patterns() { + for m in re.find_iter(text) { + found.push((m.start(), m.end(), m.as_str().to_string())) + } + } + found.sort_by_key(|x| x.0); + found.dedup_by(|a, b| a.0 == b.0 && a.1 == b.1); + let mut out = String::new(); + let mut last = 0; + let mut tokens = vec![]; + for (start, end, t) in found { + if start < last { + continue; + } + out.push_str(&text[last..start]); + let ph = format!("⟨P_{}⟩", tokens.len()); + out.push_str(&ph); + tokens.push((ph, t)); + last = end; + } + out.push_str(&text[last..]); + (out, tokens) +} +fn restore(text: &str, tokens: &[(String, String)]) -> String { + tokens + .iter() + .fold(text.to_string(), |s, (p, t)| s.replace(p, t)) +} + +fn protect_for_translation( + text: &str, + glossary: Option<&glossary::Loaded>, +) -> (String, Vec<(String, String)>) { + let (protected, mut tokens) = protect(text); + let Some(glossary) = glossary else { + return (protected, tokens); + }; + let (protected, glossary_tokens) = glossary.protect(&protected); + tokens.extend(glossary_tokens); + (protected, tokens) +} +fn warnings(source: &str, target: &str) -> Vec { + let (_, st) = protect(source); + let (_, tt) = protect(target); + let mut w = vec![]; + for (c, n) in [('\n', "换行"), ('\r', "回车"), ('\t', "制表符")] { + if source.matches(c).count() != target.matches(c).count() { + w.push(format!("{n}数量不一致")) + } + } + let mut sc = st.iter().map(|x| x.1.clone()).collect::>(); + let mut tc = tt.iter().map(|x| x.1.clone()).collect::>(); + sc.sort(); + tc.sort(); + if sc != tc { + w.push("格式码、占位符或资源标识不一致".into()) + } + if let Ok(src) = serde_json::from_str::(source) { + match serde_json::from_str::(target) { + Ok(tgt) => { + if json_shape(&src) != json_shape(&tgt) { + w.push("JSON 文本组件结构发生变化".into()) + } + } + Err(_) => w.push("JSON 文本组件不再是有效 JSON".into()), + } + } + w +} +fn json_shape(v: &Value) -> Value { + match v { + Value::Object(m) => Value::Object( + m.iter() + .map(|(k, v)| { + ( + k.clone(), + if k == "text" && v.is_string() { + Value::String("$text".into()) + } else { + json_shape(v) + }, + ) + }) + .collect(), + ), + Value::Array(a) => Value::Array(a.iter().map(json_shape).collect()), + Value::String(s) => Value::String(s.clone()), + x => x.clone(), + } +} +fn cache_key(source: &str, s: &Settings) -> String { + let mut h = Sha256::new(); + let cache_model = if s.provider == providers::OPENAI_COMPATIBLE { + s.model.clone() + } else { + format!( + "{}:{}:{}", + s.provider, + s.model, + s.base_url.trim_end_matches('/') + ) + }; + let cache_data = if s.glossary_enabled { + json!({ + "source_text":source, + "model":cache_model, + "target_locale":"zh_cn", + "style":s.style, + "glossary_enabled":true, + "glossary_fingerprint":s.glossary_fingerprint + }) + } else { + json!({"source_text":source,"model":cache_model,"target_locale":"zh_cn","style":s.style}) + }; + h.update(cache_data.to_string()); + hex::encode(h.finalize()) +} +fn load_cache(q: &Path) -> HashMap { + fs::read(q.join(".ftb-translater/cache.json")) + .ok() + .and_then(|x| serde_json::from_slice(&x).ok()) + .unwrap_or_default() +} +fn save_cache(q: &Path, c: &HashMap) -> Result<(), String> { + let p = q.join(".ftb-translater/cache.json"); + fs::create_dir_all(p.parent().unwrap()).map_err(|e| e.to_string())?; + fs::write(p, serde_json::to_vec_pretty(c).unwrap()).map_err(|e| e.to_string()) +} +fn backup(q: &Path, m: &str) -> Result { + let root = q + .join(".ftb-translater/backups") + .join(Local::now().format("%Y%m%d-%H%M%S").to_string()); + let name = if m == "lang" { "lang" } else { "chapters" }; + for e in WalkDir::new(q.join(name)) { + let e = e.map_err(|e| e.to_string())?; + let rel = e.path().strip_prefix(q.join(name)).unwrap(); + let dest = root.join(name).join(rel); + if e.file_type().is_dir() { + fs::create_dir_all(&dest).map_err(|e| e.to_string())? + } else { + fs::copy(e.path(), dest).map_err(|e| e.to_string())?; + } + } + Ok(root) +} +async fn request( + client: &Client, + s: &Settings, + batch: &[Item], +) -> Result, String> { + let input = batch + .iter() + .map(|x| (x.id.clone(), x.protected.clone())) + .collect::>(); + providers::request(client, s, &input).await +} +pub async fn translate(app: AppHandle, data_dir: PathBuf, payload: Value) -> Result<(), String> { + let q = PathBuf::from(payload["quests_dir"].as_str().ok_or("缺少任务书目录")?); + let m = mode(&q)?; + let mut settings = crate_root::storage::load_settings(&data_dir); + for k in [ + "api_key", + "provider", + "base_url", + "model", + "style", + "batch_size", + "concurrency", + "glossary_path", + ] { + if let Some(v) = payload[k].as_str() { + match k { + "api_key" => settings.api_key = v.into(), + "provider" => settings.provider = v.into(), + "base_url" => settings.base_url = v.into(), + "model" => settings.model = v.into(), + "style" => settings.style = v.into(), + "batch_size" => settings.batch_size = v.into(), + "glossary_path" => settings.glossary_path = v.into(), + _ => settings.concurrency = v.into(), + } + } + } + if let Some(enabled) = payload["glossary_enabled"].as_bool() { + settings.glossary_enabled = enabled; + } + providers::normalize(&settings.provider)?; + if !providers::requires_api_key(&settings.provider) { + settings.glossary_enabled = false; + settings.batch_size = "auto".into(); + settings.concurrency = "auto".into(); + } + if providers::requires_api_key(&settings.provider) && settings.api_key.trim().is_empty() { + settings.api_key = crate_root::storage::translation_api_key(&settings.provider)?; + } + let loaded_glossary = if settings.glossary_enabled { + let path = if settings.glossary_path.trim().is_empty() { + glossary::ensure_default(&data_dir)? + } else { + PathBuf::from(&settings.glossary_path) + }; + let loaded = glossary::Loaded::load(&path)?; + settings.glossary_path = path.display().to_string(); + settings.glossary_fingerprint = loaded.fingerprint().to_string(); + Some(loaded) + } else { + None + }; + let mut items = vec![]; + let mut lang = None; + let mut chapter_segs = vec![]; + if m == "lang" { + let map = snbt::load(&q.join("lang/en_us.snbt"))?; + for (k, v) in &map { + let source = match v { + LangValue::Text(x) => x.clone(), + LangValue::Lines(x) => x.join("\n"), + }; + let (p, t) = protect_for_translation(&source, loaded_glossary.as_ref()); + items.push(Item { + id: k.clone(), + source, + protected: p, + tokens: t, + }); + } + lang = Some(map) + } else { + for file in chapters::files(&q) { + for s in chapters::extract(&file)? { + let (p, t) = protect_for_translation(&s.source, loaded_glossary.as_ref()); + items.push(Item { + id: s.cache_id.clone(), + source: s.source.clone(), + protected: p, + tokens: t, + }); + chapter_segs.push(s) + } + } + } + let mut cache = load_cache(&q); + let mut results = HashMap::new(); + let mut pending = vec![]; + let mut hits = 0; + for x in items.clone() { + if let Some(v) = cache.get(&cache_key(&x.source, &settings)) { + results.insert(x.id, v.clone()); + hits += 1 + } else { + pending.push(x) + } + } + let bs = parse_auto(&settings.batch_size, 25)?; + let mut concurrency = parse_auto(&settings.concurrency, 6)?.min(12); + if let Some(limit) = providers::concurrency_limit(&settings.provider) { + concurrency = concurrency.min(limit); + } + let batches = pending.chunks(bs).map(|x| x.to_vec()).collect::>(); + let total = pending.len(); + let client = Client::new(); + let app2 = app.clone(); + let settings2 = settings.clone(); + let stream = stream::iter(batches.into_iter().map(|batch| { + let c = client.clone(); + let s = settings2.clone(); + async move { + let r = request(&c, &s, &batch).await; + (batch, r) + } + })) + .buffer_unordered(concurrency); + tokio::pin!(stream); + let mut failed = vec![]; + while let Some((batch, r)) = stream.next().await { + match r { + Ok(map) => { + for x in batch { + let raw = map.get(&x.id).cloned().unwrap_or(x.protected); + let restored = restore(&raw, &x.tokens); + results.insert(x.id, restored); + } + } + Err(e) => { + for x in batch { + failed.push(format!("{}: {e}", x.id)); + results.insert(x.id, x.source); + } + } + } + let done = results.len().saturating_sub(hits); + let _ = app2.emit( + "translation-event", + json!({"type":"progress","stage":"translating","done":done,"total":total}), + ); + } + let backup = backup(&q, m)?; + let mut warns = BTreeMap::new(); + let mut details = BTreeMap::new(); + for x in &items { + let translated = results.get(&x.id).unwrap_or(&x.source); + let w = warnings(&x.source, translated); + if !w.is_empty() { + warns.insert(x.id.clone(), w); + details.insert(x.id.clone(), json!({"source":x.source,"failed":translated})); + results.insert(x.id.clone(), x.source.clone()); + } else if translated != &x.source { + cache.insert(cache_key(&x.source, &settings), translated.clone()); + } + } + let mut outputs = vec![]; + let (source_file, target_file) = if let Some(mut map) = lang { + for (k, v) in &mut map { + if let Some(t) = results.get(k) { + *v = match v { + LangValue::Text(_) => LangValue::Text(t.clone()), + LangValue::Lines(_) => { + LangValue::Lines(t.split('\n').map(str::to_string).collect()) + } + } + } + } + let target = q.join("lang/zh_cn.snbt"); + snbt::write(&target, &map)?; + let content = fs::read_to_string(&target).map_err(|e| e.to_string())?; + outputs.push(("lang/zh_cn.snbt".into(), content, json!({}))); + (q.join("lang/en_us.snbt"), target) + } else { + let mut by_file: HashMap> = HashMap::new(); + for s in chapter_segs { + by_file + .entry(s.path) + .or_default() + .push((s.index, results[&s.cache_id].clone())); + } + for (file, r) in by_file { + chapters::replace(&file, &r)?; + outputs.push(( + format!("chapters/{}", file.file_name().unwrap().to_string_lossy()), + fs::read_to_string(file).map_err(|e| e.to_string())?, + json!({}), + )); + } + (q.join("chapters"), q.join("chapters")) + }; + save_cache(&q, &cache)?; + let report = Report { + source_file: source_file.display().to_string(), + target_file: target_file.display().to_string(), + backup_dir: backup.display().to_string(), + total_entries: items.len(), + translated_entries: items.len() - failed.len(), + cache_hits: hits, + failed_entries: failed, + warnings: warns, + failed_translations: details, + }; + let rv = serde_json::to_value(&report).unwrap(); + let id = History::new(&data_dir)?.insert(&q, m, &settings, &rv, &outputs)?; + fs::create_dir_all(q.join(".ftb-translater")).map_err(|e| e.to_string())?; + fs::write( + q.join(".ftb-translater/report-latest.json"), + serde_json::to_vec_pretty(&rv).unwrap(), + ) + .map_err(|e| e.to_string())?; + app.emit( + "translation-event", + json!({"type":"done","report":report,"run_id":id}), + ) + .map_err(|e| e.to_string())?; + Ok(()) +} + +pub fn save_review(v: &Value) -> Result { + let target = PathBuf::from(v["target_file"].as_str().unwrap_or("")); + let key = v["key"].as_str().unwrap_or(""); + let text = v["text"].as_str().unwrap_or("").trim(); + if text.is_empty() { + return Err("译文不能为空".into()); + } + if target.is_file() { + let mut map = snbt::load(&target)?; + let entry = map + .iter_mut() + .find(|x| x.0 == key) + .ok_or("目标文件中找不到此条目")?; + entry.1 = LangValue::Text(text.into()); + snbt::write(&target, &map)?; + } else { + let p = key.splitn(3, ':').collect::>(); + if p.len() != 3 { + return Err("章节条目标识无效".into()); + } + chapters::replace( + &target.join(p[0]), + &[(p[1].parse().map_err(|_| "章节序号无效")?, text.into())], + )?; + } + Ok(json!({"saved":true})) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + #[test] + fn protects_format_tokens() { + let src = "Use &e%s&r on in assets/mod/textures/a.png\\n"; + let (p, t) = protect(src); + assert!(!p.contains("minecraft:stone")); + assert_eq!(restore(&p, &t), src); + assert!(warnings( + src, + "使用 &e%s&r 于 ,位于 assets/mod/textures/a.png\\n" + ) + .is_empty()); + } + #[test] + fn rejects_missing_token() { + assert!(!warnings("Use %s and &eGold&r", "使用黄金").is_empty()) + } + #[test] + fn rejects_changed_json_shape() { + let a = r#"{"text":"Hello","color":"red"}"#; + let b = r#"{"text":"你好","color":"blue"}"#; + assert!(!warnings(a, b).is_empty()) + } + #[test] + fn glossary_is_optional_and_restores_curated_terms() { + let source = "Use Mekanism with an Enchanting Table"; + let (plain, plain_tokens) = protect_for_translation(source, None); + assert_eq!(restore(&plain, &plain_tokens), source); + assert!(!plain.contains("⟨G_")); + + let d = tempdir().unwrap(); + let path = glossary::ensure_default(d.path()).unwrap(); + let loaded = glossary::Loaded::load(&path).unwrap(); + let (protected, tokens) = protect_for_translation(source, Some(&loaded)); + assert_eq!(protected.matches("⟨G_").count(), 2); + assert_eq!(restore(&protected, &tokens), "Use 通用机械 with an 附魔台"); + } + #[test] + fn disabled_glossary_keeps_legacy_cache_key() { + let settings = Settings { + provider: providers::OPENAI_COMPATIBLE.into(), + base_url: "https://api.deepseek.com".into(), + model: "deepseek-chat".into(), + ..Settings::default() + }; + let mut hash = Sha256::new(); + hash.update( + json!({ + "source_text":"Mekanism", + "model":"deepseek-chat", + "target_locale":"zh_cn", + "style":settings.style + }) + .to_string(), + ); + assert_eq!( + cache_key("Mekanism", &settings), + hex::encode(hash.finalize()) + ); + + let mut enabled = settings; + enabled.glossary_enabled = true; + enabled.glossary_fingerprint = "custom-content-hash".into(); + assert_ne!( + cache_key("Mekanism", &enabled), + cache_key("Mekanism", &Settings::default()) + ); + } + #[test] + fn scans_lang_pack() { + let d = tempdir().unwrap(); + let q = d.path().join("config/ftbquests/quests/lang"); + fs::create_dir_all(&q).unwrap(); + fs::write(q.join("en_us.snbt"), "{ title: \"Hello\" }").unwrap(); + let result = scan(&json!({"path":d.path(),"batch_size":"auto"})).unwrap(); + assert_eq!(result["entry_count"], 1); + assert_eq!(result["mode"], "lang"); + } +} diff --git a/src-tauri/src/glossary.rs b/src-tauri/src/glossary.rs new file mode 100644 index 0000000..383b1b0 --- /dev/null +++ b/src-tauri/src/glossary.rs @@ -0,0 +1,226 @@ +use serde::Deserialize; +use sha2::{Digest, Sha256}; +use std::{ + collections::HashSet, + fs, + path::{Path, PathBuf}, +}; + +pub const DEFAULT_FILENAME: &str = "minecraft_glossary.json"; +const DEFAULT_CONTENT: &str = include_str!("../resources/minecraft_glossary.json"); + +#[derive(Clone, Debug, Deserialize)] +struct Entry { + source: String, + target: String, + #[serde(default)] + case_sensitive: bool, +} + +#[derive(Deserialize)] +struct GlossaryFile { + version: u32, + entries: Vec, +} + +#[derive(Clone, Debug)] +pub struct Loaded { + entries: Vec, + fingerprint: String, +} + +pub fn default_path(data_dir: &Path) -> PathBuf { + data_dir.join(DEFAULT_FILENAME) +} + +pub fn ensure_default(data_dir: &Path) -> Result { + fs::create_dir_all(data_dir).map_err(|e| format!("无法创建应用数据目录:{e}"))?; + let path = default_path(data_dir); + if !path.is_file() { + fs::write(&path, DEFAULT_CONTENT) + .map_err(|e| format!("无法创建默认 Minecraft 词表:{e}"))?; + } + Ok(path) +} + +impl Loaded { + pub fn load(path: &Path) -> Result { + let bytes = fs::read(path) + .map_err(|e| format!("无法读取 Minecraft 词表 {}:{e}", path.display()))?; + Self::from_bytes(&bytes).map_err(|e| format!("Minecraft 词表 {} 无效:{e}", path.display())) + } + + fn from_bytes(bytes: &[u8]) -> Result { + let mut file: GlossaryFile = + serde_json::from_slice(bytes).map_err(|e| format!("JSON 解析失败:{e}"))?; + if file.version == 0 { + return Err("version 必须大于 0".into()); + } + if file.entries.is_empty() { + return Err("entries 不能为空".into()); + } + let mut seen = HashSet::new(); + for entry in &file.entries { + if entry.source.trim().is_empty() || entry.target.trim().is_empty() { + return Err("source 和 target 不能为空".into()); + } + let key = if entry.case_sensitive { + format!("case:{}", entry.source) + } else { + format!("fold:{}", entry.source.to_ascii_lowercase()) + }; + if !seen.insert(key) { + return Err(format!("存在重复术语:{}", entry.source)); + } + } + file.entries + .sort_by_key(|entry| std::cmp::Reverse(entry.source.len())); + Ok(Self { + entries: file.entries, + fingerprint: hex::encode(Sha256::digest(bytes)), + }) + } + + pub fn fingerprint(&self) -> &str { + &self.fingerprint + } + + #[cfg(test)] + fn len(&self) -> usize { + self.entries.len() + } + + pub fn protect(&self, text: &str) -> (String, Vec<(String, String)>) { + let mut output = String::with_capacity(text.len()); + let mut tokens = vec![]; + let mut pos = 0; + while pos < text.len() { + let found = self + .entries + .iter() + .find_map(|entry| matches_at(text, pos, entry).map(|end| (entry, end))); + if let Some((entry, end)) = found { + let placeholder = format!("⟨G_{}⟩", tokens.len()); + output.push_str(&placeholder); + tokens.push((placeholder, entry.target.clone())); + pos = end; + } else { + let ch = text[pos..].chars().next().expect("pos is a char boundary"); + output.push(ch); + pos += ch.len_utf8(); + } + } + (output, tokens) + } +} + +fn boundary(text: &str, start: usize, end: usize, source: &str) -> bool { + let word = |c: char| c.is_ascii_alphanumeric() || c == '_'; + let needs_left = source.chars().next().is_some_and(word); + let needs_right = source.chars().last().is_some_and(word); + let left_ok = !needs_left || text[..start].chars().next_back().is_none_or(|c| !word(c)); + let right_ok = !needs_right || text[end..].chars().next().is_none_or(|c| !word(c)); + left_ok && right_ok +} + +fn matches_at(text: &str, start: usize, entry: &Entry) -> Option { + let end = start.checked_add(entry.source.len())?; + let candidate = text.get(start..end)?; + let same = if entry.case_sensitive { + candidate == entry.source + } else { + candidate.eq_ignore_ascii_case(&entry.source) + }; + (same && boundary(text, start, end, &entry.source)).then_some(end) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + fn default_glossary() -> Loaded { + Loaded::from_bytes(DEFAULT_CONTENT.as_bytes()).unwrap() + } + + #[test] + fn creates_an_editable_default_file_without_overwriting_it() { + let dir = tempdir().unwrap(); + let path = ensure_default(dir.path()).unwrap(); + assert_eq!(Loaded::load(&path).unwrap().len(), default_glossary().len()); + fs::write( + &path, + r#"{"version":2,"entries":[{"source":"Demo","target":"演示"}]}"#, + ) + .unwrap(); + ensure_default(dir.path()).unwrap(); + assert_eq!(Loaded::load(&path).unwrap().len(), 1); + } + + #[test] + fn content_changes_produce_a_new_cache_fingerprint() { + let a = Loaded::from_bytes( + r#"{"version":1,"entries":[{"source":"Demo","target":"演示"}]}"#.as_bytes(), + ) + .unwrap(); + let b = Loaded::from_bytes( + r#"{"version":1,"entries":[{"source":"Demo","target":"示例"}]}"#.as_bytes(), + ) + .unwrap(); + assert_ne!(a.fingerprint(), b.fingerprint()); + } + + #[test] + fn loads_a_large_curated_glossary() { + assert!(default_glossary().len() >= 620); + } + + #[test] + fn prefers_longest_term_and_respects_boundaries() { + let glossary = default_glossary(); + let (text, tokens) = + glossary.protect("Use an Enchanting Table, not a timetable, with Mekanism."); + assert_eq!(text.matches("⟨G_").count(), 2); + assert!(text.contains("timetable")); + assert_eq!(tokens[0].1, "附魔台"); + assert_eq!(tokens[1].1, "通用机械"); + } + + #[test] + fn preserves_uncertain_mod_names_instead_of_machine_translating_them() { + let (_, tokens) = default_glossary().protect("Oritech and XNet"); + assert_eq!(tokens[0].1, "Oritech"); + assert_eq!(tokens[1].1, "XNet"); + } + + #[test] + fn covers_terms_from_different_modpack_ecosystems() { + let glossary = default_glossary(); + let (_, tokens) = glossary.protect( + "Use a Mechanical Press from the Create mod, a Smeltery Controller from Tinkers' Construct, and a Mana Pool from Botania.", + ); + let targets = tokens + .into_iter() + .map(|(_, target)| target) + .collect::>(); + assert_eq!( + targets, + vec![ + "动力冲压机", + "机械动力模组", + "冶炼炉控制器", + "匠魂", + "魔力池", + "植物魔法" + ] + ); + } + + #[test] + fn avoids_ambiguous_standalone_mod_names() { + let glossary = default_glossary(); + let (text, tokens) = glossary.protect("Create a spectrum carefully."); + assert_eq!(text, "Create a spectrum carefully."); + assert!(tokens.is_empty()); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..801096e --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,74 @@ +mod chapters; +mod core; +mod glossary; +mod providers; +mod snbt; +mod storage; + +use serde_json::{json, Value}; +use std::path::PathBuf; +use storage::History; +use tauri::{Emitter, Manager}; + +fn data_dir(app: &tauri::AppHandle) -> Result { + app.path().app_data_dir().map_err(|e| e.to_string()) +} + +#[tauri::command] +fn bridge(app: tauri::AppHandle, command: String, payload: Option) -> Result { + let v = payload.unwrap_or_else(|| json!({})); + let dir = data_dir(&app)?; + match command.as_str() { + "scan" => core::scan(&v), + "settings" => serde_json::to_value(storage::load_settings(&dir)).map_err(|e| e.to_string()), + "save-settings" => storage::save_settings(&dir, &v), + "default-glossary" => { + let path = glossary::ensure_default(&dir)?; + Ok(json!({"path":path})) + } + "provider-credential" => { + storage::provider_credential(v["provider"].as_str().ok_or("缺少翻译提供商")?) + } + "history-list" => History::new(&dir)?.list(), + "history-delete" => { + History::new(&dir)?.delete(v["run_id"].as_i64().ok_or("缺少历史编号")?)?; + Ok(json!({"deleted":true})) + } + "history-export" => { + History::new(&dir)?.export( + v["run_id"].as_i64().ok_or("缺少历史编号")?, + std::path::Path::new(v["path"].as_str().ok_or("缺少导出路径")?), + )?; + Ok(json!({"path":v["path"]})) + } + "save-review" => core::save_review(&v), + _ => Err(format!("未知命令:{command}")), + } +} + +#[tauri::command] +fn start_translation(app: tauri::AppHandle, payload: Value) -> Result<(), String> { + let dir = data_dir(&app)?; + let task_app = app.clone(); + tauri::async_runtime::spawn(async move { + if let Err(e) = core::translate(task_app.clone(), dir, payload).await { + let _ = task_app.emit("translation-event", json!({"type":"error","message":e})); + } + }); + Ok(()) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_dialog::init()) + .invoke_handler(tauri::generate_handler![bridge, start_translation]) + .setup(|app| { + if let Some(w) = app.get_webview_window("main") { + let _ = w.set_title("FTB Translater — 任务书汉化"); + } + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("error while running FTB Translater") +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..a2427f7 --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + ftb_translater_lib::run(); +} diff --git a/src-tauri/src/providers.rs b/src-tauri/src/providers.rs new file mode 100644 index 0000000..b2c59f1 --- /dev/null +++ b/src-tauri/src/providers.rs @@ -0,0 +1,479 @@ +use crate::storage::Settings; +use reqwest::{header, Client, Response, StatusCode}; +use serde_json::{json, Map, Value}; +use std::{collections::HashMap, time::Duration}; + +pub const OPENAI_COMPATIBLE: &str = "openai_compatible"; +pub const DEEPL: &str = "deepl"; +pub const GOOGLE_WEB: &str = "google_web"; +pub const DEEPL_WEB: &str = "deepl_web"; + +const GOOGLE_MAX_CHARS: usize = 4500; +const DEEPL_WEB_MAX_CHARS: usize = 1500; +const USER_AGENT: &str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"; + +pub fn normalize(provider: &str) -> Result<&str, String> { + match provider.trim() { + OPENAI_COMPATIBLE => Ok(OPENAI_COMPATIBLE), + DEEPL => Ok(DEEPL), + GOOGLE_WEB => Ok(GOOGLE_WEB), + DEEPL_WEB => Ok(DEEPL_WEB), + other => Err(format!("不支持的翻译提供商:{other}")), + } +} + +pub fn requires_api_key(provider: &str) -> bool { + !matches!(provider, GOOGLE_WEB | DEEPL_WEB) +} + +pub fn concurrency_limit(provider: &str) -> Option { + matches!(provider, GOOGLE_WEB | DEEPL_WEB).then_some(1) +} + +pub async fn request( + client: &Client, + settings: &Settings, + batch: &[(String, String)], +) -> Result, String> { + match normalize(&settings.provider)? { + DEEPL => request_deepl(client, settings, batch).await, + GOOGLE_WEB => request_google(client, settings, batch).await, + DEEPL_WEB => request_deepl_web(client, settings, batch).await, + _ => request_openai(client, settings, batch).await, + } +} + +async fn request_openai( + client: &Client, + s: &Settings, + batch: &[(String, String)], +) -> Result, String> { + let input = batch + .iter() + .map(|(id, text)| (id.clone(), Value::String(text.clone()))) + .collect::>(); + let prompt = format!( + "Task / 任务:Translate this FTB Quests language map to Simplified Chinese.\nStyle / 风格:{}。\nReturn one JSON object with exactly the same keys. Opaque placeholders like ⟨P_0⟩ and ⟨G_0⟩ must remain byte-for-byte unchanged and appear exactly once. Preserve item IDs, tags, line breaks, numbers and units.\n\n{}", + s.style, + serde_json::to_string_pretty(&input).unwrap() + ); + let url = format!("{}/chat/completions", s.base_url.trim_end_matches('/')); + let messages = json!([ + {"role":"system","content":"You are a Minecraft modpack localization assistant. Translate only player-facing English into natural Simplified Chinese. Never modify opaque placeholders; G placeholders are curated Minecraft glossary terms."}, + {"role":"user","content":prompt} + ]); + let mut use_response_format = true; + let mut last = String::new(); + for attempt in 0..3 { + let mut body = json!({"model":s.model,"messages":messages,"temperature":0.2}); + if use_response_format { + body["response_format"] = json!({"type":"json_object"}); + } + match client + .post(&url) + .bearer_auth(&s.api_key) + .json(&body) + .send() + .await + { + Ok(response) => { + let status = response.status(); + let text = response.text().await.unwrap_or_default(); + if !status.is_success() { + if use_response_format + && matches!( + status, + StatusCode::BAD_REQUEST | StatusCode::UNPROCESSABLE_ENTITY + ) + && (text.contains("response_format") || text.contains("json_object")) + { + use_response_format = false; + continue; + } + last = format!("HTTP {status}: {text}"); + } else { + let value: Value = serde_json::from_str(&text) + .map_err(|e| format!("OpenAI 兼容接口返回无效 JSON:{e}"))?; + let content = value + .pointer("/choices/0/message/content") + .and_then(Value::as_str) + .ok_or("OpenAI 兼容接口返回内容为空")?; + let map = parse_json_map(content)?; + if batch.iter().all(|(id, _)| map.contains_key(id)) { + return Ok(map); + } + last = "OpenAI 兼容接口返回内容缺少条目".into(); + } + } + Err(error) => last = error.to_string(), + } + tokio::time::sleep(Duration::from_millis(800 * (attempt + 1))).await; + } + Err(last) +} + +fn parse_json_map(content: &str) -> Result, String> { + let mut text = content.trim(); + if text.starts_with("```") { + text = text + .strip_prefix("```json") + .or_else(|| text.strip_prefix("```")) + .unwrap_or(text) + .trim(); + text = text.strip_suffix("```").unwrap_or(text).trim(); + } + if let (Some(start), Some(end)) = (text.find('{'), text.rfind('}')) { + text = &text[start..=end]; + } + serde_json::from_str(text).map_err(|e| format!("翻译接口返回的 JSON 无效:{e}")) +} + +async fn request_deepl( + client: &Client, + s: &Settings, + batch: &[(String, String)], +) -> Result, String> { + let url = format!("{}/v2/translate", s.base_url.trim_end_matches('/')); + let body = json!({ + "text": batch.iter().map(|(_, text)| text).collect::>(), + "source_lang": "EN", + "target_lang": "ZH-HANS" + }); + let response = send_with_retry(|| { + client + .post(&url) + .header( + header::AUTHORIZATION, + format!("DeepL-Auth-Key {}", s.api_key), + ) + .json(&body) + }) + .await?; + parse_ordered_translations(response, batch).await +} + +async fn request_google( + client: &Client, + s: &Settings, + batch: &[(String, String)], +) -> Result, String> { + let mut units = vec![]; + for (id, text) in batch { + for piece in split_text(text, GOOGLE_MAX_CHARS - 100) { + units.push((id.clone(), piece)); + } + } + let mut result = batch + .iter() + .map(|(id, _)| (id.clone(), String::new())) + .collect::>(); + let mut chunk = vec![]; + let mut chars = 0; + for unit in units { + let size = unit.1.chars().count() + 40; + if !chunk.is_empty() && chars + size > GOOGLE_MAX_CHARS { + append_google_chunk(client, s, &chunk, &mut result).await?; + chunk.clear(); + chars = 0; + } + chunk.push(unit); + chars += size; + } + if !chunk.is_empty() { + append_google_chunk(client, s, &chunk, &mut result).await?; + } + Ok(result) +} + +async fn append_google_chunk( + client: &Client, + s: &Settings, + chunk: &[(String, String)], + result: &mut HashMap, +) -> Result<(), String> { + let markers = (0..chunk.len()) + .map(|index| format!("⟪FTB_TRANSLATER_BATCH_{index}⟫")) + .collect::>(); + let combined = chunk + .iter() + .zip(&markers) + .map(|((_, text), marker)| format!("{marker}{text}")) + .collect::>() + .join("\n"); + let url = format!("{}/translate_a/single", s.base_url.trim_end_matches('/')); + let form = [ + ("client", "gtx"), + ("sl", "en"), + ("tl", "zh-CN"), + ("dt", "t"), + ("q", combined.as_str()), + ]; + let response = send_with_retry(|| { + client + .post(&url) + .header(header::USER_AGENT, USER_AGENT) + .form(&form) + }) + .await?; + let value: Value = response.json().await.map_err(|e| e.to_string())?; + let translated = value + .get(0) + .and_then(Value::as_array) + .ok_or("Google 网页翻译返回结构无效")? + .iter() + .filter_map(|segment| segment.get(0).and_then(Value::as_str)) + .collect::(); + let parts = split_marked_translation(&translated, &markers)?; + for ((id, _), translated) in chunk.iter().zip(parts) { + result.entry(id.clone()).or_default().push_str(&translated); + } + Ok(()) +} + +fn split_marked_translation(text: &str, markers: &[String]) -> Result, String> { + let positions = markers + .iter() + .map(|marker| text.find(marker)) + .collect::>>() + .ok_or("Google 网页翻译未保留批次标记")?; + if positions.windows(2).any(|pair| pair[0] >= pair[1]) { + return Err("Google 网页翻译打乱了批次标记".into()); + } + let mut result = vec![]; + for (index, marker) in markers.iter().enumerate() { + let start = positions[index] + marker.len(); + let end = positions.get(index + 1).copied().unwrap_or(text.len()); + let mut value = text[start..end].to_string(); + if index + 1 < markers.len() && value.ends_with('\n') { + value.pop(); + } + result.push(value); + } + Ok(result) +} + +async fn request_deepl_web( + client: &Client, + s: &Settings, + batch: &[(String, String)], +) -> Result, String> { + let mut units = vec![]; + for (id, text) in batch { + for piece in split_text(text, DEEPL_WEB_MAX_CHARS) { + units.push((id.clone(), piece)); + } + } + let mut result = batch + .iter() + .map(|(id, _)| (id.clone(), String::new())) + .collect::>(); + let mut chunk = vec![]; + let mut chars = 0; + for unit in units { + let size = unit.1.chars().count(); + if !chunk.is_empty() && chars + size > DEEPL_WEB_MAX_CHARS { + append_deepl_web_chunk(client, s, &chunk, &mut result).await?; + chunk.clear(); + chars = 0; + } + chunk.push(unit); + chars += size; + } + if !chunk.is_empty() { + append_deepl_web_chunk(client, s, &chunk, &mut result).await?; + } + Ok(result) +} + +async fn append_deepl_web_chunk( + client: &Client, + s: &Settings, + chunk: &[(String, String)], + result: &mut HashMap, +) -> Result<(), String> { + let url = format!("{}/v1/translate", s.base_url.trim_end_matches('/')); + let body = json!({ + "text": chunk.iter().map(|(_, text)| text).collect::>(), + "target_lang": "zh-Hans", + "source_lang": "en", + "usage_type": "Translate", + "app_information": { + "os": "brex_macOS", + "os_version": "brex_chrome_120.0.0.0", + "app_version": "1.86.0", + "app_build": "chrome_web_store", + "instance_id": format!("00000000-0000-4000-8000-{:012x}", std::process::id()) + } + }); + let response = send_with_retry(|| { + client + .post(&url) + .header(header::AUTHORIZATION, "None") + .header( + header::ORIGIN, + "chrome-extension://cofdbpoegempjloogbagkncekinflcnj", + ) + .header("Sec-Fetch-Site", "cross-site") + .header("Sec-Fetch-Mode", "cors") + .header("Sec-Fetch-Dest", "empty") + .header(header::USER_AGENT, USER_AGENT) + .json(&body) + }) + .await?; + let values = ordered_translation_values(response).await?; + if values.len() != chunk.len() { + return Err("DeepL 网页翻译返回条目数量不一致".into()); + } + for ((id, _), translated) in chunk.iter().zip(values) { + result.entry(id.clone()).or_default().push_str(&translated); + } + Ok(()) +} + +async fn parse_ordered_translations( + response: Response, + batch: &[(String, String)], +) -> Result, String> { + let values = ordered_translation_values(response).await?; + if values.len() != batch.len() { + return Err("翻译接口返回条目数量不一致".into()); + } + Ok(batch + .iter() + .zip(values) + .map(|((id, _), text)| (id.clone(), text)) + .collect()) +} + +async fn ordered_translation_values(response: Response) -> Result, String> { + let value: Value = response.json().await.map_err(|e| e.to_string())?; + value["translations"] + .as_array() + .ok_or_else(|| "翻译接口返回结构无效".to_string())? + .iter() + .map(|item| { + item["text"] + .as_str() + .map(str::to_string) + .ok_or("翻译接口返回了无效文本".into()) + }) + .collect() +} + +async fn send_with_retry(mut build: F) -> Result +where + F: FnMut() -> reqwest::RequestBuilder, +{ + let mut last = String::new(); + for attempt in 0..3 { + match build().send().await { + Ok(response) if response.status().is_success() => return Ok(response), + Ok(response) => { + let status = response.status(); + let body = response.text().await.unwrap_or_default(); + last = format!("HTTP {status}: {body}"); + } + Err(error) => last = error.to_string(), + } + tokio::time::sleep(Duration::from_millis(1000 * (attempt + 1))).await; + } + Err(last) +} + +fn split_text(text: &str, max_chars: usize) -> Vec { + if text.is_empty() { + return vec![String::new()]; + } + let chars = text.chars().collect::>(); + let mut result = vec![]; + let mut start = 0; + while chars.len() - start > max_chars { + let end = start + max_chars; + let mut cut = (start..end) + .rev() + .find(|index| { + matches!( + chars[*index], + '\n' | '.' | '!' | '?' | '。' | '!' | '?' | ';' | ';' | ' ' + ) + }) + .map(|index| index + 1) + .filter(|index| *index - start >= max_chars / 2) + .unwrap_or(end); + let window = &chars[start..cut]; + let last_open = window.iter().rposition(|c| *c == '⟨'); + let last_close = window.iter().rposition(|c| *c == '⟩'); + if last_open > last_close { + cut = start + last_open.unwrap(); + } + if cut <= start { + cut = end; + } + result.push(chars[start..cut].iter().collect()); + start = cut; + } + result.push(chars[start..].iter().collect()); + result +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn splits_google_markers_back_into_entries() { + let markers = vec![ + "⟪FTB_TRANSLATER_BATCH_0⟫".into(), + "⟪FTB_TRANSLATER_BATCH_1⟫".into(), + ]; + let text = "⟪FTB_TRANSLATER_BATCH_0⟫制作表格\n⟪FTB_TRANSLATER_BATCH_1⟫击败巨龙"; + assert_eq!( + split_marked_translation(text, &markers).unwrap(), + vec!["制作表格", "击败巨龙"] + ); + } + + #[test] + fn long_text_split_preserves_placeholder() { + let text = format!("{}⟨P_123⟩{}", "A".repeat(20), "B".repeat(20)); + let chunks = split_text(&text, 25); + assert_eq!(chunks.concat(), text); + assert!(chunks + .iter() + .all(|chunk| !chunk.contains('⟨') || chunk.contains("⟨P_123⟩"))); + } + + #[test] + fn parses_fenced_openai_json() { + let map = parse_json_map("```json\n{\"a\":\"甲\"}\n```").unwrap(); + assert_eq!(map["a"], "甲"); + } + + #[test] + #[ignore = "calls anonymous web translation services"] + fn live_web_provider_smoke_test() { + tauri::async_runtime::block_on(async { + let client = Client::new(); + let batch = vec![ + ("title".into(), "Craft a table".into()), + ("desc".into(), "Defeat ⟨P_0⟩Ignis⟨P_1⟩ in the arena".into()), + ("hint".into(), "Collect ten pieces of stone".into()), + ]; + for (provider, base_url, model) in [ + (GOOGLE_WEB, "https://translate.googleapis.com", "google-web"), + (DEEPL_WEB, "https://oneshot-free.www.deepl.com", "deepl-web"), + ] { + let settings = Settings { + provider: provider.into(), + base_url: base_url.into(), + model: model.into(), + ..Settings::default() + }; + let translated = request(&client, &settings, &batch).await.unwrap(); + assert_eq!(translated.len(), batch.len()); + assert!(translated["desc"].contains("⟨P_0⟩")); + assert!(translated["desc"].contains("⟨P_1⟩")); + } + }); + } +} diff --git a/src-tauri/src/snbt.rs b/src-tauri/src/snbt.rs new file mode 100644 index 0000000..2e2081f --- /dev/null +++ b/src-tauri/src/snbt.rs @@ -0,0 +1,188 @@ +use serde::{Deserialize, Serialize}; +use std::{fs, path::Path}; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(untagged)] +pub enum LangValue { + Text(String), + Lines(Vec), +} + +pub type LangMap = Vec<(String, LangValue)>; + +struct Parser<'a> { + text: &'a str, + pos: usize, +} +impl<'a> Parser<'a> { + fn new(text: &'a str) -> Self { + Self { text, pos: 0 } + } + fn peek(&self) -> Option { + self.text[self.pos..].chars().next() + } + fn bump(&mut self) -> Option { + let c = self.peek()?; + self.pos += c.len_utf8(); + Some(c) + } + fn skip(&mut self) { + loop { + while self.peek().is_some_and(char::is_whitespace) { + self.bump(); + } + if self.text[self.pos..].starts_with("//") || self.peek() == Some('#') { + while self.peek().is_some_and(|c| c != '\n') { + self.bump(); + } + } else { + break; + } + } + } + fn expect(&mut self, c: char) -> Result<(), String> { + if self.bump() == Some(c) { + Ok(()) + } else { + Err(format!("SNBT 在偏移 {} 处缺少 {c}", self.pos)) + } + } + fn string(&mut self) -> Result { + let q = self.bump().ok_or("字符串意外结束")?; + if q != '\'' && q != '"' { + return Err("需要引号字符串".into()); + } + let mut out = String::new(); + while let Some(c) = self.bump() { + if c == q { + return Ok(out); + } + if c == '\\' { + let e = self.bump().ok_or("转义序列不完整")?; + out.push(match e { + 'n' => '\n', + 'r' => '\r', + 't' => '\t', + x => x, + }) + } else { + out.push(c) + } + } + Err("字符串没有结束引号".into()) + } + fn key(&mut self) -> Result { + if matches!(self.peek(), Some('\'' | '"')) { + return self.string(); + } + let start = self.pos; + while self.peek().is_some_and(|c| !c.is_whitespace() && c != ':') { + self.bump(); + } + if start == self.pos { + Err("缺少 SNBT key".into()) + } else { + Ok(self.text[start..self.pos].to_string()) + } + } + fn value(&mut self) -> Result { + if matches!(self.peek(), Some('\'' | '"')) { + Ok(LangValue::Text(self.string()?)) + } else if self.peek() == Some('[') { + self.bump(); + let mut v = vec![]; + loop { + self.skip(); + if self.peek() == Some(']') { + self.bump(); + break; + } + v.push(self.string()?); + self.skip(); + if self.peek() == Some(',') { + self.bump(); + } + } + Ok(LangValue::Lines(v)) + } else { + Err("语言值必须是字符串或字符串数组".into()) + } + } + fn parse(mut self) -> Result { + self.skip(); + self.expect('{')?; + let mut out = vec![]; + loop { + self.skip(); + if self.peek() == Some('}') { + self.bump(); + break; + } + let k = self.key()?; + self.skip(); + self.expect(':')?; + self.skip(); + out.push((k, self.value()?)); + self.skip(); + if self.peek() == Some(',') { + self.bump(); + } + } + Ok(out) + } +} +pub fn parse(text: &str) -> Result { + Parser::new(text.trim_start_matches('\u{feff}')).parse() +} +pub fn load(path: &Path) -> Result { + parse(&fs::read_to_string(path).map_err(|e| format!("无法读取 {}:{e}", path.display()))?) +} +fn escape(s: &str) -> String { + s.replace('\\', "\\\\") + .replace('"', "\\\"") + .replace('\n', "\\n") + .replace('\r', "\\r") + .replace('\t', "\\t") +} +pub fn dump(values: &LangMap) -> String { + let mut out = String::from("{\n"); + for (i, (k, v)) in values.iter().enumerate() { + out.push_str(&format!(" \"{}\": ", escape(k))); + match v { + LangValue::Text(s) => out.push_str(&format!("\"{}\"", escape(s))), + LangValue::Lines(lines) => { + out.push_str("[\n"); + for (j, s) in lines.iter().enumerate() { + out.push_str(&format!( + " \"{}\"{}\n", + escape(s), + if j + 1 < lines.len() { "," } else { "" } + )); + } + out.push_str(" ]"); + } + } + if i + 1 < values.len() { + out.push(',') + } + out.push('\n') + } + out.push_str("}\n"); + out +} +pub fn write(path: &Path, values: &LangMap) -> Result<(), String> { + let text = dump(values); + parse(&text)?; + fs::write(path, text).map_err(|e| format!("无法写入 {}:{e}", path.display())) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn roundtrip() { + let src = "{ title: \"Hello\", desc: [\"A\", \"B\"] }"; + let v = parse(src).unwrap(); + assert_eq!(parse(&dump(&v)).unwrap(), v) + } +} diff --git a/src-tauri/src/storage.rs b/src-tauri/src/storage.rs new file mode 100644 index 0000000..d4c3854 --- /dev/null +++ b/src-tauri/src/storage.rs @@ -0,0 +1,422 @@ +use chrono::Local; +use rusqlite::{params, Connection}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use std::{ + collections::HashMap, + fs, + io::Write, + path::{Path, PathBuf}, + sync::{Mutex, OnceLock}, +}; +use zip::{write::SimpleFileOptions, ZipWriter}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Settings { + pub api_key: String, + pub has_api_key: bool, + pub credential_backend: String, + pub provider: String, + pub base_url: String, + pub model: String, + pub style: String, + pub batch_size: String, + pub concurrency: String, + pub glossary_enabled: bool, + pub glossary_path: String, + #[serde(default, skip_serializing)] + pub glossary_fingerprint: String, +} +impl Default for Settings { + fn default() -> Self { + Self { + api_key: String::new(), + has_api_key: false, + credential_backend: "系统凭证管理器".into(), + provider: crate::providers::GOOGLE_WEB.into(), + base_url: "https://translate.googleapis.com".into(), + model: "google-web".into(), + style: "自然玩家向简体中文汉化".into(), + batch_size: "auto".into(), + concurrency: "auto".into(), + glossary_enabled: false, + glossary_path: String::new(), + glossary_fingerprint: String::new(), + } + } +} +#[derive(Serialize, Deserialize)] +struct Config { + #[serde(default = "default_provider")] + provider: String, + base_url: String, + model: String, + style: String, + batch_size: String, + concurrency: String, + #[serde(default)] + glossary_enabled: bool, + #[serde(default)] + glossary_path: String, +} +fn default_provider() -> String { + crate::providers::GOOGLE_WEB.into() +} +fn entry(provider: &str) -> Result { + let account = if provider == crate::providers::OPENAI_COMPATIBLE { + "deepseek_api_key".to_string() + } else { + format!( + "{}_api_key", + provider.replace(|c: char| !c.is_ascii_alphanumeric() && c != '_', "") + ) + }; + keyring::Entry::new("ftb-translater", &account).map_err(|e| e.to_string()) +} + +fn credential_cache() -> &'static Mutex> { + static CACHE: OnceLock>> = OnceLock::new(); + CACHE.get_or_init(|| Mutex::new(HashMap::new())) +} + +fn cached_credential(provider: &str) -> Option { + credential_cache().lock().ok()?.get(provider).cloned() +} + +fn cache_credential(provider: &str, value: Option<&str>) { + if let Ok(mut cache) = credential_cache().lock() { + if let Some(value) = value.filter(|value| !value.is_empty()) { + cache.insert(provider.to_string(), value.to_string()); + } else { + cache.remove(provider); + } + } +} + +pub fn translation_api_key(provider: &str) -> Result { + crate::providers::normalize(provider)?; + if let Some(value) = cached_credential(provider) { + return Ok(value); + } + let value = entry(provider)? + .get_password() + .map_err(|_| "没有可用的 API Key,请在设置中查看或修改 API Key 后重试".to_string())?; + cache_credential(provider, Some(&value)); + Ok(value) +} +pub fn load_settings(dir: &Path) -> Settings { + let mut s = Settings::default(); + s.glossary_path = crate::glossary::ensure_default(dir) + .unwrap_or_else(|_| crate::glossary::default_path(dir)) + .display() + .to_string(); + if let Ok(raw) = fs::read_to_string(dir.join("settings.json")) { + if let Ok(c) = serde_json::from_str::(&raw) { + s.provider = c.provider; + s.base_url = c.base_url; + s.model = c.model; + s.style = c.style; + s.batch_size = c.batch_size; + s.concurrency = c.concurrency; + s.glossary_enabled = c.glossary_enabled; + if !c.glossary_path.trim().is_empty() { + s.glossary_path = c.glossary_path; + } + } + } + s +} +pub fn save_settings(dir: &Path, v: &Value) -> Result { + let parse = |k: &str| -> Result { + let x = v + .get(k) + .and_then(Value::as_str) + .unwrap_or("auto") + .trim() + .to_string(); + if ["batch_size", "concurrency"].contains(&k) + && x != "auto" + && x.parse::().ok().filter(|n| *n > 0).is_none() + { + return Err(format!("{k} 必须是 auto 或正整数")); + } + Ok(x) + }; + fs::create_dir_all(dir).map_err(|e| e.to_string())?; + let provider = v["provider"] + .as_str() + .unwrap_or(crate::providers::GOOGLE_WEB); + crate::providers::normalize(provider)?; + if v["api_key_changed"].as_bool().unwrap_or(false) { + let key = v["api_key"].as_str().unwrap_or("").trim(); + let e = entry(provider)?; + if key.is_empty() { + let _ = e.delete_credential(); + cache_credential(provider, None); + } else { + e.set_password(key) + .map_err(|e| format!("无法保存系统凭证:{e}"))?; + cache_credential(provider, Some(key)); + } + } + let web_provider = !crate::providers::requires_api_key(provider); + let glossary_enabled = !web_provider && v["glossary_enabled"].as_bool().unwrap_or(false); + let glossary_path = match v["glossary_path"].as_str().unwrap_or("").trim() { + "" => crate::glossary::ensure_default(dir)?, + path => PathBuf::from(path), + }; + if glossary_enabled { + crate::glossary::Loaded::load(&glossary_path)?; + } + let c = Config { + provider: provider.into(), + base_url: parse("base_url")?, + model: parse("model")?, + style: parse("style")?, + batch_size: if web_provider { + "auto".into() + } else { + parse("batch_size")? + }, + concurrency: if web_provider { + "auto".into() + } else { + parse("concurrency")? + }, + glossary_enabled, + glossary_path: glossary_path.display().to_string(), + }; + fs::write( + dir.join("settings.json"), + serde_json::to_vec_pretty(&c).unwrap(), + ) + .map_err(|e| e.to_string())?; + Ok(json!({"credential_backend":"系统凭证管理器","glossary_path":glossary_path})) +} + +pub fn provider_credential(provider: &str) -> Result { + crate::providers::normalize(provider)?; + let api_key = if let Some(value) = cached_credential(provider) { + value + } else { + let value = entry(provider)?.get_password().unwrap_or_default(); + cache_credential(provider, Some(&value)); + value + }; + Ok(json!({"api_key":api_key,"has_api_key":!api_key.is_empty()})) +} + +pub struct History { + path: PathBuf, +} +impl History { + pub fn new(dir: &Path) -> Result { + fs::create_dir_all(dir).map_err(|e| e.to_string())?; + let h = Self { + path: dir.join("history.sqlite3"), + }; + h.conn()?; + Ok(h) + } + fn conn(&self) -> Result { + let c = Connection::open(&self.path).map_err(|e| e.to_string())?; + c.execute_batch("PRAGMA foreign_keys=ON;CREATE TABLE IF NOT EXISTS translation_runs(id INTEGER PRIMARY KEY,quests_dir TEXT,pack_name TEXT,mode TEXT,model TEXT,style TEXT,total_entries INTEGER,translated_entries INTEGER,cache_hits INTEGER,failed_count INTEGER,warning_count INTEGER,created_at TEXT);CREATE TABLE IF NOT EXISTS translation_files(id INTEGER PRIMARY KEY,run_id INTEGER,filename TEXT,mapping TEXT,output_content TEXT,FOREIGN KEY(run_id) REFERENCES translation_runs(id) ON DELETE CASCADE);").map_err(|e|e.to_string())?; + Ok(c) + } + pub fn insert( + &self, + quests: &Path, + mode: &str, + settings: &Settings, + report: &Value, + outputs: &[(String, String, Value)], + ) -> Result { + let mut c = self.conn()?; + let tx = c.transaction().map_err(|e| e.to_string())?; + tx.execute("INSERT INTO translation_runs(quests_dir,pack_name,mode,model,style,total_entries,translated_entries,cache_hits,failed_count,warning_count,created_at)VALUES(?,?,?,?,?,?,?,?,?,?,?)",params![quests.display().to_string(),pack_name(quests),mode,settings.model,settings.style,report["total_entries"].as_i64(),report["translated_entries"].as_i64(),report["cache_hits"].as_i64(),report["failed_entries"].as_array().map_or(0,Vec::len)as i64,report["warnings"].as_object().map_or(0,|x|x.len())as i64,Local::now().to_rfc3339()]).map_err(|e|e.to_string())?; + let id = tx.last_insert_rowid(); + for (name, content, map) in outputs { + tx.execute("INSERT INTO translation_files(run_id,filename,mapping,output_content)VALUES(?,?,?,?)",params![id,name,map.to_string(),content]).map_err(|e|e.to_string())?; + } + tx.commit().map_err(|e| e.to_string())?; + Ok(id) + } + pub fn list(&self) -> Result { + let c = self.conn()?; + let mut q=c.prepare("SELECT id,pack_name,quests_dir,mode,model,style,total_entries,translated_entries,cache_hits,failed_count,warning_count,created_at FROM translation_runs ORDER BY created_at DESC,id DESC LIMIT 100").map_err(|e|e.to_string())?; + let rows=q.query_map([],|r|Ok(json!({"id":r.get::<_,i64>(0)?,"pack_name":r.get::<_,String>(1)?,"quests_dir":r.get::<_,String>(2)?,"mode":r.get::<_,String>(3)?,"model":r.get::<_,String>(4)?,"style":r.get::<_,String>(5)?,"total_entries":r.get::<_,i64>(6)?,"translated_entries":r.get::<_,i64>(7)?,"cache_hits":r.get::<_,i64>(8)?,"failed_count":r.get::<_,i64>(9)?,"warning_count":r.get::<_,i64>(10)?,"created_at":r.get::<_,String>(11)?}))).map_err(|e|e.to_string())?; + Ok(Value::Array(rows.filter_map(Result::ok).collect())) + } + pub fn delete(&self, id: i64) -> Result<(), String> { + let c = self.conn()?; + c.execute("DELETE FROM translation_runs WHERE id=?", [id]) + .map_err(|e| e.to_string())?; + Ok(()) + } + pub fn export(&self, id: i64, dest: &Path) -> Result<(), String> { + let c = self.conn()?; + let mode: String = c + .query_row("SELECT mode FROM translation_runs WHERE id=?", [id], |r| { + r.get(0) + }) + .map_err(|e| e.to_string())?; + let mut q = c + .prepare("SELECT filename,output_content FROM translation_files WHERE run_id=?") + .map_err(|e| e.to_string())?; + let rows = q + .query_map([id], |r| { + Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)) + }) + .map_err(|e| e.to_string())?; + let file = fs::File::create(dest).map_err(|e| e.to_string())?; + let mut zip = ZipWriter::new(file); + let opts = + SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated); + zip.start_file("manifest.json", opts) + .map_err(|e| e.to_string())?; + zip.write_all(json!({"run_id":id,"mode":mode}).to_string().as_bytes()) + .map_err(|e| e.to_string())?; + for row in rows { + let (name, content) = row.map_err(|e| e.to_string())?; + if name.contains("..") || name.starts_with('/') { + return Err("历史文件路径不安全".into()); + } + zip.start_file(name, opts).map_err(|e| e.to_string())?; + zip.write_all(content.as_bytes()) + .map_err(|e| e.to_string())?; + } + zip.finish().map_err(|e| e.to_string())?; + Ok(()) + } +} +fn pack_name(q: &Path) -> String { + q.ancestors() + .find(|p| p.file_name().is_some_and(|n| n == "config")) + .and_then(Path::parent) + .and_then(Path::file_name) + .map(|x| x.to_string_lossy().into_owned()) + .unwrap_or_else(|| { + q.file_name() + .unwrap_or_default() + .to_string_lossy() + .into_owned() + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + #[test] + fn defaults_to_google_web_with_optional_glossary_disabled() { + let settings = Settings::default(); + assert_eq!(settings.provider, crate::providers::GOOGLE_WEB); + assert_eq!(settings.base_url, "https://translate.googleapis.com"); + assert_eq!(settings.model, "google-web"); + assert!(!settings.glossary_enabled); + + let old_config = r#"{ + "provider":"openai_compatible", + "base_url":"https://api.deepseek.com", + "model":"deepseek-chat", + "style":"自然中文", + "batch_size":"auto", + "concurrency":"auto" + }"#; + let config: Config = serde_json::from_str(old_config).unwrap(); + assert!(!config.glossary_enabled); + } + + #[test] + fn settings_expose_an_editable_default_glossary_path() { + let d = tempdir().unwrap(); + let settings = load_settings(d.path()); + let path = PathBuf::from(&settings.glossary_path); + assert!(path.is_file()); + assert_eq!(path.file_name().unwrap(), crate::glossary::DEFAULT_FILENAME); + fs::write( + &path, + r#"{"version":3,"entries":[{"source":"Custom","target":"自定义"}]}"#, + ) + .unwrap(); + load_settings(d.path()); + assert!(fs::read_to_string(path).unwrap().contains("Custom")); + } + + #[test] + fn web_provider_persists_only_automatic_settings() { + let d = tempdir().unwrap(); + save_settings( + d.path(), + &json!({ + "provider":crate::providers::GOOGLE_WEB, + "base_url":"https://translate.googleapis.com", + "model":"google-web", + "style":"ignored", + "batch_size":"99", + "concurrency":"8", + "glossary_enabled":true, + "glossary_path":"", + "api_key_changed":false + }), + ) + .unwrap(); + let settings = load_settings(d.path()); + assert!(!settings.glossary_enabled); + assert_eq!(settings.batch_size, "auto"); + assert_eq!(settings.concurrency, "auto"); + } + + #[test] + fn ordinary_settings_save_reuses_session_key_without_keyring_write() { + let d = tempdir().unwrap(); + let provider = crate::providers::OPENAI_COMPATIBLE; + cache_credential(provider, Some("session-only-key")); + save_settings( + d.path(), + &json!({ + "provider":provider, + "base_url":"https://api.deepseek.com", + "model":"deepseek-chat", + "style":"自然中文", + "batch_size":"auto", + "concurrency":"auto", + "glossary_enabled":false, + "api_key":"", + "api_key_changed":false + }), + ) + .unwrap(); + assert_eq!(translation_api_key(provider).unwrap(), "session-only-key"); + cache_credential(provider, None); + } + + #[test] + fn history_roundtrip_and_export() { + let d = tempdir().unwrap(); + let history = History::new(d.path()).unwrap(); + let report = json!({ + "total_entries": 2, + "translated_entries": 2, + "cache_hits": 1, + "failed_entries": [], + "warnings": {} + }); + let id = history + .insert( + Path::new("/packs/demo/config/ftbquests/quests"), + "lang", + &Settings::default(), + &report, + &[("lang/zh_cn.snbt".into(), "{\"a\":\"甲\"}".into(), json!({}))], + ) + .unwrap(); + assert_eq!(history.list().unwrap().as_array().unwrap().len(), 1); + let archive = d.path().join("translation.zip"); + history.export(id, &archive).unwrap(); + assert!(archive.is_file()); + history.delete(id).unwrap(); + assert!(history.list().unwrap().as_array().unwrap().is_empty()); + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..b69de99 --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "FTB Translater", + "version": "0.2.0", + "identifier": "com.openres.ftb-translater", + "build": { "beforeDevCommand": "npm run dev", "devUrl": "http://localhost:1420", "beforeBuildCommand": "npm run build", "frontendDist": "../dist" }, + "app": { + "windows": [{ "title": "FTB Translater", "width": 1240, "height": 820, "minWidth": 940, "minHeight": 680, "decorations": true, "transparent": false }], + "security": { "csp": null } + }, + "bundle": { + "active": true, + "targets": "all", + "shortDescription": "FTB Quests 任务书汉化工具", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..c3958ea --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,185 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { createRoot } from "react-dom/client"; +import { invoke } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; +import { open, save } from "@tauri-apps/plugin-dialog"; +import { Archive, ArrowRight, BookOpen, Check, ChevronRight, CircleAlert, Copy, Eye, EyeOff, FileSearch, FolderOpen, History, KeyRound, Languages, Moon, Play, RefreshCw, Save, Settings, ShieldCheck, Sparkles, Sun, Trash2, X } from "lucide-react"; +import "./styles.css"; + +type View = "workbench" | "history" | "settings"; +type Stage = "idle" | "scanned" | "running" | "done" | "error"; +type Provider = "openai_compatible"|"deepl"|"google_web"|"deepl_web"; +type SettingsData = { api_key:string; api_key_changed:boolean; has_api_key:boolean; credential_backend:string; provider:Provider; base_url:string; model:string; style:string; batch_size:string; concurrency:string; glossary_enabled:boolean; glossary_path:string }; +type ScanResult = { quests_dir:string; pack_name:string; mode:"lang"|"chapters"; mode_label:string; source:string; entry_count:number; file_count:number; estimated_batches:number }; +type Report = { source_file:string; target_file:string; backup_dir:string; total_entries:number; translated_entries:number; cache_hits:number; failed_entries:string[]; warnings:Record; failed_translations:Record }; +type Run = { id:number; pack_name:string; quests_dir:string; mode:string; model:string; style:string; total_entries:number; translated_entries:number; cache_hits:number; failed_count:number; warning_count:number; created_at:string }; +type TranslationEvent = { type:"progress"|"log"|"done"|"error"; stage?:string; done?:number; total?:number; message?:string; report?:Report; run_id?:number }; + +type ProviderPreset = { + label:string; + description:string; + base_url:string; + model:string; + credentialLabel?:string; + supportsGlossary:boolean; + supportsTaskParameters:boolean; + configuration:"none"|"deepl"|"openai"; +}; +const providerOptions:Record={ + google_web:{label:"Google 网页翻译(默认)",description:"无需 API Key,使用内置的大批次、低并发策略。",base_url:"https://translate.googleapis.com",model:"google-web",supportsGlossary:false,supportsTaskParameters:false,configuration:"none"}, + deepl_web:{label:"DeepL 网页翻译(实验性)",description:"无需 API Key,使用匿名网页接口与内置安全参数。",base_url:"https://oneshot-free.www.deepl.com",model:"deepl-web",supportsGlossary:false,supportsTaskParameters:false,configuration:"none"}, + deepl:{label:"DeepL 翻译 API",description:"使用 DeepL 官方 API,可配置认证密钥、接口地址和任务参数。",base_url:"https://api-free.deepl.com",model:"deepl",credentialLabel:"DeepL Authentication Key",supportsGlossary:true,supportsTaskParameters:true,configuration:"deepl"}, + openai_compatible:{label:"DeepSeek / OpenAI 兼容",description:"可配置 API Key、兼容接口、模型、翻译要求和任务参数。",base_url:"https://api.deepseek.com",model:"deepseek-chat",credentialLabel:"API Key",supportsGlossary:true,supportsTaskParameters:true,configuration:"openai"}, +}; +const defaults: SettingsData = { api_key:"", api_key_changed:false, has_api_key:false, credential_backend:"系统凭证管理器", provider:"google_web", base_url:"https://translate.googleapis.com", model:"google-web", style:"准确、自然地翻译为简体中文,保留 Minecraft 与模组专有名词。", batch_size:"auto", concurrency:"auto", glossary_enabled:false, glossary_path:"" }; + +async function call(command:string, payload:Record={}) { return invoke("bridge", { command, payload }); } + +function QuestMark({compact=false}:{compact?:boolean}) { + return ; +} + +function App() { + const [view,setView]=useState("workbench"); const [stage,setStage]=useState("idle"); + const [theme,setTheme]=useState<"light"|"dark">(()=>localStorage.theme==="dark"?"dark":"light"); + const [settings,setSettings]=useState(defaults); const [scan,setScan]=useState(null); + const [selectedPath,setSelectedPath]=useState(""); const [busy,setBusy]=useState(false); const [progress,setProgress]=useState(0); + const [logs,setLogs]=useState([]); const [report,setReport]=useState(null); const [runs,setRuns]=useState([]); + const [toast,setToast]=useState(""); const [confirm,setConfirm]=useState(false); + + useEffect(()=>{ document.documentElement.dataset.theme=theme; localStorage.theme=theme; },[theme]); + useEffect(()=>{ call("settings").then(value=>setSettings({...value,api_key:"",api_key_changed:false})).catch(e=>notify(String(e))); },[]); + useEffect(()=>{ const unlisten=listen("translation-event",({payload:e})=>{ + if(e.type==="log"&&e.message) setLogs(v=>[...v.slice(-99),e.message!]); + if(e.type==="progress") { setProgress(e.total?Math.min(100,Math.round((e.done||0)/e.total*100)):100); } + if(e.type==="done"&&e.report) { setBusy(false); setProgress(100); setStage("done"); setReport(e.report); setLogs(v=>[...v,"翻译完成,输出与备份均已写入。"]); notify("任务书汉化完成"); loadHistory(); } + if(e.type==="error") { setBusy(false); setStage("error"); notify(e.message||"翻译失败"); } + }); return()=>{unlisten.then(fn=>fn())}; },[]); + const notify=(text:string)=>{setToast(text); window.setTimeout(()=>setToast(""),3200)}; + const loadHistory=()=>call("history-list").then(setRuns).catch(e=>notify(String(e))); + useEffect(()=>{if(view==="history")loadHistory()},[view]); + + async function chooseFolder(){ const value=await open({directory:true,multiple:false,title:"选择整合包或 FTB Quests 目录"}); if(typeof value==="string"){setSelectedPath(value); await doScan(value)} } + async function doScan(path=selectedPath){ if(!path.trim())return notify("请先选择整合包目录"); setBusy(true); setReport(null); try { const result=await call("scan",{path,batch_size:settings.batch_size}); setScan(result); setSelectedPath(result.quests_dir); setStage("scanned"); setProgress(0); setLogs([`已找到 ${result.entry_count} 条可翻译文本。`,`源目录:${result.source}`]); } catch(e){setStage("error");notify(String(e))} finally{setBusy(false)} } + async function beginTranslation(){setConfirm(false);if(!scan)return;setBusy(true);setStage("running");setProgress(0);setLogs(["正在启动安全翻译任务…"]);try{await invoke("start_translation",{payload:{quests_dir:scan.quests_dir,...settings}})}catch(e){setBusy(false);setStage("error");notify(String(e))}} + async function saveSettings(){try{const r=await call<{credential_backend:string;glossary_path:string}>("save-settings",settings);setSettings(v=>({...v,api_key:"",api_key_changed:false,has_api_key:v.api_key_changed?!!v.api_key.trim():v.has_api_key,credential_backend:r.credential_backend,glossary_path:r.glossary_path}));notify("设置已保存")}catch(e){notify(String(e))}} + function changeProvider(provider:Provider){const preset=providerOptions[provider];setSettings(v=>({...v,provider,api_key:"",api_key_changed:false,has_api_key:false,base_url:preset.base_url,model:preset.model,glossary_enabled:preset.supportsGlossary?v.glossary_enabled:false,batch_size:preset.supportsTaskParameters?v.batch_size:"auto",concurrency:preset.supportsTaskParameters?v.concurrency:"auto"}))} + const warningCount=report?Object.keys(report.warnings).length:0; + + return
+ +
+ {view==="workbench"&&doScan()} onTranslate={()=>setConfirm(true)} onSettings={()=>setView("settings")}/>} + {view==="settings"&&} + {view==="history"&&} +
+ {confirm&&scan&&setConfirm(false)} onConfirm={beginTranslation}/>} + {toast&&
{toast}
} +
+} + +function Nav({active,icon,label,onClick,badge}:{active:boolean;icon:React.ReactNode;label:string;onClick:()=>void;badge?:number}){return } + +function Workbench(p:{stage:Stage;scan:ScanResult|null;path:string;setPath:(v:string)=>void;busy:boolean;progress:number;logs:string[];report:Report|null;warnings:number;onChoose:()=>void;onScan:()=>void;onTranslate:()=>void;onSettings:()=>void}){ + const steps=[{key:"idle",label:"选择任务书"},{key:"scanned",label:"确认内容"},{key:"running",label:"自动汉化"},{key:"done",label:"检查结果"}]; const index={idle:0,scanned:1,running:2,done:3,error:p.scan?1:0}[p.stage]; + return
+

TRANSLATION WORKBENCH

把任务书带给中文玩家

选择整合包,确认扫描结果,然后安全写回汉化内容。

{p.stage==="running"?"正在汉化":p.stage==="done"?"本次完成":p.stage==="error"?"需要处理":p.stage==="scanned"?"等待开始":"准备就绪"}
+
{steps.map((s,i)=>
{i:i+1}{s.label}
{i<3&&
}
)}
+
+

任务书位置

整合包根目录或 quests、lang、chapters 目录都可以

p.setPath(e.target.value)} placeholder="选择一个整合包目录…" onKeyDown={e=>e.key==="Enter"&&p.onScan()}/>
{p.scan?:
从扫描开始我们会自动判断任务书格式,不会在扫描时改动文件。
}
+ +
+ {(p.logs.length>0||p.report)&&

运行记录

只显示对排查问题有帮助的信息

实时
{p.logs.map((l,i)=>
{String(i+1).padStart(2,"0")}

{l}

)}
{p.report&&

本次结果

{p.report.translated_entries} / {p.report.total_entries} 条已处理

0}/>0}/>
{p.report.backup_dir}
}
} + {p.report&&p.warnings>0&&} +
+} + +function ScanSummary({scan}:{scan:ScanResult}){return
已识别整合包{scan.pack_name||"FTB Quests"}
{scan.mode_label}
{scan.entry_count.toLocaleString()}可翻译条目
{scan.file_count}{scan.mode==="lang"?"语言文件":"章节文件"}
{scan.estimated_batches}预计请求批次

{scan.source}

} +function Metric({label,value,warn=false}:{label:string;value:number;warn?:boolean}){return
{label}{value}
} + +function ReviewPanel({report}:{report:Report}){const entries=Object.entries(report.warnings);return

MANUAL REVIEW

检查格式告警

守卫已经保留原文。确认颜色码、占位符和换行后,可直接修正写入。

{entries.length} 条待检查
{entries.map(([key,warnings])=>)}
} +function ReviewCard({entryKey,warnings,detail,target}:{entryKey:string;warnings:string[];detail?:{source:string;failed:string;error?:string};target:string}){const [text,setText]=useState(detail?.failed||detail?.source||"");const [status,setStatus]=useState("");async function saveText(){setStatus("正在保存…");try{await call("save-review",{target_file:target,key:entryKey,text});setStatus("已写入目标文件 ✓")}catch(e){setStatus(String(e))}}return
{entryKey}{warnings.length} 个问题

{detail?.source||"未记录原文"}