diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 27de2822c..aa1703588 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,11 +4,11 @@ "features": { "ghcr.io/devcontainers/features/node:2": { - "version": "24.21.0", - "pnpmVersion": "12.11.2" + "version": "24.22.1", + "pnpmVersion": "12.14.0" }, "ghcr.io/devcontainers/features/rust:1": { - "version": "1.99.0", + "version": "1.99.1", "profile": "default" }, "ghcr.io/devcontainers/features/git:1": {}, diff --git a/.github/ISSUE_GUIDELINES.md b/.github/ISSUE_GUIDELINES.md new file mode 100644 index 000000000..d1740006c --- /dev/null +++ b/.github/ISSUE_GUIDELINES.md @@ -0,0 +1,33 @@ +# Issue Guidelines / Issue 提交指引 + +Use these suggestions to make reports easier to assess. State what you observed +and what you could not verify; unknown details are fine. + +以下建议有助于维护者评估问题。请说明实际观察到的现象和无法确认的部分;未知信息可以留空。 + +- Search existing issues by symptom, conditions, and outcome. Add new evidence + to an existing report when it describes the same problem. + 按现象、条件和结果搜索已有 Issue。若描述的是同一问题,请在原 Issue 中补充新证据。 +- Keep each issue to one distinct user problem or requested outcome. Split + unrelated findings instead of bundling an audit or checklist. + 每个 Issue 聚焦一个独立的问题或需求结果;无关发现请拆开,不要把审计清单打包成一个 Issue。 +- For bugs, include the version, operating system, steps, and actual versus + expected behavior when known. A root-cause diagnosis is not required. Share + only relevant logs and redact tokens or personal information. + 反馈 Bug 时,尽可能提供版本、操作系统、复现步骤及实际与预期行为。无需先判断根因;只提供相关日志,并遮盖令牌和个人信息。 +- For feature requests, describe the user task, current limitation, desired + outcome, and affected platforms. An implementation proposal is optional. + 提交功能请求时,请描述用户要完成的任务、当前限制、期望结果和适用平台;实现方案不是必填项。 +- Separate observations from guesses. An AI or scanner finding is a hypothesis + until its affected configuration, concrete impact, and supporting evidence + are stated; do not present a list of theoretical risks as confirmed bugs. + 区分观察结果和推测。AI 或扫描器的发现应先视为假设;请说明受影响的配置、具体影响和支持证据,不要把理论风险清单写成已确认的 Bug。 +- Do not post secrets, personal data, or sensitive exploit details in a public + issue. Ask maintainers how to share sensitive security information safely. + 请勿在公开 Issue 中发布密钥、个人信息或敏感利用细节;如涉及安全敏感信息,请先询问维护者如何安全提供。 +- AI assistance is welcome; human contributors do not need to disclose it. If + an AI agent or automation creates or co-authors an issue, add a final body line + such as `Assisted by: GPT-5.6 High`. Omit effort when the runtime does not + report it. Disclosure is transparency only, not evidence for or against issue + quality. + 欢迎使用 AI 辅助,人工提交者无需披露。如果 Issue 由 AI Agent 或自动化创建或共同撰写,请在正文末尾添加披露行,例如 `Assisted by: GPT-5.6 High`。运行环境未提供 effort 时可省略。披露仅用于提高透明度,不作为 Issue 质量的正面或负面证据。 diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index ebce301f4..876cdd09c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -10,6 +10,8 @@ body: value: | ## 在提交问题之前,请确认以下事项: + 提交前可参考 [Issue 提交指引](https://github.com/clash-verge-rev/clash-verge-rev/blob/dev/.github/ISSUE_GUIDELINES.md),说明可观察到的问题和证据;不确定的原因请标为猜测。 + 1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide/term.html) 以及 [常见问题](https://clash-verge-rev.github.io/faq/windows.html) 2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似 issue,否则请在已有的 issue 下进行讨论 3. 请 **务必** 给 issue 填写一个简洁明了的标题,以便他人快速检索 @@ -19,6 +21,8 @@ body: ## Before submitting the issue, please make sure of the following checklist: + See the [Issue guidelines](https://github.com/clash-verge-rev/clash-verge-rev/blob/dev/.github/ISSUE_GUIDELINES.md) for how to describe observed behavior and evidence; mark uncertain causes as hypotheses. + 1. Please make sure you have read the [Clash Verge Rev official documentation](https://clash-verge-rev.github.io/guide/term.html) and [FAQ](https://clash-verge-rev.github.io/faq/windows.html) 2. Please make sure there is no similar issue in the [existing issues](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue), otherwise please discuss under the existing issue 3. Please be sure to fill in a concise and clear title for the issue so that others can quickly search diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index d30728a51..1ef2780ca 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -9,12 +9,16 @@ body: attributes: value: | ## 在提交问题之前,请确认以下事项: + 可参考 [Issue 提交指引](https://github.com/clash-verge-rev/clash-verge-rev/blob/dev/.github/ISSUE_GUIDELINES.md),描述用户任务、当前限制和期望结果。 + 1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide/term.html) 确认软件不存在类似的功能 2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似 issue,否则请在已有的 issue 下进行讨论 3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索 4. 请 **务必** 先下载 [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) 版本测试,确保该功能还未实现 5. 请 **务必** 按照模板规范详细描述问题,否则 issue 将会被关闭 ## Before submitting the issue, please make sure of the following checklist: + See the [Issue guidelines](https://github.com/clash-verge-rev/clash-verge-rev/blob/dev/.github/ISSUE_GUIDELINES.md) for describing the user task, current limitation, and desired outcome. + 1. Please make sure you have read the [Clash Verge Rev official documentation](https://clash-verge-rev.github.io/guide/term.html) to confirm that the software does not have similar functions 2. Please make sure there is no similar issue in the [existing issues](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue), otherwise please discuss under the existing issue 3. Please be sure to fill in a concise and clear title for the issue so that others can quickly search diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9d3da294d..5137b940b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,12 @@ ## Problem -What breaks or what is missing. Link the issue if one exists (`Fixes #123`). +What breaks or what is missing. Product bug fixes and features require a +pre-existing issue; link it here (`Fixes #123` only when this PR fully resolves +it). Maintenance work (such as chore, perf, docs, refactor, test, CI/build, +dependency, style, routine localization, or release changes) does not require +an issue link; link one when it defines the scope. For reverts, link the PR or +commit being reverted. For security-sensitive fixes that cannot be tracked +publicly, follow maintainer-approved private tracking. ## Approach diff --git a/.github/workflows/autobuild.yml b/.github/workflows/autobuild.yml index 49dd85ae2..577d7d360 100644 --- a/.github/workflows/autobuild.yml +++ b/.github/workflows/autobuild.yml @@ -51,7 +51,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.21.0' + node-version: '24.22.1' - name: Install dependencies run: pnpm install --frozen-lockfile @@ -154,7 +154,7 @@ jobs: - name: Install Rust Stable uses: dtolnay/rust-toolchain@master with: - toolchain: '1.99.0' + toolchain: '1.99.1' targets: ${{ matrix.target }} - name: Add Rust Target @@ -185,7 +185,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.21.0' + node-version: '24.22.1' cache: 'pnpm' - name: Pnpm Cache @@ -206,7 +206,7 @@ jobs: - name: Add Rust Target run: | - rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }} + rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }} rustup target list --installed echo "Rust target ${{ matrix.target }} installed." @@ -257,7 +257,7 @@ jobs: - name: Install Rust Stable uses: dtolnay/rust-toolchain@master with: - toolchain: '1.99.0' + toolchain: '1.99.1' targets: ${{ matrix.target }} - name: Add Rust Target @@ -282,7 +282,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.21.0' + node-version: '24.22.1' cache: 'pnpm' - name: Pnpm Cache @@ -347,7 +347,7 @@ jobs: - name: Add Rust Target run: | - rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }} + rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }} rustup target list --installed echo "Rust target ${{ matrix.target }} installed." @@ -426,7 +426,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.21.0' + node-version: '24.22.1' cache: 'pnpm' - name: Pnpm Cache @@ -454,7 +454,7 @@ jobs: - name: Add Rust Target run: | - rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }} + rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }} rustup target list --installed echo "Rust target ${{ matrix.target }} installed." @@ -524,7 +524,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.21.0' + node-version: '24.22.1' - uses: pnpm/action-setup@v6.1.0 name: Install pnpm diff --git a/.github/workflows/cross_check.yaml b/.github/workflows/cross_check.yaml index 83289f20e..1366e0c7a 100644 --- a/.github/workflows/cross_check.yaml +++ b/.github/workflows/cross_check.yaml @@ -43,7 +43,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.21.0' + node-version: '24.22.1' - uses: pnpm/action-setup@v6 name: Install pnpm diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 36ef5baab..ec8a7d320 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -74,7 +74,7 @@ jobs: - name: Install Rust Stable if: github.event.inputs[matrix.input] == 'true' - uses: dtolnay/rust-toolchain@1.99.0 + uses: dtolnay/rust-toolchain@1.99.1 - name: Rust Cache uses: Swatinem/rust-cache@v2 @@ -103,7 +103,7 @@ jobs: if: github.event.inputs[matrix.input] == 'true' uses: actions/setup-node@v7 with: - node-version: '24.21.0' + node-version: '24.22.1' cache: 'pnpm' - name: Pnpm Cache @@ -129,7 +129,7 @@ jobs: if: github.event.inputs[matrix.input] == 'true' run: | # Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed - rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }} + rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }} rustup target list --installed echo "Rust target ${{ matrix.target }} installed." diff --git a/.github/workflows/frontend-check.yml b/.github/workflows/frontend-check.yml index 937f9c89b..2df2f0cdd 100644 --- a/.github/workflows/frontend-check.yml +++ b/.github/workflows/frontend-check.yml @@ -52,7 +52,7 @@ jobs: - uses: actions/setup-node@v7 if: steps.check_frontend.outputs.frontend == 'true' with: - node-version: '24.21.0' + node-version: '24.22.1' cache: 'pnpm' - name: Restore pnpm cache @@ -138,7 +138,7 @@ jobs: - uses: actions/setup-node@v7 if: steps.check_inputs.outputs.contract == 'true' with: - node-version: '24.21.0' + node-version: '24.22.1' cache: 'pnpm' - run: pnpm install --frozen-lockfile diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d2191eeb2..0e075ad35 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -204,7 +204,7 @@ jobs: - name: Install Rust Stable uses: dtolnay/rust-toolchain@master with: - toolchain: '1.99.0' + toolchain: '1.99.1' targets: ${{ matrix.target }} - name: Add Rust Target @@ -230,7 +230,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.21.0' + node-version: '24.22.1' - uses: pnpm/action-setup@v6 name: Install pnpm @@ -244,7 +244,7 @@ jobs: - name: Add Rust Target run: | - rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }} + rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }} rustup target list --installed echo "Rust target ${{ matrix.target }} installed." @@ -311,7 +311,7 @@ jobs: - name: Install Rust Stable uses: dtolnay/rust-toolchain@master with: - toolchain: '1.99.0' + toolchain: '1.99.1' targets: ${{ matrix.target }} - name: Add Rust Target @@ -331,7 +331,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.21.0' + node-version: '24.22.1' - name: Install pnpm uses: pnpm/action-setup@v6 @@ -389,7 +389,7 @@ jobs: - name: Add Rust Target run: | - rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }} + rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }} rustup target list --installed echo "Rust target ${{ matrix.target }} installed." @@ -446,7 +446,7 @@ jobs: - name: Install Rust Stable uses: dtolnay/rust-toolchain@master with: - toolchain: '1.99.0' + toolchain: '1.99.1' targets: ${{ matrix.target }} - name: Add Rust Target @@ -466,7 +466,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.21.0' + node-version: '24.22.1' - uses: pnpm/action-setup@v6 name: Install pnpm @@ -487,7 +487,7 @@ jobs: - name: Add Rust Target run: | - rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }} + rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }} rustup target list --installed echo "Rust target ${{ matrix.target }} installed." @@ -547,7 +547,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.21.0' + node-version: '24.22.1' - uses: pnpm/action-setup@v6 name: Install pnpm @@ -573,7 +573,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.21.0' + node-version: '24.22.1' - uses: pnpm/action-setup@v6 name: Install pnpm @@ -638,7 +638,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.21.0' + node-version: '24.22.1' - uses: pnpm/action-setup@v6 name: Install pnpm diff --git a/.github/workflows/signpath-test.yml b/.github/workflows/signpath-test.yml index 6d8c0c864..2d2055d24 100644 --- a/.github/workflows/signpath-test.yml +++ b/.github/workflows/signpath-test.yml @@ -22,13 +22,13 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@master with: - toolchain: '1.99.0' + toolchain: '1.99.1' targets: x86_64-pc-windows-msvc - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.21.0' + node-version: '24.22.1' - name: Install pnpm uses: pnpm/action-setup@v6 diff --git a/.github/workflows/telegram-notify.yml b/.github/workflows/telegram-notify.yml index f8d296324..ac3e7e82d 100644 --- a/.github/workflows/telegram-notify.yml +++ b/.github/workflows/telegram-notify.yml @@ -34,7 +34,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.21.0' + node-version: '24.22.1' - uses: pnpm/action-setup@v6 name: Install pnpm diff --git a/.github/workflows/updater.yml b/.github/workflows/updater.yml index c610dfdd3..f55927bf6 100644 --- a/.github/workflows/updater.yml +++ b/.github/workflows/updater.yml @@ -15,7 +15,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.21.0' + node-version: '24.22.1' - uses: pnpm/action-setup@v6 name: Install pnpm @@ -39,7 +39,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.21.0' + node-version: '24.22.1' - uses: pnpm/action-setup@v6 name: Install pnpm diff --git a/.tool-versions b/.tool-versions index 25be9ef6d..a9a8ee0a3 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ -nodejs 24.21.0 -pnpm 12.11.2 -rust 1.99.0 +nodejs 24.22.1 +pnpm 12.14.0 +rust 1.99.1 diff --git a/AGENTS.md b/AGENTS.md index ce9822479..d6ec809a9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,22 +18,25 @@ These rules apply to every change, whether human- or agent-authored. They match the ownership evidence the AI-slop review evaluates (see [`pr-ai-slop-review.md`](.github/workflows/pr-ai-slop-review.md)). -1. **Issue first.** Non-trivial changes require a pre-existing issue describing - the problem. If none exists, ask the maintainers to open or approve one - before implementing. +1. **Issue first.** Product bug fixes and features require a pre-existing issue; + self-scoped maintenance work does not. Follow the PR template for linkage and + revert references. If a required issue is missing, ask maintainers to open or + approve one before implementing. For security-sensitive fixes that cannot be + tracked publicly, follow maintainer-approved private tracking. 2. **Scope discipline.** Every changed file must be justifiable from the linked - issue. No drive-by refactors, renames, formatting churn, or dependency bumps - unrelated to the problem being fixed. + issue or the stated PR scope. No drive-by refactors, renames, formatting + churn, or dependency bumps unrelated to the work. 3. **Author accountability.** AI assistance is welcome, but the contributor owns the result: understand the change, describe the problem and approach in your own words, and verify the change against the reported behavior before submitting. 4. **Tests are justified, not default.** Do not add tests, test scaffolding, or - speculative defensive code unless the linked issue demands them. When a test - is genuinely necessary — it reproduces the reported regression or guards - behavior whose breakage would otherwise go unnoticed — keep it minimal and - state in the PR body why it is needed. Bulk test files and defensive - programming for hypothetical failure modes are PR bloat, not rigor. + speculative defensive code unless the linked issue or stated task demands + them. When a test is genuinely necessary — it reproduces the reported + regression or guards behavior whose breakage would otherwise go unnoticed — + keep it minimal and state in the PR body why it is needed. Bulk test files + and defensive programming for hypothetical failure modes are PR bloat, not + rigor. 5. **Comments state constraints, not narration.** Write a comment only for a non-obvious constraint the code cannot express; never restate what the code does. @@ -44,13 +47,12 @@ the ownership evidence the AI-slop review evaluates (see real evidence instead: reproduction steps, failure output, targeted tests. 8. **Minimal diffs.** Match the surrounding code's style, naming, and comment density. Do not introduce new dependencies or restructure working code unless - the issue demands it. -9. **Disclose AI automation.** When an agent produces or co-produces a change, - append a footer line to the PR body with the model and effort used (e.g. - `Assisted by: GPT-5.6 High`). The PR template intentionally omits this line — - the agent adds it itself, humans are not asked to declare anything. Effort may - be omitted when the runtime does not report it. Disclosure is transparency - only; it does not substitute for any rule above. + the linked issue or stated task demands it. +9. **Disclose AI automation.** When an agent creates or co-creates a PR or Issue, + append a final body line with the model and effort used (e.g. + `Assisted by: GPT-5.6 High`). Effort may be omitted when the runtime does not + report it. Human contributors are not asked to disclose AI assistance. + Disclosure is transparency only and carries no positive or negative weight. 10. **Compiled workflows.** The AI-slop review policy in [pr-ai-slop-review.md](.github/workflows/pr-ai-slop-review.md) is compiled: after editing it, run `gh aw compile` and commit the regenerated @@ -59,8 +61,10 @@ the ownership evidence the AI-slop review evaluates (see [`template/Changelog.md`](template/Changelog.md): one line per user-visible change, no internals. -## Pull Request Shape +## Issue and PR Workflows -Describe three things, briefly: the problem (with issue link), why this approach -solves it, and what changed. See -[`PULL_REQUEST_TEMPLATE.md`](.github/PULL_REQUEST_TEMPLATE.md). +When drafting, triaging, splitting, or implementing from a community issue, use +the conditional checklist in [`.github/ISSUE_GUIDELINES.md`](.github/ISSUE_GUIDELINES.md). +Do not make GitHub issue changes without explicit authorization. For PRs, follow +the directly loaded [PR template](.github/PULL_REQUEST_TEMPLATE.md); do not +duplicate its prompts here. diff --git a/Cargo.lock b/Cargo.lock index 32c453c88..384c6330f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1119,7 +1119,7 @@ dependencies = [ "getrandom 0.4.3", "libc", "log", - "minisign-verify 0.3.0", + "minisign-verify", "nanoid", "network-interface", "objc2", @@ -1138,7 +1138,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml_ng", - "sha2 0.11.0", + "sha2 0.11.1", "smartstring", "socket2", "specta", @@ -1175,7 +1175,7 @@ dependencies = [ "windows-service", "windows-sys 0.61.2", "winreg 0.56.0", - "zip 9.0.1", + "zip 9.0.4", ] [[package]] @@ -1247,7 +1247,7 @@ dependencies = [ "once_cell", "serde", "serde_json", - "sha2 0.11.0", + "sha2 0.11.1", "strum", "strum_macros", "sysproxy", @@ -1462,9 +1462,12 @@ dependencies = [ [[package]] name = "constant_time_eq" -version = "0.4.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" +checksum = "9eb6feb175df65d0a3ccd3e73484e940d0ac664e8034f0519fd49a93db3071d9" +dependencies = [ + "rustversion", +] [[package]] name = "cookie" @@ -3951,9 +3954,9 @@ checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" [[package]] name = "libc" -version = "0.2.190" +version = "0.2.191" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" [[package]] name = "libdbus-sys" @@ -4048,11 +4051,11 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "lzma-rust2" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde178a3caf126c440fa15628147772d8ee590a39477d711353fbe2e58a73a5b" +checksum = "7277c9742229dc25243236d578b6471a798c554512c6f6a04365ec2e87c2a3bb" dependencies = [ - "sha2 0.11.0", + "sha2 0.11.1", ] [[package]] @@ -4167,12 +4170,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "minisign-verify" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" - [[package]] name = "minisign-verify" version = "0.3.0" @@ -5343,9 +5340,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppmd-rust" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" +checksum = "196a7c80b9a7652aba7cc070827516c2abe4ccdf53d128e1944003cf5726cff1" [[package]] name = "ppv-lite86" @@ -5996,12 +5993,13 @@ dependencies = [ [[package]] name = "rust-ini" -version = "0.21.3" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f" dependencies = [ "cfg-if", "ordered-multimap", + "trim-in-place", ] [[package]] @@ -6067,9 +6065,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.45" +version = "0.23.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" dependencies = [ "aws-lc-rs", "log", @@ -6410,9 +6408,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.151" +version = "1.0.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" dependencies = [ "itoa", "memchr", @@ -6597,9 +6595,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" dependencies = [ "cfg-if", "cpufeatures 0.3.0", @@ -6622,7 +6620,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" dependencies = [ "libc", - "sigchld", "windows-sys 0.60.2", ] @@ -6642,17 +6639,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" -[[package]] -name = "sigchld" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" -dependencies = [ - "libc", - "os_pipe", - "signal-hook 0.3.18", -] - [[package]] name = "signal-hook" version = "0.1.17" @@ -6663,16 +6649,6 @@ dependencies = [ "signal-hook-registry", ] -[[package]] -name = "signal-hook" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" -dependencies = [ - "libc", - "signal-hook-registry", -] - [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -6878,6 +6854,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "static_vcruntime" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff7589a1859b09232522bb9edf076ca7fcf14ded96685d3fa629db73023ffbb7" + [[package]] name = "string_cache" version = "0.9.0" @@ -6937,9 +6919,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "swift-rs" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e" +checksum = "cefd87076fd385308ee4aff597256c902f1fbb4d031de1558efc12f8ffefadd9" dependencies = [ "base64 0.21.7", "serde", @@ -7186,9 +7168,9 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "tauri" -version = "2.12.1" +version = "2.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed99ee9694a2deb776d91cae48ac7411ddfc89ecae2f9b5041111d8c88f2ace9" +checksum = "f316751027fc2644221d1a448564d519efb07067fda584879d9cda966391d212" dependencies = [ "anyhow", "bytes", @@ -7234,16 +7216,16 @@ dependencies = [ "tray-icon", "url", "webkit2gtk", - "webview2-com 0.39.1", + "webview2-com", "window-vibrancy", "windows 0.62.2", ] [[package]] name = "tauri-build" -version = "2.7.1" +version = "2.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59563ca5b331fd97f27ee9672ec8d36d33bb3c9e2c9f6710a1a5687256052e45" +checksum = "641dd5fd297c99ab92717b0428cca5b9c6f960b2ac96ca131b3eebc9d70b8ddc" dependencies = [ "anyhow", "cargo_toml", @@ -7255,6 +7237,8 @@ dependencies = [ "semver 1.0.28", "serde", "serde_json", + "static_vcruntime", + "strsim", "tauri-utils", "tauri-winres", "walkdir", @@ -7262,11 +7246,11 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "2.7.1" +version = "2.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e50213e463086ecc283d7e9f51c30cc0fd304ad457425158018be26dbf0d0a80" +checksum = "a6585c5b536c00f039d12ce70a9755172d5423bbcfd3969863baa70314c3e02f" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "brotli", "ico", "json-patch", @@ -7289,9 +7273,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.7.1" +version = "2.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8129fb43bb6e0d6b73ad3315cd172beaab17cd28899c594b1b32c80edbbfd99c" +checksum = "e2e83a0cfa1bae376e8fbbdc0b1692707aed50cd6072ce85cae4789af5d6c19f" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -7347,9 +7331,9 @@ dependencies = [ [[package]] name = "tauri-plugin-clipboard-manager" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "333d3b6beeb1c2c7fc040233d4ea7b6c03a9f8f039592b7400833c43fb5b568a" +checksum = "9be5ed8d94c33afe8377e885119069f6508780659271d0a25cfc54d441f2bc81" dependencies = [ "arboard", "log", @@ -7362,9 +7346,9 @@ dependencies = [ [[package]] name = "tauri-plugin-deep-link" -version = "2.6.1" +version = "2.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac7a92a46ab5c88f44532ca50906d6e448a948d4ddf8c5376ab125e3260f736" +checksum = "1b3c64e24ebcbc63aaa2d88a9c51e2507084b46620baf39ecb0baa0738a08bb4" dependencies = [ "dunce", "plist", @@ -7410,9 +7394,9 @@ dependencies = [ [[package]] name = "tauri-plugin-dialog" -version = "2.8.1" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf9a5c92e39bdd84f22f6be9230e39d0c4e140345f71ffe6058dca4f71127cf" +checksum = "b7461aaacf1901915ea7b9f5cd74f8224a351ed96af0a62c07fce934003cb34b" dependencies = [ "log", "raw-window-handle", @@ -7428,9 +7412,9 @@ dependencies = [ [[package]] name = "tauri-plugin-fs" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "466eb32466fe5021b6728b06862f9ff14a9f7e1af890047654e03a96d7c0e1bb" +checksum = "5d58eb1d4984e535f47a642ffec09a2da6f5c18a12b5902d12126cb7626d628b" dependencies = [ "anyhow", "dunce", @@ -7452,9 +7436,9 @@ dependencies = [ [[package]] name = "tauri-plugin-global-shortcut" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bd2e1f725891a1613af8f25bf918ba3357654b0fd3f2d794c0ce76cbdd92ed2" +checksum = "93ff17919fe09852d269bd37b1d3d2e993b9dbb514afe7acbf3346c1d3627e2d" dependencies = [ "global-hotkey", "log", @@ -7467,9 +7451,9 @@ dependencies = [ [[package]] name = "tauri-plugin-http" -version = "2.8.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93e9d6b3319e101bc1be27cfa0fb4054d7784a12509c67ead0033b82180e5ed1" +checksum = "2c57e3451553f95a1db3bfed6a358a77ce5080b6399a242c6eadac113f97abd2" dependencies = [ "bytes", "cookie_store", @@ -7486,13 +7470,13 @@ dependencies = [ "thiserror 2.0.20", "tokio", "url", - "urlpattern 0.3.0", + "urlpattern", ] [[package]] name = "tauri-plugin-mihomo" version = "0.6.0" -source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo?branch=main#7a03d16f4d39b1bb885b7babe66b9e153e34b615" +source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo?rev=7a03d16f4d39b1bb885b7babe66b9e153e34b615#7a03d16f4d39b1bb885b7babe66b9e153e34b615" dependencies = [ "arc-swap", "clashmap", @@ -7516,9 +7500,9 @@ dependencies = [ [[package]] name = "tauri-plugin-notification" -version = "2.5.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533770889abb60a0407d5a32096182f0e6463f5a3dfaa675be94580aa14070e1" +checksum = "b939d7b19c8af1381a72ef8474b3aba2b56c1ddac2c23f0c8e96abbcf86b198b" dependencies = [ "log", "notify-rust", @@ -7531,13 +7515,14 @@ dependencies = [ "thiserror 2.0.20", "time", "url", + "zbus", ] [[package]] name = "tauri-plugin-opener" -version = "2.7.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d39404b2572927e0d3ed30a7ca226037f7508d22e79c96ee6ef0d42b6a4b3345" +checksum = "53810be1b4fed22bb6ac3f4cab5a2b80d9391d1af436bda145607532bc0bb219" dependencies = [ "dunce", "glob", @@ -7567,11 +7552,12 @@ dependencies = [ [[package]] name = "tauri-plugin-shell" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "094bdbc47e5efe36544ba21b3a53ab5c724150236cf55e6c9be46191deb6eab6" +checksum = "8567d42b2c7264355daa1df415ec77f52aa42e885924c86a969be2e920e05521" dependencies = [ "encoding_rs", + "libc", "log", "open", "os_pipe", @@ -7584,13 +7570,14 @@ dependencies = [ "tauri-plugin", "thiserror 2.0.20", "tokio", + "windows-sys 0.60.2", ] [[package]] name = "tauri-plugin-updater" -version = "2.13.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cb0b2ea3e85ca287990d3859a29cc5cb18024240fd78f62e027fb7963670f18" +checksum = "7259989d4fe11b2bf51f7d671642249b1837c4ae2da1430dc713ae38f98ceeec" dependencies = [ "base64 0.22.1", "dirs 7.0.0", @@ -7598,8 +7585,9 @@ dependencies = [ "futures-util", "http", "infer", + "libc", "log", - "minisign-verify 0.2.5", + "minisign-verify", "osakit", "percent-encoding", "reqwest 0.13.5", @@ -7621,9 +7609,9 @@ dependencies = [ [[package]] name = "tauri-plugin-wdio-webdriver" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52c5e97428174a52b7f4357bbc4535fcce52c6a06c8ee7a379502f7cda1b6045" +checksum = "4f76676d510d271d80abcf8b0552987b5b4f0eae5e3f906e9b5660d43c97c62e" dependencies = [ "async-trait", "axum", @@ -7647,9 +7635,9 @@ dependencies = [ "tracing", "uuid", "webkit2gtk", - "webview2-com 0.38.2", - "windows 0.61.3", - "windows-core 0.61.2", + "webview2-com", + "windows 0.62.2", + "windows-core 0.62.2", ] [[package]] @@ -7669,9 +7657,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.12.1" +version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a57a2a9b8c8b7fc00d35b510fe808ded7f9f1b44fc1cdda6aee38632ea5413" +checksum = "7c55766d99cea69055904d1735b93c2049302b2e09effd462120deb5581d78e9" dependencies = [ "cookie", "dpi", @@ -7688,15 +7676,15 @@ dependencies = [ "thiserror 2.0.20", "url", "webkit2gtk", - "webview2-com 0.39.1", + "webview2-com", "windows 0.62.2", ] [[package]] name = "tauri-runtime-wry" -version = "2.12.1" +version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e7e0c9f80a5130773cfabb6df4dc924518fb1b2c923726966804634b57d22" +checksum = "8d61892587aea2a85245bcaec98c6fe46268874c0a0deae5535fb71ca7b93eae" dependencies = [ "gtk", "http", @@ -7714,7 +7702,7 @@ dependencies = [ "tracing", "url", "webkit2gtk", - "webview2-com 0.39.1", + "webview2-com", "windows 0.62.2", "wry", ] @@ -7752,9 +7740,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.10.1" +version = "2.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ff55a614843b9a3f010f211df637c94df2607175b8d0f6e0f4a7c19e18621ee" +checksum = "362ab21cd4cc713f2a02b1a9b5688198b1541cfb6907c4281e9190e0f0ef4aa3" dependencies = [ "anyhow", "brotli", @@ -7783,7 +7771,7 @@ dependencies = [ "thiserror 2.0.20", "toml 1.1.6+spec-1.1.0", "url", - "urlpattern 0.6.0", + "urlpattern", "uuid", "walkdir", ] @@ -7902,7 +7890,7 @@ dependencies = [ "regex", "semver 0.11.0", "sha2 0.9.9", - "signal-hook 0.1.17", + "signal-hook", "terminfo", "termios", "thiserror 1.0.69", @@ -8073,9 +8061,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.53.2" +version = "1.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" +checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" dependencies = [ "bytes", "libc", @@ -8135,9 +8123,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.19" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" dependencies = [ "bytes", "futures-core", @@ -8500,6 +8488,12 @@ dependencies = [ "petgraph", ] +[[package]] +name = "trim-in-place" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daa60874b45b168f67f3f9ba1ee821ad77144d6fa19cef1cd1874a7450a948da" + [[package]] name = "triomphe" version = "0.1.16" @@ -8592,47 +8586,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[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 = "unicase" version = "2.9.0" @@ -8692,18 +8645,6 @@ dependencies = [ "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 = "urlpattern" version = "0.6.0" @@ -9095,20 +9036,6 @@ 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 0.38.2", - "windows 0.61.3", - "windows-core 0.61.2", - "windows-implement", - "windows-interface", -] - [[package]] name = "webview2-com" version = "0.39.1" @@ -9116,7 +9043,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6" dependencies = [ "webview2-com-macros", - "webview2-com-sys 0.39.1", + "webview2-com-sys", "windows 0.62.2", "windows-core 0.62.2", ] @@ -9132,17 +9059,6 @@ dependencies = [ "syn 2.0.119", ] -[[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.20", - "windows 0.61.3", - "windows-core 0.61.2", -] - [[package]] name = "webview2-com-sys" version = "0.39.1" @@ -9882,7 +9798,7 @@ dependencies = [ "url", "webkit2gtk", "webkit2gtk-sys", - "webview2-com 0.39.1", + "webview2-com", "windows 0.62.2", "windows-core 0.62.2", "windows-version", @@ -10150,9 +10066,9 @@ dependencies = [ [[package]] name = "zip" -version = "9.0.1" +version = "9.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e49ade69c4434c55dd651d36fe05f38e0578db948b773cbc01c48c4b5e560692" +checksum = "a954114b47a54df49f83d2722814e86b23d4bb1b54a1b90fb7d92b41619ddc22" dependencies = [ "aes", "bzip2", @@ -10202,27 +10118,27 @@ dependencies = [ [[package]] name = "zstd" -version = "0.13.3" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +checksum = "db3ae9bf90af6e40197bef767eeefc46ad6397420fe873830bd85e5fbbfb3c70" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "7.2.4" +version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +checksum = "ded520f16b29e1a9b771c722221b2b0a0398c2316d9bb3c42b458a5414d7434b" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" +version = "2.1.2+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +checksum = "481ada9c543b2323cbb7bbdbb5249177fbf0188ed959ed2c15e4b22421c84ed3" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 3dffbca78..8f52bdadb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,13 +61,13 @@ tracing-estuary = { git = "https://github.com/Tunglies/tracing-estuary.git", bra tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" } tracing = "0.1.44" -tauri = { version = "2.12.1" } -tauri-plugin-clipboard-manager = "2.4.1" +tauri = { version = "2.12.4" } +tauri-plugin-clipboard-manager = "2.5.0" parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] } anyhow = "1.0.104" chrono = "0.4.45" criterion = { version = "0.8.2", features = ["async_tokio"] } -tokio = { version = "1.53.2", features = [ +tokio = { version = "1.53.3", features = [ "rt-multi-thread", "macros", "time", @@ -88,7 +88,7 @@ rust_iso3166 = "0.2.0" smartstring = { version = "1.0.1" } serde = { version = "1.0.229" } -serde_json = { version = "1.0.151" } +serde_json = { version = "1.0.154" } serde_yaml_ng = { version = "0.10.0" } bitflags = { version = "2.13.2" } socket2 = "0.6.5" diff --git a/Changelog.md b/Changelog.md index a37b4132c..0110ee42a 100644 --- a/Changelog.md +++ b/Changelog.md @@ -3,8 +3,10 @@
🐞 修复问题 -- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置的问题 -- 修复「首页」网站测速失败后卡片一直显示测试中的问题 +- 修复托盘测速与代理页行为不一致的问题,移除托盘「测试延迟」入口 +- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置 +- 修复 首页 网站测速失败后卡片一直显示测试中 +- 修复 代理 页展开分组后不显示最新测速结果,需切换节点才刷新 **🐧 Linux** @@ -21,6 +23,7 @@
🚀 优化改进 +- 优化「链式代理」:开启前需确认网速影响提示,空面板新增「关闭」按钮 **🖥️ Windows** diff --git a/crates/clash-verge-i18n/locales/ar.yml b/crates/clash-verge-i18n/locales/ar.yml index 81dec23dc..523f49c55 100644 --- a/crates/clash-verge-i18n/locales/ar.yml +++ b/crates/clash-verge-i18n/locales/ar.yml @@ -70,8 +70,6 @@ tray: global: عام profiles: الملفات التعريفية proxies: وكلاء - testLatency: اختبار التأخير - testingLatency: جارٍ اختبار التأخير… systemProxy: وكيل النظام tunMode: وضع TUN closeAllConnections: إغلاق كل الاتصالات diff --git a/crates/clash-verge-i18n/locales/de.yml b/crates/clash-verge-i18n/locales/de.yml index 058f3b3d3..d8accd74b 100644 --- a/crates/clash-verge-i18n/locales/de.yml +++ b/crates/clash-verge-i18n/locales/de.yml @@ -70,8 +70,6 @@ tray: global: Global profiles: Profile proxies: Proxy - testLatency: Latenz testen - testingLatency: Latenz wird getestet… systemProxy: Systemproxy tunMode: TUN-Modus closeAllConnections: Alle Verbindungen schließen diff --git a/crates/clash-verge-i18n/locales/en.yml b/crates/clash-verge-i18n/locales/en.yml index 9db8ff3cd..d1b144b94 100644 --- a/crates/clash-verge-i18n/locales/en.yml +++ b/crates/clash-verge-i18n/locales/en.yml @@ -70,8 +70,6 @@ tray: global: Global profiles: Profiles proxies: Proxies - testLatency: Test Latency - testingLatency: Testing latency… systemProxy: System Proxy tunMode: TUN Mode closeAllConnections: Close All Connections diff --git a/crates/clash-verge-i18n/locales/es.yml b/crates/clash-verge-i18n/locales/es.yml index 85ad7d95d..119d630d7 100644 --- a/crates/clash-verge-i18n/locales/es.yml +++ b/crates/clash-verge-i18n/locales/es.yml @@ -70,8 +70,6 @@ tray: global: Global profiles: Perfiles proxies: Proxies - testLatency: Probar latencia - testingLatency: Probando latencia… systemProxy: Proxy del sistema tunMode: Modo TUN closeAllConnections: Cerrar todas las conexiones diff --git a/crates/clash-verge-i18n/locales/fa.yml b/crates/clash-verge-i18n/locales/fa.yml index 2a3453405..e5270cebd 100644 --- a/crates/clash-verge-i18n/locales/fa.yml +++ b/crates/clash-verge-i18n/locales/fa.yml @@ -70,8 +70,6 @@ tray: global: سراسری profiles: پروفایل‌ها proxies: پروکسی‌ها - testLatency: آزمایش تأخیر - testingLatency: در حال آزمایش تأخیر… systemProxy: پروکسی سیستم tunMode: حالت TUN closeAllConnections: بستن همه اتصال‌ها diff --git a/crates/clash-verge-i18n/locales/id.yml b/crates/clash-verge-i18n/locales/id.yml index 9c037b90d..316b3e523 100644 --- a/crates/clash-verge-i18n/locales/id.yml +++ b/crates/clash-verge-i18n/locales/id.yml @@ -70,8 +70,6 @@ tray: global: Global profiles: Profil proxies: Proksi - testLatency: Uji Latensi - testingLatency: Menguji latensi… systemProxy: Proksi Sistem tunMode: Mode TUN closeAllConnections: Tutup Semua Koneksi diff --git a/crates/clash-verge-i18n/locales/jp.yml b/crates/clash-verge-i18n/locales/jp.yml index 327383e8b..9817405e6 100644 --- a/crates/clash-verge-i18n/locales/jp.yml +++ b/crates/clash-verge-i18n/locales/jp.yml @@ -70,8 +70,6 @@ tray: global: グローバル profiles: プロファイル proxies: プロキシ - testLatency: 遅延テスト - testingLatency: 遅延をテスト中… systemProxy: システムプロキシ tunMode: TUN モード closeAllConnections: すべての接続を閉じる diff --git a/crates/clash-verge-i18n/locales/ko.yml b/crates/clash-verge-i18n/locales/ko.yml index 9fcaa6a79..241d0d327 100644 --- a/crates/clash-verge-i18n/locales/ko.yml +++ b/crates/clash-verge-i18n/locales/ko.yml @@ -70,8 +70,6 @@ tray: global: 글로벌 profiles: 프로필 proxies: 프록시 - testLatency: 지연 테스트 - testingLatency: 지연 테스트 중… systemProxy: 시스템 프록시 tunMode: TUN 모드 closeAllConnections: 모든 연결 닫기 diff --git a/crates/clash-verge-i18n/locales/pt.yml b/crates/clash-verge-i18n/locales/pt.yml index 5999d1b91..2373529ee 100644 --- a/crates/clash-verge-i18n/locales/pt.yml +++ b/crates/clash-verge-i18n/locales/pt.yml @@ -70,8 +70,6 @@ tray: global: Global profiles: Perfis proxies: Proxies - testLatency: Test Latency - testingLatency: Testing latency… systemProxy: Proxy do sistema tunMode: Modo TUN closeAllConnections: Fechar todas as conexões diff --git a/crates/clash-verge-i18n/locales/ru.yml b/crates/clash-verge-i18n/locales/ru.yml index 630366a8f..985b9d1e6 100644 --- a/crates/clash-verge-i18n/locales/ru.yml +++ b/crates/clash-verge-i18n/locales/ru.yml @@ -70,8 +70,6 @@ tray: global: Глобальный profiles: Профили proxies: Прокси - testLatency: Проверить задержку - testingLatency: Проверка задержки… systemProxy: Системный прокси tunMode: Режим TUN closeAllConnections: Закрыть все соединения diff --git a/crates/clash-verge-i18n/locales/tr.yml b/crates/clash-verge-i18n/locales/tr.yml index 3ced8c82d..0d0f0c945 100644 --- a/crates/clash-verge-i18n/locales/tr.yml +++ b/crates/clash-verge-i18n/locales/tr.yml @@ -70,8 +70,6 @@ tray: global: Küresel profiles: Profiller proxies: Vekil'ler - testLatency: Gecikmeyi Test Et - testingLatency: Gecikme test ediliyor… systemProxy: Sistem Vekil'i tunMode: TUN Modu closeAllConnections: Tüm Bağlantıları Kapat diff --git a/crates/clash-verge-i18n/locales/tt.yml b/crates/clash-verge-i18n/locales/tt.yml index 2f067cc07..c136061ae 100644 --- a/crates/clash-verge-i18n/locales/tt.yml +++ b/crates/clash-verge-i18n/locales/tt.yml @@ -70,8 +70,6 @@ tray: global: Глобаль profiles: Профильләр proxies: Проксилар - testLatency: Кичектерүне тикшерү - testingLatency: Кичектерү тикшерелә… systemProxy: Системалы прокси tunMode: TUN режимы closeAllConnections: Барлык тоташуларны ябу diff --git a/crates/clash-verge-i18n/locales/zh.yml b/crates/clash-verge-i18n/locales/zh.yml index dbb40569b..d87c1e2f0 100644 --- a/crates/clash-verge-i18n/locales/zh.yml +++ b/crates/clash-verge-i18n/locales/zh.yml @@ -70,8 +70,6 @@ tray: global: 全局 profiles: 订阅 proxies: 代理 - testLatency: 测试延迟 - testingLatency: 正在测试延迟… systemProxy: 系统代理 tunMode: TUN 模式 closeAllConnections: 关闭所有连接 diff --git a/crates/clash-verge-i18n/locales/zhtw.yml b/crates/clash-verge-i18n/locales/zhtw.yml index ccf2ab2ac..b6baa3adb 100644 --- a/crates/clash-verge-i18n/locales/zhtw.yml +++ b/crates/clash-verge-i18n/locales/zhtw.yml @@ -70,8 +70,6 @@ tray: global: 全域 profiles: 訂閱 proxies: 代理 - testLatency: 測試延遲 - testingLatency: 正在測試延遲… systemProxy: 系統代理 tunMode: 虛擬網路介面卡模式 closeAllConnections: 關閉所有連線 diff --git a/crates/tauri-plugin-clash-verge-sysinfo/Cargo.toml b/crates/tauri-plugin-clash-verge-sysinfo/Cargo.toml index aa5f1ba02..839fcbd04 100644 --- a/crates/tauri-plugin-clash-verge-sysinfo/Cargo.toml +++ b/crates/tauri-plugin-clash-verge-sysinfo/Cargo.toml @@ -15,7 +15,7 @@ serde = { workspace = true, features = ["derive"] } sysinfo = { version = "0.39.6", features = ["network", "system"] } [target.'cfg(not(windows))'.dependencies] -libc = "0.2.190" +libc = "0.2.191" [target.'cfg(windows)'.dependencies] deelevate = { workspace = true } diff --git a/package.json b/package.json index 9ef745150..ce9ba44b9 100644 --- a/package.json +++ b/package.json @@ -54,13 +54,13 @@ "@mui/icons-material": "^9.4.0", "@mui/material": "^9.4.0", "@tanstack/react-virtual": "^3.14.13", - "@tauri-apps/api": "2.12.1", - "@tauri-apps/plugin-clipboard-manager": "2.4.0", - "@tauri-apps/plugin-dialog": "2.8.0", - "@tauri-apps/plugin-fs": "2.6.0", - "@tauri-apps/plugin-http": "2.8.0", - "@tauri-apps/plugin-opener": "2.7.0", - "@tauri-apps/plugin-updater": "2.13.1", + "@tauri-apps/api": "2.12.4", + "@tauri-apps/plugin-clipboard-manager": "2.5.0", + "@tauri-apps/plugin-dialog": "2.8.3", + "@tauri-apps/plugin-fs": "2.7.1", + "@tauri-apps/plugin-http": "2.8.3", + "@tauri-apps/plugin-opener": "2.7.2", + "@tauri-apps/plugin-updater": "2.14.0", "ahooks": "^3.10.0", "cidr-block": "^2.3.0", "compare-versions": "^6.1.1", @@ -75,11 +75,11 @@ "monaco-yaml": "^5.5.1", "nanoid": "^6.0.1", "qrcode.react": "^4.2.0", - "react": "19.3.0", - "react-dom": "19.3.0", + "react": "19.3.1", + "react-dom": "19.3.1", "react-error-boundary": "6.1.6", "react-hook-form": "^7.88.0", - "react-i18next": "17.0.15", + "react-i18next": "17.0.19", "react-markdown": "10.1.0", "react-router": "^8.4.0", "rehype-raw": "^7.0.0", @@ -95,7 +95,7 @@ "@biomejs/biome": "^2.5.14", "@eslint-react/eslint-plugin": "^5.20.5", "@eslint/js": "^10.0.1", - "@tauri-apps/cli": "2.12.1", + "@tauri-apps/cli": "2.12.3", "@types/lodash-es": "^4.17.12", "@types/node": "^24.13.6", "@types/react": "19.3.0", @@ -103,7 +103,7 @@ "@types/validator": "^13.15.10", "@vitejs/plugin-legacy": "^8.2.3", "@vitejs/plugin-react": "^6.1.1", - "@wdio/tauri-service": "1.4.0", + "@wdio/tauri-service": "1.5.0", "adm-zip": "^0.6.1", "axios": "^1.20.0", "cli-color": "^2.0.4", @@ -123,7 +123,7 @@ "https-proxy-agent": "^9.1.0", "husky": "^9.1.7", "jiti": "^2.7.0", - "knip": "6.39.0", + "knip": "6.41.0", "lint-staged": "^17.5.1", "sass": "^1.104.1", "tar": "^7.5.22", @@ -133,7 +133,7 @@ "vite": "^8.3.0", "vite-plugin-svgr": "^5.2.0", "vitest": "^5.0.1", - "webdriverio": "10.0.0", + "webdriverio": "10.0.2", "ws": "8.22.0" }, "lint-staged": { @@ -146,5 +146,5 @@ ] }, "type": "module", - "packageManager": "pnpm@12.11.2" + "packageManager": "pnpm@12.14.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ceac328bc..187ebdd18 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,153 +7,153 @@ importers: configDependencies: {} packageManagerDependencies: pnpm: - specifier: 12.11.2 - version: 12.11.2 + specifier: 12.14.0 + version: 12.14.0 packages: - '@pnpm/exe.android-arm64@12.11.2': - resolution: {integrity: sha512-owLw13OfPkEoY+hLBNeeG+dCFhywr7q2d/EHPz4mNL2RhKnv8gNwMARkjkiDQu234aItVwFNDeGnmQXm+ikkAg==} + '@pnpm/exe.android-arm64@12.14.0': + resolution: {integrity: sha512-+iSh7x21C3A2hqc+Z0ZmbKTcn6YNlzrXM142PsnQH/dLj0DYxFzoBvc6XAnL/yfQ5nnH2ebBiRj/zVxr61fZbQ==} cpu: [arm64] os: [android] - '@pnpm/exe.android-x64@12.11.2': - resolution: {integrity: sha512-GYPkVebl4iokOA/4MPSpGoUVfFlsth7uqOCe1XiaiM23LAdvUOMJBObqHJijr89ZqjwHm9pO6vO45GRwJ2U2qA==} + '@pnpm/exe.android-x64@12.14.0': + resolution: {integrity: sha512-qT+0LqEr2wBVaxcG2T8cX/R8SSRz2Rvpp8nRR4fh8tnGUZRch87gFmsHUAoSQh7w6j1h0qTjrA384j4xi8frvw==} cpu: [x64] os: [android] - '@pnpm/exe.darwin-arm64@12.11.2': - resolution: {integrity: sha512-n9M4On66y8FDxUAKgMO1FXt0qihyG4e+56p3/F0uo8Bx9KplW8BwKVnXigzCJ8aNM6sVVPkOE6M9XXbeVIxnZA==} + '@pnpm/exe.darwin-arm64@12.14.0': + resolution: {integrity: sha512-tnrdUB8ecJoehuKKqi1YHCBVl61xtJ9XVGcx5EviC6fWO4DeGI8GtXrjvJxDBM3nuVjJeWistgsUp/N142x4sw==} cpu: [arm64] os: [darwin] - '@pnpm/exe.darwin-x64@12.11.2': - resolution: {integrity: sha512-oSW7n3sBT0c0cyk4kj10T2Y6Mpm+OFEiCAWozgX039tdoI6yqJF1uaFD3sw7ugLdu6VV3+uY0pa1TUZIPdxDWA==} + '@pnpm/exe.darwin-x64@12.14.0': + resolution: {integrity: sha512-vlBfGkruqQRsEHTW43cPwpYd7QlbIJ//cvpAdb3bVXkI8SgTPoXhCnc05m+ZNiPrrEkBsX/t7KHw9aXpkzPm3Q==} cpu: [x64] os: [darwin] - '@pnpm/exe.freebsd-x64@12.11.2': - resolution: {integrity: sha512-qbzc3KoDpa/Lkf8vs3jnDe6dUUbso+6eLIyssYFMiliLpYNxn8dirk11YoNnOQcdHVB50v5RFls1Yid6ttT5pg==} + '@pnpm/exe.freebsd-x64@12.14.0': + resolution: {integrity: sha512-Qg5rxBrGYl6Tfso+EyygPf84fY1Fzy55E/0or+iuYKtnf0vc0MlHJfVIlT7pBukMRntQHRik7hUi0Z4s7AlJsw==} cpu: [x64] os: [freebsd] - '@pnpm/exe.linux-arm64-musl@12.11.2': - resolution: {integrity: sha512-uMEYDwKRktGwANubQ+kYCwmhNwg3Ik6qK89ahsbQ5cP30R/5FVJb/T99vr9XRUBC/A7mZI6eeT92oyVas9ACyg==} + '@pnpm/exe.linux-arm64-musl@12.14.0': + resolution: {integrity: sha512-DRIe4yQkXBGA/PhVJdzlbnllxBdAtnMMvTVLLcGpsK5FvJz9kxD2Hyxtn68ovmNkVX06f6p45cQwfm3wx2jn1A==} cpu: [arm64] os: [linux] libc: [musl] - '@pnpm/exe.linux-arm64@12.11.2': - resolution: {integrity: sha512-6e8ZAQUwFXAWHHHv5FesWqf/kEK04wuitBv5f/gxzkC0DiBfaixr+f+6JQede8SF4Ji5LcMwVFJbzAtffpWxEg==} + '@pnpm/exe.linux-arm64@12.14.0': + resolution: {integrity: sha512-7joGShZ44Nk3WpNU+60JoOPjCXlgxhJAq11S2UiVtwCUDgTer1yTPPfxGW8pxaD6046actYLIYDbZNlubHWs0A==} cpu: [arm64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-ppc64@12.11.2': - resolution: {integrity: sha512-Ui8A4nepkVupx2VpuFfysEluMvWK6gTB5rgVts7Ss/+8dj0RD28x3F/seNeH/zSypgC6yKdV6qsKVQh/uffTuQ==} + '@pnpm/exe.linux-ppc64@12.14.0': + resolution: {integrity: sha512-gSoryn7ySdXd//gjgf3MvvA8ZerJayaSbuMZMtAWq4vb2/wlVEgpl0Kvqt54y58TRj533Qjq2txKgnxd4JyoAQ==} cpu: [ppc64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-riscv64@12.11.2': - resolution: {integrity: sha512-i1yLdQ5bu+wjQ+srtFoJAaY2gN/+oXHFdjul4llCeoDxHE0rTIG3R41NhHfCjyFPW4+CpbBT58933CL9sMOCnA==} + '@pnpm/exe.linux-riscv64@12.14.0': + resolution: {integrity: sha512-EMjew1H9WqQ7pL+tYqCPwiuB+0PeCiJRV7fyWeuFw0wwYUajFBjcaPO3IGAVu9sdmJDU+e+HYv/Zey6aKae0Vw==} cpu: [riscv64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-s390x@12.11.2': - resolution: {integrity: sha512-YS1/lhPjjTsN4QL50VaFZKRa3Uc/bDnhE8eKbJMFvuX06UuFS5YaiDg04APoZdfVSGqTfX27QAg7rfLMSWaqxA==} + '@pnpm/exe.linux-s390x@12.14.0': + resolution: {integrity: sha512-shYOj/SFoXD35fHe0pWr2JofwSXulC0BnEIXLj2QokDi0EF//AfKsui35yKIkTEl+s2VJybc3PO4iJNhKIj5Qw==} cpu: [s390x] os: [linux] libc: [glibc] - '@pnpm/exe.linux-x64-musl@12.11.2': - resolution: {integrity: sha512-7eIxQR05fvJxCujUSKp5KTtehJ7QqrgqP/HD1+BcB/SaV2Be4XI1PEHiOgA1zxsiMIeMvTF6E4G4dYHrFjeXRg==} + '@pnpm/exe.linux-x64-musl@12.14.0': + resolution: {integrity: sha512-btZiZ2Ot0llKK1ndSfA5i3IRX7/S0VrmzseNH/BJ4c+Cady+S81H6vvU1zByggHMyqQhC8OgxO5PiIY7D9bH0Q==} cpu: [x64] os: [linux] libc: [musl] - '@pnpm/exe.linux-x64@12.11.2': - resolution: {integrity: sha512-FK7QKe24AYgppJwO3mpooYptduuvIGqKJPi5ac7MdnWkjUy46JBfYhCW0grnkWKC8YOf0x+vGviuQqJEj3SpQQ==} + '@pnpm/exe.linux-x64@12.14.0': + resolution: {integrity: sha512-F6K8e/jWxiuEYJaQtRRUG9ynBSTEHa+t7Lf86MKuOO5UA7WVVMbi+oB8nY6iKW++ncWS2yG6T9faXZGxqNsfoQ==} cpu: [x64] os: [linux] libc: [glibc] - '@pnpm/exe.win32-arm64@12.11.2': - resolution: {integrity: sha512-PCTjVDdJb6VvraZxrEeFv6EqEA0hhIsAySx28e95J2Tqpi88hLpgOXgNqV0vpxq9A+b8GLQfyk/D+1UlJPv/YQ==} + '@pnpm/exe.win32-arm64@12.14.0': + resolution: {integrity: sha512-DOjlkVB6TtTKeAAe0hNZXn5S7YqESmo1Z6SGbR4BZNQ4isRg31XVHjX2RFTbY8gnZq68IwzyLmPqlEjMmYxofg==} cpu: [arm64] os: [win32] - '@pnpm/exe.win32-x64@12.11.2': - resolution: {integrity: sha512-8sgX86OvRjJ3yZs/TvGR2L31wH9ScPZ+f+geKI3ikuB0RRiK3i4LlTRO8OXN6pdRqj8epMde4312MCjj7JsJdA==} + '@pnpm/exe.win32-x64@12.14.0': + resolution: {integrity: sha512-/LSX6mew/sLf08XqcjemuklNKf1K/Ftpgj4//hK/HKzI2dKbmxDB27a/2OaQ5a0O3/3OFzlssXDziZsi+yVBwQ==} cpu: [x64] os: [win32] - pnpm@12.11.2: - resolution: {integrity: sha512-JaUWUqGWeFr18ovabf6NxYLr76SsHRgCIc4TkCMcX7dmN4IupLbkYzznSs3LCnf3Ja8RutOBd/3t2u6BipVcuQ==} + pnpm@12.14.0: + resolution: {integrity: sha512-wYum2HmT09Fky16JSPMrLAbuhUSH4GzY2GeBNC+z5btGNDZwNHEehoArqpw3vqkGzhkprQjXCITJfYAd8TOJpg==} engines: {node: '>=18.*'} hasBin: true snapshots: - '@pnpm/exe.android-arm64@12.11.2': + '@pnpm/exe.android-arm64@12.14.0': optional: true - '@pnpm/exe.android-x64@12.11.2': + '@pnpm/exe.android-x64@12.14.0': optional: true - '@pnpm/exe.darwin-arm64@12.11.2': + '@pnpm/exe.darwin-arm64@12.14.0': optional: true - '@pnpm/exe.darwin-x64@12.11.2': + '@pnpm/exe.darwin-x64@12.14.0': optional: true - '@pnpm/exe.freebsd-x64@12.11.2': + '@pnpm/exe.freebsd-x64@12.14.0': optional: true - '@pnpm/exe.linux-arm64-musl@12.11.2': + '@pnpm/exe.linux-arm64-musl@12.14.0': optional: true - '@pnpm/exe.linux-arm64@12.11.2': + '@pnpm/exe.linux-arm64@12.14.0': optional: true - '@pnpm/exe.linux-ppc64@12.11.2': + '@pnpm/exe.linux-ppc64@12.14.0': optional: true - '@pnpm/exe.linux-riscv64@12.11.2': + '@pnpm/exe.linux-riscv64@12.14.0': optional: true - '@pnpm/exe.linux-s390x@12.11.2': + '@pnpm/exe.linux-s390x@12.14.0': optional: true - '@pnpm/exe.linux-x64-musl@12.11.2': + '@pnpm/exe.linux-x64-musl@12.14.0': optional: true - '@pnpm/exe.linux-x64@12.11.2': + '@pnpm/exe.linux-x64@12.14.0': optional: true - '@pnpm/exe.win32-arm64@12.11.2': + '@pnpm/exe.win32-arm64@12.14.0': optional: true - '@pnpm/exe.win32-x64@12.11.2': + '@pnpm/exe.win32-x64@12.14.0': optional: true - pnpm@12.11.2: + pnpm@12.14.0: optionalDependencies: - '@pnpm/exe.android-arm64': 12.11.2 - '@pnpm/exe.android-x64': 12.11.2 - '@pnpm/exe.darwin-arm64': 12.11.2 - '@pnpm/exe.darwin-x64': 12.11.2 - '@pnpm/exe.freebsd-x64': 12.11.2 - '@pnpm/exe.linux-arm64': 12.11.2 - '@pnpm/exe.linux-arm64-musl': 12.11.2 - '@pnpm/exe.linux-ppc64': 12.11.2 - '@pnpm/exe.linux-riscv64': 12.11.2 - '@pnpm/exe.linux-s390x': 12.11.2 - '@pnpm/exe.linux-x64': 12.11.2 - '@pnpm/exe.linux-x64-musl': 12.11.2 - '@pnpm/exe.win32-arm64': 12.11.2 - '@pnpm/exe.win32-x64': 12.11.2 + '@pnpm/exe.android-arm64': 12.14.0 + '@pnpm/exe.android-x64': 12.14.0 + '@pnpm/exe.darwin-arm64': 12.14.0 + '@pnpm/exe.darwin-x64': 12.14.0 + '@pnpm/exe.freebsd-x64': 12.14.0 + '@pnpm/exe.linux-arm64': 12.14.0 + '@pnpm/exe.linux-arm64-musl': 12.14.0 + '@pnpm/exe.linux-ppc64': 12.14.0 + '@pnpm/exe.linux-riscv64': 12.14.0 + '@pnpm/exe.linux-s390x': 12.14.0 + '@pnpm/exe.linux-x64': 12.14.0 + '@pnpm/exe.linux-x64-musl': 12.14.0 + '@pnpm/exe.win32-arm64': 12.14.0 + '@pnpm/exe.win32-x64': 12.14.0 --- lockfileVersion: '9.0' @@ -177,52 +177,52 @@ importers: version: 0.5.0 '@dnd-kit/react': specifier: ^0.5.0 - version: 0.5.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + version: 0.5.0(react-dom@19.3.1(react@19.3.1))(react@19.3.1) '@emotion/react': specifier: ^11.14.0 - version: 11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0) + version: 11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0) '@emotion/styled': specifier: ^11.14.1 - version: 11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0) + version: 11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0) '@juggle/resize-observer': specifier: ^3.4.0 version: 3.4.0 '@monaco-editor/react': specifier: ^4.7.0 - version: 4.7.0(monaco-editor@0.57.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + version: 4.7.0(monaco-editor@0.57.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) '@mui/icons-material': specifier: ^9.4.0 - version: 9.4.0(@mui/material@9.4.0(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(@types/react@19.3.0)(react@19.3.0) + version: 9.4.0(@mui/material@9.4.0(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1))(@types/react@19.3.0)(react@19.3.1) '@mui/material': specifier: ^9.4.0 - version: 9.4.0(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + version: 9.4.0(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) '@tanstack/react-virtual': specifier: ^3.14.13 - version: 3.14.13(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + version: 3.14.13(react-dom@19.3.1(react@19.3.1))(react@19.3.1) '@tauri-apps/api': - specifier: 2.12.1 - version: 2.12.1 + specifier: 2.12.4 + version: 2.12.4 '@tauri-apps/plugin-clipboard-manager': - specifier: 2.4.0 - version: 2.4.0 + specifier: 2.5.0 + version: 2.5.0 '@tauri-apps/plugin-dialog': - specifier: 2.8.0 - version: 2.8.0 + specifier: 2.8.3 + version: 2.8.3 '@tauri-apps/plugin-fs': - specifier: 2.6.0 - version: 2.6.0 + specifier: 2.7.1 + version: 2.7.1 '@tauri-apps/plugin-http': - specifier: 2.8.0 - version: 2.8.0 + specifier: 2.8.3 + version: 2.8.3 '@tauri-apps/plugin-opener': - specifier: 2.7.0 - version: 2.7.0 + specifier: 2.7.2 + version: 2.7.2 '@tauri-apps/plugin-updater': - specifier: 2.13.1 - version: 2.13.1 + specifier: 2.14.0 + version: 2.14.0 ahooks: specifier: ^3.10.0 - version: 3.10.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + version: 3.10.0(react-dom@19.3.1(react@19.3.1))(react@19.3.1) cidr-block: specifier: ^2.3.0 version: 2.3.0 @@ -234,7 +234,7 @@ importers: version: 1.11.23 foxact: specifier: ^0.3.12 - version: 0.3.12(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + version: 0.3.12(react-dom@19.3.1(react@19.3.1))(react@19.3.1) foxts: specifier: ^5.9.1 version: 5.9.1 @@ -261,28 +261,28 @@ importers: version: 6.0.1 qrcode.react: specifier: ^4.2.0 - version: 4.2.0(react@19.3.0) + version: 4.2.0(react@19.3.1) react: - specifier: 19.3.0 - version: 19.3.0 + specifier: 19.3.1 + version: 19.3.1 react-dom: - specifier: 19.3.0 - version: 19.3.0(react@19.3.0) + specifier: 19.3.1 + version: 19.3.1(react@19.3.1) react-error-boundary: specifier: 6.1.6 - version: 6.1.6(@types/react@19.3.0)(react@19.3.0) + version: 6.1.6(@types/react@19.3.0)(react@19.3.1) react-hook-form: specifier: ^7.88.0 - version: 7.88.0(react@19.3.0) + version: 7.88.0(react@19.3.1) react-i18next: - specifier: 17.0.15 - version: 17.0.15(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3) + specifier: 17.0.19 + version: 17.0.19(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@6.0.3) react-markdown: specifier: 10.1.0 - version: 10.1.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0) + version: 10.1.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0) react-router: specifier: ^8.4.0 - version: 8.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + version: 8.4.0(react-dom@19.3.1(react@19.3.1))(react@19.3.1) rehype-raw: specifier: ^7.0.0 version: 7.0.0 @@ -294,7 +294,7 @@ importers: version: 4.0.1(supports-color@7.2.0) swr: specifier: ^2.5.1 - version: 2.5.1(react@19.3.0) + version: 2.5.1(react@19.3.1) tauri-plugin-mihomo-api: specifier: github:clash-verge-rev/tauri-plugin-mihomo#main version: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/7a03d16f4d39b1bb885b7babe66b9e153e34b615 @@ -318,8 +318,8 @@ importers: specifier: ^10.0.1 version: 10.0.1(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0)) '@tauri-apps/cli': - specifier: 2.12.1 - version: 2.12.1 + specifier: 2.12.3 + version: 2.12.3 '@types/lodash-es': specifier: ^4.17.12 version: 4.17.12 @@ -342,8 +342,8 @@ importers: specifier: ^6.1.1 version: 6.1.1(vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)) '@wdio/tauri-service': - specifier: 1.4.0 - version: 1.4.0(expect-webdriverio@5.7.0)(supports-color@7.2.0)(webdriverio@10.0.0(supports-color@7.2.0)(yauzl@2.10.0)) + specifier: 1.5.0 + version: 1.5.0(expect-webdriverio@6.1.0)(supports-color@7.2.0)(webdriverio@10.0.2(supports-color@7.2.0)(yauzl@2.10.0)) adm-zip: specifier: ^0.6.1 version: 0.6.1 @@ -402,8 +402,8 @@ importers: specifier: ^2.7.0 version: 2.7.0 knip: - specifier: 6.39.0 - version: 6.39.0 + specifier: 6.41.0 + version: 6.41.0 lint-staged: specifier: ^17.5.1 version: 17.5.1 @@ -432,8 +432,8 @@ importers: specifier: ^5.0.1 version: 5.0.1(@types/node@24.13.6)(vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)) webdriverio: - specifier: 10.0.0 - version: 10.0.0(supports-color@7.2.0)(yauzl@2.10.0) + specifier: 10.0.2 + version: 10.0.2(supports-color@7.2.0)(yauzl@2.10.0) ws: specifier: 8.22.0 version: 8.22.0 @@ -1599,124 +1599,124 @@ packages: '@octokit/types@18.0.0': resolution: {integrity: sha512-l6bAF43PNxkJp6g+W4PjoUSSkxHomXw2nOum5CTftJz1NlV3vu93NImgOYtLf6CbBUb5j+fiuzW0PPQ5JTSvZA==} - '@oxc-parser/binding-android-arm-eabi@0.150.0': - resolution: {integrity: sha512-oQef2Zu4Prz1KLKznz3HqZzU9uVoA5PMoDZuuLmqms7hKmKSAPzlaMnLllJq3t+rgKmfJJ2siPrpZfFrW06btw==} + '@oxc-parser/binding-android-arm-eabi@0.153.0': + resolution: {integrity: sha512-BcWu25dMwMjHpaDCEidmpAi7rGg1qZoihJeUUH9bD55pVZ0z4Wa5CQgqSTo0R41H81oozisQKW3WqvAN2m77IQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxc-parser/binding-android-arm64@0.150.0': - resolution: {integrity: sha512-B6ofpoFiAUwIZ0MJ2IgHPvZK8FAtL4qzSZRwOkAKIfxEobE1mQC8nDdiFZj7pUaJiVUVCsfkMsBJKedzO8rZvA==} + '@oxc-parser/binding-android-arm64@0.153.0': + resolution: {integrity: sha512-9l5rdp2DhRqVriBHsNpu5cEf+hizZ4B58rt93HzyQEBUgZiC6qe2oFpaAAnXGCxHGnQExI1s7S2G0QhFacUZ6w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxc-parser/binding-darwin-arm64@0.150.0': - resolution: {integrity: sha512-J+9IHKzx/bSz1JetOfD4zKXSK9sOm4/a7+0qomJODcTL6JsRXGeV/ZdkAPSIDydfFmWzYCraMlQEosSZEyBYkQ==} + '@oxc-parser/binding-darwin-arm64@0.153.0': + resolution: {integrity: sha512-EcpsyRXHhENDpekpG4QNaSZj2F/BAz4Gh4o89zZwNss86R8KyRSQ6sXbAj74Xyeb4z3j/yBZZ6joQZhraIWyfw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxc-parser/binding-darwin-x64@0.150.0': - resolution: {integrity: sha512-v6IPfcAcSYrBWXV5Tce1DmxDMXLhEYpIIWiRFPJopgCVscZdLaV4MRQOUxvL3usQlw+yrwYOjBFB9IwBx+jUiQ==} + '@oxc-parser/binding-darwin-x64@0.153.0': + resolution: {integrity: sha512-hlj80HUQy07/2lYp6rmxH+3u+H2ik3rjLR52CUdPApxmZCNxASDUf/+ykTzAHS/fehftmVMATKvPaWmuLHuasg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxc-parser/binding-freebsd-x64@0.150.0': - resolution: {integrity: sha512-AoR/4jD02HET0KO0yNT4nbTE+XJUxiO9jB1X/ycEjUE3WrIJ3IjV/Vf+gskhWLcqqeXfvNnkDtBR7epllAm88A==} + '@oxc-parser/binding-freebsd-x64@0.153.0': + resolution: {integrity: sha512-fWRcv2pMSh1qtez46MOu8vuUJ5fiaQZPHSphbWulbL7SHBMKfRgZl2PzjXZYQJbq7NEXyXbgIuIuUm+/Rmkv8w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxc-parser/binding-linux-arm-gnueabihf@0.150.0': - resolution: {integrity: sha512-A/hycCFjLUrCmLoL5O/vBp40ohlaXO1Ta3v5hqYZxicYFs129wZmgZJggcW4mYGYbZebQLhup+N8JjeQl8qwyw==} + '@oxc-parser/binding-linux-arm-gnueabihf@0.153.0': + resolution: {integrity: sha512-paZeUK1GXShl/bOJQET1/8AQxayFeuN4SIWAkgTRxE7ZQwv9uYSB2n0648smygP22N7jcI3zxAm0Z+ALNiIKxg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxc-parser/binding-linux-arm-musleabihf@0.150.0': - resolution: {integrity: sha512-pbqahg1Pkz7J4RKmXm30s/iQu99hv64ayXCu8P4p75HcEH5azOdR4eGqiB6lFGkFR3mMpzaYsSKLkS8oJbjmeQ==} + '@oxc-parser/binding-linux-arm-musleabihf@0.153.0': + resolution: {integrity: sha512-qFPp5NwMf4K1C0O+DCVGmcbOaYyYAq98Rvv9p4VSfb777iUwlJCqMUxf6QG9N5f5ls4TEX6LitOZHi+6GL6rYQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxc-parser/binding-linux-arm64-gnu@0.150.0': - resolution: {integrity: sha512-HV11aRbBQGwqv8bEo+K/6qr88uB4fEe1mhXncMhlVCrj+WpBSraxrUzHaPVmeQRU6x6x8v/3DuCbbo93rpp+fw==} + '@oxc-parser/binding-linux-arm64-gnu@0.153.0': + resolution: {integrity: sha512-oufEhZCX7saeo27Logra8cvBR1jQ/yw2zUI2hHmWw3qA/XYqEW31FOvLQBv7DWN32NjVdlcaP11WBBZTJo4k9g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-arm64-musl@0.150.0': - resolution: {integrity: sha512-k6pVkJqALwtEuP4zukVhGRhdIy4+ofChUIUUsAHHyqDZlNsknmel3JjbggrJiWGaes6h/dIcWmEXsKW4SmK9oQ==} + '@oxc-parser/binding-linux-arm64-musl@0.153.0': + resolution: {integrity: sha512-zXYzuAqng85h6+LI3iRm0zeMLX1/K2z41ZAMWxVRgt0D10VvZUaV5q+TEB46KDhoKBpdexoTLC6QoB/EiP2oqQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxc-parser/binding-linux-ppc64-gnu@0.150.0': - resolution: {integrity: sha512-tEFg39mw/rHO5n8GK2DR4ZMFfsPQZtnQIPbsIpjoQRomJc/2tRfsCSmCT6gNit+NZGoeJG5aH9ATDH5bf0WnoQ==} + '@oxc-parser/binding-linux-ppc64-gnu@0.153.0': + resolution: {integrity: sha512-J7+NLwLEzdkKM9cKyPHH/AjXXstWOs98KYgLrPPnO4N9MO8EZRBfPNnBcS3S67uMOBkRpUmCl2FhRnWdQ5CbBA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-riscv64-gnu@0.150.0': - resolution: {integrity: sha512-0JO7IFkoek6HqV589Menx3hJySNXi6quRhO4tq2P7kpEHKEXoDATnoPVUpn5B7gDH2KLkdo751N0uIrGJFCTCw==} + '@oxc-parser/binding-linux-riscv64-gnu@0.153.0': + resolution: {integrity: sha512-sD0K11GQTLWylln54nPMMTtgmBbQrR5It/vMpZpy/fogiw8O2AvIE+9LuWUXddrWk9zvFIHGRcSgFn4IHOfoYg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-riscv64-musl@0.150.0': - resolution: {integrity: sha512-7XPzREnyAS5wHU9aB+49Uaqgoo1gVCklHc3DRlc3fJy2tXD/eAJFN1QFz/crj+Ulup5P1+axNREF+/RGaB/IRA==} + '@oxc-parser/binding-linux-riscv64-musl@0.153.0': + resolution: {integrity: sha512-7L66uql2BhvSz51LwfExjOdn6et/ydTiQxl23VApdKzJhTc3TczA1Qt6VR/UcvlQrL2ZbytIJb447vh7p5wmUw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxc-parser/binding-linux-s390x-gnu@0.150.0': - resolution: {integrity: sha512-7n7ZxcbRWDFaTdyj8M8p0O9OfzoMKm8O6syBAIgcSutbOALq0Bb9G52Me+XH0anMLZV+NgXc726gqgG2q39vcQ==} + '@oxc-parser/binding-linux-s390x-gnu@0.153.0': + resolution: {integrity: sha512-hXV3T7mNyrPR19rbPYkglkQJtoM/UNo/xwbZeeqdAxEg8PXgb4LXXuxPpGgI4bzRRCWwLUxdDSS58xIyC2u1TA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-x64-gnu@0.150.0': - resolution: {integrity: sha512-Vx0GSA9ZCRTUiczoEQnIIyXkMvtEY8uc6IiwLQtMe5ci2sXPqjrry0Ek5fsPP1k2kCzkML3ow9UOOEgnEDi7Bw==} + '@oxc-parser/binding-linux-x64-gnu@0.153.0': + resolution: {integrity: sha512-Da8Vqs1xdtoDYtEjmozD7wGgJeLBQkMbR0WUBECOSxodyX5DXy8ySlvJWbukyO2Q0+TM9EK+4LT6tbq0jCg4hQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-x64-musl@0.150.0': - resolution: {integrity: sha512-ii4/9m3viDLssMnfXU7/Pni/3nYplApuGayceX4qsVGaqqQJCx3tHIH9M/HWIeSty5i8LjS21zPYT6lVIkwLxw==} + '@oxc-parser/binding-linux-x64-musl@0.153.0': + resolution: {integrity: sha512-zGAPxNTIIw6X5WyTRscIFE/oDjpdNtrrl32ing6NOXdGjzRucadnxBilexLdeYQ9YxixF93GujpXyQ0jw7ddSg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxc-parser/binding-openharmony-arm64@0.150.0': - resolution: {integrity: sha512-ZtoxX5rez36ZWkwtiEhjkCPnlTPoQ2I7lIL0IYZ1yjxMYohbvoOjBmUIZzrf9W/HouONq0omIfTeCWEY+R380w==} + '@oxc-parser/binding-openharmony-arm64@0.153.0': + resolution: {integrity: sha512-GoHzpqP5XyPIp9DHokoEdCp+K4UPl/h1neFj1uXnUB3pJYyOQ7NOhFVOttNUgCGcj2eywAvu5cRivTOSq4S10g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxc-parser/binding-win32-arm64-msvc@0.150.0': - resolution: {integrity: sha512-VqeRb5JX/bKYBrff7TUDvJyKY0914UzuCkuXIGxJS4FUmvMNfqkCbc7Sq90iMz9DlmAtlggwaBYQ9eg3h3ltiw==} + '@oxc-parser/binding-win32-arm64-msvc@0.153.0': + resolution: {integrity: sha512-xGcy12tO9k7zFbeyQzEuilfWfQpyRq23/fLPfN3wjSWuWv58k46fQT1hsO1KFNykZuVGW9xSJE/z592TmAoSDA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxc-parser/binding-win32-ia32-msvc@0.150.0': - resolution: {integrity: sha512-EPqJfeZ4Pgg2BJSsCV8GozxV0FDltRzpVtGVa1r2noJu1iu+opOw3gtBWXwIo9odCT/MdFfyHxdy0/CRqs3OqA==} + '@oxc-parser/binding-win32-ia32-msvc@0.153.0': + resolution: {integrity: sha512-o1ykUjWncvXX6N5a4KebgLjtD8zRui+f1llMhSGoSC9oP2KOqapRpdwmZ3NcNG0axFouwVKmhGBjd+IhA8BksA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxc-parser/binding-win32-x64-msvc@0.150.0': - resolution: {integrity: sha512-n5YMzbqwPQqozbkrexi0lNZrUz5cnPHalYpFWe6Dau7AmKIWNo+y24IwzDuZEQtEdUHuKhXmMMih/MPjOI+CMw==} + '@oxc-parser/binding-win32-x64-msvc@0.153.0': + resolution: {integrity: sha512-gxQz7G96Up136fd+2zWrP+o6FYGs9NO4Dyv55IoeJFJUDwLFHcLYY11hhA5wQKs0HoM57Q8esQ3nEVhnCdsjAQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1724,6 +1724,9 @@ packages: '@oxc-project/types@0.150.0': resolution: {integrity: sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==} + '@oxc-project/types@0.153.0': + resolution: {integrity: sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==} + '@oxc-resolver/binding-android-arm-eabi@11.24.2': resolution: {integrity: sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA==} cpu: [arm] @@ -2126,102 +2129,102 @@ packages: '@tanstack/virtual-core@3.17.11': resolution: {integrity: sha512-+ILjvtHup6Y2hzQ6YzwMgX1Q+oQpxEGOXCEsCNaPoIP0VxMbizIBTmYTDtkerkIQS8/CbP1BRuyt8V/8BCsy1g==} - '@tauri-apps/api@2.12.1': - resolution: {integrity: sha512-DeyFHa3wynpyoqTDikDEDGTJIq4LQ5USfolQGRmGIWT6JMADyxZBTDa5cAdT3tDg73rUXufPaCwN7aXBos4OnQ==} + '@tauri-apps/api@2.12.4': + resolution: {integrity: sha512-pVmfis+4zT8H0Qhp3xPZx9Ju7kpzvA6rr/PSJPyuwfvucTQ8sVt61NYU9KMkK703UFlYadFmjKA4HhybOtN0gg==} - '@tauri-apps/cli-darwin-arm64@2.12.1': - resolution: {integrity: sha512-8bMnbpJ2jKO/ufrOPL65EMHC3ptw7fzeraoQkwibyfGLqIl+UCWsC0152kmNbW6r9NvVQduXqAZF5mH1QgTWhw==} + '@tauri-apps/cli-darwin-arm64@2.12.3': + resolution: {integrity: sha512-xmVNoJOOKzKLj7GLHXmXLimRby9M42aBIJa0sB80N7o24eKV5JiVmToKTRLVH9yhL05GZ/B6rSSOmbzNFJwBAA==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tauri-apps/cli-darwin-x64@2.12.1': - resolution: {integrity: sha512-G+2qte8GNqNlEZ9ftd21rbu/APVkl3r/nzNDI7LVxEoqJWVNe9GbEgQUMH1r4PHT20Z2UxR3KjCkltIb0e/Wjg==} + '@tauri-apps/cli-darwin-x64@2.12.3': + resolution: {integrity: sha512-URd+0usfmUQQyb5kNPiKt2OT8gp7oGU7QR+qOppB6e35mpVBFFVNhWc9oWb3p0u71fDJ09DpF4T6m9xYxoIB8g==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tauri-apps/cli-linux-arm-gnueabihf@2.12.1': - resolution: {integrity: sha512-5b3n4DaTW+A1Zntnh4Yn44VyLrlPEpB8kMOBRfWkzNnzolBk+AYhKIfKjCLmoC8Mob3ETodxAEOMkE2YPKEWzQ==} + '@tauri-apps/cli-linux-arm-gnueabihf@2.12.3': + resolution: {integrity: sha512-0kwUVx2LbmOoTRlwF1oDww7th4/NSluv4ZBQ6KRZsFKg4g0TlTcIiJ0VehqkQYUeC9xIT1Mk6XtF52mI155VSQ==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tauri-apps/cli-linux-arm64-gnu@2.12.1': - resolution: {integrity: sha512-APArHhDu+8pXSCkCQmvaz1nD3d8WTmlCoSS/oG475TJ6AATBvyQOAmf5qELgsQ4s2+8MOsop0Zd699RKELF1Cw==} + '@tauri-apps/cli-linux-arm64-gnu@2.12.3': + resolution: {integrity: sha512-gKSGDx6fx+HBLQmG6guChFdisbrCSdx9Yli+j5teOa64gm1CeM+C6NLru5Pj3ZkkEep05BmCFjxvX+cjqAycZQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@tauri-apps/cli-linux-arm64-musl@2.12.1': - resolution: {integrity: sha512-JaCxcLoV0oGtllVkLr4UAHWG/wTfkEunnUqnsEsGYOp0VRxnUwHg63wFBD0jU2ul5eFfSLIDS4RF2gfzsSo5Jg==} + '@tauri-apps/cli-linux-arm64-musl@2.12.3': + resolution: {integrity: sha512-Yr59qMy/gcvAfBuFdCGlAmGiLsYWMFDbsP4bAI6iNBzcJTGrYWZGeiQhGDo29MQwfF/NUBqyM2qljXu5tZ6wjQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@tauri-apps/cli-linux-riscv64-gnu@2.12.1': - resolution: {integrity: sha512-K5+VXM7+SGVDwZleA2+lMgvbTVuA5e1tkiVEawu1Yxj26ZyEfO8h8xTC1rBdRX2xgJDSwCk6vPUBCS6i+Z5slQ==} + '@tauri-apps/cli-linux-riscv64-gnu@2.12.3': + resolution: {integrity: sha512-jKr0/REBMnLKC0OwPQZkyge0mKDaNZ0b/AbfGwVyCgFfwS730EVLfVDo3nt7MUu6jJaytRq08+xSj5qfy+U1+Q==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] libc: [glibc] - '@tauri-apps/cli-linux-x64-gnu@2.12.1': - resolution: {integrity: sha512-Z1QGPr49HJZMktu+Spu6Q1b/f9ANadXvqZZZt2645ua1x1ev6prOgp85eiGv2jRzohFEMLo6aJyaLjLUX5KIvA==} + '@tauri-apps/cli-linux-x64-gnu@2.12.3': + resolution: {integrity: sha512-gTkEqJAGCMROGanxHnzaJzQ+/rXl39HWVg0/hDODS1wDWBGqoceqGC6Qm1+mQbyT11xnsB8mKu/gyVID/YpFIw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@tauri-apps/cli-linux-x64-musl@2.12.1': - resolution: {integrity: sha512-nHuEhRPOMSJ/8RRhtMRxB9B9lVr7xxpe4DapBz8x1GIau3vdzjHHJ1hAfmw+PyayPLZ6XoaVIHPgI17rX6ii8w==} + '@tauri-apps/cli-linux-x64-musl@2.12.3': + resolution: {integrity: sha512-T2dTxL1svRym71fZtaW4pZYXbthbfFsEJr1NeOUg/p7laWXZbUt/8DMSZf8recu9f7+1F/Lv00HtzsEu6vDO9w==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@tauri-apps/cli-win32-arm64-msvc@2.12.1': - resolution: {integrity: sha512-I0oPk2uGh86R8dAs2X5OxSP+/GqSc8jh1Px3l8EqrocSNivsRHDTh2k3PVOqdv163dlV6PIVvmhZAbH/lPfZrA==} + '@tauri-apps/cli-win32-arm64-msvc@2.12.3': + resolution: {integrity: sha512-eoiQwpEZWbpsVHcM9BAzqqe4TCHszvr3rsM7pUQ4ofLK0lF1i1d/jX0Qo5DYMqcrUtwQuDdDuOCbcT0Izf1VwQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tauri-apps/cli-win32-ia32-msvc@2.12.1': - resolution: {integrity: sha512-baNZRBfepAJ5qNFRMwzgBk94xQmkcmTZrNDjZjnzrq2vAaTC3NAhallb/zo3hyoCGQlMS13sdovktrg46NZz6Q==} + '@tauri-apps/cli-win32-ia32-msvc@2.12.3': + resolution: {integrity: sha512-kE+KUg6Dbbkgvcw0m99wJVIToFQzJqpMBAXah74WqxaLzssNwSTUccrHp6+QVzMhjQP330tvekFmnQF+0NbE6Q==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@tauri-apps/cli-win32-x64-msvc@2.12.1': - resolution: {integrity: sha512-dIFaKslrzcbesb+bBh+E1R9km241pHyj32w6D9FPtvQeYpRUFbiCLEa/2S+S0UnIytfxepp4P+X8IerGqnXeYg==} + '@tauri-apps/cli-win32-x64-msvc@2.12.3': + resolution: {integrity: sha512-L7wndWAgX2mLW7alswpL3d1R0P9FN2WYBgHaQkAYEBxHHGkY4vFX9CjcRQ1LAxDAMIJxrY7/aDziFJeChS84NQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tauri-apps/cli@2.12.1': - resolution: {integrity: sha512-kEDEiGzG+yAc5FeLxtXpES/VN+F2C8H0r4gDVtfRLKxT9np9101d9REG/Kgo2lr0hKi0IpDsODiHnU3+naOmLg==} + '@tauri-apps/cli@2.12.3': + resolution: {integrity: sha512-f7/LsY74SVb1ChmOPw1ajOpxii0GSzDAgmWHRci8JUYuuai04rXsSbFobpmB0sP2DdlvMD/B5ZotEn8GOGMRUg==} engines: {node: '>= 10'} hasBin: true - '@tauri-apps/plugin-clipboard-manager@2.4.0': - resolution: {integrity: sha512-yAuTffXc/sm+4oYnrwxbUkaJXQMTesPJ9dim0ADcA31rpboyhAN2TGQqs9VCcPIpb60LFOeePkvhpP2I3xhejg==} + '@tauri-apps/plugin-clipboard-manager@2.5.0': + resolution: {integrity: sha512-dibUy1nKURYDXRMvjNk4deAbTZ5jPQ8wCE4nolt5xniryWgXQ4sK2GkFXoXlHbRdPzfZptZj95BRLfalA7m/2Q==} - '@tauri-apps/plugin-dialog@2.8.0': - resolution: {integrity: sha512-HSSD5C8UFE9ijD1wue84F7jjaIReFmj8AAPtS+xyjYRltJafIc4BiWHY7SJ6vbbew6u+YljEJHfyNfy5VwywXg==} + '@tauri-apps/plugin-dialog@2.8.3': + resolution: {integrity: sha512-Hd/yBfk2R5v2MFrz/0VArSXeh8xKLLXGunqSiTtcHKn1zNZwaKujuA0VSwUvNfwC4hDoF1PveS+gBDNe3nHspw==} - '@tauri-apps/plugin-fs@2.6.0': - resolution: {integrity: sha512-MhBAZ4rryd+h9fijd/GWDfArqiAeGI0ZNduV4wuRyUVI6wDZ+oDBY7atqtoXKDwlTUVSvpvmWJAY0ehQd/v6Uw==} + '@tauri-apps/plugin-fs@2.7.1': + resolution: {integrity: sha512-4TBVjpi/q8PuNzjpkAc2iBGXMTLjOrAyfTiiOL8ORaJCZJRG3tjzmQ4sqs2CMG75FvLyRkOkERfP7AyY4iBNwQ==} - '@tauri-apps/plugin-http@2.8.0': - resolution: {integrity: sha512-cPvZvfUSaBkqGxef/dgbh/iEl6T3jQfSJyypi9OdcWr1VcuTt4ZRlk9DLAHEHF2q2NML+0UOkjyXRPNo6Cky6g==} + '@tauri-apps/plugin-http@2.8.3': + resolution: {integrity: sha512-MrgCnnxSnzxkMX+Tk1XXB8rSI9hfhUHQmtbn0EpflEDcSx04K1BDfrv51tcK27SCuU/B1j8sNHL0UZur/1iqKA==} - '@tauri-apps/plugin-opener@2.7.0': - resolution: {integrity: sha512-mBorYfVKh9Lt7s4ZFtKbMUTxBw+Y/sVmSlKCtkkoBgMTC/DIbt/kaPm67L/2djAUqDI1wbixy5OHHVmipjuGMw==} + '@tauri-apps/plugin-opener@2.7.2': + resolution: {integrity: sha512-LyybP1VSzTMQiaFYcsbAse47KpivlyXdLZMQzGLEnkgQacGESnpjAfFoQ6h5sqWgAdZDH9szYjPzHMpv0Sz+TQ==} - '@tauri-apps/plugin-updater@2.13.1': - resolution: {integrity: sha512-+STDzJ0sdQLImjc63zkv6Nncimqeo6jyCWoTQtbSwgGLNj1qbP9I6LC4p5R5ocBk3gNKAZs6i/AJz0jxysytiw==} + '@tauri-apps/plugin-updater@2.14.0': + resolution: {integrity: sha512-gdylJ9tger4hkyOn24QZd6MNOKVo2Eh53iw5th2Qj1vKTR/+u4PIwJWuiis5hohYvobn6iOvlnALfrqIPSzh7Q==} '@tybys/wasm-util@0.10.4': resolution: {integrity: sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==} @@ -2563,16 +2566,16 @@ packages: '@vscode/l10n@0.0.18': resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} - '@wdio/config@10.0.0': - resolution: {integrity: sha512-g0xaQxKJM5hIhZBW05Ivs7vZi/txB+UcWeoiuzNl0/fS5rvQYvofnYlROvCBKeEdf72ooIRtfkRnW8nKQ97P+A==} + '@wdio/config@10.0.1': + resolution: {integrity: sha512-n6elHE4ItKAtr4lwAaWIh/sAFuEFhyesLM27i5l9IWBJEK6VECdt0Pigj/4vUD2tliOoBtsWxphSW4oiNFXotg==} engines: {node: '>=22.19.0'} - '@wdio/globals@9.29.1': - resolution: {integrity: sha512-F96BKppx4HGD64v+s57TM4K4zaxqUCg2RXHk6sjB2xrSa7P+d2VYV28ID2ddF7iSodVfPlpa1i2/jy8jMGrU8w==} + '@wdio/globals@9.31.3': + resolution: {integrity: sha512-ZzmRKUlwUBahNoNxHbAzst+9dNInEjLQc7Z3c86OhDRxAf3Ivw8k2rc56UCbyYiZnkd1dOpuA91zFiTrUNNORw==} engines: {node: '>=18.20.0'} peerDependencies: - expect-webdriverio: ^5.6.5 - webdriverio: ^9.0.0 + expect-webdriverio: ^6.0.9 + webdriverio: ^9.28.0 '@wdio/logger@10.0.0': resolution: {integrity: sha512-kUIw0ZzMs1mvXOaLEcjrM1nqNXY1reaJKReKNcjDSnfhnfUIb0Q5pAsxiMeil2uR0WsSUPzn0r3D9v//7vTOkA==} @@ -2582,59 +2585,59 @@ packages: resolution: {integrity: sha512-0ZAEIo6PNyMIJPlOGkIgyOJUjcd0pC8/QHlVAAe1c91/IcjZ1X+k0yidXHaboJdN7dq1XPUacmhRdtua0U5EZg==} engines: {node: '>=18.20.0'} - '@wdio/native-core@1.2.0': - resolution: {integrity: sha512-Z/Qa2xmV/2IMSCsCKyVk79tda8n47FOOOS9XEOWGpHUiFr1z4gGOFxt13Fd9RQu9Kk1IA9fJB1VmJZtrtkXvEw==} - engines: {node: ^18.12.0 || ^20.9.0 || >=22.11.0} + '@wdio/native-core@1.3.0': + resolution: {integrity: sha512-wUwc0QTdvhD0LwH4YcRaN9JWLX/yac8sovddUR5/4sqHMNJr3yaN11bpIfYoD/bEk+1nTLjWuDCFpe5l5wUgLQ==} + engines: {node: '>=18.20.0'} - '@wdio/native-spy@1.3.0': - resolution: {integrity: sha512-Kw1/HsOM8CNnlPHozlv0FP+nFnms5efICq5WqpaNdwa0urv/C9K/jhM69OqAbocBfRXkDVuWecNPmCV9S/cXGw==} - engines: {node: ^18.12.0 || ^20.9.0 || >=22.11.0} + '@wdio/native-spy@1.4.0': + resolution: {integrity: sha512-unsC2Vgu5T9atD478Zj/p2N+rVSK4i/NibIaaEw4rZF/T5u5bmAHChFntEiBEfyMzd62VwbhKhzwwOSKjzrfJA==} + engines: {node: '>=18.20.0'} - '@wdio/native-types@2.5.0': - resolution: {integrity: sha512-NjaIfcsEKpU9gm0goywEG0A7J4pyEsgORKNrQ1xboSy10BeTk5QpgHWP1bNvqUnZrKoKJRonpSn81ZyYKghO1g==} - engines: {node: ^18.12.0 || ^20.9.0 || >=22.11.0} + '@wdio/native-types@2.6.0': + resolution: {integrity: sha512-MVMVpZTH+UY6nUtLClCzY4BQSIs/nCCHo81i4ClLR+b2NehyBvwccttXDAGPgf0icyHUhd+PgIPXaLgRcm4M9A==} + engines: {node: '>=18.20.0'} - '@wdio/native-utils@2.7.0': - resolution: {integrity: sha512-qCzc+5o4YVESApQB5//NxomZ+F+M3Pc02hUNnJ1/w9naMb8mfgaHXBLWVwejEfO36z2MnwGatBNWZAm38G1frA==} - engines: {node: ^18.12.0 || ^20.9.0 || >=22.11.0} + '@wdio/native-utils@2.8.0': + resolution: {integrity: sha512-TmIkPAd19FkJ747AdFVEtawbakVV3HHCh8zk3Kl/FLc6B2bbea7hn90jtLxqBZgCVqUuJIBcMNFg/nV/hgBFOw==} + engines: {node: '>=18.20.0'} peerDependencies: tsx: ^4.22.4 peerDependenciesMeta: tsx: optional: true - '@wdio/protocols@10.0.0': - resolution: {integrity: sha512-426ENC79fH7N4ggoGRHhZBY/exkdHAndbBiXcs9pQkzGP356H8C7VkQUx8g78m4k1TUC0RhOl8Se8VE0Mutpqw==} + '@wdio/protocols@10.0.2': + resolution: {integrity: sha512-ewuQuu+niq/ZpV1TLn904nDA4F9n4LLu9X1U8Juwwm1ahq09dpEP8lEytDnpaD0f21uIx//mFBKmBAjSexMdEA==} engines: {node: '>=22.19.0'} '@wdio/repl@10.0.0': resolution: {integrity: sha512-wAC9ksdWWv2E3Sj86eNKxB3IUt0gEyw9rgDGAWjk/GWgi6K3e+/n3erGNweI9JMJyC/+RNe0qHgVPQpRJMRVpg==} engines: {node: '>=22.19.0'} - '@wdio/reporter@9.30.1': - resolution: {integrity: sha512-0whWaLfJtOAy3PzbGUxGrCAGitgaUCMe8qUgAuP2l7ess70AXqvp1wJibw74a/Z2giegJlJiAc4PxS0abs9/tw==} + '@wdio/reporter@9.31.2': + resolution: {integrity: sha512-3cKE6vCOsro/Ep9dGTIbo3FBn3WNvJ++BIn32lFd9m19pFdjuwlVgLtPXHb9jWNCJs740h5AIUcVkcscqUYhAA==} engines: {node: '>=18.20.0'} - '@wdio/spec-reporter@9.30.1': - resolution: {integrity: sha512-En74iLuj8xjNanSPA05rTCii0qHZQe6Zpwi74vTGO6LtfGxvcNr1Z1zUj8xWwnoclnAne3DtmCAyXTezLX1pEA==} + '@wdio/spec-reporter@9.31.2': + resolution: {integrity: sha512-lNLLPUk4y1Yp5pWwBFF+tjZIy7bOv0TPTk4HDWV4LnnHnkc24abI0nN+kb2y5/JV8ZgwLCCVKUFRdRUxfKsAaQ==} engines: {node: '>=18.20.0'} - '@wdio/tauri-service@1.4.0': - resolution: {integrity: sha512-EUTS8tfMOXK1yPNEaRd6yiSdPoX19zeV+gtp2m7UuPTWFZGayPuMUoFyttP6Lx3OFvJC3lnbxjr1vcCNAOA7tg==} - engines: {node: ^18.12.0 || ^20.9.0 || >=22.11.0} + '@wdio/tauri-service@1.5.0': + resolution: {integrity: sha512-S3fs8T+jgNOFi4x/X2z69unS/1K3ZB28+owBouhHtVnrMKeBWAKP46IHnp8c8AZXppn6fiPNC4M1jfRZbLKeKw==} + engines: {node: '>=18.20.0'} peerDependencies: webdriverio: ^9.0.0 - '@wdio/types@10.0.0': - resolution: {integrity: sha512-qUJbKEHjsN0PuC3S2r7Gy9wYtbChmX+PYghKGHYQhjyH9P9t+sWhSPQWUBk3S+IUTYT1f+Z4VbF+5FmvlHL6Lg==} + '@wdio/types@10.0.1': + resolution: {integrity: sha512-HGP9lAYXCsLAXzugfT4khCh7tYByWOMIxZHCG+tUvgmpwNzi6lfIsxyIlWownXK1wYJUpk6tkDBX/iXwN+q0lg==} engines: {node: '>=22.19.0'} - '@wdio/types@9.30.1': - resolution: {integrity: sha512-umKQDbSmu3/ucUsIA5UUTK5gCWnClyVpWz3fC39044rCXQ3zCd1a3GoJibbAITKiwnPFf2yU3LvTa+C78sh9Lg==} + '@wdio/types@9.31.2': + resolution: {integrity: sha512-lQKaiQDCJJ6VC1K/cKzaAxCpwB9pL1U0VbmX2uGksMxO4EDbNuLvIxWryuUDJ7OaSYTjxILTKYFErdbT/xEikg==} engines: {node: '>=18.20.0'} - '@wdio/utils@10.0.0': - resolution: {integrity: sha512-Ta3wq+u83DT0Vnz1QcuqkagBSwWBnTg8LCIQmDO6DGkMIiHXJrhUMfz6ibrlMJPslt4g0P/Ri5wm7WkJXo9HXw==} + '@wdio/utils@10.0.1': + resolution: {integrity: sha512-aYvTtF+7IYsvkFdXdu32Q2vexEaXYK79R1wLTnAXrT2Q7fp0oCDL0UgBx8Jgo5Tn98ZLWx4JlUeZsDRAIkV4Zw==} engines: {node: '>=22.19.0'} '@zip.js/zip.js@2.16.0': @@ -3415,13 +3418,13 @@ packages: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} - expect-webdriverio@5.7.0: - resolution: {integrity: sha512-jLOTrJoPBC3Wtd83ryHMbRcEajMGtAnn6OWtSnoJoDLt16nHvxVdjcI4Bc0n+1KAOr8DvQtlJ55f0M48kJtEpw==} + expect-webdriverio@6.1.0: + resolution: {integrity: sha512-RXgaXoH1U8IiAHPelkMhZSnWUT0HfIhkjKK8nBkIpnXRO+zJ8dRafoyZhVy5mu/LtoyIr9juRWRwN284OSNM6Q==} engines: {node: '>=20'} peerDependencies: '@wdio/globals': ^9.0.0 '@wdio/logger': ^9.0.0 - webdriverio: ^9.0.0 + webdriverio: ^9.28.0 expect@30.5.2: resolution: {integrity: sha512-0LNuMvs8/5mRYhnCR4k+lGV7fqPMs6Bnksda4S3zsCUmdxBpAn4zU4NNzXdq1pMKe/H4W5t/zVIDSJ/H3e0vDA==} @@ -3505,9 +3508,9 @@ packages: resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} - formatly@0.7.1: - resolution: {integrity: sha512-ESuCEJE0SGgabl9q+zABI8LhJDmF+8nhpuqDzuM48HfJ9QTD5o6PqFJDFZY7w6f8xsh5lixuj5nJsQNvYOWvKQ==} - engines: {node: '>=18.3.0'} + formatly@0.11.4: + resolution: {integrity: sha512-TeCC10lP8KCgoX+Fe5lryILIT0/Dy+nP0OSVnsuB5CwXoLuOdqxCYlPFF1inR8S/zTy1ec7TjhS2TeRKWzzImA==} + engines: {node: '>=18.14.0'} hasBin: true foxact@0.3.12: @@ -3863,8 +3866,8 @@ packages: keyv@5.6.0: resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} - knip@6.39.0: - resolution: {integrity: sha512-Y7g6Ve1ATMox1u1v+gx3z9LTlvzVLTJISm/LVn9TgBU4qc7OoC63AWGO8qCFPVDDdaqlYdXkDDvvO1METBfTRw==} + knip@6.41.0: + resolution: {integrity: sha512-DrhQlIuV6xScd5Q+WTF1znd1w41QzxFGCtAHXCajsH/ndGMotHoNYbdXbuFtzW3mIOmniHtZ7zCtvnQkUmJtQQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -4279,8 +4282,8 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - oxc-parser@0.150.0: - resolution: {integrity: sha512-zwajKw1GUa57IOdafIS7/yOObGF2NihwIfZfs5yJTRprMSkiwhWOc/k+n8bwnUPbHr2/XsjfBO8KyTPaW3VpWg==} + oxc-parser@0.153.0: + resolution: {integrity: sha512-cEnpyyV1OJRXv++SFNw0z8i+2o/cr88rVLaOYJafELDoHM5yHvCAbwGRLvxuany6SNLcbblSMUyZaGzH+quSYg==} engines: {node: ^20.19.0 || >=22.12.0} oxc-resolver@11.24.2: @@ -4432,10 +4435,10 @@ packages: query-selector-shadow-dom@1.0.1: resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==} - react-dom@19.3.0: - resolution: {integrity: sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==} + react-dom@19.3.1: + resolution: {integrity: sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==} peerDependencies: - react: ^19.3.0 + react: ^19.3.1 react-error-boundary@6.1.6: resolution: {integrity: sha512-CDXPnXDGyFIbkwaaJ6u+xgsRmJhSi6YdgUDW1vnyKHfXp1a9pfAlM+ZET2CDu80/A+8iRcmXN0NSY9BCCoWP5A==} @@ -4455,8 +4458,8 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - react-i18next@17.0.15: - resolution: {integrity: sha512-7uYpx/oBkesLto7cOzOmfITlUBaXHCAcFahmgETMfWr8nbbWRRnVGvz8wsH/xpbNcyoH+NpCcnOQEqWPMO6/hw==} + react-i18next@17.0.19: + resolution: {integrity: sha512-BRHqiqOfW4J/LBVtbBFHoBoZm/Ez2/tkHPitYtn0CSpkYhEaEAsZMPsg7hZlomu30UJYQ6ZKiP3pwa3QnxGvQw==} peerDependencies: i18next: '>= 26.2.0' react: '>= 16.8.0' @@ -4502,8 +4505,8 @@ packages: react: '>=16.6.0' react-dom: '>=16.6.0' - react@19.3.0: - resolution: {integrity: sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==} + react@19.3.1: + resolution: {integrity: sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==} engines: {node: '>=0.10.0'} readable-stream@2.3.8: @@ -4654,8 +4657,8 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - smol-toml@1.8.0: - resolution: {integrity: sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ==} + smol-toml@1.9.1: + resolution: {integrity: sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ==} engines: {node: '>= 18'} snake-case@3.0.4: @@ -4877,8 +4880,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - unbash@4.0.11: - resolution: {integrity: sha512-FoSOKV7NEofQSkAefMVHam4ZPKYMxjAydxiV72UFEDNV/YofxjGfiZ2A9pZjdL/lRJzTjcu4PABo1JYJX8N5iQ==} + unbash@5.1.0: + resolution: {integrity: sha512-YVGyy4XqoZ2AVgHjVdL9cchMSurlXFHWviORa8RiU/Bnol4ooW5Lpyur/5XmbgDdyCKxymmgsEQsLGBMIJ/Agg==} engines: {node: '>=14'} undici-types@6.21.0: @@ -5096,12 +5099,12 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} - webdriver@10.0.0: - resolution: {integrity: sha512-EyMSQjY+Nq9mAUTCzzW1hjarwHNJWtCBRtGSZkiTcdJdFZ6siKF0Z01hHJFVkF1Xm+aCkFxD1atm1S2cNxCQxA==} + webdriver@10.0.2: + resolution: {integrity: sha512-q2eKiapaOyCH2QfVykBjrYznQiL4gjmGakTYVzQqq3Ycqpd2VPOMSkmbxGHFo8+CpO628QwvpxgKvAxA3hpDaA==} engines: {node: '>=22.19.0'} - webdriverio@10.0.0: - resolution: {integrity: sha512-DiDflg731z/b+qg4EmVAGihMJSWhHJYhlwrS6Q1uSFqL4c48+xHDj9VjhQr7sFzZ/MBJySjGZfMBGyjP7p547Q==} + webdriverio@10.0.2: + resolution: {integrity: sha512-o+T+DXwsn2e0niAmz9PWUMDwE/8R06euZ4O85faB6fGG9X/uVOom8sloIH+SYc1tI0fjcI34sU9hptrQj9fDdQ==} engines: {node: '>=22.19.0'} peerDependencies: puppeteer-core: '>=24 <26' @@ -5986,13 +5989,13 @@ snapshots: '@dnd-kit/abstract': 0.5.0 tslib: 2.8.1 - '@dnd-kit/react@0.5.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + '@dnd-kit/react@0.5.0(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': dependencies: '@dnd-kit/abstract': 0.5.0 '@dnd-kit/dom': 0.5.0 '@dnd-kit/state': 0.5.0 - react: 19.3.0 - react-dom: 19.3.0(react@19.3.0) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) tslib: 2.8.1 '@dnd-kit/state@0.5.0': @@ -6064,17 +6067,17 @@ snapshots: '@emotion/memoize@0.9.0': {} - '@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0)': + '@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0)': dependencies: '@babel/runtime': 7.29.7 '@emotion/babel-plugin': 11.13.5(supports-color@7.2.0) '@emotion/cache': 11.14.0 '@emotion/serialize': 1.3.3 - '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.3.0) + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.3.1) '@emotion/utils': 1.4.2 '@emotion/weak-memoize': 0.4.0 hoist-non-react-statics: 3.3.2 - react: 19.3.0 + react: 19.3.1 optionalDependencies: '@types/react': 19.3.0 transitivePeerDependencies: @@ -6090,16 +6093,16 @@ snapshots: '@emotion/sheet@1.4.0': {} - '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0)': + '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0)': dependencies: '@babel/runtime': 7.29.7 '@emotion/babel-plugin': 11.13.5(supports-color@7.2.0) '@emotion/is-prop-valid': 1.4.0 - '@emotion/react': 11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0) + '@emotion/react': 11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0) '@emotion/serialize': 1.3.3 - '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.3.0) + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.3.1) '@emotion/utils': 1.4.2 - react: 19.3.0 + react: 19.3.1 optionalDependencies: '@types/react': 19.3.0 transitivePeerDependencies: @@ -6107,9 +6110,9 @@ snapshots: '@emotion/unitless@0.10.0': {} - '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.3.0)': + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.3.1)': dependencies: - react: 19.3.0 + react: 19.3.1 '@emotion/utils@1.4.2': {} @@ -6409,54 +6412,54 @@ snapshots: dependencies: state-local: 1.0.7 - '@monaco-editor/react@4.7.0(monaco-editor@0.57.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + '@monaco-editor/react@4.7.0(monaco-editor@0.57.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': dependencies: '@monaco-editor/loader': 1.7.0 monaco-editor: 0.57.0 - react: 19.3.0 - react-dom: 19.3.0(react@19.3.0) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) '@mui/core-downloads-tracker@9.4.0': {} - '@mui/icons-material@9.4.0(@mui/material@9.4.0(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(@types/react@19.3.0)(react@19.3.0)': + '@mui/icons-material@9.4.0(@mui/material@9.4.0(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1))(@types/react@19.3.0)(react@19.3.1)': dependencies: '@babel/runtime': 7.29.7 - '@mui/material': 9.4.0(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) - react: 19.3.0 + '@mui/material': 9.4.0(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + react: 19.3.1 optionalDependencies: '@types/react': 19.3.0 - '@mui/material@9.4.0(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + '@mui/material@9.4.0(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': dependencies: '@babel/runtime': 7.29.7 '@mui/core-downloads-tracker': 9.4.0 - '@mui/system': 9.4.0(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.0) + '@mui/system': 9.4.0(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.1) '@mui/types': 9.4.0(@types/react@19.3.0) - '@mui/utils': 9.4.0(@types/react@19.3.0)(react@19.3.0) + '@mui/utils': 9.4.0(@types/react@19.3.0)(react@19.3.1) '@popperjs/core': 2.11.8 '@types/react-transition-group': 4.4.12(@types/react@19.3.0) clsx: 2.1.1 csstype: 3.2.3 prop-types: 15.8.1 - react: 19.3.0 - react-dom: 19.3.0(react@19.3.0) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) react-is: 19.3.0 - react-transition-group: 4.4.5(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + react-transition-group: 4.4.5(react-dom@19.3.1(react@19.3.1))(react@19.3.1) optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0) + '@emotion/react': 11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0) '@types/react': 19.3.0 - '@mui/private-theming@9.4.0(@types/react@19.3.0)(react@19.3.0)': + '@mui/private-theming@9.4.0(@types/react@19.3.0)(react@19.3.1)': dependencies: '@babel/runtime': 7.29.7 - '@mui/utils': 9.4.0(@types/react@19.3.0)(react@19.3.0) + '@mui/utils': 9.4.0(@types/react@19.3.0)(react@19.3.1) prop-types: 15.8.1 - react: 19.3.0 + react: 19.3.1 optionalDependencies: '@types/react': 19.3.0 - '@mui/styled-engine@9.4.0(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(react@19.3.0)': + '@mui/styled-engine@9.4.0(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(react@19.3.1)': dependencies: '@babel/runtime': 7.29.7 '@emotion/cache': 11.14.0 @@ -6464,25 +6467,25 @@ snapshots: '@emotion/sheet': 1.4.0 csstype: 3.2.3 prop-types: 15.8.1 - react: 19.3.0 + react: 19.3.1 optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0) + '@emotion/react': 11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0) - '@mui/system@9.4.0(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.0)': + '@mui/system@9.4.0(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.1)': dependencies: '@babel/runtime': 7.29.7 - '@mui/private-theming': 9.4.0(@types/react@19.3.0)(react@19.3.0) - '@mui/styled-engine': 9.4.0(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(react@19.3.0) + '@mui/private-theming': 9.4.0(@types/react@19.3.0)(react@19.3.1) + '@mui/styled-engine': 9.4.0(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(react@19.3.1) '@mui/types': 9.4.0(@types/react@19.3.0) - '@mui/utils': 9.4.0(@types/react@19.3.0)(react@19.3.0) + '@mui/utils': 9.4.0(@types/react@19.3.0)(react@19.3.1) clsx: 2.1.1 csstype: 3.2.3 prop-types: 15.8.1 - react: 19.3.0 + react: 19.3.1 optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0) + '@emotion/react': 11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0))(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0) '@types/react': 19.3.0 '@mui/types@9.4.0(@types/react@19.3.0)': @@ -6491,14 +6494,14 @@ snapshots: optionalDependencies: '@types/react': 19.3.0 - '@mui/utils@9.4.0(@types/react@19.3.0)(react@19.3.0)': + '@mui/utils@9.4.0(@types/react@19.3.0)(react@19.3.1)': dependencies: '@babel/runtime': 7.29.7 '@mui/types': 9.4.0(@types/react@19.3.0) '@types/prop-types': 15.7.15 clsx: 2.1.1 prop-types: 15.8.1 - react: 19.3.0 + react: 19.3.1 react-is: 19.3.0 optionalDependencies: '@types/react': 19.3.0 @@ -6575,65 +6578,67 @@ snapshots: dependencies: '@octokit/openapi-types': 29.0.1 - '@oxc-parser/binding-android-arm-eabi@0.150.0': + '@oxc-parser/binding-android-arm-eabi@0.153.0': optional: true - '@oxc-parser/binding-android-arm64@0.150.0': + '@oxc-parser/binding-android-arm64@0.153.0': optional: true - '@oxc-parser/binding-darwin-arm64@0.150.0': + '@oxc-parser/binding-darwin-arm64@0.153.0': optional: true - '@oxc-parser/binding-darwin-x64@0.150.0': + '@oxc-parser/binding-darwin-x64@0.153.0': optional: true - '@oxc-parser/binding-freebsd-x64@0.150.0': + '@oxc-parser/binding-freebsd-x64@0.153.0': optional: true - '@oxc-parser/binding-linux-arm-gnueabihf@0.150.0': + '@oxc-parser/binding-linux-arm-gnueabihf@0.153.0': optional: true - '@oxc-parser/binding-linux-arm-musleabihf@0.150.0': + '@oxc-parser/binding-linux-arm-musleabihf@0.153.0': optional: true - '@oxc-parser/binding-linux-arm64-gnu@0.150.0': + '@oxc-parser/binding-linux-arm64-gnu@0.153.0': optional: true - '@oxc-parser/binding-linux-arm64-musl@0.150.0': + '@oxc-parser/binding-linux-arm64-musl@0.153.0': optional: true - '@oxc-parser/binding-linux-ppc64-gnu@0.150.0': + '@oxc-parser/binding-linux-ppc64-gnu@0.153.0': optional: true - '@oxc-parser/binding-linux-riscv64-gnu@0.150.0': + '@oxc-parser/binding-linux-riscv64-gnu@0.153.0': optional: true - '@oxc-parser/binding-linux-riscv64-musl@0.150.0': + '@oxc-parser/binding-linux-riscv64-musl@0.153.0': optional: true - '@oxc-parser/binding-linux-s390x-gnu@0.150.0': + '@oxc-parser/binding-linux-s390x-gnu@0.153.0': optional: true - '@oxc-parser/binding-linux-x64-gnu@0.150.0': + '@oxc-parser/binding-linux-x64-gnu@0.153.0': optional: true - '@oxc-parser/binding-linux-x64-musl@0.150.0': + '@oxc-parser/binding-linux-x64-musl@0.153.0': optional: true - '@oxc-parser/binding-openharmony-arm64@0.150.0': + '@oxc-parser/binding-openharmony-arm64@0.153.0': optional: true - '@oxc-parser/binding-win32-arm64-msvc@0.150.0': + '@oxc-parser/binding-win32-arm64-msvc@0.153.0': optional: true - '@oxc-parser/binding-win32-ia32-msvc@0.150.0': + '@oxc-parser/binding-win32-ia32-msvc@0.153.0': optional: true - '@oxc-parser/binding-win32-x64-msvc@0.150.0': + '@oxc-parser/binding-win32-x64-msvc@0.153.0': optional: true '@oxc-project/types@0.150.0': {} + '@oxc-project/types@0.153.0': {} + '@oxc-resolver/binding-android-arm-eabi@11.24.2': optional: true @@ -6897,86 +6902,86 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/react-virtual@3.14.13(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + '@tanstack/react-virtual@3.14.13(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': dependencies: '@tanstack/virtual-core': 3.17.11 - react: 19.3.0 - react-dom: 19.3.0(react@19.3.0) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) '@tanstack/virtual-core@3.17.11': {} - '@tauri-apps/api@2.12.1': {} + '@tauri-apps/api@2.12.4': {} - '@tauri-apps/cli-darwin-arm64@2.12.1': + '@tauri-apps/cli-darwin-arm64@2.12.3': optional: true - '@tauri-apps/cli-darwin-x64@2.12.1': + '@tauri-apps/cli-darwin-x64@2.12.3': optional: true - '@tauri-apps/cli-linux-arm-gnueabihf@2.12.1': + '@tauri-apps/cli-linux-arm-gnueabihf@2.12.3': optional: true - '@tauri-apps/cli-linux-arm64-gnu@2.12.1': + '@tauri-apps/cli-linux-arm64-gnu@2.12.3': optional: true - '@tauri-apps/cli-linux-arm64-musl@2.12.1': + '@tauri-apps/cli-linux-arm64-musl@2.12.3': optional: true - '@tauri-apps/cli-linux-riscv64-gnu@2.12.1': + '@tauri-apps/cli-linux-riscv64-gnu@2.12.3': optional: true - '@tauri-apps/cli-linux-x64-gnu@2.12.1': + '@tauri-apps/cli-linux-x64-gnu@2.12.3': optional: true - '@tauri-apps/cli-linux-x64-musl@2.12.1': + '@tauri-apps/cli-linux-x64-musl@2.12.3': optional: true - '@tauri-apps/cli-win32-arm64-msvc@2.12.1': + '@tauri-apps/cli-win32-arm64-msvc@2.12.3': optional: true - '@tauri-apps/cli-win32-ia32-msvc@2.12.1': + '@tauri-apps/cli-win32-ia32-msvc@2.12.3': optional: true - '@tauri-apps/cli-win32-x64-msvc@2.12.1': + '@tauri-apps/cli-win32-x64-msvc@2.12.3': optional: true - '@tauri-apps/cli@2.12.1': + '@tauri-apps/cli@2.12.3': optionalDependencies: - '@tauri-apps/cli-darwin-arm64': 2.12.1 - '@tauri-apps/cli-darwin-x64': 2.12.1 - '@tauri-apps/cli-linux-arm-gnueabihf': 2.12.1 - '@tauri-apps/cli-linux-arm64-gnu': 2.12.1 - '@tauri-apps/cli-linux-arm64-musl': 2.12.1 - '@tauri-apps/cli-linux-riscv64-gnu': 2.12.1 - '@tauri-apps/cli-linux-x64-gnu': 2.12.1 - '@tauri-apps/cli-linux-x64-musl': 2.12.1 - '@tauri-apps/cli-win32-arm64-msvc': 2.12.1 - '@tauri-apps/cli-win32-ia32-msvc': 2.12.1 - '@tauri-apps/cli-win32-x64-msvc': 2.12.1 + '@tauri-apps/cli-darwin-arm64': 2.12.3 + '@tauri-apps/cli-darwin-x64': 2.12.3 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.12.3 + '@tauri-apps/cli-linux-arm64-gnu': 2.12.3 + '@tauri-apps/cli-linux-arm64-musl': 2.12.3 + '@tauri-apps/cli-linux-riscv64-gnu': 2.12.3 + '@tauri-apps/cli-linux-x64-gnu': 2.12.3 + '@tauri-apps/cli-linux-x64-musl': 2.12.3 + '@tauri-apps/cli-win32-arm64-msvc': 2.12.3 + '@tauri-apps/cli-win32-ia32-msvc': 2.12.3 + '@tauri-apps/cli-win32-x64-msvc': 2.12.3 - '@tauri-apps/plugin-clipboard-manager@2.4.0': + '@tauri-apps/plugin-clipboard-manager@2.5.0': dependencies: - '@tauri-apps/api': 2.12.1 + '@tauri-apps/api': 2.12.4 - '@tauri-apps/plugin-dialog@2.8.0': + '@tauri-apps/plugin-dialog@2.8.3': dependencies: - '@tauri-apps/api': 2.12.1 + '@tauri-apps/api': 2.12.4 - '@tauri-apps/plugin-fs@2.6.0': + '@tauri-apps/plugin-fs@2.7.1': dependencies: - '@tauri-apps/api': 2.12.1 + '@tauri-apps/api': 2.12.4 - '@tauri-apps/plugin-http@2.8.0': + '@tauri-apps/plugin-http@2.8.3': dependencies: - '@tauri-apps/api': 2.12.1 + '@tauri-apps/api': 2.12.4 - '@tauri-apps/plugin-opener@2.7.0': + '@tauri-apps/plugin-opener@2.7.2': dependencies: - '@tauri-apps/api': 2.12.1 + '@tauri-apps/api': 2.12.4 - '@tauri-apps/plugin-updater@2.13.1': + '@tauri-apps/plugin-updater@2.14.0': dependencies: - '@tauri-apps/api': 2.12.1 + '@tauri-apps/api': 2.12.4 '@tybys/wasm-util@0.10.4': dependencies: @@ -7304,11 +7309,11 @@ snapshots: '@vscode/l10n@0.0.18': {} - '@wdio/config@10.0.0(supports-color@7.2.0)(yauzl@2.10.0)': + '@wdio/config@10.0.1(supports-color@7.2.0)(yauzl@2.10.0)': dependencies: '@wdio/logger': 10.0.0 - '@wdio/types': 10.0.0 - '@wdio/utils': 10.0.0(supports-color@7.2.0)(yauzl@2.10.0) + '@wdio/types': 10.0.1 + '@wdio/utils': 10.0.1(supports-color@7.2.0)(yauzl@2.10.0) deepmerge-ts: 8.0.2 glob: 10.5.0 import-meta-resolve: 4.2.0 @@ -7318,10 +7323,10 @@ snapshots: - supports-color - yauzl - '@wdio/globals@9.29.1(expect-webdriverio@5.7.0)(webdriverio@10.0.0(supports-color@7.2.0)(yauzl@2.10.0))': + '@wdio/globals@9.31.3(expect-webdriverio@6.1.0)(webdriverio@10.0.2(supports-color@7.2.0)(yauzl@2.10.0))': dependencies: - expect-webdriverio: 5.7.0(@wdio/globals@9.29.1)(@wdio/logger@9.29.1)(webdriverio@10.0.0(supports-color@7.2.0)(yauzl@2.10.0)) - webdriverio: 10.0.0(supports-color@7.2.0)(yauzl@2.10.0) + expect-webdriverio: 6.1.0(@wdio/globals@9.31.3)(@wdio/logger@9.29.1)(webdriverio@10.0.2(supports-color@7.2.0)(yauzl@2.10.0)) + webdriverio: 10.0.2(supports-color@7.2.0)(yauzl@2.10.0) '@wdio/logger@10.0.0': dependencies: @@ -7339,11 +7344,11 @@ snapshots: safe-regex2: 5.1.1 strip-ansi: 7.2.0 - '@wdio/native-core@1.2.0(supports-color@7.2.0)': + '@wdio/native-core@1.3.0(supports-color@7.2.0)': dependencies: '@wdio/logger': 9.29.1 - '@wdio/native-types': 2.5.0 - '@wdio/native-utils': 2.7.0(supports-color@7.2.0) + '@wdio/native-types': 2.6.0 + '@wdio/native-utils': 2.8.0(supports-color@7.2.0) debug: 4.4.3(supports-color@7.2.0) get-port: 7.2.0 tslib: 2.8.1 @@ -7351,77 +7356,77 @@ snapshots: - supports-color - tsx - '@wdio/native-spy@1.3.0': {} + '@wdio/native-spy@1.4.0': {} - '@wdio/native-types@2.5.0': {} + '@wdio/native-types@2.6.0': {} - '@wdio/native-utils@2.7.0(supports-color@7.2.0)': + '@wdio/native-utils@2.8.0(supports-color@7.2.0)': dependencies: '@wdio/logger': 9.29.1 debug: 4.4.3(supports-color@7.2.0) esbuild: 0.28.2 find-up-simple: 1.0.1 json5: 2.2.3 - smol-toml: 1.8.0 + smol-toml: 1.9.1 yaml: 2.9.1 transitivePeerDependencies: - supports-color - '@wdio/protocols@10.0.0': {} + '@wdio/protocols@10.0.2': {} '@wdio/repl@10.0.0': dependencies: '@types/node': 22.20.5 - '@wdio/reporter@9.30.1': + '@wdio/reporter@9.31.2': dependencies: '@types/node': 20.19.43 '@wdio/logger': 9.29.1 - '@wdio/types': 9.30.1 + '@wdio/types': 9.31.2 diff: 8.0.4 object-inspect: 1.13.4 - '@wdio/spec-reporter@9.30.1': + '@wdio/spec-reporter@9.31.2': dependencies: - '@wdio/reporter': 9.30.1 - '@wdio/types': 9.30.1 + '@wdio/reporter': 9.31.2 + '@wdio/types': 9.31.2 chalk: 5.6.2 easy-table: 1.2.0 pretty-ms: 9.3.1 - '@wdio/tauri-service@1.4.0(expect-webdriverio@5.7.0)(supports-color@7.2.0)(webdriverio@10.0.0(supports-color@7.2.0)(yauzl@2.10.0))': + '@wdio/tauri-service@1.5.0(expect-webdriverio@6.1.0)(supports-color@7.2.0)(webdriverio@10.0.2(supports-color@7.2.0)(yauzl@2.10.0))': dependencies: - '@wdio/globals': 9.29.1(expect-webdriverio@5.7.0)(webdriverio@10.0.0(supports-color@7.2.0)(yauzl@2.10.0)) + '@wdio/globals': 9.31.3(expect-webdriverio@6.1.0)(webdriverio@10.0.2(supports-color@7.2.0)(yauzl@2.10.0)) '@wdio/logger': 9.29.1 - '@wdio/native-core': 1.2.0(supports-color@7.2.0) - '@wdio/native-spy': 1.3.0 - '@wdio/native-types': 2.5.0 - '@wdio/native-utils': 2.7.0(supports-color@7.2.0) - '@wdio/spec-reporter': 9.30.1 - '@wdio/types': 9.30.1 + '@wdio/native-core': 1.3.0(supports-color@7.2.0) + '@wdio/native-spy': 1.4.0 + '@wdio/native-types': 2.6.0 + '@wdio/native-utils': 2.8.0(supports-color@7.2.0) + '@wdio/spec-reporter': 9.31.2 + '@wdio/types': 9.31.2 debug: 4.4.3(supports-color@7.2.0) get-port: 7.2.0 tslib: 2.8.1 - webdriverio: 10.0.0(supports-color@7.2.0)(yauzl@2.10.0) + webdriverio: 10.0.2(supports-color@7.2.0)(yauzl@2.10.0) transitivePeerDependencies: - expect-webdriverio - supports-color - tsx - '@wdio/types@10.0.0': + '@wdio/types@10.0.1': dependencies: '@types/node': 22.20.5 - '@wdio/types@9.30.1': + '@wdio/types@9.31.2': dependencies: '@types/node': 20.19.43 - '@wdio/utils@10.0.0(supports-color@7.2.0)(yauzl@2.10.0)': + '@wdio/utils@10.0.1(supports-color@7.2.0)(yauzl@2.10.0)': dependencies: '@puppeteer/browsers': 3.2.3(yauzl@2.10.0) '@types/split2': 4.2.3 '@wdio/logger': 10.0.0 - '@wdio/types': 10.0.0 + '@wdio/types': 10.0.1 aria-query: 5.3.2 decamelize: 6.0.1 deepmerge-ts: 8.0.2 @@ -7464,14 +7469,14 @@ snapshots: agent-base@9.0.0: {} - ahooks@3.10.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0): + ahooks@3.10.0(react-dom@19.3.1(react@19.3.1))(react@19.3.1): dependencies: '@babel/runtime': 7.29.7 dayjs: 1.11.23 js-cookie: 3.0.8 lodash: 4.18.1 - react: 19.3.0 - react-dom: 19.3.0(react@19.3.0) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) react-fast-compare: 3.2.2 resize-observer-polyfill: 1.5.1 screenfull: 5.2.0 @@ -8326,15 +8331,15 @@ snapshots: expect-type@1.4.0: {} - expect-webdriverio@5.7.0(@wdio/globals@9.29.1)(@wdio/logger@9.29.1)(webdriverio@10.0.0(supports-color@7.2.0)(yauzl@2.10.0)): + expect-webdriverio@6.1.0(@wdio/globals@9.31.3)(@wdio/logger@9.29.1)(webdriverio@10.0.2(supports-color@7.2.0)(yauzl@2.10.0)): dependencies: '@vitest/snapshot': 4.1.11 - '@wdio/globals': 9.29.1(expect-webdriverio@5.7.0)(webdriverio@10.0.0(supports-color@7.2.0)(yauzl@2.10.0)) + '@wdio/globals': 9.31.3(expect-webdriverio@6.1.0)(webdriverio@10.0.2(supports-color@7.2.0)(yauzl@2.10.0)) '@wdio/logger': 9.29.1 deep-eql: 5.0.2 expect: 30.5.2 jest-matcher-utils: 30.5.2 - webdriverio: 10.0.0(supports-color@7.2.0)(yauzl@2.10.0) + webdriverio: 10.0.2(supports-color@7.2.0)(yauzl@2.10.0) expect@30.5.2: dependencies: @@ -8414,19 +8419,19 @@ snapshots: hasown: 2.0.4 mime-types: 2.1.35 - formatly@0.7.1: + formatly@0.11.4: dependencies: fd-package-json: 2.0.0 package-manager-detector: 1.8.0 - foxact@0.3.12(react-dom@19.3.0(react@19.3.0))(react@19.3.0): + foxact@0.3.12(react-dom@19.3.1(react@19.3.1))(react@19.3.1): dependencies: client-only: 0.0.1 event-target-bus: 1.1.0 server-only: 0.0.1 optionalDependencies: - react: 19.3.0 - react-dom: 19.3.0(react@19.3.0) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) foxts@5.9.1: dependencies: @@ -8824,19 +8829,18 @@ snapshots: dependencies: '@keyv/serialize': 1.1.1 - knip@6.39.0: + knip@6.41.0: dependencies: - fdir: 6.5.0(picomatch@4.0.7) - formatly: 0.7.1 + formatly: 0.11.4 get-tsconfig: 4.14.3 jiti: 2.7.0 - oxc-parser: 0.150.0 + oxc-parser: 0.153.0 oxc-resolver: 11.24.2 picomatch: 4.0.7 - smol-toml: 1.8.0 + smol-toml: 1.9.1 strip-json-comments: 5.0.3 tinyglobby: 0.2.17 - unbash: 4.0.11 + unbash: 5.1.0 yaml: 2.9.1 zod: 4.6.5 @@ -9434,29 +9438,29 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - oxc-parser@0.150.0: + oxc-parser@0.153.0: dependencies: - '@oxc-project/types': 0.150.0 + '@oxc-project/types': 0.153.0 optionalDependencies: - '@oxc-parser/binding-android-arm-eabi': 0.150.0 - '@oxc-parser/binding-android-arm64': 0.150.0 - '@oxc-parser/binding-darwin-arm64': 0.150.0 - '@oxc-parser/binding-darwin-x64': 0.150.0 - '@oxc-parser/binding-freebsd-x64': 0.150.0 - '@oxc-parser/binding-linux-arm-gnueabihf': 0.150.0 - '@oxc-parser/binding-linux-arm-musleabihf': 0.150.0 - '@oxc-parser/binding-linux-arm64-gnu': 0.150.0 - '@oxc-parser/binding-linux-arm64-musl': 0.150.0 - '@oxc-parser/binding-linux-ppc64-gnu': 0.150.0 - '@oxc-parser/binding-linux-riscv64-gnu': 0.150.0 - '@oxc-parser/binding-linux-riscv64-musl': 0.150.0 - '@oxc-parser/binding-linux-s390x-gnu': 0.150.0 - '@oxc-parser/binding-linux-x64-gnu': 0.150.0 - '@oxc-parser/binding-linux-x64-musl': 0.150.0 - '@oxc-parser/binding-openharmony-arm64': 0.150.0 - '@oxc-parser/binding-win32-arm64-msvc': 0.150.0 - '@oxc-parser/binding-win32-ia32-msvc': 0.150.0 - '@oxc-parser/binding-win32-x64-msvc': 0.150.0 + '@oxc-parser/binding-android-arm-eabi': 0.153.0 + '@oxc-parser/binding-android-arm64': 0.153.0 + '@oxc-parser/binding-darwin-arm64': 0.153.0 + '@oxc-parser/binding-darwin-x64': 0.153.0 + '@oxc-parser/binding-freebsd-x64': 0.153.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.153.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.153.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.153.0 + '@oxc-parser/binding-linux-arm64-musl': 0.153.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.153.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.153.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.153.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.153.0 + '@oxc-parser/binding-linux-x64-gnu': 0.153.0 + '@oxc-parser/binding-linux-x64-musl': 0.153.0 + '@oxc-parser/binding-openharmony-arm64': 0.153.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.153.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.153.0 + '@oxc-parser/binding-win32-x64-msvc': 0.153.0 oxc-resolver@11.24.2: optionalDependencies: @@ -9604,38 +9608,38 @@ snapshots: dependencies: hookified: 2.2.0 - qrcode.react@4.2.0(react@19.3.0): + qrcode.react@4.2.0(react@19.3.1): dependencies: - react: 19.3.0 + react: 19.3.1 query-selector-shadow-dom@1.0.1: {} - react-dom@19.3.0(react@19.3.0): + react-dom@19.3.1(react@19.3.1): dependencies: - react: 19.3.0 + react: 19.3.1 scheduler: 0.28.0 - react-error-boundary@6.1.6(@types/react@19.3.0)(react@19.3.0): + react-error-boundary@6.1.6(@types/react@19.3.0)(react@19.3.1): dependencies: - react: 19.3.0 + react: 19.3.1 optionalDependencies: '@types/react': 19.3.0 react-fast-compare@3.2.2: {} - react-hook-form@7.88.0(react@19.3.0): + react-hook-form@7.88.0(react@19.3.1): dependencies: - react: 19.3.0 + react: 19.3.1 - react-i18next@17.0.15(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3): + react-i18next@17.0.19(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@6.0.3): dependencies: '@babel/runtime': 7.29.7 html-parse-stringify: 4.0.1 i18next: 26.4.2(typescript@6.0.3) - react: 19.3.0 - use-sync-external-store: 1.7.0(react@19.3.0) + react: 19.3.1 + use-sync-external-store: 1.7.0(react@19.3.1) optionalDependencies: - react-dom: 19.3.0(react@19.3.0) + react-dom: 19.3.1(react@19.3.1) typescript: 6.0.3 react-is@16.13.1: {} @@ -9644,7 +9648,7 @@ snapshots: react-is@19.3.0: {} - react-markdown@10.1.0(@types/react@19.3.0)(react@19.3.0)(supports-color@7.2.0): + react-markdown@10.1.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0): dependencies: '@types/hast': 3.0.5 '@types/mdast': 4.0.4 @@ -9653,7 +9657,7 @@ snapshots: hast-util-to-jsx-runtime: 2.3.6(supports-color@7.2.0) html-url-attributes: 3.0.1 mdast-util-to-hast: 13.2.1 - react: 19.3.0 + react: 19.3.1 remark-parse: 11.0.0(supports-color@7.2.0) remark-rehype: 11.1.2 unified: 11.0.5 @@ -9662,24 +9666,24 @@ snapshots: transitivePeerDependencies: - supports-color - react-router@8.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0): + react-router@8.4.0(react-dom@19.3.1(react@19.3.1))(react@19.3.1): dependencies: '@remix-run/route-pattern': 0.22.1 cookie-es: 3.1.1 - react: 19.3.0 + react: 19.3.1 optionalDependencies: - react-dom: 19.3.0(react@19.3.0) + react-dom: 19.3.1(react@19.3.1) - react-transition-group@4.4.5(react-dom@19.3.0(react@19.3.0))(react@19.3.0): + react-transition-group@4.4.5(react-dom@19.3.1(react@19.3.1))(react@19.3.1): dependencies: '@babel/runtime': 7.29.7 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 - react: 19.3.0 - react-dom: 19.3.0(react@19.3.0) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) - react@19.3.0: {} + react@19.3.1: {} readable-stream@2.3.8: dependencies: @@ -9859,7 +9863,7 @@ snapshots: slash@3.0.0: {} - smol-toml@1.8.0: {} + smol-toml@1.9.1: {} snake-case@3.0.4: dependencies: @@ -9972,11 +9976,11 @@ snapshots: svg-parser@2.1.0: {} - swr@2.5.1(react@19.3.0): + swr@2.5.1(react@19.3.1): dependencies: dequal: 2.0.3 - react: 19.3.0 - use-sync-external-store: 1.7.0(react@19.3.0) + react: 19.3.1 + use-sync-external-store: 1.7.0(react@19.3.1) systemjs@6.15.1: {} @@ -10001,7 +10005,7 @@ snapshots: tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/7a03d16f4d39b1bb885b7babe66b9e153e34b615: dependencies: - '@tauri-apps/api': 2.12.1 + '@tauri-apps/api': 2.12.4 teex@1.0.1: dependencies: @@ -10080,7 +10084,7 @@ snapshots: typescript@6.0.3: {} - unbash@4.0.11: {} + unbash@5.1.0: {} undici-types@6.21.0: {} @@ -10177,9 +10181,9 @@ snapshots: urlpattern-polyfill@10.1.0: {} - use-sync-external-store@1.7.0(react@19.3.0): + use-sync-external-store@1.7.0(react@19.3.1): dependencies: - react: 19.3.0 + react: 19.3.1 userhome@1.0.1: {} @@ -10280,15 +10284,15 @@ snapshots: web-namespaces@2.0.1: {} - webdriver@10.0.0(supports-color@7.2.0)(yauzl@2.10.0): + webdriver@10.0.2(supports-color@7.2.0)(yauzl@2.10.0): dependencies: '@types/node': 22.20.5 '@types/ws': 8.18.1 - '@wdio/config': 10.0.0(supports-color@7.2.0)(yauzl@2.10.0) + '@wdio/config': 10.0.1(supports-color@7.2.0)(yauzl@2.10.0) '@wdio/logger': 10.0.0 - '@wdio/protocols': 10.0.0 - '@wdio/types': 10.0.0 - '@wdio/utils': 10.0.0(supports-color@7.2.0)(yauzl@2.10.0) + '@wdio/protocols': 10.0.2 + '@wdio/types': 10.0.1 + '@wdio/utils': 10.0.1(supports-color@7.2.0)(yauzl@2.10.0) deepmerge-ts: 8.0.2 https-proxy-agent: 7.0.6(supports-color@7.2.0) undici: 8.11.2 @@ -10300,16 +10304,16 @@ snapshots: - utf-8-validate - yauzl - webdriverio@10.0.0(supports-color@7.2.0)(yauzl@2.10.0): + webdriverio@10.0.2(supports-color@7.2.0)(yauzl@2.10.0): dependencies: '@types/node': 22.20.5 '@types/sinonjs__fake-timers': 8.1.5 - '@wdio/config': 10.0.0(supports-color@7.2.0)(yauzl@2.10.0) + '@wdio/config': 10.0.1(supports-color@7.2.0)(yauzl@2.10.0) '@wdio/logger': 10.0.0 - '@wdio/protocols': 10.0.0 + '@wdio/protocols': 10.0.2 '@wdio/repl': 10.0.0 - '@wdio/types': 10.0.0 - '@wdio/utils': 10.0.0(supports-color@7.2.0)(yauzl@2.10.0) + '@wdio/types': 10.0.1 + '@wdio/utils': 10.0.1(supports-color@7.2.0)(yauzl@2.10.0) archiver: 8.0.0 aria-query: 5.3.2 cheerio: 1.2.0 @@ -10324,7 +10328,7 @@ snapshots: rgb2hex: 0.2.5 serialize-error: 12.0.0 urlpattern-polyfill: 10.1.0 - webdriver: 10.0.0(supports-color@7.2.0)(yauzl@2.10.0) + webdriver: 10.0.2(supports-color@7.2.0)(yauzl@2.10.0) transitivePeerDependencies: - bare-abort-controller - bare-buffer diff --git a/renovate.json b/renovate.json index edc0f4167..ec7f97412 100644 --- a/renovate.json +++ b/renovate.json @@ -45,6 +45,17 @@ "matchManagers": ["npm"], "groupName": "npm dependencies" }, + { + "description": "Update Tauri npm packages and Cargo crates together so frontend and backend stay in sync", + "matchPackageNames": [ + "@tauri-apps/**", + "@wdio/tauri-service", + "tauri", + "tauri-*" + ], + "groupName": "tauri", + "addLabels": ["tauri"] + }, { "description": "Group all GitHub Actions updates into a single PR", "matchManagers": ["github-actions"], diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 099ed62f9..9ec55b3b6 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.99.0" +channel = "1.99.1" components = ["rustfmt", "clippy"] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a6b6a53fe..824c83107 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -29,7 +29,7 @@ perf-harness = ["dep:tauri-plugin-wdio-webdriver"] identifier = "io.github.clash-verge-rev.clash-verge-rev" [build-dependencies] -tauri-build = { version = "2.7.1", features = [] } +tauri-build = { version = "2.7.3", features = [] } [dependencies] clash-verge-draft = { workspace = true } @@ -72,55 +72,55 @@ sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs.git", branch "guard", ] } network-interface = { version = "2.0.5", features = ["serde"] } -tauri-plugin-shell = "2.4.0" -tauri-plugin-opener = "2.7.0" -tauri-plugin-dialog = "2.8.1" -tauri-plugin-fs = "2.6.0" +tauri-plugin-shell = "2.5.0" +tauri-plugin-opener = "2.7.2" +tauri-plugin-dialog = "2.8.3" +tauri-plugin-fs = "2.7.1" tauri-plugin-process = "2.4.0" -tauri-plugin-deep-link = "2.6.1" +tauri-plugin-deep-link = "2.6.2" tauri-plugin-window-state = "2.5.0" -zip = "9.0.1" +zip = "9.0.4" flate2 = "1.1.10" reqwest_dav = "0.3.3" aes-gcm = { version = "0.11.1" } base64 = "0.23.1" minisign-verify = "0.3.0" -sha2 = "0.11" +sha2 = "0.11.1" getrandom = "0.4.3" futures = "0.3.34" gethostname = "1.1.0" scopeguard = "1.2.0" -tauri-plugin-notification = "2.5.1" +tauri-plugin-notification = "2.7.0" tokio-stream = "0.1.19" backon = { version = "1.6.0", features = ["tokio-sleep"] } -tauri-plugin-http = "2.8.0" +tauri-plugin-http = "2.8.3" tracing = { workspace = true } tauri-plugin-devtools = { version = "2.2.2", optional = true } -tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", branch = "main" } +tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", rev = "7a03d16f4d39b1bb885b7babe66b9e153e34b615" } async-trait = "0.1.92" clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.6", version = "2.7.6", features = [ "client", ] } arc-swap = "1.9.2" tokio-rustls = "0.26.6" -rustls = { version = "0.23.45", features = ["ring"] } +rustls = { version = "0.23.46", features = ["ring"] } webpki-roots = "1.0.9" dark-light = "3.0.0" bytes = "1.12.1" -tokio-util = { version = "0.7.19", features = ["time"] } +tokio-util = { version = "0.7.20", features = ["time"] } specta = { version = "=2.0.0-rc.25", features = ["derive", "function", "serde_json"] } tauri-specta = { version = "=2.0.0-rc.25", features = ["derive", "typescript"] } specta-typescript = "=0.0.12" [target.'cfg(unix)'.dependencies] -libc = "0.2.190" +libc = "0.2.191" [target.'cfg(target_os = "linux")'.dependencies] # Upstream has no session-end signal support on Linux; see crates/clash-verge-signal. clash-verge-signal = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] -tauri-plugin-wdio-webdriver = { version = "=1.4.0", optional = true } +tauri-plugin-wdio-webdriver = { version = "=1.5.0", optional = true } objc2 = "0.6.5" objc2-foundation = { version = "0.3.2", features = [ "NSString", @@ -153,8 +153,8 @@ windows-sys.workspace = true [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-autostart = "2.7.0" -tauri-plugin-global-shortcut = "2.4.0" -tauri-plugin-updater = "2.13.1" +tauri-plugin-global-shortcut = "2.4.1" +tauri-plugin-updater = "2.14.0" [dev-dependencies] criterion = { workspace = true } diff --git a/src-tauri/build.rs b/src-tauri/build.rs index 8cdf2daf5..13df77750 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -1,10 +1,9 @@ fn main() -> Result<(), Box> { if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") && std::env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("msvc") - && std::env::var_os("CARGO_FEATURE_CLIPPY").is_some() { - // Tauri's resource is linked to the app binary, but not to Rust unit-test harnesses. - // Give mock-context builds their own manifest so tests can resolve TaskDialogIndirect. + // Tauri's resource is linked to the app binary, but not to Rust unit-test harnesses or examples. + // Give non-app targets their own manifest so tests and examples can resolve TaskDialogIndirect. let out_dir = std::env::var_os("OUT_DIR").ok_or_else(|| std::io::Error::other("OUT_DIR is not set"))?; let manifest_path = std::path::PathBuf::from(out_dir).join("windows-test-manifest.xml"); std::fs::write( @@ -25,8 +24,14 @@ fn main() -> Result<(), Box> { "#, )?; - println!("cargo:rustc-link-arg=/MANIFEST:EMBED"); - println!("cargo:rustc-link-arg=/MANIFESTINPUT:{}", manifest_path.display()); + let manifest_display = manifest_path.display(); + if std::env::var_os("CARGO_FEATURE_CLIPPY").is_some() { + println!("cargo:rustc-link-arg=/MANIFEST:EMBED"); + println!("cargo:rustc-link-arg=/MANIFESTINPUT:{manifest_display}"); + } else { + println!("cargo:rustc-link-arg-examples=/MANIFEST:EMBED"); + println!("cargo:rustc-link-arg-examples=/MANIFESTINPUT:{manifest_display}"); + } } #[cfg(feature = "clippy")] diff --git a/src-tauri/src/cmd/profile.rs b/src-tauri/src/cmd/profile.rs index 8c20504e8..6b36e0fc9 100644 --- a/src-tauri/src/cmd/profile.rs +++ b/src-tauri/src/cmd/profile.rs @@ -282,7 +282,9 @@ async fn handle_validation_failure( ) -> CmdResult { logging!(warn, Type::Cmd, "配置验证失败: {}", outcome); discard_and_restore(current_profile).await?; - handle_validation_notice(&outcome, ValidationNoticeTarget::Runtime, "运行时配置"); + if !matches!(outcome, ValidationOutcome::Busy) { + handle_validation_notice(&outcome, ValidationNoticeTarget::Runtime, "运行时配置"); + } Ok(outcome) } @@ -311,9 +313,6 @@ async fn perform_config_update( current_value: Option<&String>, current_profile: Option<&String>, ) -> CmdResult { - defer! { - CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release); - } let update_result = run_profile_config_update_transition(|| CoreManager::global().update_config_forced()).await; match update_result { @@ -373,8 +372,6 @@ pub struct ProfilePatch { #[specta(type = Option)] pub url: Option, pub selected: Option>, - #[specta(type = Option>)] - pub latency_test_urls: Option>, pub extra: Option, pub updated: Option, pub option: Option, @@ -389,7 +386,6 @@ impl From for PrfItem { file: patch.file, url: patch.url, selected: patch.selected, - latency_test_urls: patch.latency_test_urls, extra: patch.extra, updated: patch.updated, option: patch.option, @@ -409,6 +405,9 @@ pub async fn patch_profiles_config(profiles: ProfileSelection) -> CmdResult) -> BTreeMap CmdResult { - proxy_view(None).await -} - -pub(crate) async fn proxy_view(provider_timeout: Option) -> CmdResult { let runtime = Config::runtime().await; let latest_runtime = runtime.latest_arc(); let runtime_group_order = runtime_group_order(latest_runtime.config.as_ref()); let group_scopes = runtime_group_scopes(latest_runtime.config.as_ref()); let mihomo = Handle::mihomo(); - let (proxies, providers) = tokio::join!(mihomo.get_proxies(), async { - match provider_timeout { - Some(timeout) => tokio::time::timeout(timeout, mihomo.get_proxy_providers()) - .await - .ok() - .and_then(Result::ok), - None => mihomo.get_proxy_providers().await.ok(), - } - }); + let (proxies, providers) = tokio::join!(mihomo.get_proxies(), mihomo.get_proxy_providers(),); let proxies = proxies.stringify_err()?; Ok(ProxyViewBuilder::build(ProxyViewInput { runtime_group_order, group_scopes, proxies, - providers, + providers: providers.ok(), })) } diff --git a/src-tauri/src/config/prfitem.rs b/src-tauri/src/config/prfitem.rs index 0c99355d5..da8728d21 100644 --- a/src-tauri/src/config/prfitem.rs +++ b/src-tauri/src/config/prfitem.rs @@ -53,10 +53,6 @@ pub struct PrfItem { #[serde(skip_serializing_if = "Option::is_none")] pub selected: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - #[specta(type = Option>)] - pub latency_test_urls: Option>, - #[serde(skip_serializing_if = "Option::is_none")] pub extra: Option, @@ -233,7 +229,6 @@ impl PrfItem { desc: Some(desc), file: Some(file), url: None, - latency_test_urls: None, selected: None, extra: None, option: Some(PrfOption { @@ -413,7 +408,6 @@ impl PrfItem { desc: desc.cloned(), file: Some(file), url: Some(url.as_str().into()), - latency_test_urls: None, selected: None, extra, option: Some(PrfOption { diff --git a/src-tauri/src/config/profiles.rs b/src-tauri/src/config/profiles.rs index 7680fff58..4b149b716 100644 --- a/src-tauri/src/config/profiles.rs +++ b/src-tauri/src/config/profiles.rs @@ -250,7 +250,6 @@ impl IProfiles { patch!(each, item, file); patch!(each, item, url); patch!(each, item, selected); - patch!(each, item, latency_test_urls); patch!(each, item, extra); patch!(each, item, updated); patch!(each, item, option); diff --git a/src-tauri/src/core/proxy_view.rs b/src-tauri/src/core/proxy_view.rs index 54d488fc0..0fe394280 100644 --- a/src-tauri/src/core/proxy_view.rs +++ b/src-tauri/src/core/proxy_view.rs @@ -1,8 +1,8 @@ -use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::collections::{BTreeMap, BTreeSet}; -use serde::{Serialize, Serializer, ser::SerializeMap as _}; +use serde::Serialize; use tauri_plugin_mihomo::models::{ - DelayHistory, Extra, Proxies, Proxy, ProxyProvider, ProxyProviders, ProxyType, VehicleType, + DelayHistory, Proxies, Proxy, ProxyProvider, ProxyProviders, ProxyType, VehicleType, }; pub struct ProxyViewInput { @@ -57,28 +57,6 @@ pub struct ProxyCapabilities { pub smux: bool, } -#[derive(Serialize, specta::Type)] -struct ProxyLatencyHistory<'a> { - #[specta(type = Vec)] - history: &'a [DelayHistory], -} - -fn serialize_proxy_latency_history( - extra: &HashMap, - serializer: S, -) -> Result { - let mut map = serializer.serialize_map(Some(extra.len()))?; - for (url, extra) in extra { - map.serialize_entry( - url, - &ProxyLatencyHistory { - history: &extra.history, - }, - )?; - } - map.end() -} - #[derive(Debug, PartialEq, Eq, Serialize, specta::Type)] #[serde(rename_all = "camelCase")] pub struct ProxyGroupView { @@ -98,9 +76,6 @@ pub struct ProxyGroupView { pub test_url: Option, #[specta(type = Vec)] pub history: Vec, - #[serde(serialize_with = "serialize_proxy_latency_history")] - #[specta(type = HashMap>)] - pub extra: HashMap, #[serde(flatten)] pub capabilities: ProxyCapabilities, pub members: Vec, @@ -116,9 +91,6 @@ pub struct ProxyNodeView { pub proxy_type: ProxyType, #[specta(type = Vec)] pub history: Vec, - #[serde(serialize_with = "serialize_proxy_latency_history")] - #[specta(type = HashMap>)] - pub extra: HashMap, #[serde(skip_serializing_if = "Option::is_none")] pub id: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -284,7 +256,7 @@ impl ProxyViewBuilder { } else { ProxyViewProviderState::Unavailable }; - let (mut core_groups, core_nodes) = partition_core(proxies, providers.is_some()); + let (mut core_groups, core_nodes) = partition_core(proxies); let (mut records, core_node_ids) = build_core_records(core_nodes); let (providers, provider_candidates) = build_provider_records(providers, &mut records); let resolver = MemberResolver { @@ -317,14 +289,14 @@ impl ProxyViewBuilder { } } -fn partition_core(proxies: Proxies, providers_available: bool) -> (BTreeMap, BTreeMap) { +fn partition_core(proxies: Proxies) -> (BTreeMap, BTreeMap) { let mut groups = BTreeMap::new(); let mut nodes = BTreeMap::new(); for (name, proxy) in proxies.proxies { if proxy.all.is_some() { groups.insert(name, proxy); - } else if !providers_available || proxy.provider_name.is_empty() { + } else { nodes.insert(name, proxy); } } @@ -431,7 +403,6 @@ fn build_group(name: String, proxy: Proxy, resolver: &MemberResolver<'_>) -> Pro now, test_url, history, - extra, udp, xudp, tfo, @@ -464,7 +435,6 @@ fn build_group(name: String, proxy: Proxy, resolver: &MemberResolver<'_>) -> Pro icon, test_url, history, - extra, capabilities: ProxyCapabilities { udp, xudp, @@ -483,7 +453,6 @@ fn build_node(record_id: String, name: String, proxy: Proxy, source: ProxyNodeSo icon, test_url, history, - extra, udp, xudp, tfo, @@ -498,7 +467,6 @@ fn build_node(record_id: String, name: String, proxy: Proxy, source: ProxyNodeSo name, proxy_type, history, - extra, id, hidden, icon, @@ -552,62 +520,3 @@ fn build_standalone(core_node_ids: &BTreeMap) -> Vec { ); standalone } - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - #[test] - fn provider_echo_in_proxies_does_not_override_group_membership() -> anyhow::Result<()> { - let view = ProxyViewBuilder::build(ProxyViewInput { - runtime_group_order: vec!["A".into(), "B".into()], - group_scopes: ["A", "B"] - .into_iter() - .map(|name| { - ( - name.into(), - GroupScope { - providers: vec![name.into()], - exclude_types: vec![], - }, - ) - }) - .collect(), - proxies: serde_json::from_value(json!({"proxies": { - "A": {"all": ["node"], "type": "Selector"}, - "B": {"all": ["node"], "type": "Selector"}, - "node": {"name": "node", "provider-name": "A", "history": [{"time": "2026-10-08T00:00:00Z", "delay": 32}]} - }}))?, - providers: Some(serde_json::from_value(json!({"providers": { - "A": {"vehicleType": "Inline", "proxies": [{"name": "node", "history": [{"time": "2026-10-08T00:00:00Z", "delay": 32}]}]}, - "B": {"vehicleType": "Inline", "proxies": [{"name": "node", "history": [{"time": "2026-10-08T00:00:00Z", "delay": 123}]}]} - }}))?), - }); - for (group, expected) in view.groups.iter().zip([32, 123]) { - let ProxyMemberRef::Node { record_id, .. } = &group.members[0] else { - anyhow::bail!("Expected node") - }; - let record = &view.records[record_id]; - assert_eq!(record.history.last().map(|entry| entry.delay), Some(expected)); - assert!( - matches!(&record.source, ProxyNodeSource::Provider { provider_name, .. } if provider_name == &group.name) - ); - } - Ok(()) - } - #[test] - fn unavailable_provider_keeps_core_echo_interactable() -> anyhow::Result<()> { - let view = ProxyViewBuilder::build(ProxyViewInput { - runtime_group_order: vec!["A".into()], - group_scopes: BTreeMap::new(), - proxies: serde_json::from_value(json!({"proxies": { - "A": {"all": ["node"], "type": "Selector"}, - "node": {"name": "node", "provider-name": "provider"} - }}))?, - providers: None, - }); - assert!(matches!(&view.groups[0].members[0], ProxyMemberRef::Node { .. })); - Ok(()) - } -} diff --git a/src-tauri/src/core/tray/latency.rs b/src-tauri/src/core/tray/latency.rs deleted file mode 100644 index 5c1106ede..000000000 --- a/src-tauri/src/core/tray/latency.rs +++ /dev/null @@ -1,156 +0,0 @@ -use super::Tray; -use crate::{ - Type, cmd, - config::Config, - core::{ - handle, - notify::{Refresh, announce}, - proxy_view::{ProxyMemberRef, ProxyNodeSource}, - }, - logging, -}; - -const TRAY_DELAY_TEST_CONCURRENCY: usize = 10; -const TRAY_DEFAULT_LATENCY_URL: &str = "http://cp.cloudflare.com/generate_204"; - -fn is_latency_test_url(raw: &str) -> bool { - let raw = raw.trim(); - raw.split_once("://") - .is_some_and(|(scheme, _)| scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https")) - && tauri::Url::parse(raw).is_ok_and(|url| url.has_host() && matches!(url.scheme(), "http" | "https")) -} - -pub(super) fn test_url( - group: &crate::core::proxy_view::ProxyGroupView, - verge: &crate::config::IVerge, - profiles: &crate::config::IProfiles, -) -> String { - let custom_url = profiles - .current - .as_ref() - .and_then(|uid| profiles.get_item(uid).ok()) - .and_then(|profile| profile.latency_test_urls.as_ref()) - .and_then(|urls| urls.get(group.name.as_str())) - .map(|url| url.as_str()) - .filter(|url| !url.trim().is_empty()); - let group_url = group.test_url.as_deref().filter(|url| !url.trim().is_empty()); - let default_url = verge - .default_latency_test - .as_deref() - .filter(|url| !url.trim().is_empty()); - custom_url - .or(group_url) - .or(default_url) - .filter(|&url| is_latency_test_url(url)) - .unwrap_or(TRAY_DEFAULT_LATENCY_URL) - .trim() - .to_owned() -} - -static RUNNING: parking_lot::Mutex> = - parking_lot::Mutex::new(std::collections::BTreeSet::new()); - -pub(super) fn is_running(group_name: &str) -> bool { - RUNNING.lock().contains(group_name) -} - -pub(super) async fn test_proxy_group_delay(group_name: &str) { - if !RUNNING.lock().insert(group_name.to_owned()) { - return; - } - Tray::global().refresh_latency_item(group_name); - let _running = scopeguard::guard(group_name, |name| { - RUNNING.lock().remove(name); - Tray::global().refresh_latency_item(name); - }); - use futures::{StreamExt as _, stream}; - - let view = match cmd::proxy::get_proxy_view().await { - Ok(view) => view, - Err(err) => { - logging!(warn, Type::Tray, "Failed to read proxies for tray latency test: {err}"); - return; - } - }; - let verge = Config::verge().await.latest_arc(); - - let group = view - .groups - .iter() - .chain(view.global.iter()) - .find(|group| group.name.as_str() == group_name); - let Some(group) = group else { - logging!(warn, Type::Tray, "Tray latency test group not found: {group_name}"); - return; - }; - - let profiles = Config::profiles().await.latest_arc(); - let url = test_url(group, &verge, &profiles); - let timeout = verge - .default_latency_timeout - .filter(|timeout| *timeout > 0) - .unwrap_or(10000) as u32; - - let mut targets: Vec<(std::string::String, Option)> = Vec::with_capacity(group.members.len()); - for member in &group.members { - match member { - ProxyMemberRef::Node { name, record_id } => match view.records.get(record_id).map(|node| &node.source) { - Some(ProxyNodeSource::Provider { - provider_name, - proxy_name, - }) => targets.push((proxy_name.clone(), Some(provider_name.clone()))), - _ => targets.push((name.clone(), None)), - }, - ProxyMemberRef::Group { name } => targets.push((name.clone(), None)), - ProxyMemberRef::Unresolved { .. } => {} - } - } - let mihomo = handle::Handle::mihomo(); - - stream::iter(targets.into_iter().map(move |(member, provider)| { - let url = url.clone(); - async move { - let result = match provider { - Some(provider_name) => { - mihomo - .healthcheck_node_in_provider(&provider_name, &member, &url, timeout) - .await - } - None => mihomo.delay_proxy_by_name(&member, &url, timeout).await, - }; - if let Err(err) = result { - logging!(debug, Type::Tray, "Tray latency test failed: {member} - {err}"); - } - } - })) - .buffer_unordered(TRAY_DELAY_TEST_CONCURRENCY) - .for_each(|()| std::future::ready(())) - .await; - - if let Err(err) = cmd::proxy::sync_tray_proxy_selection().await { - logging!(warn, Type::Tray, "Failed to refresh tray after latency test: {err}"); - } - - announce(Refresh::Proxies); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn latency_urls_require_a_valid_http_host_and_port() { - for invalid in [ - "http://?", - "http://host:invalid", - "http://bad host/", - "ftp://localhost", - "http:localhost", - ] { - assert!(!is_latency_test_url(invalid), "{invalid}"); - } - for valid in ["http://localhost/", " https://example.com/ ", "HTTP://localhost:8080/"] { - assert!(is_latency_test_url(valid), "{valid}"); - } - } -} diff --git a/src-tauri/src/core/tray/menu_def.rs b/src-tauri/src/core/tray/menu_def.rs index 6c9641773..7012ce219 100644 --- a/src-tauri/src/core/tray/menu_def.rs +++ b/src-tauri/src/core/tray/menu_def.rs @@ -32,7 +32,6 @@ define_menu! { outbound_modes => OUTBOUND_MODES, "tray_outbound_modes", "tray.outboundModes", profiles => PROFILES, "tray_profiles", "tray.profiles", proxies => PROXIES, "tray_proxies", "tray.proxies", - test_latency => TEST_LATENCY, "tray_test_latency", "tray.testLatency", system_proxy => SYSTEM_PROXY, "tray_system_proxy", "tray.systemProxy", tun_mode => TUN_MODE, "tray_tun_mode", "tray.tunMode", close_all_connections => CLOSE_ALL_CONNECTIONS, "tray_close_all_connections", "tray.closeAllConnections", diff --git a/src-tauri/src/core/tray/mod.rs b/src-tauri/src/core/tray/mod.rs index 8afb56751..ae8c63c2c 100644 --- a/src-tauri/src/core/tray/mod.rs +++ b/src-tauri/src/core/tray/mod.rs @@ -1,6 +1,5 @@ use crate::config::{IProfilePreview, IVerge}; use crate::core::lightweight; -use crate::core::proxy_view::{ProxyGroupView, ProxyMemberRef, ProxyNodeSource, ProxyViewProviderState, ProxyViewV1}; use crate::core::tray::menu_def::TrayAction; use crate::process::AsyncHandler; use crate::singleton; @@ -15,6 +14,7 @@ use crate::{ use clash_verge_limiter::{Limiter, SystemClock, SystemLimiter}; use clash_verge_logging::logging_error; use tauri::tray::{MouseButton, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent}; +use tauri_plugin_mihomo::models::Proxies; use tokio::fs; use super::handle; @@ -28,7 +28,6 @@ use tauri::{ menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu}, }; -mod latency; mod menu_def; #[cfg(target_os = "macos")] mod speed_task; @@ -56,25 +55,8 @@ enum IconKind { Tun, } -struct LatencyMenuItem { - item: MenuItem, - running: bool, - proxies: Vec, - provider_state: ProxyViewProviderState, -} - -struct ProxyLatencyItem { - item: CheckMenuItem, - name: std::string::String, - provider_name: Option, - text: std::string::String, -} - pub struct Tray { limiter: SystemLimiter, - proxy_hover_limiter: SystemLimiter, - menu_update: tokio::sync::Mutex<()>, - latency_items: parking_lot::Mutex>, #[cfg(target_os = "macos")] speed_controller: speed_task::TraySpeedController, } @@ -148,9 +130,6 @@ impl Default for Tray { fn default() -> Self { Self { limiter: Limiter::new(Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS), SystemClock), - proxy_hover_limiter: Limiter::new(Duration::from_secs(1), SystemClock), - menu_update: tokio::sync::Mutex::new(()), - latency_items: parking_lot::Mutex::new(HashMap::new()), #[cfg(target_os = "macos")] speed_controller: speed_task::TraySpeedController::new(), } @@ -216,7 +195,6 @@ impl Tray { } async fn update_menu_internal(&self, app_handle: &AppHandle, include_proxy_groups: bool) -> Result<()> { - let _update = self.menu_update.lock().await; let Some(tray) = app_handle.tray_by_id(TRAY_ID) else { logging!(warn, Type::Tray, "Failed to update tray menu: tray not found"); return Ok(()); @@ -241,133 +219,29 @@ impl Tray { let profiles_preview = profiles_arc.profiles_preview().unwrap_or_default(); let is_lightweight_mode = is_in_lightweight_mode(); - let (menu, latency_items) = create_tray_menu( - app_handle, - Some(mode.as_str()), - *system_proxy, - *tun_mode, - tun_mode_available, - profiles_preview, - TrayMenuOptions { - is_lightweight_mode, - include_proxy_groups, - }, - ) - .await?; - tray.set_menu(Some(menu))?; - let groups: Vec<_> = latency_items.keys().cloned().collect(); - *self.latency_items.lock() = latency_items; - for group in groups { - self.refresh_latency_item(&group); - } + logging_error!( + Type::Tray, + tray.set_menu(Some( + create_tray_menu( + app_handle, + Some(mode.as_str()), + *system_proxy, + *tun_mode, + tun_mode_available, + profiles_preview, + TrayMenuOptions { + is_lightweight_mode, + include_proxy_groups, + }, + ) + .await?, + )) + ); logging!(debug, Type::Tray, "托盘菜单更新成功"); Ok(()) } - async fn refresh_proxy_items(&'static self) { - let Ok(update) = self.menu_update.try_lock() else { - return; - }; - if self.latency_items.lock().is_empty() || handle::Handle::global().is_exiting() { - return; - } - let Ok(Ok(view)) = tokio::time::timeout( - Duration::from_secs(1), - cmd::proxy::proxy_view(Some(Duration::from_millis(750))), - ) - .await - else { - return; - }; - if self - .latency_items - .lock() - .values() - .any(|entry| entry.provider_state != view.provider_state) - { - // Provider fallback changes member identities; rebuild through the existing queue. - drop(update); - logging_error!(Type::Tray, cmd::proxy::sync_tray_proxy_selection().await); - return; - } - let verge = Config::verge().await.latest_arc(); - let profiles = Config::profiles().await.latest_arc(); - logging_error!( - Type::Tray, - handle::Handle::app_handle().run_on_main_thread(move || { - // Keep rebuilding serialized until these handles have received their snapshot. - let _update = update; - let mut items = self.latency_items.lock(); - for (group_name, entry) in items.iter_mut() { - let Some(group) = find_group(&view, group_name) else { - continue; - }; - let url = latency::test_url(group, &verge, &profiles); - let mut members = HashMap::with_capacity(group.members.len()); - for member in &group.members { - members - .entry((member_name(member), member_provider(&view, member))) - .or_insert(member); - } - for proxy in &mut entry.proxies { - let Some(member) = members.get(&(proxy.name.as_str(), proxy.provider_name.as_deref())) else { - continue; - }; - let text = proxy_item_text(&view, member, &url, &verge); - let checked = group.now.as_ref() == Some(&proxy.name); - if text != proxy.text { - match proxy.item.set_text(&text) { - Ok(()) => proxy.text = text, - Err(err) => logging!(warn, Type::Tray, "Failed to refresh proxy latency: {err}"), - } - } - match proxy.item.is_checked() { - Ok(current) if current != checked => { - logging_error!(Type::Tray, proxy.item.set_checked(checked)) - } - Err(err) => logging!(warn, Type::Tray, "Failed to read proxy selection: {err}"), - _ => {} - } - } - } - }) - ); - } - - fn refresh_latency_item(&self, group_name: &str) { - let group_name = group_name.to_owned(); - logging_error!( - Type::Tray, - handle::Handle::app_handle().run_on_main_thread(move || { - Self::global().refresh_latency_item_on_main_thread(&group_name); - }) - ); - } - - fn refresh_latency_item_on_main_thread(&self, group_name: &str) { - let running = latency::is_running(group_name); - let item = { - let mut items = self.latency_items.lock(); - items.get_mut(group_name).and_then(|entry| { - if entry.running == running { - return None; - } - entry.running = running; - Some(entry.item.clone()) - }) - }; - if let Some(item) = item { - let label = if running { - clash_verge_i18n::t!("tray.testingLatency") - } else { - clash_verge_i18n::t!("tray.testLatency") - }; - logging_error!(Type::Tray, item.set_text(label)); - logging_error!(Type::Tray, item.set_enabled(!running)); - } - } - pub async fn update_icon(&self, verge: &IVerge) -> Result<()> { if handle::Handle::global().is_exiting() { logging!(debug, Type::Tray, "应用正在退出,跳过托盘图标更新"); @@ -582,175 +456,96 @@ fn create_profile_menu_item( .collect() } -const UNTESTED_DELAY_TEXT: &str = "-ms"; -const TIMEOUT_DELAY_TEXT: &str = "T/O"; -const TRAY_GROUP_DEPTH_LIMIT: usize = 8; - -const fn member_name(member: &ProxyMemberRef) -> &str { - match member { - ProxyMemberRef::Node { name, .. } - | ProxyMemberRef::Group { name } - | ProxyMemberRef::Unresolved { name, .. } => name.as_str(), - } -} - -fn find_group<'a>(view: &'a ProxyViewV1, name: &str) -> Option<&'a ProxyGroupView> { - view.groups - .iter() - .chain(view.global.iter()) - .find(|group| group.name.as_str() == name) -} - -/// The delay recorded for `url`, falling back to the node's latest measurement under any URL, so a -/// node the core tested on its own still shows a value. -fn node_delay(view: &ProxyViewV1, record_id: &str, url: &str) -> Option { - view.records - .get(record_id) - .map(|node| node.extra.get(url).map_or(&node.history, |extra| &extra.history)) - .and_then(|history| history.last()) - .map(|history| history.delay) -} - -/// A group row shows the delay of the node the group currently selects. The core never writes a -/// history for the group itself, so `now` is followed until the chain reaches a node. -fn selected_member_delay(view: &ProxyViewV1, group_name: &str, url: &str) -> Option { - let mut current = group_name; - for _ in 0..TRAY_GROUP_DEPTH_LIMIT { - let Some(group) = find_group(view, current) else { break }; - let Some(now) = group.now.as_deref().filter(|now| !now.is_empty() && *now != current) else { - break; - }; - let Some(member) = group.members.iter().find(|member| member_name(member) == now) else { - break; - }; - match member { - ProxyMemberRef::Node { record_id, .. } => return node_delay(view, record_id, url), - ProxyMemberRef::Group { name } => current = name.as_str(), - ProxyMemberRef::Unresolved { .. } => break, - } - } - find_group(view, group_name) - .map(|group| group.extra.get(url).map_or(&group.history, |extra| &extra.history)) - .and_then(|history| history.last()) - .map(|history| history.delay) -} - -fn member_provider<'a>(view: &'a ProxyViewV1, member: &ProxyMemberRef) -> Option<&'a str> { - let ProxyMemberRef::Node { record_id, .. } = member else { - return None; - }; - match &view.records.get(record_id)?.source { - ProxyNodeSource::Provider { provider_name, .. } => Some(provider_name), - ProxyNodeSource::Core { .. } => None, - } -} - -fn proxy_item_text(view: &ProxyViewV1, member: &ProxyMemberRef, url: &str, verge: &IVerge) -> std::string::String { - let timeout = verge - .default_latency_timeout - .filter(|timeout| *timeout > 0) - .unwrap_or(10000); - let delay = match member { - ProxyMemberRef::Node { record_id, .. } => node_delay(view, record_id, url), - ProxyMemberRef::Group { name } => selected_member_delay(view, name, url), - ProxyMemberRef::Unresolved { .. } => None, - }; - let delay_text = delay.map_or_else( - || UNTESTED_DELAY_TEXT.to_owned(), - |delay| { - if delay == 0 || u32::from(delay) >= timeout as u32 { - TIMEOUT_DELAY_TEXT.to_owned() - } else { - format!("{delay}ms") - } - }, - ); - format!("{} | {delay_text}", member_name(member)) -} - fn create_subcreate_proxy_menu_item( app_handle: &AppHandle, proxy_mode: &str, - view: Option, - latency_items: &mut HashMap, - texts: &MenuTexts, - verge: &IVerge, - profiles: &crate::config::IProfiles, + proxy_group_order_map: Option>, + proxy_nodes_data: Option, ) -> Vec> { - let Some(view) = view else { return Vec::new() }; - view.groups - .iter() - .chain(view.global.iter()) - .filter_map(|group| { - if group.hidden.unwrap_or_default() || (proxy_mode == "global") != (group.name == "GLOBAL") { - return None; - } - let url = latency::test_url(group, verge, profiles); - let proxies: Vec = group - .members - .iter() - .filter_map(|member| { - let name = member_name(member); - let text = proxy_item_text(&view, member, &url, verge); - let checked = group.now.as_deref() == Some(name); - CheckMenuItem::with_id( - app_handle, - format!("proxy_{}_{}", group.name, name), - &text, - true, - checked, - None::<&str>, - ) - .map(|item| ProxyLatencyItem { - item, - name: name.to_owned(), - provider_name: member_provider(&view, member).map(str::to_owned), - text, + let proxy_submenus: Vec> = { + let mut submenus: Vec<(String, usize, Submenu)> = Vec::new(); + + // TODO: 应用启动时,内核还未启动完全,无法获取代理节点信息 + if let Some(proxy_nodes_data) = proxy_nodes_data { + for (group_name, group_data) in proxy_nodes_data.proxies.iter() { + let should_show = match proxy_mode { + "global" => group_name == "GLOBAL", + _ => group_name != "GLOBAL", + } && !group_data.hidden.unwrap_or_default(); + + if !should_show { + continue; + } + + let Some(all_proxies) = group_data.all.as_ref() else { + continue; + }; + + let now_proxy = group_data.now.as_deref().unwrap_or_default(); + + let group_items: Vec> = all_proxies + .iter() + .filter_map(|proxy_str| { + let is_selected = *proxy_str == now_proxy; + let item_id = format!("proxy_{}_{}", group_name, proxy_str); + + let delay_text = proxy_nodes_data + .proxies + .get(proxy_str) + .and_then(|h| h.history.last()) + .map(|h| match h.delay { + 0 => "-ms".into(), + delay if delay >= 10000 => "-ms".into(), + _ => format!("{}ms", h.delay), + }) + .unwrap_or_else(|| "-ms".into()); + + let display_text = format!("{} | {}", proxy_str, delay_text); + + CheckMenuItem::with_id(app_handle, item_id, display_text, true, is_selected, None::<&str>) + .map_err(|e| logging!(warn, Type::Tray, "Failed to create proxy menu item: {}", e)) + .ok() }) - .map_err(|e| logging!(warn, Type::Tray, "Failed to create proxy menu item: {e}")) - .ok() - }) - .collect(); - let group_items: Vec<_> = proxies.iter().map(|proxy| proxy.item.clone()).collect(); - if group_items.is_empty() { - return None; - } - let running = latency::is_running(&group.name); - let test_item = MenuItem::with_id( - app_handle, - format!("{}_{}", MenuIds::TEST_LATENCY, group.name), - if running { - clash_verge_i18n::t!("tray.testingLatency") + .collect(); + + if group_items.is_empty() { + continue; + } + + let group_display_name = group_name.to_string(); + + let group_items_refs: Vec<&dyn IsMenuItem> = + group_items.iter().map(|item| item as &dyn IsMenuItem).collect(); + + if let Ok(submenu) = Submenu::with_id_and_items( + app_handle, + format!("proxy_group_{}", group_name), + group_display_name, + true, + &group_items_refs, + ) { + let insertion_index = submenus.len(); + submenus.push((group_name.into(), insertion_index, submenu)); } else { - texts.test_latency.clone() - }, - !running, - None::<&str>, - ) - .ok()?; - latency_items.insert( - group.name.clone(), - LatencyMenuItem { - item: test_item.clone(), - running, - proxies, - provider_state: view.provider_state, - }, - ); - let separator = PredefinedMenuItem::separator(app_handle).ok()?; - let mut items: Vec<&dyn IsMenuItem> = vec![&test_item, &separator]; - items.extend(group_items.iter().map(|item| item as &dyn IsMenuItem)); - Submenu::with_id_and_items( - app_handle, - format!("proxy_group_{}", group.name), - &group.name, - true, - &items, - ) - .map_err(|e| logging!(warn, Type::Tray, "Failed to create proxy group submenu: {e}")) - .ok() - }) - .collect() + logging!(warn, Type::Tray, "Failed to create proxy group submenu: {}", group_name); + } + } + } + + if let Some(order_map) = proxy_group_order_map.as_ref() { + submenus.sort_by(|(name_a, original_index_a, _), (name_b, original_index_b, _)| { + match (order_map.get(name_a), order_map.get(name_b)) { + (Some(index_a), Some(index_b)) => index_a.cmp(index_b), + (Some(_), None) => std::cmp::Ordering::Less, + (None, Some(_)) => std::cmp::Ordering::Greater, + (None, None) => original_index_a.cmp(original_index_b), + } + }); + } + + submenus.into_iter().map(|(_, _, submenu)| submenu).collect() + }; + proxy_submenus } fn create_proxy_menu_item( @@ -797,38 +592,54 @@ async fn create_tray_menu( tun_mode_available: bool, profiles_preview: Vec>, options: TrayMenuOptions, -) -> Result<(tauri::menu::Menu, HashMap)> { +) -> Result> { let current_proxy_mode = mode.unwrap_or(""); let mut verge_settings = Config::verge().await.latest_arc(); let fetch_proxy_groups = options.include_proxy_groups && verge_settings.tray_proxy_groups_display_mode.as_deref() != Some("disable"); - // Proxy data must remain optional so a stopped Core cannot roll back unrelated settings. - let proxy_view = if fetch_proxy_groups { - tokio::time::timeout( - Duration::from_millis(1000), - cmd::proxy::proxy_view(Some(Duration::from_millis(750))), - ) - .await - .ok() - .and_then(Result::ok) + // TODO: should update tray menu again when it was timeout error + let (proxy_nodes_data, runtime_proxy_groups_order) = if fetch_proxy_groups { + let proxy_nodes_data = + tokio::time::timeout(Duration::from_millis(1000), handle::Handle::mihomo().get_proxies()) + .await + .map_or(None, |res| res.ok()); + + let runtime = Config::runtime().await.latest_arc(); + let runtime_proxy_groups_order = runtime.config.as_ref().map(|config| { + config + .get("proxy-groups") + .and_then(|groups| groups.as_sequence()) + .map(|groups| { + groups + .iter() + .filter_map(|group| group.get("name")) + .filter_map(|name| name.as_str()) + .enumerate() + .map(|(index, name)| (name.into(), index)) + .collect::>() + }) + .unwrap_or_default() + }); + + (proxy_nodes_data, runtime_proxy_groups_order) } else { - None + (None, None) }; if fetch_proxy_groups { verge_settings = Config::verge().await.latest_arc(); } - let profiles_config = Config::profiles().await.latest_arc(); - let tray_proxy_groups_display_mode = verge_settings .tray_proxy_groups_display_mode .as_deref() .unwrap_or("default"); let include_proxy_groups = options.include_proxy_groups && tray_proxy_groups_display_mode != "disable"; + let proxy_group_order_map = runtime_proxy_groups_order; + let show_outbound_modes_inline = verge_settings.tray_inline_outbound_modes.unwrap_or(false); let version = env!("CARGO_PKG_VERSION"); @@ -908,17 +719,9 @@ async fn create_tray_menu( &profile_menu_items_refs, )?; - let mut latency_items = HashMap::new(); let (proxies_menu, inline_proxy_items) = if include_proxy_groups { - let proxy_sub_menus = create_subcreate_proxy_menu_item( - app_handle, - current_proxy_mode, - proxy_view, - &mut latency_items, - &texts, - &verge_settings, - &profiles_config, - ); + let proxy_sub_menus = + create_subcreate_proxy_menu_item(app_handle, current_proxy_mode, proxy_group_order_map, proxy_nodes_data); match tray_proxy_groups_display_mode { "default" => create_proxy_menu_item(app_handle, false, proxy_sub_menus, &texts.proxies)?, @@ -1062,25 +865,10 @@ async fn create_tray_menu( ]); let menu = tauri::menu::MenuBuilder::new(app_handle).items(&menu_items).build()?; - Ok((menu, latency_items)) + Ok(menu) } fn on_tray_icon_event(_tray_icon: &TrayIcon, tray_event: TrayIconEvent) { - let refresh_proxies = match tray_event { - TrayIconEvent::Enter { .. } => Tray::global().proxy_hover_limiter.check(), - TrayIconEvent::Click { - button: MouseButton::Left | MouseButton::Right, - button_state: MouseButtonState::Down, - .. - } => true, - _ => false, - }; - if refresh_proxies { - AsyncHandler::spawn(|| async { - Tray::global().refresh_proxy_items().await; - }); - } - if matches!( tray_event, TrayIconEvent::Move { .. } | TrayIconEvent::Leave { .. } | TrayIconEvent::Enter { .. } @@ -1202,15 +990,6 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { }; feat::toggle_proxy_profile(profile_index.into()).await; } - id if id.starts_with(MenuIds::TEST_LATENCY) => { - let group_name = id - .strip_prefix(MenuIds::TEST_LATENCY) - .and_then(|rest| rest.strip_prefix('_')); - if let Some(group_name) = group_name { - logging!(info, Type::Tray, "Tray menu: test group latency {}", group_name); - latency::test_proxy_group_delay(group_name).await; - } - } id if id.starts_with("proxy_") => { let rest = match id.strip_prefix("proxy_") { Some(r) => r, diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 659b574b8..eb0af3195 100755 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -16,7 +16,7 @@ fn main() -> ExitCode { #[cfg(not(all(feature = "perf-harness", target_os = "macos")))] fn main() -> ExitCode { let default_parallelism = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1); - let worker_limit = std::cmp::min(default_parallelism, 8); + let worker_limit = std::cmp::min(default_parallelism, 4); let blocking_limit = 2 * worker_limit; #[allow(clippy::unwrap_used)] diff --git a/src-tauri/src/utils/tray_speed.rs b/src-tauri/src/utils/tray_speed.rs index 8223ea3fc..c3bda426e 100644 --- a/src-tauri/src/utils/tray_speed.rs +++ b/src-tauri/src/utils/tray_speed.rs @@ -27,8 +27,12 @@ const TRAY_STATUS_ITEM_EXTRA_WIDTH: f64 = 30.0; const TRAY_STATUS_ITEM_MIN_LENGTH: f64 = 58.0; const NS_VARIABLE_STATUS_ITEM_LENGTH: f64 = -1.0; +/// (button_height, mono_advance, attributes). +type CachedAttrs = (f64, f64, Retained>); + thread_local! { static LAST_DISPLAY_STR: RefCell = const { RefCell::new(String::new()) }; + static CACHED_ATTRS: RefCell> = const { RefCell::new(None) }; } fn format_tray_speed(up: u64, down: u64) -> String { @@ -74,8 +78,26 @@ fn create_attributed_string( } } -fn status_item_length_for_speed(attr_str: &NSAttributedString) -> f64 { - (attr_str.size().width.ceil() + TRAY_STATUS_ITEM_EXTRA_WIDTH).max(TRAY_STATUS_ITEM_MIN_LENGTH) +/// Attributes and per-glyph advance, rebuilt only when the button height changes. +fn cached_attributes(button_height: f64) -> (f64, Retained>) { + CACHED_ATTRS.with(|cache| { + if let Some((height, advance, attrs)) = cache.borrow().as_ref() + && *height == button_height + { + return (*advance, attrs.clone()); + } + let attrs = build_attributes(button_height); + let probe = NSString::from_str("0"); + let advance = create_attributed_string(&probe, Some(&attrs)).size().width; + *cache.borrow_mut() = Some((button_height, advance, attrs.clone())); + (advance, attrs) + }) +} + +/// Longest line times the monospaced advance; matches `size().width` for the ASCII-only speed text. +fn arithmetic_width(text: &str, advance: f64) -> f64 { + let glyphs = text.split('\n').map(str::len).max().unwrap_or(0) as f64; + glyphs * advance } fn sync_click_target_frame(button: &NSStatusBarButton) { @@ -89,7 +111,7 @@ fn sync_click_target_frame(button: &NSStatusBarButton) { } /// `with_inner_tray_icon` should run this on the main thread; fail safely if it does not. -fn apply_status_item_attributed_title(status_item: &NSStatusItem, text: &NSString, show_speed: bool) { +fn apply_status_item_attributed_title(status_item: &NSStatusItem, text: &str, show_speed: bool) { let Some(mtm) = MainThreadMarker::new() else { logging!(warn, Type::Tray, "托盘速率富文本设置跳过:非主线程调用"); return; @@ -97,17 +119,19 @@ fn apply_status_item_attributed_title(status_item: &NSStatusItem, text: &NSStrin let Some(button) = status_item.button(mtm) else { return; }; + let ns_string = NSString::from_str(text); let attr_str = if show_speed { - let attrs = build_attributes(button.bounds().size.height); + let (advance, attrs) = cached_attributes(button.bounds().size.height); let attrs: &NSDictionary = &attrs; - let attr_str = create_attributed_string(text, Some(attrs)); - let length = status_item_length_for_speed(&attr_str); + let attr_str = create_attributed_string(&ns_string, Some(attrs)); + let width = arithmetic_width(text, advance); + let length = (width.ceil() + TRAY_STATUS_ITEM_EXTRA_WIDTH).max(TRAY_STATUS_ITEM_MIN_LENGTH); if status_item.length() != length { status_item.setLength(length); } attr_str } else { - create_attributed_string(text, None) + create_attributed_string(&ns_string, None) }; button.setAttributedTitle(&attr_str); sync_click_target_frame(&button); @@ -121,8 +145,7 @@ pub fn set_speed_attributed_title(status_item: &NSStatusItem, up: u64, down: u64 return; } *last = speed_text; - let ns_string = NSString::from_str(last.as_str()); - apply_status_item_attributed_title(status_item, &ns_string, true); + apply_status_item_attributed_title(status_item, last.as_str(), true); }); } @@ -130,7 +153,6 @@ pub fn clear_speed_attributed_title(status_item: &NSStatusItem) { LAST_DISPLAY_STR.with(|last| { last.borrow_mut().clear(); }); - let empty = NSString::from_str(""); status_item.setLength(NS_VARIABLE_STATUS_ITEM_LENGTH); - apply_status_item_attributed_title(status_item, &empty, false); + apply_status_item_attributed_title(status_item, "", false); } diff --git a/src/components/home/current-proxy-card.tsx b/src/components/home/current-proxy-card.tsx index c7145b05f..4b891a623 100644 --- a/src/components/home/current-proxy-card.tsx +++ b/src/components/home/current-proxy-card.tsx @@ -47,10 +47,7 @@ import { useCoreDataStatus, useProxiesData, } from '@/providers/app-data-context' -import { syncTrayProxySelection } from '@/services/cmds' import delayManager from '@/services/delay' -import { mutate } from '@/services/mutate' -import { profileTestUrls } from '@/services/proxy-test-url' import { findCurrentGroupMember, getRecord, @@ -678,20 +675,6 @@ export const CurrentProxyCard = () => { [proxyView], ) - useEffect(() => { - if (!selectedGroup) return - const customUrl = currentProfile - ? profileTestUrls(currentProfile)[selectedGroup.name] - : undefined - delayManager.setUrl( - selectedGroup.name, - customUrl?.trim() || - selectedGroup.testUrl?.trim() || - verge?.default_latency_test?.trim() || - 'http://cp.cloudflare.com/generate_204', - ) - }, [selectedGroup, currentProfile, verge?.default_latency_test]) - const unsortedProxyOptions = useMemo( () => optionsForGroup(selectedGroup), [optionsForGroup, selectedGroup], @@ -924,12 +907,6 @@ export const CurrentProxyCard = () => { } refreshProxy() - mutate(() => syncTrayProxySelection(), { - id: 'sync-tray-selection', - errorNotice: false, - }).catch((error) => { - console.error('[CurrentProxyCard] Failed to sync tray latency:', error) - }) }) const proxyOptions = useMemo( diff --git a/src/components/proxy/proxy-chain.tsx b/src/components/proxy/proxy-chain.tsx index 439b23e2e..1eb188165 100644 --- a/src/components/proxy/proxy-chain.tsx +++ b/src/components/proxy/proxy-chain.tsx @@ -75,6 +75,7 @@ interface ProxyChainProps { onMarkUnsavedChanges?: () => void mode?: string selectedGroup?: string | null + onExitChainMode: () => void } interface ProxyChainItemProps { @@ -331,6 +332,7 @@ export const ProxyChain = ({ onMarkUnsavedChanges, mode, selectedGroup, + onExitChainMode, }: ProxyChainProps) => { const theme = useTheme() const { t } = useTranslation() @@ -696,13 +698,18 @@ export const ProxyChain = ({ {t('proxies.page.chain.empty')} + ) : ( (null) useEffect(() => { - delayManager.setUrl( - groupName, - testUrl?.trim() || url?.trim() || defaultLatencyUrl, - ) + delayManager.setUrl(groupName, testUrl?.trim() || url || defaultLatencyUrl) }, [groupName, testUrl, defaultLatencyUrl, url]) // 过滤输入是高频操作,且每次都会触发整组代理的重新过滤/排序与虚拟列表重渲染, @@ -116,8 +112,7 @@ export const ProxyGroupTools = memo(function ProxyGroupTools(props: Props) { )} {textState === 'url' && ( - onHeadState({ testUrl })} + onChange={(e) => onHeadState({ testUrl: e.target.value })} sx={{ flex: '1 1 auto', input: { py: 0.65, px: 1 } }} /> )} @@ -181,7 +176,7 @@ export const ProxyGroupTools = memo(function ProxyGroupTools(props: Props) { if (testUrl?.trim() && textState !== 'filter') { onHeadState({ textState: 'url' }) } - if (testUrl?.trim() && !isValidLatencyTestUrl(testUrl)) { + if (testUrl?.trim() && !isValidUrl(testUrl)) { showNotice.warning('proxies.feedback.warnings.invalidTestUrl') return } diff --git a/src/components/proxy/proxy-groups-chain.tsx b/src/components/proxy/proxy-groups-chain.tsx index ab4b61d88..c3b3744e6 100644 --- a/src/components/proxy/proxy-groups-chain.tsx +++ b/src/components/proxy/proxy-groups-chain.tsx @@ -77,6 +77,7 @@ interface GroupSelectMenuProps { interface ProxyGroupsChainProps { mode: string chainConfigData?: string | null + onExitChainMode: () => void availableGroups: any[] activeSelectedGroup: string | null showScrollTop: boolean @@ -305,6 +306,7 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) { const { mode, chainConfigData, + onExitChainMode, availableGroups, activeSelectedGroup, showScrollTop, @@ -521,6 +523,7 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) { chainConfigData={chainConfigData} mode={mode} selectedGroup={activeSelectedGroup} + onExitChainMode={onExitChainMode} /> diff --git a/src/components/proxy/proxy-groups.tsx b/src/components/proxy/proxy-groups.tsx index 0b4fa96b1..7a852543b 100644 --- a/src/components/proxy/proxy-groups.tsx +++ b/src/components/proxy/proxy-groups.tsx @@ -23,9 +23,7 @@ import { useProfiles } from '@/hooks/use-profiles' import { useProxySelection } from '@/hooks/use-proxy-selection' import { useVerge } from '@/hooks/use-verge' import { useProxiesData, useSystemData } from '@/providers/app-data-context' -import { syncTrayProxySelection } from '@/services/cmds' import delayManager from '@/services/delay' -import { mutate } from '@/services/mutate' import { isInteractableMember, resolveMember, @@ -44,6 +42,7 @@ import { ProxyGroupNavigator, } from './proxy-group-navigator' import { ProxyRender } from './proxy-render' +import type { HeadState } from './use-head-state' import { PROXY_GROUP_HEADER_SENSORS, useProxyGroupHeaderLayout, @@ -70,6 +69,7 @@ interface Props { mode: string isChainMode?: boolean chainConfigData?: string | null + onExitChainMode: () => void } function useEmptyRenderList() { @@ -90,11 +90,11 @@ function useProxyRenderState( ) { const { verge } = useVerge() const { proxyView } = useProxiesData() - const { renderList, onProxies, onHeadState } = useRenderList( - mode, - isChainMode, - activeSelectedGroup, - ) + const { + renderList, + onProxies, + onHeadState: setListHeadState, + } = useRenderList(mode, isChainMode, activeSelectedGroup) const scrollPositionKey = useMemo( () => isChainMode @@ -103,6 +103,14 @@ function useProxyRenderState( [activeSelectedGroup, isChainMode, mode], ) + const onHeadState = useCallback( + (groupName: string, patch: Partial) => { + if (patch.open) onProxies() + setListHeadState(groupName, patch) + }, + [onProxies, setListHeadState], + ) + const timeout = verge?.default_latency_timeout || 10000 const handleCheckAll = useStableCallback( @@ -135,12 +143,6 @@ function useProxyRenderState( console.error(`[ProxyGroups] 延迟测试出错,组: ${groupName}`, error) } finally { onProxies() - mutate(() => syncTrayProxySelection(), { - id: 'sync-tray-selection', - errorNotice: false, - }).catch((error) => { - console.error('[ProxyGroups] Failed to sync tray latency:', error) - }) } }), ) @@ -190,8 +192,9 @@ function useProxyRenderState( function ChainProxyGroups(props: { mode: string chainConfigData?: string | null + onExitChainMode: () => void }) { - const { mode, chainConfigData } = props + const { mode, chainConfigData, onExitChainMode } = props const { proxyView } = useProxiesData() const [selectedGroup, setSelectedGroup] = useState(null) @@ -346,6 +349,7 @@ function ChainProxyGroups(props: { { - const { mode, isChainMode = false, chainConfigData } = props + const { mode, isChainMode = false, chainConfigData, onExitChainMode } = props const { profiles, isLoading: isProfilesLoading } = useProfiles() const { isProxyViewPending } = useProxiesData() const { isRunningModePending } = useSystemData() @@ -639,7 +643,11 @@ export const ProxyGroups = (props: Props) => { return case 'render': return isChainMode ? ( - + ) : ( ) diff --git a/src/components/proxy/proxy-head.tsx b/src/components/proxy/proxy-head.tsx index 4fa9aefc7..4db54f36f 100644 --- a/src/components/proxy/proxy-head.tsx +++ b/src/components/proxy/proxy-head.tsx @@ -11,7 +11,7 @@ import { SortByAlphaRounded, SortRounded, } from '@mui/icons-material' -import { Box, IconButton, type SxProps } from '@mui/material' +import { Box, IconButton, TextField, type SxProps } from '@mui/material' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -20,9 +20,8 @@ import { useVerge } from '@/hooks/use-verge' import delayManager from '@/services/delay' import { showNotice } from '@/services/notice-service' import { debugLog } from '@/utils/debug' -import { isValidLatencyTestUrl } from '@/utils/network' +import { isValidUrl } from '@/utils/network' -import { ProxyTestUrlInput } from './proxy-test-url-input' import type { ProxySortType } from './use-filter-sort' import type { HeadState } from './use-head-state' @@ -73,10 +72,7 @@ export const ProxyHead = ({ 'http://cp.cloudflare.com/generate_204' useEffect(() => { - delayManager.setUrl( - groupName, - testUrl?.trim() || url?.trim() || defaultLatencyUrl, - ) + delayManager.setUrl(groupName, testUrl?.trim() || url || defaultLatencyUrl) }, [groupName, testUrl, defaultLatencyUrl, url]) return ( @@ -101,7 +97,7 @@ export const ProxyHead = ({ debugLog(`[ProxyHead] 使用自定义测试URL: ${testUrl}`) onHeadState({ textState: 'url' }) } - if (testUrl?.trim() && !isValidLatencyTestUrl(testUrl)) { + if (testUrl?.trim() && !isValidUrl(testUrl)) { showNotice.warning('proxies.feedback.warnings.invalidTestUrl') return } @@ -192,8 +188,7 @@ export const ProxyHead = ({ )} {textState === 'url' && ( - onHeadState({ testUrl })} + onChange={(e) => onHeadState({ testUrl: e.target.value })} sx={{ ml: 0.5, flex: '1 1 auto', input: { py: 0.65, px: 1 } }} /> )} diff --git a/src/components/proxy/proxy-test-url-input.tsx b/src/components/proxy/proxy-test-url-input.tsx deleted file mode 100644 index 287e2f0f5..000000000 --- a/src/components/proxy/proxy-test-url-input.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { TextField, type TextFieldProps } from '@mui/material' -import { useEffect, useEffectEvent, useRef, useState } from 'react' - -import { useProfiles } from '@/hooks/use-profiles' - -type Props = Omit & { - value: string - onCommit: (url: string) => void -} - -export const ProxyTestUrlInput = ({ value, name, ...props }: Props) => { - const { profiles } = useProfiles() - return ( - - ) -} - -const DraftTestUrlInput = ({ value, onCommit, ...props }: Props) => { - const [draft, setDraft] = useState(null) - const pendingRef = useRef(null) - const commitOnUnmount = useEffectEvent(() => { - if (pendingRef.current !== null && pendingRef.current !== value) - onCommit(pendingRef.current) - }) - useEffect(() => () => commitOnUnmount(), []) - - return ( - { - pendingRef.current = event.target.value - setDraft(event.target.value) - }} - onBlur={() => { - const next = pendingRef.current - pendingRef.current = null - setDraft(null) - if (next !== null && next !== value) onCommit(next) - }} - onKeyDown={(event) => { - if (event.key === 'Enter') { - event.preventDefault() - const input = event.target as HTMLInputElement - input.blur() - } - }} - /> - ) -} diff --git a/src/components/proxy/use-filter-sort.test.ts b/src/components/proxy/use-filter-sort.test.ts index dd5effc33..2f8d0d46e 100644 --- a/src/components/proxy/use-filter-sort.test.ts +++ b/src/components/proxy/use-filter-sort.test.ts @@ -13,17 +13,11 @@ const node = (memberIndex: number, delay: number, name = `${memberIndex}`) => kind: 'node', ref: { kind: 'node', name, recordId: `${memberIndex}` }, node: { - recordId: `${memberIndex}`, history: [{ delay }], - extra: { - 'http://cp.cloudflare.com/generate_204': { - history: [{ time: new Date(0).toISOString(), delay }], - }, - }, source: { kind: 'provider', providerName: `${memberIndex}` }, }, }, - }) as unknown as ResolvedMemberOccurrence + }) as ResolvedMemberOccurrence afterEach(() => vi.restoreAllMocks()) @@ -42,9 +36,7 @@ test('matches the previous comparator for cached, fallback and sentinel delays', for (const cached of [false, true]) { const group = `sort-${cached}` if (cached) { - values.forEach((delay, i) => - delayManager.setDelay(list[i].member, group, delay), - ) + values.forEach((delay, i) => delayManager.setDelay(`${i}`, group, delay)) } for (const timeout of [10000, 20, 0, NaN]) { const expected = list @@ -74,7 +66,7 @@ test('reads each occurrence once and observes cache updates and expiry on the ne node(3, 25), ] const cachedOrder = [list[0], list[2], list[1], list[3]] - delayManager.setDelay(list[0].member, 'expiry', 1) + delayManager.setDelay('0', 'expiry', 1) const get = vi.spyOn(delayManager, 'getDelayFix') expect(filterSort(list, 'expiry', '', 1)).toEqual(cachedOrder) expect(get).toHaveBeenCalledTimes(list.length) @@ -88,7 +80,7 @@ test('reads each occurrence once and observes cache updates and expiry on the ne list[3], list[0], ]) - delayManager.setDelay(list[0].member, 'expiry', 2) + delayManager.setDelay('0', 'expiry', 2) expect(filterSort(list, 'expiry', '', 1)).toEqual(cachedOrder) get.mockClear() filterSort([], 'expiry', '', 1) diff --git a/src/components/proxy/use-head-state.ts b/src/components/proxy/use-head-state.ts index d536564cc..5d30c5e1c 100644 --- a/src/components/proxy/use-head-state.ts +++ b/src/components/proxy/use-head-state.ts @@ -1,10 +1,6 @@ import { useCallback, useEffect, useReducer } from 'react' import { useProfiles } from '@/hooks/use-profiles' -import { - profileTestUrls, - commitProfileTestUrl, -} from '@/services/proxy-test-url' import { ProxySortType } from './use-filter-sort' @@ -36,45 +32,22 @@ export const DEFAULT_STATE: HeadState = { } type HeadStateAction = - | { type: 'reset'; profile: string } - | { type: 'replace'; profile: string; payload: Record } + | { type: 'reset' } + | { type: 'replace'; payload: Record } | { type: 'update'; groupName: string; patch: Partial } - | { type: 'urls'; urls: Record } - -interface LoadedHeadState { - profile: string | null - heads: Record -} function headStateReducer( - state: LoadedHeadState, + state: Record, action: HeadStateAction, -): LoadedHeadState { +): Record { switch (action.type) { case 'reset': - return { profile: action.profile, heads: {} } + return {} case 'replace': - return { profile: action.profile, heads: action.payload } - case 'urls': { - let heads = state.heads - for (const [name, testUrl] of Object.entries(action.urls)) { - if (heads[name]?.testUrl === testUrl) continue - heads = { - ...heads, - [name]: { ...DEFAULT_STATE, ...heads[name], testUrl }, - } - } - return heads === state.heads ? state : { ...state, heads } - } + return action.payload case 'update': { - const prev = state.heads[action.groupName] || DEFAULT_STATE - return { - ...state, - heads: { - ...state.heads, - [action.groupName]: { ...prev, ...action.patch }, - }, - } + const prev = state[action.groupName] || DEFAULT_STATE + return { ...state, [action.groupName]: { ...prev, ...action.patch } } } default: return state @@ -82,42 +55,30 @@ function headStateReducer( } export function useHeadStateNew() { - const { profiles, current: profile } = useProfiles() + const { profiles } = useProfiles() const current = profiles?.current || '' - const [{ profile: loadedProfile, heads: state }, dispatch] = useReducer( - headStateReducer, - { profile: null, heads: {} }, - ) + const [state, dispatch] = useReducer(headStateReducer, {}) useEffect(() => { - const urls = profile ? profileTestUrls(profile) : {} - if (loadedProfile === current) { - dispatch({ type: 'urls', urls }) - return - } try { const data = JSON.parse( - localStorage.getItem(HEAD_STATE_KEY) || '{}', + localStorage.getItem(HEAD_STATE_KEY)!, ) as HeadStateStorage - const value = data?.[current] || {} - for (const [name, testUrl] of Object.entries(urls)) { - value[name] = { ...DEFAULT_STATE, ...value[name], testUrl } - } + const value = data[current] || {} if (value && typeof value === 'object') { - dispatch({ type: 'replace', profile: current, payload: value }) + dispatch({ type: 'replace', payload: value }) } else { - dispatch({ type: 'reset', profile: current }) + dispatch({ type: 'reset' }) } } catch { - dispatch({ type: 'reset', profile: current }) + dispatch({ type: 'reset' }) } - }, [current, loadedProfile, profile]) + }, [current]) useEffect(() => { - if (loadedProfile !== current) return const timer = setTimeout(() => { try { const item = localStorage.getItem(HEAD_STATE_KEY) @@ -133,16 +94,13 @@ export function useHeadStateNew() { }) return () => clearTimeout(timer) - }, [state, current, loadedProfile]) + }, [state, current]) const setHeadState = useCallback( (groupName: string, obj: Partial) => { dispatch({ type: 'update', groupName, patch: obj }) - if (current && obj.testUrl !== undefined) { - void commitProfileTestUrl(current, groupName, obj.testUrl) - } }, - [current], + [], ) return [state, setHeadState] as const diff --git a/src/components/proxy/use-render-list.ts b/src/components/proxy/use-render-list.ts index 826331bde..659e0af4a 100644 --- a/src/components/proxy/use-render-list.ts +++ b/src/components/proxy/use-render-list.ts @@ -116,7 +116,6 @@ const virtualGroup = (members: ProxyGroupView['members']): ProxyGroupView => ({ mptcp: false, smux: false, history: [], - extra: {}, members, }) diff --git a/src/components/setting/mods/backup-config-viewer.tsx b/src/components/setting/mods/backup-config-viewer.tsx index dd4972628..c0ce70c10 100644 --- a/src/components/setting/mods/backup-config-viewer.tsx +++ b/src/components/setting/mods/backup-config-viewer.tsx @@ -1,20 +1,11 @@ -import Visibility from '@mui/icons-material/Visibility' -import VisibilityOff from '@mui/icons-material/VisibilityOff' -import { - TextField, - Button, - Grid, - Stack, - IconButton, - InputAdornment, -} from '@mui/material' +import { Button, Grid, Stack, TextField } from '@mui/material' import { useLockFn } from 'ahooks' -import { useState, useRef, memo, useEffect } from 'react' +import { memo, useEffect, useRef } from 'react' import { useForm } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { useVerge } from '@/hooks/use-verge' -import { saveWebdavConfig, createWebdavBackup } from '@/services/cmds' +import { createWebdavBackup, saveWebdavConfig } from '@/services/cmds' import { mutate } from '@/services/mutate' import { errorDetail, showNotice } from '@/services/notice-service' import { @@ -43,7 +34,6 @@ export const BackupConfigViewer = memo( const { t } = useTranslation() const { verge, mutateVerge } = useVerge() const { webdav_url, webdav_username, webdav_password } = verge || {} - const [showPassword, setShowPassword] = useState(false) const usernameRef = useRef(null) const passwordRef = useRef(null) const urlRef = useRef(null) @@ -68,10 +58,6 @@ export const BackupConfigViewer = memo( const webdavStatus = getWebdavStatus(webdavSignature) const shouldAutoInit = webdavStatus !== 'failed' - const handleClickShowPassword = () => { - setShowPassword((prev) => !prev) - } - useEffect(() => { if (!webdav_url || !webdav_username || !webdav_password) { return @@ -215,7 +201,7 @@ export const BackupConfigViewer = memo( - - {showPassword ? : } - - - ), - }, - }} /> diff --git a/src/hooks/use-profiles.ts b/src/hooks/use-profiles.ts index 2ebbd3c2a..71196bd36 100644 --- a/src/hooks/use-profiles.ts +++ b/src/hooks/use-profiles.ts @@ -1,9 +1,8 @@ -import { useCallback, useEffect, useRef } from 'react' +import { useCallback, useRef } from 'react' import type { ProfilePatch, ProfileSelection } from '@/services/bindings' import { getProfiles, patchProfile, patchProfilesConfig } from '@/services/cmds' import { mutate } from '@/services/mutate' -import { migrateProfileTestUrls } from '@/services/proxy-test-url' import { fetchCacheData, setCacheData, useQuery } from '@/services/query-client' import { debugLog } from '@/utils/debug' @@ -36,10 +35,6 @@ export const useProfiles = () => { refetchInterval: false, }) - useEffect(() => { - if (profiles) migrateProfileTestUrls(profiles) - }, [profiles]) - const refetchRef = useRef(refetch) refetchRef.current = refetch const mutateProfiles = useCallback(async () => { @@ -51,6 +46,7 @@ export const useProfiles = () => { try { const result = await mutate(() => patchProfilesConfig(value), { id: 'patch-profiles-config', + errorNotice: false, onFulfilled: (outcome) => { if (outcome.status === 'valid') { void setCacheData(profilesQueryKey, (current) => @@ -60,12 +56,8 @@ export const useProfiles = () => { }, }) - if (!result.ok) { - // Backend Busy keeps local state untouched, as before. - return result.value - } const outcome = result.value - if (outcome.status !== 'valid' && outcome.status !== 'busy') { + if (outcome.status !== 'valid') { await mutateProfiles() } return outcome diff --git a/src/hooks/use-proxy-delay-state.ts b/src/hooks/use-proxy-delay-state.ts index 8b999edba..f8a07f299 100644 --- a/src/hooks/use-proxy-delay-state.ts +++ b/src/hooks/use-proxy-delay-state.ts @@ -34,6 +34,7 @@ export function useProxyDelayState( groupName: string, ): UseProxyDelayState { const name = member.ref.name + const details = memberDetails(member) const unresolved = member.kind === 'unresolved' const isPreset = unresolved || PRESET_PROXY_NAMES.includes(name) const [delayState, setDelayState] = useReducer(identity, INITIAL_DELAY) @@ -42,18 +43,18 @@ export function useProxyDelayState( useEffect(() => { if (isPreset) return - delayManager.setListener(member, groupName, setDelayState) + delayManager.setListener(name, groupName, setDelayState) return () => { - delayManager.removeListener(member, groupName) + delayManager.removeListener(name, groupName) } - }, [member, groupName, isPreset]) + }, [name, groupName, isPreset]) const updateDelay = useCallback(() => { if (unresolved) { setDelayState(INITIAL_DELAY) return } - const cachedUpdate = delayManager.getDelayUpdate(member, groupName) + const cachedUpdate = delayManager.getDelayUpdate(name, groupName) if (cachedUpdate) { setDelayState({ ...cachedUpdate }) return @@ -66,9 +67,7 @@ export function useProxyDelayState( } let updatedAt = 0 - const history = - delayManager.getHistory(member, groupName) ?? - memberDetails(member)?.history + const history = details?.history if (history && history.length > 0) { const lastRecord = history[history.length - 1] const parsed = Date.parse(lastRecord.time) @@ -78,18 +77,16 @@ export function useProxyDelayState( } setDelayState({ delay: fallbackDelay, updatedAt }) - }, [groupName, member, unresolved]) + }, [details?.history, groupName, member, name, unresolved]) useEffect(() => { updateDelay() - return delayManager.addGroupListener(groupName, updateDelay) - }, [groupName, updateDelay]) + }, [updateDelay]) const onDelay = useLockFn(async () => { if (!isInteractableMember(member)) return setDelayState({ delay: -2, updatedAt: Date.now() }) - await delayManager.checkDelay(member, groupName, timeout) - updateDelay() + setDelayState(await delayManager.checkDelay(member, groupName, timeout)) }) return { diff --git a/src/locales/ar/proxies.json b/src/locales/ar/proxies.json index 55f99cec8..c884d9a0e 100644 --- a/src/locales/ar/proxies.json +++ b/src/locales/ar/proxies.json @@ -70,6 +70,8 @@ "chain": { "header": "Chain Proxy Config", "warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.", + "confirmEnable": "I understand, enable", + "keepDisabled": "Keep disabled", "empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.", "instruction": "Click nodes in order to add to proxy chain", "minimumNodes": "Chain proxy requires at least 2 nodes", diff --git a/src/locales/de/proxies.json b/src/locales/de/proxies.json index 3e19338ea..0ae1d2af3 100644 --- a/src/locales/de/proxies.json +++ b/src/locales/de/proxies.json @@ -70,6 +70,8 @@ "chain": { "header": "Proxy-Kette konfigurieren", "warning": "Eine Proxy-Kette verringert die Netzwerkgeschwindigkeit deutlich. Wenn Sie keinen klaren Bedarf haben oder diese Konfiguration nicht verstehen, lassen Sie sie deaktiviert.", + "confirmEnable": "Verstanden, aktivieren", + "keepDisabled": "Deaktiviert lassen", "empty": "Eine Proxy-Kette verringert die Netzwerkgeschwindigkeit deutlich. Wenn Sie keinen klaren Bedarf haben oder diese Konfiguration nicht verstehen, lassen Sie sie deaktiviert.", "instruction": "Klicken Sie die Knoten nacheinander an, um sie zur Proxy-Kette hinzuzufügen", "minimumNodes": "Eine Proxy-Kette braucht mindestens 2 Knoten", diff --git a/src/locales/en/proxies.json b/src/locales/en/proxies.json index ead3f0cbf..83c2a56bc 100644 --- a/src/locales/en/proxies.json +++ b/src/locales/en/proxies.json @@ -70,6 +70,8 @@ "chain": { "header": "Chain Proxy Config", "warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.", + "confirmEnable": "I understand, enable", + "keepDisabled": "Keep disabled", "empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.", "instruction": "Click nodes in order to add to proxy chain", "minimumNodes": "Chain proxy requires at least 2 nodes", diff --git a/src/locales/es/proxies.json b/src/locales/es/proxies.json index 537790ba3..46d0b4f5a 100644 --- a/src/locales/es/proxies.json +++ b/src/locales/es/proxies.json @@ -70,6 +70,8 @@ "chain": { "header": "Chain Proxy Config", "warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.", + "confirmEnable": "I understand, enable", + "keepDisabled": "Keep disabled", "empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.", "instruction": "Click nodes in order to add to proxy chain", "minimumNodes": "Chain proxy requires at least 2 nodes", diff --git a/src/locales/fa/proxies.json b/src/locales/fa/proxies.json index b430a2475..d055557d7 100644 --- a/src/locales/fa/proxies.json +++ b/src/locales/fa/proxies.json @@ -70,6 +70,8 @@ "chain": { "header": "Chain Proxy Config", "warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.", + "confirmEnable": "I understand, enable", + "keepDisabled": "Keep disabled", "empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.", "instruction": "Click nodes in order to add to proxy chain", "minimumNodes": "Chain proxy requires at least 2 nodes", diff --git a/src/locales/id/proxies.json b/src/locales/id/proxies.json index 7bb44c604..83f3c6c23 100644 --- a/src/locales/id/proxies.json +++ b/src/locales/id/proxies.json @@ -70,6 +70,8 @@ "chain": { "header": "Chain Proxy Config", "warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.", + "confirmEnable": "I understand, enable", + "keepDisabled": "Keep disabled", "empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.", "instruction": "Click nodes in order to add to proxy chain", "minimumNodes": "Chain proxy requires at least 2 nodes", diff --git a/src/locales/jp/proxies.json b/src/locales/jp/proxies.json index 950e65646..3dcb125dd 100644 --- a/src/locales/jp/proxies.json +++ b/src/locales/jp/proxies.json @@ -70,6 +70,8 @@ "chain": { "header": "Chain Proxy Config", "warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.", + "confirmEnable": "I understand, enable", + "keepDisabled": "Keep disabled", "empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.", "instruction": "Click nodes in order to add to proxy chain", "minimumNodes": "Chain proxy requires at least 2 nodes", diff --git a/src/locales/ko/proxies.json b/src/locales/ko/proxies.json index 64e7b6a9c..1ec805f00 100644 --- a/src/locales/ko/proxies.json +++ b/src/locales/ko/proxies.json @@ -70,6 +70,8 @@ "chain": { "header": "체인 프록시 구성", "warning": "체인 프록시는 네트워크 속도를 크게 저하시킬 수 있습니다. 명확한 업무상 필요가 없거나 이 구성을 이해하지 못한다면 비활성화 상태로 두세요.", + "confirmEnable": "이해했습니다, 활성화", + "keepDisabled": "비활성화 유지", "empty": "체인 프록시는 네트워크 속도를 크게 저하시킬 수 있습니다. 명확한 업무상 필요가 없거나 이 구성을 이해하지 못한다면 비활성화 상태로 두세요.", "instruction": "프록시 체인에 추가하려면 순서대로 노드를 클릭하세요", "minimumNodes": "체인 프록시는 최소 2개의 노드가 필요합니다", diff --git a/src/locales/pt/proxies.json b/src/locales/pt/proxies.json index 1740f6043..2f32fcdc9 100644 --- a/src/locales/pt/proxies.json +++ b/src/locales/pt/proxies.json @@ -70,6 +70,8 @@ "chain": { "header": "Configuração do proxy em cadeia", "warning": "O proxy em cadeia reduz significativamente a velocidade da rede. Se você não tem uma necessidade clara ou não entende esta configuração, mantenha-o desativado.", + "confirmEnable": "Entendi, ativar", + "keepDisabled": "Manter desativado", "empty": "O proxy em cadeia reduz significativamente a velocidade da rede. Se você não tem uma necessidade clara ou não entende esta configuração, mantenha-o desativado.", "instruction": "Clique nos nós em ordem para adicioná-los à cadeia de proxy", "minimumNodes": "O proxy em cadeia requer pelo menos 2 nós", diff --git a/src/locales/ru/proxies.json b/src/locales/ru/proxies.json index 603abacc1..9ac3af1df 100644 --- a/src/locales/ru/proxies.json +++ b/src/locales/ru/proxies.json @@ -70,6 +70,8 @@ "chain": { "header": "Цепочка прокси", "warning": "Цепочка прокси может значительно снизить скорость сети. Если у вас нет явной рабочей необходимости или вы не понимаете эту настройку, оставьте ее отключенной.", + "confirmEnable": "Понятно, включить", + "keepDisabled": "Оставить выключенным", "empty": "Цепочка прокси может значительно снизить скорость сети. Если у вас нет явной рабочей необходимости или вы не понимаете эту настройку, оставьте ее отключенной.", "instruction": "Нажимайте на узлы по порядку, чтобы добавить их в цепочку", "minimumNodes": "Для цепочки требуется минимум 2 узла", diff --git a/src/locales/tr/proxies.json b/src/locales/tr/proxies.json index 6c4a84122..5fa5f8a4b 100644 --- a/src/locales/tr/proxies.json +++ b/src/locales/tr/proxies.json @@ -70,6 +70,8 @@ "chain": { "header": "Zincir Vekil Yapılandırması", "warning": "Zincir vekil ağ hızını belirgin biçimde düşürür. Açık bir ihtiyacınız yoksa ya da bu yapılandırmayı bilmiyorsanız devre dışı bırakın.", + "confirmEnable": "Anladım, etkinleştir", + "keepDisabled": "Devre dışı kalsın", "empty": "Zincir vekil ağ hızını belirgin biçimde düşürür. Açık bir ihtiyacınız yoksa ya da bu yapılandırmayı bilmiyorsanız devre dışı bırakın.", "instruction": "Vekil zincirine eklemek için düğümlere sırayla tıklayın", "minimumNodes": "Zincir vekil en az 2 düğüm gerektirir", diff --git a/src/locales/tt/proxies.json b/src/locales/tt/proxies.json index 59fcd096b..6222875fe 100644 --- a/src/locales/tt/proxies.json +++ b/src/locales/tt/proxies.json @@ -70,6 +70,8 @@ "chain": { "header": "Chain Proxy Config", "warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.", + "confirmEnable": "I understand, enable", + "keepDisabled": "Keep disabled", "empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.", "instruction": "Click nodes in order to add to proxy chain", "minimumNodes": "Chain proxy requires at least 2 nodes", diff --git a/src/locales/zh/proxies.json b/src/locales/zh/proxies.json index cc792f5e7..3cecd8eb7 100644 --- a/src/locales/zh/proxies.json +++ b/src/locales/zh/proxies.json @@ -70,6 +70,8 @@ "chain": { "header": "代理链配置", "warning": "链式代理会显著降低网速。如果您没有明确的业务需求,或不了解此项配置,请保持关闭", + "confirmEnable": "我明白,开启", + "keepDisabled": "保持关闭", "empty": "链式代理会显著降低网速。如果您没有明确的业务需求,或不了解此项配置,请保持关闭", "instruction": "顺序点击节点添加到代理链中", "minimumNodes": "链式代理至少需要 2 个节点", diff --git a/src/locales/zhtw/proxies.json b/src/locales/zhtw/proxies.json index 48150df00..890a8ac4b 100644 --- a/src/locales/zhtw/proxies.json +++ b/src/locales/zhtw/proxies.json @@ -70,6 +70,8 @@ "chain": { "header": "鏈式代理設定", "warning": "鏈式代理會顯著降低網速。如果您沒有明確的業務需求,或不了解此項設定,請保持關閉", + "confirmEnable": "我明白,開啟", + "keepDisabled": "保持關閉", "empty": "鏈式代理會顯著降低網速。如果您沒有明確的業務需求,或不了解此項設定,請保持關閉", "instruction": "依序點擊節點新增到鏈式代理中", "minimumNodes": "鏈式代理至少需要 2 個節點", diff --git a/src/pages/proxies.tsx b/src/pages/proxies.tsx index cbf1d9ce3..14b56f25f 100644 --- a/src/pages/proxies.tsx +++ b/src/pages/proxies.tsx @@ -1,10 +1,10 @@ import { LanOutlined, LanRounded, WarningRounded } from '@mui/icons-material' -import { Box, Button, ButtonGroup } from '@mui/material' +import { Box, Button, ButtonGroup, Typography } from '@mui/material' import { useLockFn } from 'ahooks' import { useCallback, useEffect, useReducer, useState } from 'react' import { useTranslation } from 'react-i18next' -import { BasePage, TooltipIcon } from '@/components/base' +import { BaseDialog, BasePage, TooltipIcon } from '@/components/base' import { ProviderButton } from '@/components/proxy/provider-button' import { ProxyGroups } from '@/components/proxy/proxy-groups' import { @@ -38,6 +38,7 @@ const ProxyPage = () => { return false } }) + const [chainConfirmOpen, setChainConfirmOpen] = useState(false) const [chainConfigData, dispatchChainConfigData] = useReducer( (_: string | null, action: string | null) => action, @@ -68,32 +69,34 @@ const ProxyPage = () => { } }) - const onToggleChainMode = useLockFn(async () => { - const newChainMode = !isChainMode + const applyChainMode = (enabled: boolean) => { + setIsChainMode(enabled) + localStorage.setItem('proxy-chain-mode-enabled', enabled.toString()) + } - if (!newChainMode) { - // 退出链式代理模式时,清除链式代理配置 - try { - debugLog('Exiting chain mode, clearing chain configuration') - const result = await mutate( - () => updateProxyChainConfigInRuntime(null), - { - id: 'update-proxy-chain-runtime', - errorNotice: false, - }, - ) - if (!result.ok) return - debugLog('Chain configuration cleared successfully') - } catch (error) { - console.error('Failed to clear chain configuration:', error) - showNotice.error(error) - return - } + const onExitChainMode = useLockFn(async () => { + // 退出链式代理模式时,清除链式代理配置 + try { + debugLog('Exiting chain mode, clearing chain configuration') + const result = await mutate(() => updateProxyChainConfigInRuntime(null), { + id: 'update-proxy-chain-runtime', + errorNotice: false, + }) + if (!result.ok) return + debugLog('Chain configuration cleared successfully') + } catch (error) { + console.error('Failed to clear chain configuration:', error) + showNotice.error(error) + return } - setIsChainMode(newChainMode) - localStorage.setItem('proxy-chain-mode-enabled', newChainMode.toString()) + applyChainMode(false) }) + const onToggleChainMode = () => { + if (isChainMode) onExitChainMode().catch(() => {}) + else setChainConfirmOpen(true) + } + // 当开启链式代理模式时,获取配置数据 useEffect(() => { if (!isChainMode) { @@ -202,7 +205,23 @@ const ProxyPage = () => { mode={curMode ?? 'rule'} isChainMode={isChainMode} chainConfigData={chainConfigData} + onExitChainMode={onExitChainMode} /> + setChainConfirmOpen(false)} + onClose={() => setChainConfirmOpen(false)} + onOk={() => { + setChainConfirmOpen(false) + applyChainMode(true) + }} + > + {chainWarning} + ) } diff --git a/src/services/bindings.ts b/src/services/bindings.ts index 73e886a69..4a1623bdc 100644 --- a/src/services/bindings.ts +++ b/src/services/bindings.ts @@ -659,7 +659,6 @@ export type PrfItem_Deserialize = { desc?: string | null url?: string | null selected?: PrfSelected[] | null - latency_test_urls?: { [key in string]: string } | null extra?: PrfExtra | null updated?: number | null option?: PrfOption_Deserialize | null @@ -675,7 +674,6 @@ export type PrfItem_Serialize = { desc?: string | null url?: string | null selected?: PrfSelected[] | null - latency_test_urls?: { [key in string]: string } | null extra?: PrfExtra | null updated: number | null option?: PrfOption_Serialize | null @@ -770,7 +768,6 @@ export type ProfilePatch_Deserialize = { file?: string | null url?: string | null selected?: PrfSelected[] | null - latency_test_urls?: { [key in string]: string } | null extra?: PrfExtra | null updated?: number | null option?: PrfOption_Deserialize | null @@ -783,7 +780,6 @@ export type ProfilePatch_Serialize = { file: string | null url: string | null selected: PrfSelected[] | null - latency_test_urls: { [key in string]: string } | null extra: PrfExtra | null updated: number | null option: PrfOption_Serialize | null @@ -818,7 +814,6 @@ export type ProxyGroupView_Deserialize = { icon: string | null testUrl: string | null history: DelayHistory[] - extra: { [key in string]: ProxyLatencyHistory } members: ProxyMemberRef[] } & ProxyCapabilities @@ -831,14 +826,9 @@ export type ProxyGroupView_Serialize = { icon?: string | null testUrl?: string | null history: DelayHistory[] - extra: { [key in string]: ProxyLatencyHistory } members: ProxyMemberRef[] } & ProxyCapabilities -export type ProxyLatencyHistory = { - history: DelayHistory[] -} - export type ProxyMemberRef = | { kind: 'group'; name: string } | { kind: 'node'; name: string; recordId: string } @@ -860,7 +850,6 @@ export type ProxyNodeView_Deserialize = { name: string type: string history: DelayHistory[] - extra: { [key in string]: ProxyLatencyHistory } id: string | null hidden: boolean | null icon: string | null @@ -873,7 +862,6 @@ export type ProxyNodeView_Serialize = { name: string type: string history: DelayHistory[] - extra: { [key in string]: ProxyLatencyHistory } id?: string | null hidden?: boolean | null icon?: string | null diff --git a/src/services/cmds.ts b/src/services/cmds.ts index 001ee3c71..f46377335 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -91,18 +91,8 @@ export async function deleteProfile(index: string) { } export async function patchProfile(index: string, profile: ProfilePatch) { - const { - type, - name, - desc, - file, - url, - selected, - latency_test_urls, - extra, - updated, - option, - } = profile + const { type, name, desc, file, url, selected, extra, updated, option } = + profile return commands.patchProfile(index, { type, name, @@ -110,7 +100,6 @@ export async function patchProfile(index: string, profile: ProfilePatch) { file, url, selected, - latency_test_urls, extra, updated, option, diff --git a/src/services/delay.test.ts b/src/services/delay.test.ts index a26d25dde..a4d7c92fd 100644 --- a/src/services/delay.test.ts +++ b/src/services/delay.test.ts @@ -59,136 +59,3 @@ describe('group delay completion', () => { stop() }) }) - -test('uses newer core history after a tray test without hiding an active page test', () => { - const member = node('tray-refresh') - if (member.kind !== 'node') throw new Error('Expected node') - const cached = delayManager.setDelay(member, 'tray-group', 50) - member.node.extra = { - [delayManager.getUrl('tray-group')]: { - history: [ - { time: new Date(cached.updatedAt + 1000).toISOString(), delay: 300 }, - ], - }, - } - - expect(delayManager.getDelayFix(member, 'tray-group')).toBe(300) - expect(delayManager.getDelayUpdate(member, 'tray-group')?.delay).toBe(300) - - delayManager.setDelay(member, 'tray-group', -2) - expect(delayManager.getDelayFix(member, 'tray-group')).toBe(-2) -}) - -test('latency URL validation rejects malformed hosts and ports and trims valid URLs', () => { - for (const url of [ - 'http://?', - 'http://host:invalid', - 'http://bad host/', - 'ftp://localhost', - 'http:localhost', - ]) { - delayManager.setUrl('url-validation', url) - expect(delayManager.getUrl('url-validation')).toBe( - 'http://cp.cloudflare.com/generate_204', - ) - } - delayManager.setUrl('url-validation', ' HTTP://localhost:8080/ ') - expect(delayManager.getUrl('url-validation')).toBe('HTTP://localhost:8080/') -}) - -test('another URL cannot replace a group measurement, but a newer matching URL can', () => { - const member = node('url-isolation') - if (member.kind !== 'node') throw new Error('Expected node') - delayManager.setUrl('url-a', 'http://localhost/a') - const cached = delayManager.setDelay(member, 'url-a', 50) - const newer = new Date(cached.updatedAt + 1000).toISOString() - Object.assign(member.node, { - history: [{ time: newer, delay: 126 }], - extra: { - 'http://localhost/a': { - history: [ - { time: new Date(cached.updatedAt - 1000).toISOString(), delay: 49 }, - ], - }, - 'http://localhost/b': { - history: [{ time: newer, delay: 126 }], - }, - }, - }) - expect(delayManager.getDelayFix(member, 'url-a')).toBe(50) - Object.assign(member.node, { - extra: { - 'http://localhost/a': { - history: [{ time: newer, delay: 75 }], - }, - }, - }) - expect(delayManager.getDelayFix(member, 'url-a')).toBe(75) -}) - -test('same-named provider members keep separate measurements and listeners', async () => { - const first = node('same-provider-name') - const second = node('same-provider-name') - if (first.kind !== 'node' || second.kind !== 'node') - throw new Error('Expected nodes') - first.node.recordId = 'provider-a:N' - first.node.source = { kind: 'provider', providerName: 'a', proxyName: 'N' } - second.node.recordId = 'provider-b:N' - second.node.source = { kind: 'provider', providerName: 'b', proxyName: 'N' } - const { healthcheckNodeInProvider } = await import('tauri-plugin-mihomo-api') - vi.mocked(healthcheckNodeInProvider) - .mockResolvedValueOnce({ delay: 40 }) - .mockResolvedValueOnce({ delay: 125 }) - const firstListener = vi.fn() - const secondListener = vi.fn() - delayManager.setListener(first, 'provider-collision', firstListener) - delayManager.setListener(second, 'provider-collision', secondListener) - await delayManager.checkListDelay([first, second], 'provider-collision', 5000) - await flush() - expect(firstListener).toHaveBeenLastCalledWith( - expect.objectContaining({ delay: 40 }), - ) - expect(secondListener).toHaveBeenLastCalledWith( - expect.objectContaining({ delay: 125 }), - ) - delayManager.removeListener(first, 'provider-collision') - delayManager.removeListener(second, 'provider-collision') - expect(delayManager.getDelayFix(first, 'provider-collision')).toBe(40) - expect(delayManager.getDelayFix(second, 'provider-collision')).toBe(125) -}) - -test.each(['core', 'provider'] as const)( - 'cached %s delays follow source identity after record IDs move', - (kind) => { - const original = node(`stable-${kind}`) - const replacement = node(`replacement-${kind}`) - if (original.kind !== 'node' || replacement.kind !== 'node') - throw new Error('Expected nodes') - const source = (proxyName: string) => - kind === 'core' - ? { kind, proxyName } - : { kind, providerName: 'subscription', proxyName } - original.node.source = source(original.ref.name) - replacement.node.source = source(replacement.ref.name) - original.node.recordId = 'position:0' - replacement.node.recordId = 'position:0' - delayManager.setDelay(original, `reorder-${kind}`, 40) - expect(delayManager.getDelayFix(replacement, `reorder-${kind}`)).toBe(-1) - original.node.recordId = 'position:1' - expect(delayManager.getDelayFix(original, `reorder-${kind}`)).toBe(40) - }, -) - -test('unmeasured selectors show automatic history without replacing a manual URL result', () => { - const member = node('selector-member') - if (member.kind !== 'node') throw new Error('Expected node') - member.node.history = [ - { time: new Date(Date.now() + 1000).toISOString(), delay: 126 }, - ] - member.node.extra = { - 'http://localhost/automatic': { history: member.node.history }, - } - expect(delayManager.getDelayFix(member, 'selector')).toBe(126) - delayManager.setDelay(member, 'selector', 50) - expect(delayManager.getDelayFix(member, 'selector')).toBe(50) -}) diff --git a/src/services/delay.ts b/src/services/delay.ts index 323809b67..18602faaf 100644 --- a/src/services/delay.ts +++ b/src/services/delay.ts @@ -12,23 +12,13 @@ import { } from '@/types/proxy-view' import { debugLog } from '@/utils/debug' import { classifyDelay, DEFAULT_DELAY_TIMEOUT } from '@/utils/delay' -import { isValidLatencyTestUrl } from '@/utils/network' +import { isValidUrl } from '@/utils/network' export type DelaySnapshot = { of: (member: ResolvedProxyMember) => number } -const hashKey = (member: ResolvedProxyMember, group: string) => { - if (member.kind !== 'node') - return JSON.stringify([group, member.kind, member.ref.name]) - const source = member.node.source - return JSON.stringify([ - group, - source.kind, - source.kind === 'provider' ? source.providerName : null, - source.proxyName, - ]) -} +const hashKey = (name: string, group: string) => `${group ?? ''}::${name}` export interface DelayUpdate { delay: number @@ -39,7 +29,7 @@ export interface DelayUpdate { const CACHE_TTL = 30 * 60 * 1000 class DelayManager { - private cache = new Map() + private cache = new Map() private urlMap = new Map() private listenerMap = new Map void>() @@ -163,13 +153,13 @@ class DelayManager { } setUrl(group: string, url: string) { - const previous = this.getUrl(group) - if (isValidLatencyTestUrl(url)) { - this.urlMap.set(group, url.trim()) - } else { + if (!isValidUrl(url)) { + debugLog(`[DelayManager] 拒绝无效测试URL,组: ${group}, URL: ${url}`) this.urlMap.delete(group) + return } - if (previous !== this.getUrl(group)) this.queueGroupNotification(group) + debugLog(`[DelayManager] 设置测试URL,组: ${group}, URL: ${url}`) + this.urlMap.set(group, url) } getUrl(group: string) { @@ -181,16 +171,16 @@ class DelayManager { } setListener( - member: ResolvedProxyMember, + name: string, group: string, listener: (update: DelayUpdate) => void, ) { - const key = hashKey(member, group) + const key = hashKey(name, group) this.listenerMap.set(key, listener) } - removeListener(member: ResolvedProxyMember, group: string) { - const key = hashKey(member, group) + removeListener(name: string, group: string) { + const key = hashKey(name, group) this.listenerMap.delete(key) } @@ -209,14 +199,14 @@ class DelayManager { } setDelay( - member: ResolvedProxyMember, + name: string, group: string, delay: number, - meta?: { elapsed?: number; url?: string }, + meta?: { elapsed?: number }, ): DelayUpdate { - const key = hashKey(member, group) + const key = hashKey(name, group) debugLog( - `[DelayManager] 设置延迟,代理: ${member.ref.name}, 组: ${group}, 延迟: ${delay}`, + `[DelayManager] 设置延迟,代理: ${name}, 组: ${group}, 延迟: ${delay}`, ) const update: DelayUpdate = { delay, @@ -224,9 +214,7 @@ class DelayManager { updatedAt: Date.now(), } - const url = meta?.url ?? this.getUrl(group) - if (url !== this.getUrl(group)) return update - this.cache.set(key, { ...update, url }) + this.cache.set(key, update) const queue = this.pendingItemUpdates.get(key) if (queue) { @@ -239,16 +227,9 @@ class DelayManager { return update } - getDelayUpdate(member: ResolvedProxyMember, group: string) { - const history = this.getHistory(member, group) - const key = hashKey(member, group) - const cached = this.cache.get(key) - const entry = cached?.url === this.getUrl(group) ? cached : undefined - const latest = history?.at(-1) - const updatedAt = latest ? Date.parse(latest.time) : 0 - if (latest && entry?.delay !== -2 && updatedAt > (entry?.updatedAt ?? 0)) { - return { delay: latest.delay || 1e6, updatedAt } - } + getDelayUpdate(name: string, group: string) { + const key = hashKey(name, group) + const entry = this.cache.get(key) if (!entry) return undefined if (Date.now() - entry.updatedAt > CACHE_TTL) { @@ -259,25 +240,23 @@ class DelayManager { return { ...entry } } - getDelay(member: ResolvedProxyMember, group: string) { - const update = this.getDelayUpdate(member, group) + getDelay(name: string, group: string) { + const update = this.getDelayUpdate(name, group) return update ? update.delay : -1 } - getHistory(member: ResolvedProxyMember, group: string) { - return memberDetails(member)?.extra?.[this.getUrl(group)]?.history - } - getDelayFix(member: ResolvedProxyMember, group: string) { if (member.kind === 'unresolved') return -1 - const history = - this.getHistory(member, group) ?? memberDetails(member)?.history - const update = this.getDelayUpdate(member, group) + const details = memberDetails(member) + const name = member.ref.name + const update = this.getDelayUpdate(name, group) if (update && (update.delay >= 0 || update.delay === -2)) { return update.delay } - if (history?.length) return history[history.length - 1].delay || 1e6 + if (details?.history && details.history.length > 0) { + return details.history[details.history.length - 1].delay || 1e6 + } return -1 } @@ -319,12 +298,12 @@ class DelayManager { `[DelayManager] 开始测试延迟,代理: ${name}, 组: ${group}, 超时: ${timeout}ms`, ) - const url = this.getUrl(group) - this.setDelay(member, group, -2, { url }) + this.setDelay(name, group, -2) const startTime = Date.now() try { + const url = this.getUrl(group) debugLog(`[DelayManager] 调用API测试延迟,代理: ${name}, URL: ${url}`) const timeoutPromise = new Promise((resolve) => { @@ -345,14 +324,14 @@ class DelayManager { const elapsed = elapsedTime debugLog(`[DelayManager] 延迟测试完成,代理: ${name}, 结果: ${delay}ms`) - return this.setDelay(member, group, delay, { elapsed, url }) + return this.setDelay(name, group, delay, { elapsed }) } catch (error) { await new Promise((resolve) => setTimeout(resolve, 500)) console.error(`[DelayManager] 延迟测试出错,代理: ${name}`, error) const delay = 1e6 // error const elapsed = Date.now() - startTime - return this.setDelay(member, group, delay, { elapsed, url }) + return this.setDelay(name, group, delay, { elapsed }) } } @@ -365,9 +344,10 @@ class DelayManager { debugLog( `[DelayManager] 批量测试延迟开始,组: ${group}, 数量: ${proxies.length}, 并发数: ${concurrency}`, ) + const names = proxies.map((member) => member.ref.name) this.activeBatches.set(group, (this.activeBatches.get(group) ?? 0) + 1) - proxies.forEach((member) => { - this.setDelay(member, group, -2) + names.forEach((name) => { + this.setDelay(name, group, -2) }) let index = 0 @@ -379,7 +359,7 @@ class DelayManager { const currName = currMember.ref.name try { - this.setDelay(currMember, group, -2) + this.setDelay(currName, group, -2) // Stagger requests so a batch does not hit the core at once. if (index > 1) { @@ -395,13 +375,13 @@ class DelayManager { `[DelayManager] 批量测试单个代理出错,代理: ${currName}`, error, ) - this.setDelay(currMember, group, 1e6) + this.setDelay(currName, group, 1e6) } return help() } - const actualConcurrency = Math.min(concurrency, proxies.length, 10) + const actualConcurrency = Math.min(concurrency, names.length, 10) debugLog(`[DelayManager] 实际并发数: ${actualConcurrency}`) const promiseList: Promise[] = [] diff --git a/src/services/notice-handlers.ts b/src/services/notice-handlers.ts index c87c5a9f2..669853a4c 100644 --- a/src/services/notice-handlers.ts +++ b/src/services/notice-handlers.ts @@ -32,230 +32,221 @@ export const forgetShownStartupError = () => { * type makes a backend status without a frontend entry a compile error (and * contract regeneration keeps the union in sync). */ +const handlers: Record< + NoticeStatus, + (msg: string, navigate: NavigateFunction) => void +> = { + info: (msg) => { + if (msg) showNotice.info(msg) + }, + 'import_sub_url::ok': (_msg, navigate) => { + // 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。 + // 未来细分事件通知时,可以考虑传入订阅 ID 或其他标识符 + // navigate("/profile", { state: { current: msg } }); + navigate('/profile') + showNotice.success( + 'shared.feedback.notifications.importSubscriptionSuccess', + ) + }, + 'import_sub_url::error': (msg, navigate) => { + navigate('/profile') + showNotice.error(msg) + }, + 'set_config::error': (msg) => showNotice.error(msg), + 'core_start::error': () => { + const read = ++startupErrorReads + void getCoreStartupError() + .then(async (failure) => { + if (read < settledStartupErrorRead) return + settledStartupErrorRead = read + if (!failure) { + shownStartupError = null + return + } + const window = getCurrentWindow() + const [visible, minimized] = await Promise.all([ + window.isVisible(), + window.isMinimized(), + ]) + const shown = `${failure.kind}:${failure.detail}` + if ( + read === settledStartupErrorRead && + visible && + !minimized && + shownStartupError !== shown + ) { + shownStartupError = shown + showNotice.error( + failure.kind === 'serviceCoreStopped' + ? 'settings.feedback.errors.clash.serviceCoreStopped' + : 'settings.feedback.errors.clash.startFailed', + failure.detail, + ) + } + }) + .catch((error) => { + console.error('Failed to read the pending core startup error', error) + }) + }, + 'service_core::repair_required': () => { + void takeServiceRepairNotice() + .then((pending) => { + if (pending) { + requestService({ reason: 'serviceLocationRefused' }) + } + }) + .catch((error) => { + console.error('Failed to read the pending service repair notice', error) + }) + }, + 'service_core::app_data_not_owned': () => { + void takeServiceOwnerNotice() + .then((command) => { + if (command) { + if (shownOwnerNotice !== null) hideNotice(shownOwnerNotice) + shownOwnerNotice = showNotice.warning( + 'settings.feedback.notifications.clashService.appDataNotOwned', + { command }, + 0, + ) + } + }) + .catch((error) => { + console.error('Failed to read the pending service owner notice', error) + }) + }, + 'service_core::sidecar_fallback': () => { + void takeServiceFallbackNotice() + .then((notice) => { + if (notice?.kind === 'coreRejected') { + showNotice.warning( + 'settings.feedback.notifications.clashService.permissionFallback', + { reason: notice.reason }, + 0, + ) + } else if (notice?.kind === 'notAutoStarted') { + requestService({ reason: 'serviceNotAutoStarted' }) + } else if (notice) { + showNotice.warning( + 'settings.feedback.notifications.clashService.sidecarFallback', + ) + } + }) + .catch((error) => { + console.error( + 'Failed to read the pending service fallback notice', + error, + ) + }) + }, + 'dns_override::auto_disabled': () => { + void takeDnsOverrideNotice() + .then((pending) => { + if (pending) { + showNotice.info('settings.modals.dns.protection.autoDisabled') + } + }) + .catch((error) => { + console.error('Failed to read the pending DNS override notice', error) + }) + }, + 'enhance::discarded_keys': () => { + void takeDiscardedKeysNotice() + .then((keys) => { + if (keys) { + showNotice.warning('profiles.page.feedback.notices.discardedKeys', { + keys, + }) + } + }) + .catch((error) => { + console.error('Failed to read the pending discarded keys notice', error) + }) + }, + 'tun_mode::auto_disabled': () => + showNotice.info( + 'settings.sections.system.notifications.tunMode.autoDisabled', + ), + 'tun_mode::auto_disable_failed': () => + showNotice.error( + 'settings.sections.system.notifications.tunMode.autoDisableFailed', + ), + 'app_restart::core_stop_failed': (msg) => + showNotice.error( + 'layout.feedback.errors.restartCoreStopFailed', + msg || undefined, + ), + 'app_quit::core_stop_failed': (msg) => + showNotice.error( + 'layout.feedback.errors.quitCoreStopFailed', + msg || undefined, + ), + update_with_clash_proxy: (msg) => + showNotice.success( + 'settings.feedback.notifications.updater.withClashProxySuccess', + msg, + ), + update_failed_even_with_clash: (msg) => + showNotice.error( + 'settings.feedback.notifications.updater.withClashProxyFailed', + msg, + ), + 'reactivate_profiles::error': (msg) => showNotice.error(msg), + update_failed: (msg) => showNotice.error(msg), + 'config_validate::boot_error': (msg) => + showNotice.error('shared.feedback.validation.config.bootFailed', msg), + 'config_validate::error': (msg) => + showNotice.error('shared.feedback.validation.config.failed', msg), + 'config_validate::process_terminated': () => + showNotice.error('shared.feedback.validation.config.processTerminated'), + 'config_validate::script_error': (msg) => + showNotice.error('shared.feedback.validation.script.fileError', msg), + 'config_validate::script_syntax_error': (msg) => + showNotice.error('shared.feedback.validation.script.syntaxError', msg), + 'config_validate::script_missing_main': (msg) => + showNotice.error('shared.feedback.validation.script.missingMain', msg), + 'config_validate::file_not_found': (msg) => + showNotice.error('shared.feedback.validation.script.fileNotFound', msg), + 'config_validate::yaml_syntax_error': (msg) => + showNotice.error('shared.feedback.validation.yaml.syntaxError', msg), + 'config_validate::yaml_read_error': (msg) => + showNotice.error('shared.feedback.validation.yaml.readError', msg), + 'config_validate::yaml_mapping_error': (msg) => + showNotice.error('shared.feedback.validation.yaml.mappingError', msg), + 'config_validate::merge_syntax_error': (msg) => + showNotice.error('shared.feedback.validation.merge.syntaxError', msg), + 'config_validate::merge_mapping_error': (msg) => + showNotice.error('shared.feedback.validation.merge.mappingError', msg), + 'config_core::change_success': (msg) => + showNotice.success( + 'settings.feedback.notifications.clash.changeSuccess', + msg, + ), + 'config_core::change_error': (msg) => + showNotice.error('settings.feedback.notifications.clash.changeFailed', msg), + 'mixed_port::fallback': (msg) => { + const [oldPort, newPort] = msg.split(',') + showNotice.info('settings.modals.clashPort.messages.automaticFallback', { + oldPort, + newPort, + }) + }, + 'mixed_port::fallback_error': (msg) => + showNotice.error( + 'settings.modals.clashPort.messages.automaticFallbackFailed', + { error: msg }, + ), +} + export const handleNoticeMessage = ( status: string, msg: string, t: TranslateFunction, navigate: NavigateFunction, ) => { - const handlers: Record void> = { - info: () => { - if (msg) showNotice.info(msg) - }, - 'import_sub_url::ok': () => { - // 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。 - // 未来细分事件通知时,可以考虑传入订阅 ID 或其他标识符 - // navigate("/profile", { state: { current: msg } }); - navigate('/profile') - showNotice.success( - 'shared.feedback.notifications.importSubscriptionSuccess', - ) - }, - 'import_sub_url::error': () => { - navigate('/profile') - showNotice.error(msg) - }, - 'set_config::error': () => showNotice.error(msg), - 'core_start::error': () => { - const read = ++startupErrorReads - void getCoreStartupError() - .then(async (failure) => { - if (read < settledStartupErrorRead) return - settledStartupErrorRead = read - if (!failure) { - shownStartupError = null - return - } - const window = getCurrentWindow() - const [visible, minimized] = await Promise.all([ - window.isVisible(), - window.isMinimized(), - ]) - const shown = `${failure.kind}:${failure.detail}` - if ( - read === settledStartupErrorRead && - visible && - !minimized && - shownStartupError !== shown - ) { - shownStartupError = shown - showNotice.error( - failure.kind === 'serviceCoreStopped' - ? 'settings.feedback.errors.clash.serviceCoreStopped' - : 'settings.feedback.errors.clash.startFailed', - failure.detail, - ) - } - }) - .catch((error) => { - console.error('Failed to read the pending core startup error', error) - }) - }, - 'service_core::repair_required': () => { - void takeServiceRepairNotice() - .then((pending) => { - if (pending) { - requestService({ reason: 'serviceLocationRefused' }) - } - }) - .catch((error) => { - console.error( - 'Failed to read the pending service repair notice', - error, - ) - }) - }, - 'service_core::app_data_not_owned': () => { - void takeServiceOwnerNotice() - .then((command) => { - if (command) { - if (shownOwnerNotice !== null) hideNotice(shownOwnerNotice) - shownOwnerNotice = showNotice.warning( - 'settings.feedback.notifications.clashService.appDataNotOwned', - { command }, - 0, - ) - } - }) - .catch((error) => { - console.error( - 'Failed to read the pending service owner notice', - error, - ) - }) - }, - 'service_core::sidecar_fallback': () => { - void takeServiceFallbackNotice() - .then((notice) => { - if (notice?.kind === 'coreRejected') { - showNotice.warning( - 'settings.feedback.notifications.clashService.permissionFallback', - { reason: notice.reason }, - 0, - ) - } else if (notice?.kind === 'notAutoStarted') { - requestService({ reason: 'serviceNotAutoStarted' }) - } else if (notice) { - showNotice.warning( - 'settings.feedback.notifications.clashService.sidecarFallback', - ) - } - }) - .catch((error) => { - console.error( - 'Failed to read the pending service fallback notice', - error, - ) - }) - }, - 'dns_override::auto_disabled': () => { - void takeDnsOverrideNotice() - .then((pending) => { - if (pending) { - showNotice.info('settings.modals.dns.protection.autoDisabled') - } - }) - .catch((error) => { - console.error('Failed to read the pending DNS override notice', error) - }) - }, - 'enhance::discarded_keys': () => { - void takeDiscardedKeysNotice() - .then((keys) => { - if (keys) { - showNotice.warning('profiles.page.feedback.notices.discardedKeys', { - keys, - }) - } - }) - .catch((error) => { - console.error( - 'Failed to read the pending discarded keys notice', - error, - ) - }) - }, - 'tun_mode::auto_disabled': () => - showNotice.info( - 'settings.sections.system.notifications.tunMode.autoDisabled', - ), - 'tun_mode::auto_disable_failed': () => - showNotice.error( - 'settings.sections.system.notifications.tunMode.autoDisableFailed', - ), - 'app_restart::core_stop_failed': () => - showNotice.error( - 'layout.feedback.errors.restartCoreStopFailed', - msg || undefined, - ), - 'app_quit::core_stop_failed': () => - showNotice.error( - 'layout.feedback.errors.quitCoreStopFailed', - msg || undefined, - ), - update_with_clash_proxy: () => - showNotice.success( - 'settings.feedback.notifications.updater.withClashProxySuccess', - msg, - ), - update_failed_even_with_clash: () => - showNotice.error( - 'settings.feedback.notifications.updater.withClashProxyFailed', - msg, - ), - 'reactivate_profiles::error': () => showNotice.error(msg), - update_failed: () => showNotice.error(msg), - 'config_validate::boot_error': () => - showNotice.error('shared.feedback.validation.config.bootFailed', msg), - 'config_validate::error': () => - showNotice.error('shared.feedback.validation.config.failed', msg), - 'config_validate::process_terminated': () => - showNotice.error('shared.feedback.validation.config.processTerminated'), - 'config_validate::script_error': () => - showNotice.error('shared.feedback.validation.script.fileError', msg), - 'config_validate::script_syntax_error': () => - showNotice.error('shared.feedback.validation.script.syntaxError', msg), - 'config_validate::script_missing_main': () => - showNotice.error('shared.feedback.validation.script.missingMain', msg), - 'config_validate::file_not_found': () => - showNotice.error('shared.feedback.validation.script.fileNotFound', msg), - 'config_validate::yaml_syntax_error': () => - showNotice.error('shared.feedback.validation.yaml.syntaxError', msg), - 'config_validate::yaml_read_error': () => - showNotice.error('shared.feedback.validation.yaml.readError', msg), - 'config_validate::yaml_mapping_error': () => - showNotice.error('shared.feedback.validation.yaml.mappingError', msg), - 'config_validate::merge_syntax_error': () => - showNotice.error('shared.feedback.validation.merge.syntaxError', msg), - 'config_validate::merge_mapping_error': () => - showNotice.error('shared.feedback.validation.merge.mappingError', msg), - 'config_core::change_success': () => - showNotice.success( - 'settings.feedback.notifications.clash.changeSuccess', - msg, - ), - 'config_core::change_error': () => - showNotice.error( - 'settings.feedback.notifications.clash.changeFailed', - msg, - ), - 'mixed_port::fallback': () => { - const [oldPort, newPort] = msg.split(',') - showNotice.info('settings.modals.clashPort.messages.automaticFallback', { - oldPort, - newPort, - }) - }, - 'mixed_port::fallback_error': () => - showNotice.error( - 'settings.modals.clashPort.messages.automaticFallbackFailed', - { error: msg }, - ), - } - const handler = handlers[status as NoticeStatus] if (handler) { - handler() + handler(msg, navigate) } else { console.warn(`未处理的通知状态: ${status}`) } diff --git a/src/services/proxy-test-url.test.ts b/src/services/proxy-test-url.test.ts deleted file mode 100644 index 4f2969c88..000000000 --- a/src/services/proxy-test-url.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { afterEach, expect, test, vi } from 'vitest' - -vi.mock('@/services/cmds', () => ({ - getProfiles: vi.fn(), - patchProfile: vi.fn().mockResolvedValue(undefined), - syncTrayProxySelection: vi.fn().mockResolvedValue(undefined), -})) -vi.mock('@/services/query-client', () => ({ revalidateQueries: vi.fn() })) -vi.mock('@/services/notice-service', () => ({ showNotice: { error: vi.fn() } })) - -import { - getProfiles, - patchProfile, - syncTrayProxySelection, -} from '@/services/cmds' -import { revalidateQueries } from '@/services/query-client' - -import { - profileTestUrls, - saveProfileTestUrls, - commitProfileTestUrl, -} from './proxy-test-url' - -afterEach(() => { - vi.useRealTimers() - vi.clearAllMocks() -}) - -test('committed edits retain their profile and merge with fresh saved URLs', async () => { - vi.mocked(getProfiles).mockResolvedValue({ - current: 'new-profile', - items: [ - { - uid: 'old-profile', - type: null, - name: null, - file: null, - updated: null, - latency_test_urls: { untouched: 'https://example.com/' }, - }, - ], - }) - await commitProfileTestUrl('old-profile', 'A', ' http://localhost/new ') - expect(patchProfile).toHaveBeenCalledExactlyOnceWith('old-profile', { - latency_test_urls: { - untouched: 'https://example.com/', - A: 'http://localhost/new', - }, - }) - expect(revalidateQueries).toHaveBeenCalledWith([['getProfiles']]) - expect(syncTrayProxySelection).toHaveBeenCalledOnce() -}) - -test('migration cannot overwrite a previously saved or cleared override', async () => { - vi.mocked(getProfiles).mockResolvedValue({ - items: [ - { - uid: 'p', - type: null, - name: null, - file: null, - updated: null, - latency_test_urls: { A: '' }, - }, - ], - }) - await saveProfileTestUrls('p', { A: 'http://localhost/stale' }, true) - expect(patchProfile).not.toHaveBeenCalled() -}) - -test('pending edits survive a reload and older save acknowledgements', async () => { - vi.useFakeTimers() - const profile = { - uid: 'pending-profile', - type: null, - name: null, - file: null, - updated: null, - latency_test_urls: { A: 'http://localhost/old' }, - } - vi.mocked(getProfiles).mockResolvedValue({ items: [profile] }) - let finish!: () => void - vi.mocked(patchProfile).mockImplementationOnce( - () => - new Promise((resolve) => { - finish = () => resolve(null) - }), - ) - const first = commitProfileTestUrl(profile.uid, 'A', 'http://localhost/new') - expect(profileTestUrls(profile).A).toBe('http://localhost/new') - await vi.advanceTimersByTimeAsync(300) - expect(profileTestUrls(profile).A).toBe('http://localhost/new') - const second = commitProfileTestUrl( - profile.uid, - 'A', - 'http://localhost/newest', - ) - finish() - await first - expect(profileTestUrls(profile).A).toBe('http://localhost/newest') - await second - expect(patchProfile).toHaveBeenLastCalledWith(profile.uid, { - latency_test_urls: { A: 'http://localhost/newest' }, - }) -}) - -test('a failed save preserves the committed input across profile refreshes', async () => { - const profile = { - uid: 'failed-profile', - type: null, - name: null, - file: null, - updated: null, - latency_test_urls: { A: 'http://localhost/old' }, - } - vi.mocked(getProfiles).mockResolvedValue({ items: [profile] }) - vi.mocked(patchProfile).mockRejectedValueOnce(new Error('save failed')) - const log = vi.spyOn(console, 'error').mockImplementation(() => {}) - await commitProfileTestUrl(profile.uid, 'A', 'http://localhost/new') - expect(profileTestUrls(profile).A).toBe('http://localhost/new') - await commitProfileTestUrl(profile.uid, 'A', 'http://localhost/new') - expect(profileTestUrls(profile).A).toBe('http://localhost/old') - log.mockRestore() -}) diff --git a/src/services/proxy-test-url.ts b/src/services/proxy-test-url.ts deleted file mode 100644 index 059b632fe..000000000 --- a/src/services/proxy-test-url.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { - getProfiles, - patchProfile, - syncTrayProxySelection, -} from '@/services/cmds' -import { mutate } from '@/services/mutate' - -function readLegacyTestUrls(uid: string): Record { - try { - const states = JSON.parse(localStorage.getItem('proxy-head-state') || '{}')[ - uid - ] - return Object.fromEntries( - Object.entries(states || {}).flatMap(([name, state]) => { - const url = (state as { testUrl?: string }).testUrl - return typeof url === 'string' && url.trim() ? [[name, url.trim()]] : [] - }), - ) - } catch { - return {} - } -} - -export async function saveProfileTestUrls( - uid: string, - urls: Record, - migrateOnly = false, -) { - return mutate( - async () => { - const profiles = await getProfiles() - const profile = profiles.items?.find((item) => item.uid === uid) - if (!profile || (migrateOnly && profile.latency_test_urls !== undefined)) - return - await patchProfile(uid, { - latency_test_urls: { - ...(profile.latency_test_urls ?? readLegacyTestUrls(uid)), - ...urls, - }, - }) - await syncTrayProxySelection().catch((error) => { - console.error( - 'Failed to refresh tray after saving latency test URL:', - error, - ) - }) - }, - { id: `patch-profile:${uid}`, revalidate: [['getProfiles']] }, - ) -} - -const migrating = new Set() - -export function migrateProfileTestUrls(profiles: IProfilesConfig) { - for (const profile of profiles.items || []) { - const uid = profile.uid - if (!uid) continue - if (profile.latency_test_urls !== undefined || migrating.has(uid)) continue - const urls = readLegacyTestUrls(uid) - if (!Object.keys(urls).length) continue - migrating.add(uid) - void saveProfileTestUrls(uid, urls, true) - .catch((error) => { - console.error('Failed to migrate latency test URLs:', error) - }) - .finally(() => migrating.delete(uid)) - } -} - -const pending = new Map>() - -export async function commitProfileTestUrl( - uid: string, - group: string, - url: string, -) { - const edit = { url: url.trim() } - const urls = { ...pending.get(uid), [group]: edit } - pending.set(uid, urls) - try { - await saveProfileTestUrls(uid, { [group]: edit.url }) - const current = pending.get(uid) - if (current?.[group] === edit) { - delete current[group] - if (!Object.keys(current).length) pending.delete(uid) - } - } catch (error) { - console.error('Failed to save latency test URL:', error) - } -} - -export function profileTestUrls(profile: IProfileItem): Record { - return { - ...profile.latency_test_urls, - ...Object.fromEntries( - Object.entries( - (profile.uid ? pending.get(profile.uid) : undefined) ?? {}, - ).map(([group, edit]) => [group, edit.url]), - ), - } -} diff --git a/src/store/app-state.ts b/src/store/app-state.ts index c0e143308..ad2fe6cd1 100644 --- a/src/store/app-state.ts +++ b/src/store/app-state.ts @@ -66,6 +66,7 @@ const withLoading = ( next.delete(uid) } } + if (next.size === state.profileUpdates.loading.size) return state return { ...state, profileUpdates: { ...state.profileUpdates, loading: next }, diff --git a/src/types/generated/i18n-keys.ts b/src/types/generated/i18n-keys.ts index 4e566dce4..07be729be 100644 --- a/src/types/generated/i18n-keys.ts +++ b/src/types/generated/i18n-keys.ts @@ -322,6 +322,8 @@ export const translationKeys = [ 'proxies.page.empty.actions.openLogs', 'proxies.page.chain.header', 'proxies.page.chain.warning', + 'proxies.page.chain.confirmEnable', + 'proxies.page.chain.keepDisabled', 'proxies.page.chain.empty', 'proxies.page.chain.instruction', 'proxies.page.chain.minimumNodes', diff --git a/src/types/generated/i18n-resources.ts b/src/types/generated/i18n-resources.ts index 87aba3645..b2f2a64bf 100644 --- a/src/types/generated/i18n-resources.ts +++ b/src/types/generated/i18n-resources.ts @@ -525,6 +525,7 @@ export interface TranslationResources { toggleChain: string } chain: { + confirmEnable: string connectFailed: string disconnectFailed: string duplicateNode: string @@ -533,6 +534,7 @@ export interface TranslationResources { exitNode: string header: string instruction: string + keepDisabled: string minimumNodes: string minimumNodesHint: string warning: string diff --git a/src/utils/network.ts b/src/utils/network.ts index 99db4a6f9..647efafc1 100644 --- a/src/utils/network.ts +++ b/src/utils/network.ts @@ -20,19 +20,6 @@ export const isValidUrl = (value: string) => require_tld: false, }) -export const isValidLatencyTestUrl = (value: string) => { - const raw = value.trim() - if (!/^https?:\/\//i.test(raw)) return false - try { - const url = new URL(raw) - return ( - (url.protocol === 'http:' || url.protocol === 'https:') && !!url.hostname - ) - } catch { - return false - } -} - export const isValidPort = (value: string) => isPort(value.trim()) export const normalizeHost = (value: string): string | null => {