mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-31 03:02:08 +08:00
Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fd6be642e | ||
|
|
433230aedc | ||
|
|
05475c7bb5 | ||
|
|
eb60c21c62 | ||
|
|
4489c9a26f | ||
|
|
98a7932742 | ||
|
|
c678d60964 | ||
|
|
d5bc4ff58b | ||
|
|
b81981162b | ||
|
|
6fa6d7664f | ||
|
|
4967ad347e | ||
|
|
6394f347f5 | ||
|
|
e56711b2db | ||
|
|
3e28b287d1 | ||
|
|
51a4aa80dd | ||
|
|
3953e125b2 | ||
|
|
cbd5ed73a5 | ||
|
|
02e5e0e4a6 | ||
|
|
0498ba3e4f | ||
|
|
f39644ee6b | ||
|
|
488f022c05 | ||
|
|
eefd025806 | ||
|
|
10ca0a5d0c | ||
|
|
324c8c9016 | ||
|
|
02520d7392 | ||
|
|
5b8edbaead | ||
|
|
1ed8050947 | ||
|
|
3d9a79a3fb | ||
|
|
8b88ddb7f2 | ||
|
|
02bd739ab6 | ||
|
|
b6c7af82a7 |
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/node:2": {
|
"ghcr.io/devcontainers/features/node:2": {
|
||||||
"version": "24.22.0",
|
"version": "24.22.1",
|
||||||
"pnpmVersion": "12.14.0"
|
"pnpmVersion": "12.15.0"
|
||||||
},
|
},
|
||||||
"ghcr.io/devcontainers/features/rust:1": {
|
"ghcr.io/devcontainers/features/rust:1": {
|
||||||
"version": "1.99.1",
|
"version": "1.99.1",
|
||||||
|
|||||||
@@ -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 质量的正面或负面证据。
|
||||||
@@ -10,6 +10,8 @@ body:
|
|||||||
value: |
|
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)
|
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 下进行讨论
|
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似 issue,否则请在已有的 issue 下进行讨论
|
||||||
3. 请 **务必** 给 issue 填写一个简洁明了的标题,以便他人快速检索
|
3. 请 **务必** 给 issue 填写一个简洁明了的标题,以便他人快速检索
|
||||||
@@ -19,6 +21,8 @@ body:
|
|||||||
|
|
||||||
## Before submitting the issue, please make sure of the following checklist:
|
## 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)
|
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
|
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
|
3. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
|
||||||
|
|||||||
@@ -9,12 +9,16 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
value: |
|
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) 确认软件不存在类似的功能
|
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 下进行讨论
|
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似 issue,否则请在已有的 issue 下进行讨论
|
||||||
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
|
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
|
||||||
4. 请 **务必** 先下载 [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) 版本测试,确保该功能还未实现
|
4. 请 **务必** 先下载 [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) 版本测试,确保该功能还未实现
|
||||||
5. 请 **务必** 按照模板规范详细描述问题,否则 issue 将会被关闭
|
5. 请 **务必** 按照模板规范详细描述问题,否则 issue 将会被关闭
|
||||||
## Before submitting the issue, please make sure of the following checklist:
|
## 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
|
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
|
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
|
3. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
## Problem
|
## 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
|
## Approach
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '24.22.0'
|
node-version: '24.22.1'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -185,7 +185,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '24.22.0'
|
node-version: '24.22.1'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Pnpm Cache
|
- name: Pnpm Cache
|
||||||
@@ -282,7 +282,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '24.22.0'
|
node-version: '24.22.1'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Pnpm Cache
|
- name: Pnpm Cache
|
||||||
@@ -426,7 +426,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '24.22.0'
|
node-version: '24.22.1'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Pnpm Cache
|
- name: Pnpm Cache
|
||||||
@@ -524,7 +524,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '24.22.0'
|
node-version: '24.22.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6.1.0
|
- uses: pnpm/action-setup@v6.1.0
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
name: Contract Parity
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "src-tauri/**/*.rs"
|
||||||
|
- "crates/**/*.rs"
|
||||||
|
- "Cargo.toml"
|
||||||
|
- "Cargo.lock"
|
||||||
|
- "src-tauri/Cargo.toml"
|
||||||
|
- "crates/**/Cargo.toml"
|
||||||
|
- "rust-toolchain.toml"
|
||||||
|
- "src-tauri/tauri.conf*.json"
|
||||||
|
- "src/services/bindings.ts"
|
||||||
|
- "scripts/gen-contract.mjs"
|
||||||
|
- "scripts/check-contract-changes.mjs"
|
||||||
|
- "biome.json"
|
||||||
|
- "biome.jsonc"
|
||||||
|
- "package.json"
|
||||||
|
- "pnpm-lock.yaml"
|
||||||
|
- "pnpm-workspace.yaml"
|
||||||
|
- "scripts/contract-inputs.json"
|
||||||
|
- ".github/workflows/contract-parity.yml"
|
||||||
|
- ".husky/pre-push"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
HUSKY: 0
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
contract-parity:
|
||||||
|
runs-on: ubuntu-26.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- name: Install Rust Stable
|
||||||
|
uses: dtolnay/rust-toolchain@master
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
prefix-key: 'v1-rust'
|
||||||
|
key: 'rust-shared-stable-ubuntu-22.04-x86_64-unknown-linux-gnu-contract'
|
||||||
|
workspaces: |
|
||||||
|
. -> target
|
||||||
|
cache-all-crates: true
|
||||||
|
cache-workspace-crates: true
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v6
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v7
|
||||||
|
with:
|
||||||
|
node-version: '24.22.1'
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Provision sidecar placeholders for tauri-build
|
||||||
|
run: |
|
||||||
|
mkdir -p src-tauri/sidecar src-tauri/resources dist
|
||||||
|
touch dist/index.html
|
||||||
|
for bin in clash-verge-service clash-verge-service-install clash-verge-service-uninstall verge-mihomo verge-mihomo-alpha; do
|
||||||
|
touch "src-tauri/sidecar/${bin}-x86_64-unknown-linux-gnu"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Check generated contract without changing the checkout
|
||||||
|
run: pnpm gen:contract --check
|
||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '24.22.0'
|
node-version: '24.22.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ jobs:
|
|||||||
if: github.event.inputs[matrix.input] == 'true'
|
if: github.event.inputs[matrix.input] == 'true'
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '24.22.0'
|
node-version: '24.22.1'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Pnpm Cache
|
- name: Pnpm Cache
|
||||||
|
|||||||
@@ -2,11 +2,23 @@ name: Frontend Check
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "src/**"
|
||||||
|
- "**/*.js"
|
||||||
|
- "**/*.ts"
|
||||||
|
- "**/*.tsx"
|
||||||
|
- "**/*.css"
|
||||||
|
- "**/*.scss"
|
||||||
|
- "**/*.json"
|
||||||
|
- "**/*.md"
|
||||||
|
- "pnpm-lock.yaml"
|
||||||
|
- "pnpm-workspace.yaml"
|
||||||
|
- "vite.config.*"
|
||||||
|
- ".github/workflows/frontend-check.yml"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: read
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
HUSKY: 0
|
HUSKY: 0
|
||||||
@@ -17,46 +29,17 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Check frontend changes
|
|
||||||
id: check_frontend
|
|
||||||
uses: dorny/paths-filter@v4
|
|
||||||
with:
|
|
||||||
filters: |
|
|
||||||
frontend:
|
|
||||||
- 'src/**'
|
|
||||||
- '**/*.js'
|
|
||||||
- '**/*.ts'
|
|
||||||
- '**/*.tsx'
|
|
||||||
- '**/*.css'
|
|
||||||
- '**/*.scss'
|
|
||||||
- '**/*.json'
|
|
||||||
- '**/*.md'
|
|
||||||
- 'package.json'
|
|
||||||
- 'pnpm-lock.yaml'
|
|
||||||
- 'pnpm-workspace.yaml'
|
|
||||||
- 'knip.json'
|
|
||||||
- 'eslint.config.ts'
|
|
||||||
- 'tsconfig.json'
|
|
||||||
- 'vite.config.*'
|
|
||||||
|
|
||||||
- name: Skip if no frontend changes
|
|
||||||
if: steps.check_frontend.outputs.frontend != 'true'
|
|
||||||
run: echo "No frontend changes, skipping frontend checks."
|
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
if: steps.check_frontend.outputs.frontend == 'true'
|
|
||||||
uses: pnpm/action-setup@v6
|
uses: pnpm/action-setup@v6
|
||||||
with:
|
with:
|
||||||
run_install: false
|
run_install: false
|
||||||
|
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@v7
|
||||||
if: steps.check_frontend.outputs.frontend == 'true'
|
|
||||||
with:
|
with:
|
||||||
node-version: '24.22.0'
|
node-version: '24.22.1'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Restore pnpm cache
|
- name: Restore pnpm cache
|
||||||
if: steps.check_frontend.outputs.frontend == 'true'
|
|
||||||
uses: actions/cache@v6
|
uses: actions/cache@v6
|
||||||
with:
|
with:
|
||||||
path: ~/.pnpm-store
|
path: ~/.pnpm-store
|
||||||
@@ -65,94 +48,21 @@ jobs:
|
|||||||
pnpm-shared-stable-${{ runner.os }}-
|
pnpm-shared-stable-${{ runner.os }}-
|
||||||
|
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
if: steps.check_frontend.outputs.frontend == 'true'
|
|
||||||
|
|
||||||
- name: Run Prettier check
|
- name: Run Prettier check
|
||||||
if: steps.check_frontend.outputs.frontend == 'true'
|
|
||||||
run: pnpm format:check
|
run: pnpm format:check
|
||||||
|
|
||||||
- name: Run ESLint
|
- name: Run ESLint
|
||||||
if: steps.check_frontend.outputs.frontend == 'true'
|
|
||||||
run: pnpm lint
|
run: pnpm lint
|
||||||
|
|
||||||
- name: Run TypeScript typecheck
|
- name: Run TypeScript typecheck
|
||||||
if: steps.check_frontend.outputs.frontend == 'true'
|
|
||||||
run: pnpm typecheck
|
run: pnpm typecheck
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
if: steps.check_frontend.outputs.frontend == 'true'
|
|
||||||
run: pnpm test
|
run: pnpm test
|
||||||
|
|
||||||
- name: Run Knip
|
- name: Run Knip
|
||||||
if: steps.check_frontend.outputs.frontend == 'true'
|
|
||||||
run: pnpm knip:check
|
run: pnpm knip:check
|
||||||
|
|
||||||
- name: Architecture gates (listen/emit only in the event bus, invoke only in services)
|
- name: Architecture gates (listen/emit only in the event bus, invoke only in services)
|
||||||
if: steps.check_frontend.outputs.frontend == 'true'
|
|
||||||
run: node scripts/check-architecture.mjs listen emit invoke
|
run: node scripts/check-architecture.mjs listen emit invoke
|
||||||
|
|
||||||
contract-parity:
|
|
||||||
runs-on: ubuntu-26.04
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
|
|
||||||
- name: Check contract inputs
|
|
||||||
id: check_inputs
|
|
||||||
uses: dorny/paths-filter@v4
|
|
||||||
with:
|
|
||||||
filters: scripts/contract-inputs.json
|
|
||||||
|
|
||||||
- name: Skip if contract inputs unchanged
|
|
||||||
if: steps.check_inputs.outputs.contract != 'true'
|
|
||||||
run: echo "No contract inputs changed, skipping parity check."
|
|
||||||
|
|
||||||
- name: Install Rust Stable
|
|
||||||
if: steps.check_inputs.outputs.contract == 'true'
|
|
||||||
uses: dtolnay/rust-toolchain@master
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
|
|
||||||
- name: Rust Cache
|
|
||||||
if: steps.check_inputs.outputs.contract == 'true'
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
prefix-key: 'v1-rust'
|
|
||||||
key: 'rust-shared-stable-ubuntu-22.04-x86_64-unknown-linux-gnu-contract'
|
|
||||||
workspaces: |
|
|
||||||
. -> target
|
|
||||||
cache-all-crates: true
|
|
||||||
cache-workspace-crates: true
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
if: steps.check_inputs.outputs.contract == 'true'
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
|
||||||
|
|
||||||
- name: Install pnpm
|
|
||||||
if: steps.check_inputs.outputs.contract == 'true'
|
|
||||||
uses: pnpm/action-setup@v6
|
|
||||||
with:
|
|
||||||
run_install: false
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
if: steps.check_inputs.outputs.contract == 'true'
|
|
||||||
with:
|
|
||||||
node-version: '24.22.0'
|
|
||||||
cache: 'pnpm'
|
|
||||||
|
|
||||||
- run: pnpm install --frozen-lockfile
|
|
||||||
if: steps.check_inputs.outputs.contract == 'true'
|
|
||||||
|
|
||||||
- name: Provision sidecar placeholders for tauri-build
|
|
||||||
if: steps.check_inputs.outputs.contract == 'true'
|
|
||||||
run: |
|
|
||||||
mkdir -p src-tauri/sidecar src-tauri/resources dist
|
|
||||||
touch dist/index.html
|
|
||||||
for bin in clash-verge-service clash-verge-service-install clash-verge-service-uninstall verge-mihomo verge-mihomo-alpha; do
|
|
||||||
touch "src-tauri/sidecar/${bin}-x86_64-unknown-linux-gnu"
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Check generated contract without changing the checkout
|
|
||||||
if: steps.check_inputs.outputs.contract == 'true'
|
|
||||||
run: pnpm gen:contract --check
|
|
||||||
|
|||||||
@@ -2,6 +2,13 @@ name: Clippy Lint
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "src-tauri/**"
|
||||||
|
- "crates/**"
|
||||||
|
- "Cargo.toml"
|
||||||
|
- "Cargo.lock"
|
||||||
|
- "rust-toolchain.toml"
|
||||||
|
- ".github/workflows/lint-clippy.yml"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
env:
|
env:
|
||||||
HUSKY: 0
|
HUSKY: 0
|
||||||
@@ -23,28 +30,6 @@ jobs:
|
|||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check src-tauri changes
|
|
||||||
if: github.event_name != 'workflow_dispatch'
|
|
||||||
id: check_changes
|
|
||||||
uses: dorny/paths-filter@v4
|
|
||||||
with:
|
|
||||||
filters: |
|
|
||||||
rust:
|
|
||||||
- 'src-tauri/**'
|
|
||||||
|
|
||||||
- name: Skip if src-tauri not changed
|
|
||||||
if: github.event_name != 'workflow_dispatch' && steps.check_changes.outputs.rust != 'true'
|
|
||||||
run: echo "No src-tauri changes, skipping clippy lint."
|
|
||||||
|
|
||||||
- name: Continue if src-tauri changed
|
|
||||||
if: github.event_name != 'workflow_dispatch' && steps.check_changes.outputs.rust == 'true'
|
|
||||||
run: echo "src-tauri changed, running clippy lint."
|
|
||||||
|
|
||||||
- name: Manual trigger - always run
|
|
||||||
if: github.event_name == 'workflow_dispatch'
|
|
||||||
run: |
|
|
||||||
echo "Manual trigger detected: skipping changes check and running clippy."
|
|
||||||
|
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
@@ -79,7 +64,7 @@ jobs:
|
|||||||
run: cargo clippy-all
|
run: cargo clippy-all
|
||||||
|
|
||||||
- name: Run Windows security tests
|
- name: Run Windows security tests
|
||||||
if: matrix.os == 'windows-latest' && (github.event_name == 'workflow_dispatch' || steps.check_changes.outputs.rust == 'true')
|
if: matrix.os == 'windows-latest'
|
||||||
working-directory: ./src-tauri
|
working-directory: ./src-tauri
|
||||||
run: cargo test --all-features --target ${{ matrix.target }}
|
run: cargo test --all-features --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '24.22.0'
|
node-version: '24.22.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -331,7 +331,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '24.22.0'
|
node-version: '24.22.1'
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v6
|
uses: pnpm/action-setup@v6
|
||||||
@@ -466,7 +466,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '24.22.0'
|
node-version: '24.22.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -547,7 +547,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '24.22.0'
|
node-version: '24.22.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -573,7 +573,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '24.22.0'
|
node-version: '24.22.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -638,7 +638,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '24.22.0'
|
node-version: '24.22.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ name: Check Formatting
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "src-tauri/**"
|
||||||
|
- "**/*.rs"
|
||||||
|
- ".github/workflows/rustfmt.yml"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -19,25 +23,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Check Rust changes
|
|
||||||
id: check_rust
|
|
||||||
uses: dorny/paths-filter@v4
|
|
||||||
with:
|
|
||||||
filters: |
|
|
||||||
rust:
|
|
||||||
- 'src-tauri/**'
|
|
||||||
- '**/*.rs'
|
|
||||||
|
|
||||||
- name: Skip if no Rust changes
|
|
||||||
if: steps.check_rust.outputs.rust != 'true'
|
|
||||||
run: echo "No Rust changes, skipping rustfmt."
|
|
||||||
|
|
||||||
- name: install Rust stable and rustfmt
|
- name: install Rust stable and rustfmt
|
||||||
if: steps.check_rust.outputs.rust == 'true'
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
components: rustfmt
|
components: rustfmt
|
||||||
|
|
||||||
- name: run cargo fmt
|
- name: run cargo fmt
|
||||||
if: steps.check_rust.outputs.rust == 'true'
|
|
||||||
run: cargo fmt --manifest-path ./src-tauri/Cargo.toml --all -- --check
|
run: cargo fmt --manifest-path ./src-tauri/Cargo.toml --all -- --check
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '24.22.0'
|
node-version: '24.22.1'
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v6
|
uses: pnpm/action-setup@v6
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '24.22.0'
|
node-version: '24.22.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '24.22.0'
|
node-version: '24.22.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '24.22.0'
|
node-version: '24.22.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
nodejs 24.22.0
|
nodejs 24.22.1
|
||||||
pnpm 12.14.0
|
pnpm 12.15.0
|
||||||
rust 1.99.1
|
rust 1.99.1
|
||||||
|
|||||||
@@ -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
|
the ownership evidence the AI-slop review evaluates (see
|
||||||
[`pr-ai-slop-review.md`](.github/workflows/pr-ai-slop-review.md)).
|
[`pr-ai-slop-review.md`](.github/workflows/pr-ai-slop-review.md)).
|
||||||
|
|
||||||
1. **Issue first.** Non-trivial changes require a pre-existing issue describing
|
1. **Issue first.** Product bug fixes and features require a pre-existing issue;
|
||||||
the problem. If none exists, ask the maintainers to open or approve one
|
self-scoped maintenance work does not. Follow the PR template for linkage and
|
||||||
before implementing.
|
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
|
2. **Scope discipline.** Every changed file must be justifiable from the linked
|
||||||
issue. No drive-by refactors, renames, formatting churn, or dependency bumps
|
issue or the stated PR scope. No drive-by refactors, renames, formatting
|
||||||
unrelated to the problem being fixed.
|
churn, or dependency bumps unrelated to the work.
|
||||||
3. **Author accountability.** AI assistance is welcome, but the contributor owns
|
3. **Author accountability.** AI assistance is welcome, but the contributor owns
|
||||||
the result: understand the change, describe the problem and approach in your
|
the result: understand the change, describe the problem and approach in your
|
||||||
own words, and verify the change against the reported behavior before
|
own words, and verify the change against the reported behavior before
|
||||||
submitting.
|
submitting.
|
||||||
4. **Tests are justified, not default.** Do not add tests, test scaffolding, or
|
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
|
speculative defensive code unless the linked issue or stated task demands
|
||||||
is genuinely necessary — it reproduces the reported regression or guards
|
them. When a test is genuinely necessary — it reproduces the reported
|
||||||
behavior whose breakage would otherwise go unnoticed — keep it minimal and
|
regression or guards behavior whose breakage would otherwise go unnoticed —
|
||||||
state in the PR body why it is needed. Bulk test files and defensive
|
keep it minimal and state in the PR body why it is needed. Bulk test files
|
||||||
programming for hypothetical failure modes are PR bloat, not rigor.
|
and defensive programming for hypothetical failure modes are PR bloat, not
|
||||||
|
rigor.
|
||||||
5. **Comments state constraints, not narration.** Write a comment only for a
|
5. **Comments state constraints, not narration.** Write a comment only for a
|
||||||
non-obvious constraint the code cannot express; never restate what the code
|
non-obvious constraint the code cannot express; never restate what the code
|
||||||
does.
|
does.
|
||||||
@@ -44,13 +47,12 @@ the ownership evidence the AI-slop review evaluates (see
|
|||||||
real evidence instead: reproduction steps, failure output, targeted tests.
|
real evidence instead: reproduction steps, failure output, targeted tests.
|
||||||
8. **Minimal diffs.** Match the surrounding code's style, naming, and comment
|
8. **Minimal diffs.** Match the surrounding code's style, naming, and comment
|
||||||
density. Do not introduce new dependencies or restructure working code unless
|
density. Do not introduce new dependencies or restructure working code unless
|
||||||
the issue demands it.
|
the linked issue or stated task demands it.
|
||||||
9. **Disclose AI automation.** When an agent produces or co-produces a change,
|
9. **Disclose AI automation.** When an agent creates or co-creates a PR or Issue,
|
||||||
append a footer line to the PR body with the model and effort used (e.g.
|
append a final body line with the model and effort used (e.g.
|
||||||
`Assisted by: GPT-5.6 High`). The PR template intentionally omits this line —
|
`Assisted by: GPT-5.6 High`). Effort may be omitted when the runtime does not
|
||||||
the agent adds it itself, humans are not asked to declare anything. Effort may
|
report it. Human contributors are not asked to disclose AI assistance.
|
||||||
be omitted when the runtime does not report it. Disclosure is transparency
|
Disclosure is transparency only and carries no positive or negative weight.
|
||||||
only; it does not substitute for any rule above.
|
|
||||||
10. **Compiled workflows.** The AI-slop review policy in
|
10. **Compiled workflows.** The AI-slop review policy in
|
||||||
[pr-ai-slop-review.md](.github/workflows/pr-ai-slop-review.md) is compiled:
|
[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
|
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
|
[`template/Changelog.md`](template/Changelog.md): one line per
|
||||||
user-visible change, no internals.
|
user-visible change, no internals.
|
||||||
|
|
||||||
## Pull Request Shape
|
## Issue and PR Workflows
|
||||||
|
|
||||||
Describe three things, briefly: the problem (with issue link), why this approach
|
When drafting, triaging, splitting, or implementing from a community issue, use
|
||||||
solves it, and what changed. See
|
the conditional checklist in [`.github/ISSUE_GUIDELINES.md`](.github/ISSUE_GUIDELINES.md).
|
||||||
[`PULL_REQUEST_TEMPLATE.md`](.github/PULL_REQUEST_TEMPLATE.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.
|
||||||
|
|||||||
Generated
+73
-160
@@ -1119,7 +1119,7 @@ dependencies = [
|
|||||||
"getrandom 0.4.3",
|
"getrandom 0.4.3",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"minisign-verify 0.3.0",
|
"minisign-verify",
|
||||||
"nanoid",
|
"nanoid",
|
||||||
"network-interface",
|
"network-interface",
|
||||||
"objc2",
|
"objc2",
|
||||||
@@ -4170,12 +4170,6 @@ dependencies = [
|
|||||||
"unicase",
|
"unicase",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "minisign-verify"
|
|
||||||
version = "0.2.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minisign-verify"
|
name = "minisign-verify"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
@@ -5999,12 +5993,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-ini"
|
name = "rust-ini"
|
||||||
version = "0.21.3"
|
version = "0.21.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7"
|
checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"ordered-multimap",
|
"ordered-multimap",
|
||||||
|
"trim-in-place",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6625,7 +6620,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7"
|
checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"sigchld",
|
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -6645,17 +6639,6 @@ version = "2.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
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]]
|
[[package]]
|
||||||
name = "signal-hook"
|
name = "signal-hook"
|
||||||
version = "0.1.17"
|
version = "0.1.17"
|
||||||
@@ -6666,16 +6649,6 @@ dependencies = [
|
|||||||
"signal-hook-registry",
|
"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]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.8"
|
version = "1.4.8"
|
||||||
@@ -6881,6 +6854,12 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "static_vcruntime"
|
||||||
|
version = "3.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff7589a1859b09232522bb9edf076ca7fcf14ded96685d3fa629db73023ffbb7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "string_cache"
|
name = "string_cache"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -6940,9 +6919,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "swift-rs"
|
name = "swift-rs"
|
||||||
version = "1.0.8"
|
version = "1.0.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e"
|
checksum = "cefd87076fd385308ee4aff597256c902f1fbb4d031de1558efc12f8ffefadd9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -7189,9 +7168,9 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri"
|
name = "tauri"
|
||||||
version = "2.12.1"
|
version = "2.12.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed99ee9694a2deb776d91cae48ac7411ddfc89ecae2f9b5041111d8c88f2ace9"
|
checksum = "f316751027fc2644221d1a448564d519efb07067fda584879d9cda966391d212"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -7237,16 +7216,16 @@ dependencies = [
|
|||||||
"tray-icon",
|
"tray-icon",
|
||||||
"url",
|
"url",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com 0.39.1",
|
"webview2-com",
|
||||||
"window-vibrancy",
|
"window-vibrancy",
|
||||||
"windows 0.62.2",
|
"windows 0.62.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-build"
|
name = "tauri-build"
|
||||||
version = "2.7.1"
|
version = "2.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "59563ca5b331fd97f27ee9672ec8d36d33bb3c9e2c9f6710a1a5687256052e45"
|
checksum = "641dd5fd297c99ab92717b0428cca5b9c6f960b2ac96ca131b3eebc9d70b8ddc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cargo_toml",
|
"cargo_toml",
|
||||||
@@ -7258,6 +7237,8 @@ dependencies = [
|
|||||||
"semver 1.0.28",
|
"semver 1.0.28",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"static_vcruntime",
|
||||||
|
"strsim",
|
||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
"tauri-winres",
|
"tauri-winres",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
@@ -7265,11 +7246,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-codegen"
|
name = "tauri-codegen"
|
||||||
version = "2.7.1"
|
version = "2.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e50213e463086ecc283d7e9f51c30cc0fd304ad457425158018be26dbf0d0a80"
|
checksum = "a6585c5b536c00f039d12ce70a9755172d5423bbcfd3969863baa70314c3e02f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.23.1",
|
||||||
"brotli",
|
"brotli",
|
||||||
"ico",
|
"ico",
|
||||||
"json-patch",
|
"json-patch",
|
||||||
@@ -7292,9 +7273,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-macros"
|
name = "tauri-macros"
|
||||||
version = "2.7.1"
|
version = "2.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8129fb43bb6e0d6b73ad3315cd172beaab17cd28899c594b1b32c80edbbfd99c"
|
checksum = "e2e83a0cfa1bae376e8fbbdc0b1692707aed50cd6072ce85cae4789af5d6c19f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -7350,9 +7331,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-clipboard-manager"
|
name = "tauri-plugin-clipboard-manager"
|
||||||
version = "2.4.1"
|
version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "333d3b6beeb1c2c7fc040233d4ea7b6c03a9f8f039592b7400833c43fb5b568a"
|
checksum = "9be5ed8d94c33afe8377e885119069f6508780659271d0a25cfc54d441f2bc81"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arboard",
|
"arboard",
|
||||||
"log",
|
"log",
|
||||||
@@ -7365,9 +7346,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-deep-link"
|
name = "tauri-plugin-deep-link"
|
||||||
version = "2.6.1"
|
version = "2.6.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ac7a92a46ab5c88f44532ca50906d6e448a948d4ddf8c5376ab125e3260f736"
|
checksum = "1b3c64e24ebcbc63aaa2d88a9c51e2507084b46620baf39ecb0baa0738a08bb4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dunce",
|
"dunce",
|
||||||
"plist",
|
"plist",
|
||||||
@@ -7413,9 +7394,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-dialog"
|
name = "tauri-plugin-dialog"
|
||||||
version = "2.8.1"
|
version = "2.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "daf9a5c92e39bdd84f22f6be9230e39d0c4e140345f71ffe6058dca4f71127cf"
|
checksum = "b7461aaacf1901915ea7b9f5cd74f8224a351ed96af0a62c07fce934003cb34b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
@@ -7431,9 +7412,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-fs"
|
name = "tauri-plugin-fs"
|
||||||
version = "2.6.0"
|
version = "2.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "466eb32466fe5021b6728b06862f9ff14a9f7e1af890047654e03a96d7c0e1bb"
|
checksum = "5d58eb1d4984e535f47a642ffec09a2da6f5c18a12b5902d12126cb7626d628b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"dunce",
|
"dunce",
|
||||||
@@ -7455,9 +7436,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-global-shortcut"
|
name = "tauri-plugin-global-shortcut"
|
||||||
version = "2.4.0"
|
version = "2.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0bd2e1f725891a1613af8f25bf918ba3357654b0fd3f2d794c0ce76cbdd92ed2"
|
checksum = "93ff17919fe09852d269bd37b1d3d2e993b9dbb514afe7acbf3346c1d3627e2d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"global-hotkey",
|
"global-hotkey",
|
||||||
"log",
|
"log",
|
||||||
@@ -7470,9 +7451,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-http"
|
name = "tauri-plugin-http"
|
||||||
version = "2.8.0"
|
version = "2.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "93e9d6b3319e101bc1be27cfa0fb4054d7784a12509c67ead0033b82180e5ed1"
|
checksum = "2c57e3451553f95a1db3bfed6a358a77ce5080b6399a242c6eadac113f97abd2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"cookie_store",
|
"cookie_store",
|
||||||
@@ -7489,7 +7470,7 @@ dependencies = [
|
|||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
"tokio",
|
"tokio",
|
||||||
"url",
|
"url",
|
||||||
"urlpattern 0.3.0",
|
"urlpattern",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7519,9 +7500,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-notification"
|
name = "tauri-plugin-notification"
|
||||||
version = "2.5.1"
|
version = "2.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "533770889abb60a0407d5a32096182f0e6463f5a3dfaa675be94580aa14070e1"
|
checksum = "b939d7b19c8af1381a72ef8474b3aba2b56c1ddac2c23f0c8e96abbcf86b198b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"notify-rust",
|
"notify-rust",
|
||||||
@@ -7534,13 +7515,14 @@ dependencies = [
|
|||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
"time",
|
"time",
|
||||||
"url",
|
"url",
|
||||||
|
"zbus",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-opener"
|
name = "tauri-plugin-opener"
|
||||||
version = "2.7.0"
|
version = "2.7.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d39404b2572927e0d3ed30a7ca226037f7508d22e79c96ee6ef0d42b6a4b3345"
|
checksum = "53810be1b4fed22bb6ac3f4cab5a2b80d9391d1af436bda145607532bc0bb219"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dunce",
|
"dunce",
|
||||||
"glob",
|
"glob",
|
||||||
@@ -7570,11 +7552,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-shell"
|
name = "tauri-plugin-shell"
|
||||||
version = "2.4.0"
|
version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "094bdbc47e5efe36544ba21b3a53ab5c724150236cf55e6c9be46191deb6eab6"
|
checksum = "8567d42b2c7264355daa1df415ec77f52aa42e885924c86a969be2e920e05521"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"open",
|
"open",
|
||||||
"os_pipe",
|
"os_pipe",
|
||||||
@@ -7587,13 +7570,14 @@ dependencies = [
|
|||||||
"tauri-plugin",
|
"tauri-plugin",
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-updater"
|
name = "tauri-plugin-updater"
|
||||||
version = "2.13.1"
|
version = "2.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3cb0b2ea3e85ca287990d3859a29cc5cb18024240fd78f62e027fb7963670f18"
|
checksum = "7259989d4fe11b2bf51f7d671642249b1837c4ae2da1430dc713ae38f98ceeec"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"dirs 7.0.0",
|
"dirs 7.0.0",
|
||||||
@@ -7601,8 +7585,9 @@ dependencies = [
|
|||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"infer",
|
"infer",
|
||||||
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"minisign-verify 0.2.5",
|
"minisign-verify",
|
||||||
"osakit",
|
"osakit",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"reqwest 0.13.5",
|
"reqwest 0.13.5",
|
||||||
@@ -7624,9 +7609,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-wdio-webdriver"
|
name = "tauri-plugin-wdio-webdriver"
|
||||||
version = "1.4.0"
|
version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "52c5e97428174a52b7f4357bbc4535fcce52c6a06c8ee7a379502f7cda1b6045"
|
checksum = "4f76676d510d271d80abcf8b0552987b5b4f0eae5e3f906e9b5660d43c97c62e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -7650,9 +7635,9 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
"uuid",
|
"uuid",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com 0.38.2",
|
"webview2-com",
|
||||||
"windows 0.61.3",
|
"windows 0.62.2",
|
||||||
"windows-core 0.61.2",
|
"windows-core 0.62.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7672,9 +7657,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "2.12.1"
|
version = "2.12.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2a57a2a9b8c8b7fc00d35b510fe808ded7f9f1b44fc1cdda6aee38632ea5413"
|
checksum = "7c55766d99cea69055904d1735b93c2049302b2e09effd462120deb5581d78e9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cookie",
|
"cookie",
|
||||||
"dpi",
|
"dpi",
|
||||||
@@ -7691,15 +7676,15 @@ dependencies = [
|
|||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
"url",
|
"url",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com 0.39.1",
|
"webview2-com",
|
||||||
"windows 0.62.2",
|
"windows 0.62.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime-wry"
|
name = "tauri-runtime-wry"
|
||||||
version = "2.12.1"
|
version = "2.12.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f9e7e0c9f80a5130773cfabb6df4dc924518fb1b2c923726966804634b57d22"
|
checksum = "8d61892587aea2a85245bcaec98c6fe46268874c0a0deae5535fb71ca7b93eae"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gtk",
|
"gtk",
|
||||||
"http",
|
"http",
|
||||||
@@ -7717,7 +7702,7 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com 0.39.1",
|
"webview2-com",
|
||||||
"windows 0.62.2",
|
"windows 0.62.2",
|
||||||
"wry",
|
"wry",
|
||||||
]
|
]
|
||||||
@@ -7755,9 +7740,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-utils"
|
name = "tauri-utils"
|
||||||
version = "2.10.1"
|
version = "2.10.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2ff55a614843b9a3f010f211df637c94df2607175b8d0f6e0f4a7c19e18621ee"
|
checksum = "362ab21cd4cc713f2a02b1a9b5688198b1541cfb6907c4281e9190e0f0ef4aa3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"brotli",
|
"brotli",
|
||||||
@@ -7786,7 +7771,7 @@ dependencies = [
|
|||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
"toml 1.1.6+spec-1.1.0",
|
"toml 1.1.6+spec-1.1.0",
|
||||||
"url",
|
"url",
|
||||||
"urlpattern 0.6.0",
|
"urlpattern",
|
||||||
"uuid",
|
"uuid",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
@@ -7905,7 +7890,7 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
"semver 0.11.0",
|
"semver 0.11.0",
|
||||||
"sha2 0.9.9",
|
"sha2 0.9.9",
|
||||||
"signal-hook 0.1.17",
|
"signal-hook",
|
||||||
"terminfo",
|
"terminfo",
|
||||||
"termios",
|
"termios",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
@@ -8503,6 +8488,12 @@ dependencies = [
|
|||||||
"petgraph",
|
"petgraph",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "trim-in-place"
|
||||||
|
version = "0.1.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "daa60874b45b168f67f3f9ba1ee821ad77144d6fa19cef1cd1874a7450a948da"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "triomphe"
|
name = "triomphe"
|
||||||
version = "0.1.16"
|
version = "0.1.16"
|
||||||
@@ -8595,47 +8586,6 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "unicase"
|
name = "unicase"
|
||||||
version = "2.9.0"
|
version = "2.9.0"
|
||||||
@@ -8695,18 +8645,6 @@ dependencies = [
|
|||||||
"serde_derive",
|
"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]]
|
[[package]]
|
||||||
name = "urlpattern"
|
name = "urlpattern"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
@@ -9098,20 +9036,6 @@ dependencies = [
|
|||||||
"rustls-pki-types",
|
"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]]
|
[[package]]
|
||||||
name = "webview2-com"
|
name = "webview2-com"
|
||||||
version = "0.39.1"
|
version = "0.39.1"
|
||||||
@@ -9119,7 +9043,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6"
|
checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"webview2-com-macros",
|
"webview2-com-macros",
|
||||||
"webview2-com-sys 0.39.1",
|
"webview2-com-sys",
|
||||||
"windows 0.62.2",
|
"windows 0.62.2",
|
||||||
"windows-core 0.62.2",
|
"windows-core 0.62.2",
|
||||||
]
|
]
|
||||||
@@ -9135,17 +9059,6 @@ dependencies = [
|
|||||||
"syn 2.0.119",
|
"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]]
|
[[package]]
|
||||||
name = "webview2-com-sys"
|
name = "webview2-com-sys"
|
||||||
version = "0.39.1"
|
version = "0.39.1"
|
||||||
@@ -9885,7 +9798,7 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webkit2gtk-sys",
|
"webkit2gtk-sys",
|
||||||
"webview2-com 0.39.1",
|
"webview2-com",
|
||||||
"windows 0.62.2",
|
"windows 0.62.2",
|
||||||
"windows-core 0.62.2",
|
"windows-core 0.62.2",
|
||||||
"windows-version",
|
"windows-version",
|
||||||
|
|||||||
+2
-2
@@ -61,8 +61,8 @@ tracing-estuary = { git = "https://github.com/Tunglies/tracing-estuary.git", bra
|
|||||||
tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" }
|
tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" }
|
||||||
tracing = "0.1.44"
|
tracing = "0.1.44"
|
||||||
|
|
||||||
tauri = { version = "2.12.1" }
|
tauri = { version = "2.12.4" }
|
||||||
tauri-plugin-clipboard-manager = "2.4.1"
|
tauri-plugin-clipboard-manager = "2.5.0"
|
||||||
parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] }
|
parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] }
|
||||||
anyhow = "1.0.104"
|
anyhow = "1.0.104"
|
||||||
chrono = "0.4.45"
|
chrono = "0.4.45"
|
||||||
|
|||||||
@@ -3,10 +3,13 @@
|
|||||||
<details>
|
<details>
|
||||||
<summary><strong> 🐞 修复问题 </strong></summary>
|
<summary><strong> 🐞 修复问题 </strong></summary>
|
||||||
|
|
||||||
|
- 修复规则扩展包含空列表项时,「编辑规则」界面报错的问题
|
||||||
- 修复托盘测速与代理页行为不一致的问题,移除托盘「测试延迟」入口
|
- 修复托盘测速与代理页行为不一致的问题,移除托盘「测试延迟」入口
|
||||||
- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置
|
- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置
|
||||||
- 修复 首页 网站测速失败后卡片一直显示测试中
|
- 修复 首页 网站测速失败后卡片一直显示测试中
|
||||||
- 修复 代理 页展开分组后不显示最新测速结果,需切换节点才刷新
|
- 修复 代理 页展开分组后不显示最新测速结果,需切换节点才刷新
|
||||||
|
- 修复 代理 在全局模式下无法展开节点组
|
||||||
|
- 修复 自定义 CSS 无法修改编辑器字体和字号的问题
|
||||||
|
|
||||||
**🐧 Linux**
|
**🐧 Linux**
|
||||||
|
|
||||||
@@ -17,6 +20,9 @@
|
|||||||
<details>
|
<details>
|
||||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||||
|
|
||||||
|
- 新增虚拟网卡设置的「DNS 模式」选项,支持配置 mihomo 系统 DNS 行为
|
||||||
|
- 新增「IP 信息」自动刷新开关,关闭后仍可手动刷新
|
||||||
|
- 新增 Muse AI 解锁检测,针对 Muse AI 特有的 Cloudflare 人机验证增加「Risky」状态标记
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ mod chatgpt;
|
|||||||
mod claude;
|
mod claude;
|
||||||
mod disney_plus;
|
mod disney_plus;
|
||||||
mod gemini;
|
mod gemini;
|
||||||
|
mod muse;
|
||||||
mod netflix;
|
mod netflix;
|
||||||
mod prime_video;
|
mod prime_video;
|
||||||
mod spotify;
|
mod spotify;
|
||||||
@@ -30,6 +31,7 @@ enum UnlockCheck {
|
|||||||
ChatGptWeb,
|
ChatGptWeb,
|
||||||
Claude,
|
Claude,
|
||||||
Gemini,
|
Gemini,
|
||||||
|
MuseAi,
|
||||||
YouTubePremium,
|
YouTubePremium,
|
||||||
BahamutAnime,
|
BahamutAnime,
|
||||||
Netflix,
|
Netflix,
|
||||||
@@ -46,6 +48,7 @@ impl UnlockCheck {
|
|||||||
Self::ChatGptWeb,
|
Self::ChatGptWeb,
|
||||||
Self::Claude,
|
Self::Claude,
|
||||||
Self::Gemini,
|
Self::Gemini,
|
||||||
|
Self::MuseAi,
|
||||||
Self::YouTubePremium,
|
Self::YouTubePremium,
|
||||||
Self::BahamutAnime,
|
Self::BahamutAnime,
|
||||||
Self::Netflix,
|
Self::Netflix,
|
||||||
@@ -66,6 +69,7 @@ impl UnlockCheck {
|
|||||||
Self::ChatGptWeb => chatgpt::CHATGPT_WEB_NAME,
|
Self::ChatGptWeb => chatgpt::CHATGPT_WEB_NAME,
|
||||||
Self::Claude => claude::CLAUDE_NAME,
|
Self::Claude => claude::CLAUDE_NAME,
|
||||||
Self::Gemini => gemini::GEMINI_NAME,
|
Self::Gemini => gemini::GEMINI_NAME,
|
||||||
|
Self::MuseAi => muse::MUSE_AI_NAME,
|
||||||
Self::YouTubePremium => youtube::YOUTUBE_PREMIUM_NAME,
|
Self::YouTubePremium => youtube::YOUTUBE_PREMIUM_NAME,
|
||||||
Self::BahamutAnime => bahamut::BAHAMUT_ANIME_NAME,
|
Self::BahamutAnime => bahamut::BAHAMUT_ANIME_NAME,
|
||||||
Self::Netflix => netflix::NETFLIX_NAME,
|
Self::Netflix => netflix::NETFLIX_NAME,
|
||||||
@@ -83,6 +87,7 @@ impl UnlockCheck {
|
|||||||
Self::ChatGptWeb => chatgpt::check_chatgpt(client).await,
|
Self::ChatGptWeb => chatgpt::check_chatgpt(client).await,
|
||||||
Self::Claude => claude::check_claude(client).await,
|
Self::Claude => claude::check_claude(client).await,
|
||||||
Self::Gemini => gemini::check_gemini(client).await,
|
Self::Gemini => gemini::check_gemini(client).await,
|
||||||
|
Self::MuseAi => muse::check_muse_ai(client).await,
|
||||||
Self::YouTubePremium => youtube::check_youtube_premium(client).await,
|
Self::YouTubePremium => youtube::check_youtube_premium(client).await,
|
||||||
Self::BahamutAnime => bahamut::check_bahamut_anime(client).await,
|
Self::BahamutAnime => bahamut::check_bahamut_anime(client).await,
|
||||||
Self::Netflix => netflix::check_netflix(client).await,
|
Self::Netflix => netflix::check_netflix(client).await,
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
use reqwest::Client;
|
||||||
|
|
||||||
|
use crate::utils::{classify_restricted_status, get_trace_location};
|
||||||
|
|
||||||
|
use super::UnlockItem;
|
||||||
|
|
||||||
|
pub(crate) const MUSE_AI_NAME: &str = "Muse AI";
|
||||||
|
|
||||||
|
const MUSE_URL: &str = "https://muse.ai";
|
||||||
|
const CLOUDFLARE_TRACE_URL: &str = "https://www.cloudflare.com/cdn-cgi/trace";
|
||||||
|
|
||||||
|
// The aymh ("Are You a Human") gateway answers plain HTTP requests with a
|
||||||
|
// generic Meta error page that always contains the Facebook logo asset.
|
||||||
|
const AYMH_ERROR_MARKERS: &[&str] = &["Sorry, something went wrong", "static.facebook.com/images/logos"];
|
||||||
|
|
||||||
|
pub(super) async fn check_muse_ai(client: &Client) -> UnlockItem {
|
||||||
|
// muse.ai itself is served by Vercel, so resolve the exit region through
|
||||||
|
// the generic Cloudflare trace endpoint instead of a service-specific one.
|
||||||
|
let region = get_trace_location(client, CLOUDFLARE_TRACE_URL)
|
||||||
|
.await
|
||||||
|
.map(|code| code.trim().to_ascii_uppercase())
|
||||||
|
.map(|code| UnlockItem::region_label(&code));
|
||||||
|
|
||||||
|
let Ok(response) = client.get(MUSE_URL).send().await else {
|
||||||
|
return UnlockItem::checked(MUSE_AI_NAME, "Failed (Network Connection)", region);
|
||||||
|
};
|
||||||
|
|
||||||
|
let status = response.status();
|
||||||
|
let landed_on_aymh = response.url().host_str() == Some("auth.muse.ai");
|
||||||
|
let body = response.text().await.unwrap_or_default();
|
||||||
|
|
||||||
|
// Every region is redirected to the aymh anti-bot gateway before the app
|
||||||
|
// loads, and a non-browser HTTP client cannot finish the JavaScript
|
||||||
|
// challenge. It returns the Meta error page, so reaching this point only
|
||||||
|
// proves the exit can connect to Muse's auth infrastructure; availability
|
||||||
|
// cannot be verified over plain HTTP and is reported as `Risky`.
|
||||||
|
let behind_anti_bot = landed_on_aymh || AYMH_ERROR_MARKERS.iter().any(|marker| body.contains(marker));
|
||||||
|
|
||||||
|
let result = if behind_anti_bot {
|
||||||
|
"Risky"
|
||||||
|
} else if status.is_success() {
|
||||||
|
"Yes"
|
||||||
|
} else {
|
||||||
|
classify_restricted_status(status).unwrap_or("Failed")
|
||||||
|
};
|
||||||
|
|
||||||
|
UnlockItem::checked(MUSE_AI_NAME, result, region)
|
||||||
|
}
|
||||||
+16
-15
@@ -14,7 +14,7 @@
|
|||||||
"build:fast": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build -- --profile fast-release",
|
"build:fast": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build -- --profile fast-release",
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"web:dev": "vite",
|
"web:dev": "vite",
|
||||||
"web:build": "tsc --noEmit && vite build",
|
"web:build": "tsrs -p . --noEmit && vite build",
|
||||||
"web:serve": "vite preview",
|
"web:serve": "vite preview",
|
||||||
"prebuild": "node scripts/prebuild.mjs",
|
"prebuild": "node scripts/prebuild.mjs",
|
||||||
"updater": "node scripts/updater.mjs",
|
"updater": "node scripts/updater.mjs",
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
"i18n:types": "node scripts/generate-i18n-keys.mjs",
|
"i18n:types": "node scripts/generate-i18n-keys.mjs",
|
||||||
"gen:contract": "node scripts/gen-contract.mjs",
|
"gen:contract": "node scripts/gen-contract.mjs",
|
||||||
"arch:check": "node scripts/check-architecture.mjs listen emit invoke",
|
"arch:check": "node scripts/check-architecture.mjs listen emit invoke",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsrs -p . --noEmit",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"knip:check": "knip",
|
"knip:check": "knip",
|
||||||
@@ -54,13 +54,13 @@
|
|||||||
"@mui/icons-material": "^9.4.0",
|
"@mui/icons-material": "^9.4.0",
|
||||||
"@mui/material": "^9.4.0",
|
"@mui/material": "^9.4.0",
|
||||||
"@tanstack/react-virtual": "^3.14.13",
|
"@tanstack/react-virtual": "^3.14.13",
|
||||||
"@tauri-apps/api": "2.12.1",
|
"@tauri-apps/api": "2.12.4",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "2.4.0",
|
"@tauri-apps/plugin-clipboard-manager": "2.5.0",
|
||||||
"@tauri-apps/plugin-dialog": "2.8.0",
|
"@tauri-apps/plugin-dialog": "2.8.3",
|
||||||
"@tauri-apps/plugin-fs": "2.6.0",
|
"@tauri-apps/plugin-fs": "2.7.1",
|
||||||
"@tauri-apps/plugin-http": "2.8.0",
|
"@tauri-apps/plugin-http": "2.8.3",
|
||||||
"@tauri-apps/plugin-opener": "2.7.0",
|
"@tauri-apps/plugin-opener": "2.7.2",
|
||||||
"@tauri-apps/plugin-updater": "2.13.1",
|
"@tauri-apps/plugin-updater": "2.14.0",
|
||||||
"ahooks": "^3.10.0",
|
"ahooks": "^3.10.0",
|
||||||
"cidr-block": "^2.3.0",
|
"cidr-block": "^2.3.0",
|
||||||
"compare-versions": "^6.1.1",
|
"compare-versions": "^6.1.1",
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
"react-dom": "19.3.1",
|
"react-dom": "19.3.1",
|
||||||
"react-error-boundary": "6.1.6",
|
"react-error-boundary": "6.1.6",
|
||||||
"react-hook-form": "^7.88.0",
|
"react-hook-form": "^7.88.0",
|
||||||
"react-i18next": "17.0.19",
|
"react-i18next": "17.1.8",
|
||||||
"react-markdown": "10.1.0",
|
"react-markdown": "10.1.0",
|
||||||
"react-router": "^8.4.0",
|
"react-router": "^8.4.0",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
"@biomejs/biome": "^2.5.14",
|
"@biomejs/biome": "^2.5.14",
|
||||||
"@eslint-react/eslint-plugin": "^5.20.5",
|
"@eslint-react/eslint-plugin": "^5.20.5",
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@tauri-apps/cli": "2.12.1",
|
"@tauri-apps/cli": "2.12.3",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^24.13.6",
|
"@types/node": "^24.13.6",
|
||||||
"@types/react": "19.3.0",
|
"@types/react": "19.3.0",
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
"@types/validator": "^13.15.10",
|
"@types/validator": "^13.15.10",
|
||||||
"@vitejs/plugin-legacy": "^8.2.3",
|
"@vitejs/plugin-legacy": "^8.2.3",
|
||||||
"@vitejs/plugin-react": "^6.1.1",
|
"@vitejs/plugin-react": "^6.1.1",
|
||||||
"@wdio/tauri-service": "1.4.0",
|
"@wdio/tauri-service": "1.5.0",
|
||||||
"adm-zip": "^0.6.1",
|
"adm-zip": "^0.6.1",
|
||||||
"axios": "^1.20.0",
|
"axios": "^1.20.0",
|
||||||
"cli-color": "^2.0.4",
|
"cli-color": "^2.0.4",
|
||||||
@@ -123,17 +123,18 @@
|
|||||||
"https-proxy-agent": "^9.1.0",
|
"https-proxy-agent": "^9.1.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"jiti": "^2.7.0",
|
"jiti": "^2.7.0",
|
||||||
"knip": "6.41.0",
|
"knip": "6.43.0",
|
||||||
"lint-staged": "^17.5.1",
|
"lint-staged": "^17.5.1",
|
||||||
"sass": "^1.104.1",
|
"sass": "^1.104.1",
|
||||||
"tar": "^7.5.22",
|
"tar": "^7.5.22",
|
||||||
"terser": "^5.51.2",
|
"terser": "^5.51.2",
|
||||||
|
"tsrs": "0.20.0-ts7.1.0-beta",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"typescript-eslint": "^8.70.0",
|
"typescript-eslint": "^8.70.0",
|
||||||
"vite": "^8.3.0",
|
"vite": "^8.3.0",
|
||||||
"vite-plugin-svgr": "^5.2.0",
|
"vite-plugin-svgr": "^5.2.0",
|
||||||
"vitest": "^5.0.1",
|
"vitest": "^5.0.1",
|
||||||
"webdriverio": "10.0.2",
|
"webdriverio": "10.1.0",
|
||||||
"ws": "8.22.0"
|
"ws": "8.22.0"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
@@ -146,5 +147,5 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@12.14.0"
|
"packageManager": "pnpm@12.15.0"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+495
-614
File diff suppressed because it is too large
Load Diff
@@ -10,3 +10,11 @@ allowBuilds:
|
|||||||
unrs-resolver: true
|
unrs-resolver: true
|
||||||
minimumReleaseAgeExclude:
|
minimumReleaseAgeExclude:
|
||||||
- dayjs@1.11.21
|
- dayjs@1.11.21
|
||||||
|
- "@tsrs/darwin-arm64@0.20.0-ts7.1.0-beta"
|
||||||
|
- "@tsrs/darwin-x64@0.20.0-ts7.1.0-beta"
|
||||||
|
- "@tsrs/linux-arm64-gnu@0.20.0-ts7.1.0-beta"
|
||||||
|
- "@tsrs/linux-arm64-musl@0.20.0-ts7.1.0-beta"
|
||||||
|
- "@tsrs/linux-x64-gnu@0.20.0-ts7.1.0-beta"
|
||||||
|
- "@tsrs/linux-x64-musl@0.20.0-ts7.1.0-beta"
|
||||||
|
- "@tsrs/win32-x64@0.20.0-ts7.1.0-beta"
|
||||||
|
- tsrs@0.20.0-ts7.1.0-beta
|
||||||
|
|||||||
@@ -79,6 +79,13 @@
|
|||||||
"matchPackageNames": ["typescript"],
|
"matchPackageNames": ["typescript"],
|
||||||
"allowedVersions": "<7.0.0 || >=7.1.0"
|
"allowedVersions": "<7.0.0 || >=7.1.0"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Review tsrs bumps alone; the version encodes the pinned TypeScript release",
|
||||||
|
"matchPackageNames": ["tsrs", "/^@tsrs//"],
|
||||||
|
"groupName": "tsrs",
|
||||||
|
"rangeStrategy": "pin",
|
||||||
|
"automerge": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Prevent project from supply chain attack",
|
"description": "Prevent project from supply chain attack",
|
||||||
"matchDatasources": ["npm"],
|
"matchDatasources": ["npm"],
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"pnpm-lock.yaml",
|
"pnpm-lock.yaml",
|
||||||
"pnpm-workspace.yaml",
|
"pnpm-workspace.yaml",
|
||||||
"scripts/contract-inputs.json",
|
"scripts/contract-inputs.json",
|
||||||
".github/workflows/frontend-check.yml",
|
".github/workflows/contract-parity.yml",
|
||||||
".husky/pre-push"
|
".husky/pre-push"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -434,12 +434,13 @@ for (let iteration = 0; iteration < repeat && !interrupted; iteration++) {
|
|||||||
devicePixelRatio,
|
devicePixelRatio,
|
||||||
])
|
])
|
||||||
run.viewport = { dom: viewport, driver: await browser.getWindowRect() }
|
run.viewport = { dom: viewport, driver: await browser.getWindowRect() }
|
||||||
|
const driverScale = run.viewport.driver.width / 1000
|
||||||
assert(
|
assert(
|
||||||
run.viewport.driver.width / viewport[2] === 1000 &&
|
viewport[0] === 1000 &&
|
||||||
run.viewport.driver.height / viewport[2] === 700 &&
|
|
||||||
viewport[0] === 1000 &&
|
|
||||||
viewport[1] > 0 &&
|
viewport[1] > 0 &&
|
||||||
viewport[1] <= 700,
|
viewport[1] <= 700 &&
|
||||||
|
(driverScale === 1 || driverScale === viewport[2]) &&
|
||||||
|
run.viewport.driver.height / driverScale === 700,
|
||||||
`unexpected window dimensions: ${JSON.stringify(run.viewport)}`,
|
`unexpected window dimensions: ${JSON.stringify(run.viewport)}`,
|
||||||
)
|
)
|
||||||
assert(
|
assert(
|
||||||
|
|||||||
+11
-11
@@ -29,7 +29,7 @@ perf-harness = ["dep:tauri-plugin-wdio-webdriver"]
|
|||||||
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2.7.1", features = [] }
|
tauri-build = { version = "2.7.3", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clash-verge-draft = { workspace = true }
|
clash-verge-draft = { workspace = true }
|
||||||
@@ -72,12 +72,12 @@ sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs.git", branch
|
|||||||
"guard",
|
"guard",
|
||||||
] }
|
] }
|
||||||
network-interface = { version = "2.0.5", features = ["serde"] }
|
network-interface = { version = "2.0.5", features = ["serde"] }
|
||||||
tauri-plugin-shell = "2.4.0"
|
tauri-plugin-shell = "2.5.0"
|
||||||
tauri-plugin-opener = "2.7.0"
|
tauri-plugin-opener = "2.7.2"
|
||||||
tauri-plugin-dialog = "2.8.1"
|
tauri-plugin-dialog = "2.8.3"
|
||||||
tauri-plugin-fs = "2.6.0"
|
tauri-plugin-fs = "2.7.1"
|
||||||
tauri-plugin-process = "2.4.0"
|
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"
|
tauri-plugin-window-state = "2.5.0"
|
||||||
zip = "9.0.4"
|
zip = "9.0.4"
|
||||||
flate2 = "1.1.10"
|
flate2 = "1.1.10"
|
||||||
@@ -90,10 +90,10 @@ getrandom = "0.4.3"
|
|||||||
futures = "0.3.34"
|
futures = "0.3.34"
|
||||||
gethostname = "1.1.0"
|
gethostname = "1.1.0"
|
||||||
scopeguard = "1.2.0"
|
scopeguard = "1.2.0"
|
||||||
tauri-plugin-notification = "2.5.1"
|
tauri-plugin-notification = "2.7.0"
|
||||||
tokio-stream = "0.1.19"
|
tokio-stream = "0.1.19"
|
||||||
backon = { version = "1.6.0", features = ["tokio-sleep"] }
|
backon = { version = "1.6.0", features = ["tokio-sleep"] }
|
||||||
tauri-plugin-http = "2.8.0"
|
tauri-plugin-http = "2.8.3"
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
tauri-plugin-devtools = { version = "2.2.2", optional = true }
|
tauri-plugin-devtools = { version = "2.2.2", optional = true }
|
||||||
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", rev = "7a03d16f4d39b1bb885b7babe66b9e153e34b615" }
|
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", rev = "7a03d16f4d39b1bb885b7babe66b9e153e34b615" }
|
||||||
@@ -120,7 +120,7 @@ libc = "0.2.191"
|
|||||||
clash-verge-signal = { workspace = true }
|
clash-verge-signal = { workspace = true }
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[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 = "0.6.5"
|
||||||
objc2-foundation = { version = "0.3.2", features = [
|
objc2-foundation = { version = "0.3.2", features = [
|
||||||
"NSString",
|
"NSString",
|
||||||
@@ -153,8 +153,8 @@ windows-sys.workspace = true
|
|||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-autostart = "2.7.0"
|
tauri-plugin-autostart = "2.7.0"
|
||||||
tauri-plugin-global-shortcut = "2.4.0"
|
tauri-plugin-global-shortcut = "2.4.1"
|
||||||
tauri-plugin-updater = "2.13.1"
|
tauri-plugin-updater = "2.14.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = { workspace = true }
|
criterion = { workspace = true }
|
||||||
|
|||||||
+10
-5
@@ -1,10 +1,9 @@
|
|||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows")
|
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("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.
|
// Tauri's resource is linked to the app binary, but not to Rust unit-test harnesses or examples.
|
||||||
// Give mock-context builds their own manifest so tests can resolve TaskDialogIndirect.
|
// 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 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");
|
let manifest_path = std::path::PathBuf::from(out_dir).join("windows-test-manifest.xml");
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
@@ -25,8 +24,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
</assembly>
|
</assembly>
|
||||||
"#,
|
"#,
|
||||||
)?;
|
)?;
|
||||||
println!("cargo:rustc-link-arg=/MANIFEST:EMBED");
|
let manifest_display = manifest_path.display();
|
||||||
println!("cargo:rustc-link-arg=/MANIFESTINPUT:{}", 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")]
|
#[cfg(feature = "clippy")]
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ pub mod tun {
|
|||||||
"auto-redirect",
|
"auto-redirect",
|
||||||
"auto-detect-interface",
|
"auto-detect-interface",
|
||||||
"dns-hijack",
|
"dns-hijack",
|
||||||
|
"dns-mode",
|
||||||
"strict-route",
|
"strict-route",
|
||||||
"mtu",
|
"mtu",
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -422,12 +422,11 @@ fn create_hotkeys(hotkeys: &Option<Vec<String>>) -> HashMap<&str, &str> {
|
|||||||
let mut parts = item.split(',');
|
let mut parts = item.split(',');
|
||||||
match (parts.next(), parts.next()) {
|
match (parts.next(), parts.next()) {
|
||||||
(Some(func), Some(key)) => {
|
(Some(func), Some(key)) => {
|
||||||
// 托盘菜单中的 `accelerator` 属性,在 Linux/Windows 中都不支持小键盘按键的解析
|
// 托盘菜单的 `accelerator` 在 Linux/Windows 下不支持小键盘按键,Windows 下也不支持 Win 键(前端上报为 `CMD`)
|
||||||
if key.to_uppercase().contains("NUMPAD") {
|
let uppercase_key = key.to_uppercase();
|
||||||
None
|
let unsupported =
|
||||||
} else {
|
uppercase_key.contains("NUMPAD") || (cfg!(windows) && uppercase_key.contains("CMD"));
|
||||||
Some((func, key))
|
if unsupported { None } else { Some((func, key)) }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,3 +21,27 @@ These rules supplement the repository's root `AGENTS.md`.
|
|||||||
- **Command ownership.** `services` owns backend command invocation and transport
|
- **Command ownership.** `services` owns backend command invocation and transport
|
||||||
wrappers. Components and hooks use those boundaries; plugin mutations follow
|
wrappers. Components and hooks use those boundaries; plugin mutations follow
|
||||||
the same intent ownership as backend commands.
|
the same intent ownership as backend commands.
|
||||||
|
- **State liveness.** Every `useState`/`useRef` member has a reachable write
|
||||||
|
that differs from its initial value and a read that observes the difference.
|
||||||
|
A value that can only ever hold its initial value is a constant: declare the
|
||||||
|
constant and delete the state. A ref nothing reads is deleted, not kept.
|
||||||
|
- **Surface liveness.** Anything a module exposes — props, parameters, context
|
||||||
|
members, imperative-handle methods — ships with at least one external caller
|
||||||
|
in the same change. "Reserved for future use" fields, `_`-prefixed params
|
||||||
|
that callers still pass, and error or callback channels that every receiver
|
||||||
|
ignores are removed, not maintained.
|
||||||
|
- **Path liveness.** A fallback or branch names the concrete runtime condition
|
||||||
|
that reaches it. If no supported runtime can reach it, the branch is not
|
||||||
|
added — or it replaces the primary path. Constant subexpressions and cleanup
|
||||||
|
of resources that cannot exist yet are simplified away.
|
||||||
|
- **Effect economy.** One user interaction produces one state write and one
|
||||||
|
command. A handler does not re-apply a value its wrapper already applied,
|
||||||
|
and an effect does not re-run a refresh another branch of the same effect
|
||||||
|
just ran.
|
||||||
|
- **Mount economy.** Components that issue commands or subscriptions on mount
|
||||||
|
are mounted only where their UI is reachable. A dialog no rendered control
|
||||||
|
can open stays unmounted, not mounted-and-idle.
|
||||||
|
- **Resource bounds.** Every cache key, timer and subscription a feature
|
||||||
|
allocates is reused or released on refresh; refresh never allocates an
|
||||||
|
unbounded sequence of new keys. Retries re-create per-attempt resources so
|
||||||
|
a retry can actually succeed.
|
||||||
|
|||||||
@@ -22,23 +22,16 @@ interface BaseSplitChipEditorProps {
|
|||||||
error?: boolean
|
error?: boolean
|
||||||
helperText?: ReactNode
|
helperText?: ReactNode
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
rows?: number
|
|
||||||
separator?: string
|
separator?: string
|
||||||
splitPattern?: RegExp
|
|
||||||
defaultMode?: BaseSplitChipEditorMode
|
|
||||||
showModeToggle?: boolean
|
|
||||||
ariaLabel?: string
|
ariaLabel?: string
|
||||||
addLabel?: ReactNode
|
|
||||||
emptyLabel?: ReactNode
|
|
||||||
modeLabels?: Partial<Record<BaseSplitChipEditorMode, ReactNode>>
|
|
||||||
renderHeader?: (modeToggle: ReactNode) => ReactNode
|
renderHeader?: (modeToggle: ReactNode) => ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_SPLIT_PATTERN = /[,\n;\r]+/
|
const DEFAULT_SPLIT_PATTERN = /[,\n;\r]+/
|
||||||
|
|
||||||
const splitValue = (value: string, splitPattern: RegExp) =>
|
const splitValue = (value: string) =>
|
||||||
value
|
value
|
||||||
.split(splitPattern)
|
.split(DEFAULT_SPLIT_PATTERN)
|
||||||
.map((item) => item.trim())
|
.map((item) => item.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
|
|
||||||
@@ -49,35 +42,25 @@ export const BaseSplitChipEditor = ({
|
|||||||
error = false,
|
error = false,
|
||||||
helperText,
|
helperText,
|
||||||
placeholder,
|
placeholder,
|
||||||
rows = 4,
|
|
||||||
separator = ',',
|
separator = ',',
|
||||||
splitPattern = DEFAULT_SPLIT_PATTERN,
|
|
||||||
defaultMode = 'visual',
|
|
||||||
showModeToggle = true,
|
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
addLabel,
|
|
||||||
emptyLabel,
|
|
||||||
modeLabels,
|
|
||||||
renderHeader,
|
renderHeader,
|
||||||
}: BaseSplitChipEditorProps) => {
|
}: BaseSplitChipEditorProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [mode, setMode] = useState<BaseSplitChipEditorMode>(defaultMode)
|
const [mode, setMode] = useState<BaseSplitChipEditorMode>('visual')
|
||||||
const [draft, setDraft] = useState('')
|
const [draft, setDraft] = useState('')
|
||||||
|
|
||||||
const resolvedLabels = useMemo(
|
const resolvedLabels = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
visual: modeLabels?.visual ?? t('shared.editorModes.visualization'),
|
visual: t('shared.editorModes.visualization'),
|
||||||
advanced: modeLabels?.advanced ?? t('shared.editorModes.advanced'),
|
advanced: t('shared.editorModes.advanced'),
|
||||||
add: addLabel ?? t('shared.actions.new'),
|
add: t('shared.actions.new'),
|
||||||
empty: emptyLabel ?? t('shared.statuses.empty'),
|
empty: t('shared.statuses.empty'),
|
||||||
}),
|
}),
|
||||||
[t, modeLabels, addLabel, emptyLabel],
|
[t],
|
||||||
)
|
)
|
||||||
|
|
||||||
const values = useMemo(
|
const values = useMemo(() => splitValue(value), [value])
|
||||||
() => splitValue(value, splitPattern),
|
|
||||||
[value, splitPattern],
|
|
||||||
)
|
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
const counts = new Map<string, number>()
|
const counts = new Map<string, number>()
|
||||||
@@ -92,7 +75,7 @@ export const BaseSplitChipEditor = ({
|
|||||||
}, [values])
|
}, [values])
|
||||||
|
|
||||||
const handleAddDraft = () => {
|
const handleAddDraft = () => {
|
||||||
const nextValues = splitValue(draft, splitPattern)
|
const nextValues = splitValue(draft)
|
||||||
if (!nextValues.length) {
|
if (!nextValues.length) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -113,7 +96,7 @@ export const BaseSplitChipEditor = ({
|
|||||||
const resolvedAriaLabel =
|
const resolvedAriaLabel =
|
||||||
ariaLabel ?? (typeof toggleLabel === 'string' ? toggleLabel : undefined)
|
ariaLabel ?? (typeof toggleLabel === 'string' ? toggleLabel : undefined)
|
||||||
|
|
||||||
const modeToggle = showModeToggle ? (
|
const modeToggle = (
|
||||||
<Tooltip title={toggleLabel}>
|
<Tooltip title={toggleLabel}>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
@@ -128,7 +111,7 @@ export const BaseSplitChipEditor = ({
|
|||||||
<ToggleIcon fontSize="small" />
|
<ToggleIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : null
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -202,7 +185,7 @@ export const BaseSplitChipEditor = ({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
size="small"
|
size="small"
|
||||||
multiline
|
multiline
|
||||||
rows={rows}
|
rows={4}
|
||||||
sx={{ width: '100%' }}
|
sx={{ width: '100%' }}
|
||||||
value={value}
|
value={value}
|
||||||
helperText={helperText}
|
helperText={helperText}
|
||||||
|
|||||||
@@ -596,7 +596,7 @@ export const CurrentProxyCard = () => {
|
|||||||
[getProfileStorageKey],
|
[getProfileStorageKey],
|
||||||
)
|
)
|
||||||
|
|
||||||
const { handleSelectChange } = useProxySelection({
|
const { select } = useProxySelection({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
refreshProxy()
|
refreshProxy()
|
||||||
},
|
},
|
||||||
@@ -766,15 +766,9 @@ export const CurrentProxyCard = () => {
|
|||||||
const previousProxy = selectedGroup.now
|
const previousProxy = selectedGroup.now
|
||||||
const nextName = option.member.ref.name
|
const nextName = option.member.ref.name
|
||||||
// The profile selection is the durable source across core restarts and run modes.
|
// The profile selection is the durable source across core restarts and run modes.
|
||||||
handleSelectChange(
|
select(selectedGroup.name, nextName, previousProxy!, selectedGroup.fixed!)
|
||||||
selectedGroup.name,
|
|
||||||
previousProxy!,
|
|
||||||
selectedGroup.fixed!,
|
|
||||||
)({
|
|
||||||
target: { value: nextName },
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
[handleSelectChange, isDirectMode, selectedGroup, unsortedProxyOptions],
|
[isDirectMode, select, selectedGroup, unsortedProxyOptions],
|
||||||
)
|
)
|
||||||
|
|
||||||
const goToProxies = useCallback(() => {
|
const goToProxies = useCallback(() => {
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { useTranslation } from 'react-i18next'
|
|||||||
|
|
||||||
import { useTrafficGraphDataEnhanced } from '@/hooks/use-traffic-monitor'
|
import { useTrafficGraphDataEnhanced } from '@/hooks/use-traffic-monitor'
|
||||||
import { useVerge } from '@/hooks/use-verge'
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
import { debugLog } from '@/utils/debug'
|
|
||||||
import parseTraffic from '@/utils/parse-traffic'
|
import parseTraffic from '@/utils/parse-traffic'
|
||||||
import {
|
import {
|
||||||
formatTrafficHourMinute,
|
formatTrafficHourMinute,
|
||||||
@@ -23,14 +22,7 @@ import {
|
|||||||
isSameTrafficData,
|
isSameTrafficData,
|
||||||
} from '@/utils/traffic-sampler'
|
} from '@/utils/traffic-sampler'
|
||||||
|
|
||||||
interface ITrafficItem {
|
|
||||||
up: number
|
|
||||||
down: number
|
|
||||||
timestamp?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EnhancedCanvasTrafficGraphRef {
|
export interface EnhancedCanvasTrafficGraphRef {
|
||||||
appendData: (data: ITrafficItem) => void
|
|
||||||
toggleStyle: () => void
|
toggleStyle: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +134,6 @@ export const EnhancedCanvasTrafficGraph = memo(
|
|||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
const debounceTimeoutRef = useRef<number | null>(null)
|
const debounceTimeoutRef = useRef<number | null>(null)
|
||||||
const [currentFPS, setCurrentFPS] = useState(GRAPH_CONFIG.targetFPS)
|
|
||||||
|
|
||||||
const colors = useMemo(
|
const colors = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -183,8 +174,6 @@ export const EnhancedCanvasTrafficGraph = memo(
|
|||||||
if (displayData.length === 0) {
|
if (displayData.length === 0) {
|
||||||
lastDataTimestampRef.current = 0
|
lastDataTimestampRef.current = 0
|
||||||
dataStaleRef.current = false
|
dataStaleRef.current = false
|
||||||
// eslint-disable-next-line @eslint-react/set-state-in-effect
|
|
||||||
setCurrentFPS(GRAPH_CONFIG.targetFPS)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,18 +189,11 @@ export const EnhancedCanvasTrafficGraph = memo(
|
|||||||
}
|
}
|
||||||
}, [displayData])
|
}, [displayData])
|
||||||
|
|
||||||
const handleFocusStateChange = useCallback(
|
const handleFocusStateChange = useCallback((focused: boolean) => {
|
||||||
(focused: boolean) => {
|
isWindowFocusedRef.current = focused
|
||||||
isWindowFocusedRef.current = focused
|
|
||||||
|
|
||||||
if (focused || !pause_render_traffic_stats_on_blur) {
|
scheduleDrawGraphRef.current()
|
||||||
setCurrentFPS(GRAPH_CONFIG.targetFPS)
|
}, [])
|
||||||
}
|
|
||||||
|
|
||||||
scheduleDrawGraphRef.current()
|
|
||||||
},
|
|
||||||
[pause_render_traffic_stats_on_blur],
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
||||||
@@ -684,19 +666,13 @@ export const EnhancedCanvasTrafficGraph = memo(
|
|||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(getX(0), getY(0))
|
ctx.moveTo(getX(0), getY(0))
|
||||||
|
|
||||||
if (chartStyle === 'bezier') {
|
for (let i = 1; i < data.length; i++) {
|
||||||
for (let i = 1; i < data.length; i++) {
|
const currentX = getX(i)
|
||||||
const currentX = getX(i)
|
const currentY = getY(i)
|
||||||
const currentY = getY(i)
|
const nextIndex = Math.min(i + 1, lastIndex)
|
||||||
const nextIndex = Math.min(i + 1, lastIndex)
|
const controlX = (currentX + getX(nextIndex)) / 2
|
||||||
const controlX = (currentX + getX(nextIndex)) / 2
|
const controlY = (currentY + getY(nextIndex)) / 2
|
||||||
const controlY = (currentY + getY(nextIndex)) / 2
|
ctx.quadraticCurveTo(currentX, currentY, controlX, controlY)
|
||||||
ctx.quadraticCurveTo(currentX, currentY, controlX, controlY)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (let i = 1; i < data.length; i++) {
|
|
||||||
ctx.lineTo(getX(i), getY(i))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.lineTo(getX(lastIndex), height - padding.bottom)
|
ctx.lineTo(getX(lastIndex), height - padding.bottom)
|
||||||
@@ -984,21 +960,7 @@ export const EnhancedCanvasTrafficGraph = memo(
|
|||||||
setChartStyle((prev) => (prev === 'bezier' ? 'line' : 'bezier'))
|
setChartStyle((prev) => (prev === 'bezier' ? 'line' : 'bezier'))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const appendData = useCallback((data: ITrafficItem) => {
|
useImperativeHandle(ref, () => ({ toggleStyle }), [toggleStyle])
|
||||||
debugLog(
|
|
||||||
'[EnhancedCanvasTrafficGraphV2] appendData called (using global data):',
|
|
||||||
data,
|
|
||||||
)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useImperativeHandle(
|
|
||||||
ref,
|
|
||||||
() => ({
|
|
||||||
appendData,
|
|
||||||
toggleStyle,
|
|
||||||
}),
|
|
||||||
[appendData, toggleStyle],
|
|
||||||
)
|
|
||||||
|
|
||||||
const getTimeRangeText = useCallback(() => {
|
const getTimeRangeText = useCallback(() => {
|
||||||
return t('home.components.traffic.patterns.minutes', {
|
return t('home.components.traffic.patterns.minutes', {
|
||||||
@@ -1142,7 +1104,7 @@ export const EnhancedCanvasTrafficGraph = memo(
|
|||||||
{t('home.components.traffic.diagnostics', {
|
{t('home.components.traffic.diagnostics', {
|
||||||
points: displayData.length,
|
points: displayData.length,
|
||||||
compressed: samplerStats.compressedBufferSize,
|
compressed: samplerStats.compressedBufferSize,
|
||||||
fps: currentFPS,
|
fps: GRAPH_CONFIG.targetFPS,
|
||||||
})}
|
})}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,14 @@ import {
|
|||||||
ArrowUpwardRounded,
|
ArrowUpwardRounded,
|
||||||
CloudDownloadRounded,
|
CloudDownloadRounded,
|
||||||
CloudUploadRounded,
|
CloudUploadRounded,
|
||||||
|
ErrorOutlineRounded,
|
||||||
LinkRounded,
|
LinkRounded,
|
||||||
MemoryRounded,
|
MemoryRounded,
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
|
Alert,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
Grid,
|
Grid,
|
||||||
PaletteColor,
|
PaletteColor,
|
||||||
Paper,
|
Paper,
|
||||||
@@ -15,9 +19,9 @@ import {
|
|||||||
useTheme,
|
useTheme,
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { ReactNode, memo, useMemo, useRef } from 'react'
|
import { ReactNode, memo, useMemo, useRef } from 'react'
|
||||||
|
import { ErrorBoundary, FallbackProps } from 'react-error-boundary'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { TrafficErrorBoundary } from '@/components/shared/traffic-error-boundary'
|
|
||||||
import { useConnectionSummaryData } from '@/hooks/use-connection-data'
|
import { useConnectionSummaryData } from '@/hooks/use-connection-data'
|
||||||
import { useMemoryData } from '@/hooks/use-memory-data'
|
import { useMemoryData } from '@/hooks/use-memory-data'
|
||||||
import { useTrafficData } from '@/hooks/use-traffic-data'
|
import { useTrafficData } from '@/hooks/use-traffic-data'
|
||||||
@@ -127,6 +131,50 @@ const CompactStatCard = memo(
|
|||||||
// 添加显示名称
|
// 添加显示名称
|
||||||
CompactStatCard.displayName = 'CompactStatCard'
|
CompactStatCard.displayName = 'CompactStatCard'
|
||||||
|
|
||||||
|
const fallbackSx = {
|
||||||
|
p: 2,
|
||||||
|
minHeight: 200,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
border: '1px dashed',
|
||||||
|
borderColor: 'error.main',
|
||||||
|
borderRadius: 2,
|
||||||
|
bgcolor: 'error.light',
|
||||||
|
}
|
||||||
|
|
||||||
|
const reloadPage = () => window.location.reload()
|
||||||
|
|
||||||
|
const TrafficFallback = ({ error, resetErrorBoundary }: FallbackProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={fallbackSx}>
|
||||||
|
<ErrorOutlineRounded color="error" sx={{ fontSize: 48, mb: 2 }} />
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
{t('shared.feedback.errors.trafficStats')}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||||
|
{t('shared.feedback.errors.trafficStatsDescription')}
|
||||||
|
</Typography>
|
||||||
|
<Alert severity="error" sx={{ mb: 2, maxWidth: 400 }}>
|
||||||
|
{error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: t('shared.feedback.errors.unknown')}
|
||||||
|
</Alert>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
|
<Button variant="contained" size="small" onClick={resetErrorBoundary}>
|
||||||
|
{t('shared.actions.retry')}
|
||||||
|
</Button>
|
||||||
|
<Button variant="outlined" size="small" onClick={reloadPage}>
|
||||||
|
{t('shared.actions.refreshPage')}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const EnhancedTrafficStats = () => {
|
export const EnhancedTrafficStats = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
@@ -254,10 +302,11 @@ export const EnhancedTrafficStats = () => {
|
|||||||
}, [t, parsedData, displayMemory])
|
}, [t, parsedData, displayMemory])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TrafficErrorBoundary
|
<ErrorBoundary
|
||||||
onError={(error, errorInfo) => {
|
FallbackComponent={TrafficFallback}
|
||||||
console.error('[EnhancedTrafficStats] 组件错误:', error, errorInfo)
|
onError={(error, errorInfo) =>
|
||||||
}}
|
console.error('[EnhancedTrafficStats]', error, errorInfo)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}>
|
<Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}>
|
||||||
{trafficGraph && (
|
{trafficGraph && (
|
||||||
@@ -273,6 +322,6 @@ export const EnhancedTrafficStats = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
</TrafficErrorBoundary>
|
</ErrorBoundary>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,19 @@ import {
|
|||||||
VisibilityOffOutlined,
|
VisibilityOffOutlined,
|
||||||
VisibilityOutlined,
|
VisibilityOutlined,
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import { Box, Button, IconButton, Skeleton, Typography } from '@mui/material'
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
FormControlLabel,
|
||||||
|
IconButton,
|
||||||
|
Skeleton,
|
||||||
|
Switch,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/material'
|
||||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
|
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||||
import { useEffect } from 'foxact/use-abortable-effect'
|
import { useEffect } from 'foxact/use-abortable-effect'
|
||||||
import { useIntersection } from 'foxact/use-intersection'
|
import { useIntersection } from 'foxact/use-intersection'
|
||||||
|
import { useLocalStorage } from 'foxact/use-local-storage'
|
||||||
import type { XOR } from 'foxts/ts-xor'
|
import type { XOR } from 'foxts/ts-xor'
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
@@ -99,9 +108,13 @@ const IPInfoCardContainer = forwardRef<HTMLElement, React.PropsWithChildren>(
|
|||||||
export const IpInfoCard = () => {
|
export const IpInfoCard = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [showIp, setShowIp] = useState(false)
|
const [showIp, setShowIp] = useState(false)
|
||||||
|
const [autoRefresh, setAutoRefresh] = useLocalStorage(
|
||||||
|
'ip-info-auto-refresh',
|
||||||
|
true,
|
||||||
|
)
|
||||||
const appWindow = useMemo(() => getCurrentWebviewWindow(), [])
|
const appWindow = useMemo(() => getCurrentWebviewWindow(), [])
|
||||||
|
|
||||||
// Once intersected, refreshes stay enabled until unmount.
|
// Once intersected, the card stays eligible for refresh until unmount.
|
||||||
const [containerRef, hasIntersected, _resetIntersected] = useIntersection({
|
const [containerRef, hasIntersected, _resetIntersected] = useIntersection({
|
||||||
rootMargin: '0px',
|
rootMargin: '0px',
|
||||||
})
|
})
|
||||||
@@ -151,6 +164,8 @@ export const IpInfoCard = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!autoRefresh) return
|
||||||
|
|
||||||
let timer: number | null = null
|
let timer: number | null = null
|
||||||
|
|
||||||
// Prefer DOM intersection state; Tauri visibility is unreliable on some platforms.
|
// Prefer DOM intersection state; Tauri visibility is unreliable on some platforms.
|
||||||
@@ -190,7 +205,7 @@ export const IpInfoCard = () => {
|
|||||||
if (timer != null) clearInterval(timer)
|
if (timer != null) clearInterval(timer)
|
||||||
document.removeEventListener('visibilitychange', onVisibilityChange)
|
document.removeEventListener('visibilitychange', onVisibilityChange)
|
||||||
}
|
}
|
||||||
}, [hasIntersected])
|
}, [autoRefresh, hasIntersected])
|
||||||
|
|
||||||
const toggleShowIp = useCallback(() => {
|
const toggleShowIp = useCallback(() => {
|
||||||
setShowIp((prev) => !prev)
|
setShowIp((prev) => !prev)
|
||||||
@@ -364,12 +379,25 @@ export const IpInfoCard = () => {
|
|||||||
fontSize: '0.7rem',
|
fontSize: '0.7rem',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="caption">
|
<FormControlLabel
|
||||||
{t('home.components.ipInfo.labels.autoRefresh')}
|
sx={{ m: 0 }}
|
||||||
{countdown.type === 'countdown'
|
control={
|
||||||
? `: ${countdown.remainingSeconds}s`
|
<Switch
|
||||||
: '...'}
|
size="small"
|
||||||
</Typography>
|
checked={autoRefresh}
|
||||||
|
onChange={(_, checked) => setAutoRefresh(checked)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<Typography variant="caption">
|
||||||
|
{t('home.components.ipInfo.labels.autoRefresh')}
|
||||||
|
{autoRefresh &&
|
||||||
|
(countdown.type === 'countdown'
|
||||||
|
? `: ${countdown.remainingSeconds}s`
|
||||||
|
: '...')}
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Typography
|
<Typography
|
||||||
variant="caption"
|
variant="caption"
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@@ -86,16 +86,11 @@ export const SystemInfoCard = () => {
|
|||||||
}, [verge, patchVerge])
|
}, [verge, patchVerge])
|
||||||
|
|
||||||
const handleRunningModeClick = useCallback(async () => {
|
const handleRunningModeClick = useCallback(async () => {
|
||||||
if (isSidecarMode || (isAdminMode && isSidecarMode)) {
|
if (isSidecarMode) {
|
||||||
await installServiceAndRestartCore()
|
await installServiceAndRestartCore()
|
||||||
await mutateSystemState()
|
await mutateSystemState()
|
||||||
}
|
}
|
||||||
}, [
|
}, [isSidecarMode, installServiceAndRestartCore, mutateSystemState])
|
||||||
isSidecarMode,
|
|
||||||
isAdminMode,
|
|
||||||
installServiceAndRestartCore,
|
|
||||||
mutateSystemState,
|
|
||||||
])
|
|
||||||
|
|
||||||
const onCheckUpdate = useLockFn(async () => {
|
const onCheckUpdate = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -122,18 +117,16 @@ export const SystemInfoCard = () => {
|
|||||||
|
|
||||||
const runningModeStyle = useMemo(
|
const runningModeStyle = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
cursor:
|
cursor: isSidecarMode ? 'pointer' : 'default',
|
||||||
isSidecarMode || (isAdminMode && isSidecarMode) ? 'pointer' : 'default',
|
textDecoration: isSidecarMode ? 'underline' : 'none',
|
||||||
textDecoration:
|
|
||||||
isSidecarMode || (isAdminMode && isSidecarMode) ? 'underline' : 'none',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 0.5,
|
gap: 0.5,
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
opacity: isSidecarMode || (isAdminMode && isSidecarMode) ? 0.7 : 1,
|
opacity: isSidecarMode ? 0.7 : 1,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[isSidecarMode, isAdminMode],
|
[isSidecarMode],
|
||||||
)
|
)
|
||||||
|
|
||||||
const getModeIcon = () => {
|
const getModeIcon = () => {
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export const TestCard = () => {
|
|||||||
x.uid === uid ? { ...x, ...patch } : x,
|
x.uid === uid ? { ...x, ...patch } : x,
|
||||||
)
|
)
|
||||||
|
|
||||||
mutateVerge({ ...verge, test_list: newList }, false)
|
mutateVerge({ ...verge, test_list: newList })
|
||||||
},
|
},
|
||||||
[testList, verge, mutateVerge],
|
[testList, verge, mutateVerge],
|
||||||
)
|
)
|
||||||
@@ -96,7 +96,7 @@ export const TestCard = () => {
|
|||||||
(uid: string) => {
|
(uid: string) => {
|
||||||
const newList = testList.filter((x) => x.uid !== uid)
|
const newList = testList.filter((x) => x.uid !== uid)
|
||||||
patchVerge({ test_list: newList })
|
patchVerge({ test_list: newList })
|
||||||
mutateVerge({ ...verge, test_list: newList }, false)
|
mutateVerge({ ...verge, test_list: newList })
|
||||||
},
|
},
|
||||||
[testList, verge, patchVerge, mutateVerge],
|
[testList, verge, patchVerge, mutateVerge],
|
||||||
)
|
)
|
||||||
@@ -129,7 +129,7 @@ export const TestCard = () => {
|
|||||||
newList.splice(newIndex, 0, removed)
|
newList.splice(newIndex, 0, removed)
|
||||||
|
|
||||||
// 优化:先本地更新,再异步 patch,避免UI卡死
|
// 优化:先本地更新,再异步 patch,避免UI卡死
|
||||||
mutateVerge({ ...verge, test_list: newList }, false)
|
mutateVerge({ ...verge, test_list: newList })
|
||||||
const patchFn = () => {
|
const patchFn = () => {
|
||||||
try {
|
try {
|
||||||
patchVerge({ test_list: newList })
|
patchVerge({ test_list: newList })
|
||||||
|
|||||||
@@ -35,10 +35,7 @@ export const LayoutSidebar = (props: LayoutSidebarProps) => {
|
|||||||
|
|
||||||
const handleMenuOrderOptimisticUpdate = useCallback(
|
const handleMenuOrderOptimisticUpdate = useCallback(
|
||||||
(order: string[]) => {
|
(order: string[]) => {
|
||||||
mutateVerge(
|
mutateVerge((prev) => (prev ? { ...prev, menu_order: order } : prev))
|
||||||
(prev) => (prev ? { ...prev, menu_order: order } : prev),
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
[mutateVerge],
|
[mutateVerge],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
ArrowDownwardRounded,
|
ArrowDownwardRounded,
|
||||||
ArrowUpwardRounded,
|
ArrowUpwardRounded,
|
||||||
|
ErrorOutlineRounded,
|
||||||
MemoryRounded,
|
MemoryRounded,
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import type { BoxProps, SvgIconProps, TypographyProps } from '@mui/material'
|
import type { BoxProps, SvgIconProps, TypographyProps } from '@mui/material'
|
||||||
import { Box, Typography } from '@mui/material'
|
import { Box, Typography } from '@mui/material'
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
|
import { ErrorBoundary } from 'react-error-boundary'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { LightweightTrafficErrorBoundary } from '@/components/shared/traffic-error-boundary'
|
|
||||||
import { useMemoryData } from '@/hooks/use-memory-data'
|
import { useMemoryData } from '@/hooks/use-memory-data'
|
||||||
import { useTrafficData } from '@/hooks/use-traffic-data'
|
import { useTrafficData } from '@/hooks/use-traffic-data'
|
||||||
import { useVerge } from '@/hooks/use-verge'
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
@@ -17,6 +18,30 @@ import parseTraffic from '@/utils/parse-traffic'
|
|||||||
|
|
||||||
import { TrafficGraph, type TrafficRef } from './traffic-graph'
|
import { TrafficGraph, type TrafficRef } from './traffic-graph'
|
||||||
|
|
||||||
|
const unavailableSx = {
|
||||||
|
p: 1,
|
||||||
|
minHeight: 60,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
bgcolor: 'error.light',
|
||||||
|
borderRadius: 1,
|
||||||
|
color: 'error.contrastText',
|
||||||
|
}
|
||||||
|
|
||||||
|
const TrafficUnavailableFallback = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={unavailableSx}>
|
||||||
|
<ErrorOutlineRounded sx={{ mr: 1, fontSize: 20 }} />
|
||||||
|
<Typography variant="caption">
|
||||||
|
{t('shared.feedback.errors.trafficUnavailable')}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const LayoutTraffic = () => {
|
export const LayoutTraffic = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { verge } = useVerge()
|
const { verge } = useVerge()
|
||||||
@@ -75,7 +100,12 @@ export const LayoutTraffic = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LightweightTrafficErrorBoundary>
|
<ErrorBoundary
|
||||||
|
FallbackComponent={TrafficUnavailableFallback}
|
||||||
|
onError={(error, errorInfo) =>
|
||||||
|
console.error('[LayoutTraffic]', error, errorInfo)
|
||||||
|
}
|
||||||
|
>
|
||||||
<Box sx={{ position: 'relative' }}>
|
<Box sx={{ position: 'relative' }}>
|
||||||
{trafficGraph && pageVisible && (
|
{trafficGraph && pageVisible && (
|
||||||
<div
|
<div
|
||||||
@@ -130,7 +160,6 @@ export const LayoutTraffic = () => {
|
|||||||
cursor: 'auto',
|
cursor: 'auto',
|
||||||
}}
|
}}
|
||||||
color={'disabled'}
|
color={'disabled'}
|
||||||
onClick={async () => {}}
|
|
||||||
>
|
>
|
||||||
<MemoryRounded {...iconStyle} />
|
<MemoryRounded {...iconStyle} />
|
||||||
<Typography {...valStyle}>{inuse}</Typography>
|
<Typography {...valStyle}>{inuse}</Typography>
|
||||||
@@ -139,6 +168,6 @@ export const LayoutTraffic = () => {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</LightweightTrafficErrorBoundary>
|
</ErrorBoundary>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import { Close, CropSquare, FilterNone, Minimize } from '@mui/icons-material'
|
import { Close, CropSquare, FilterNone, Minimize } from '@mui/icons-material'
|
||||||
import { Box, IconButton } from '@mui/material'
|
import { Box, IconButton } from '@mui/material'
|
||||||
import {
|
import { type PointerEvent, useCallback } from 'react'
|
||||||
forwardRef,
|
|
||||||
type PointerEvent,
|
|
||||||
useCallback,
|
|
||||||
useImperativeHandle,
|
|
||||||
} from 'react'
|
|
||||||
|
|
||||||
import { useWindowControls } from '@/hooks/use-window'
|
import { useWindowControls } from '@/hooks/use-window'
|
||||||
import getSystem from '@/utils/get-system'
|
import getSystem from '@/utils/get-system'
|
||||||
@@ -63,36 +58,9 @@ export const WindowResizeHandles = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WindowControls = forwardRef(function WindowControls(props, ref) {
|
export const WindowControls = () => {
|
||||||
const OS = getSystem()
|
const OS = getSystem()
|
||||||
const {
|
const { maximized, minimize, close, toggleMaximize } = useWindowControls()
|
||||||
currentWindow,
|
|
||||||
maximized,
|
|
||||||
minimize,
|
|
||||||
close,
|
|
||||||
toggleFullscreen,
|
|
||||||
toggleMaximize,
|
|
||||||
} = useWindowControls()
|
|
||||||
|
|
||||||
useImperativeHandle(
|
|
||||||
ref,
|
|
||||||
() => ({
|
|
||||||
currentWindow,
|
|
||||||
maximized,
|
|
||||||
minimize,
|
|
||||||
close,
|
|
||||||
toggleFullscreen,
|
|
||||||
toggleMaximize,
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
currentWindow,
|
|
||||||
maximized,
|
|
||||||
minimize,
|
|
||||||
close,
|
|
||||||
toggleFullscreen,
|
|
||||||
toggleMaximize,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
// 通过前端对 tauri 窗口进行翻转全屏时会短暂地与系统图标重叠渲染。
|
// 通过前端对 tauri 窗口进行翻转全屏时会短暂地与系统图标重叠渲染。
|
||||||
// 这可能是上游缺陷,保险起见跨平台以窗口的最大化翻转为准。
|
// 这可能是上游缺陷,保险起见跨平台以窗口的最大化翻转为准。
|
||||||
@@ -186,4 +154,4 @@ export const WindowControls = forwardRef(function WindowControls(props, ref) {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|||||||
@@ -397,9 +397,9 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setPrependSeq(obj?.prepend || [])
|
setPrependSeq((obj?.prepend || []).filter((rule) => rule !== null))
|
||||||
setAppendSeq(obj?.append || [])
|
setAppendSeq((obj?.append || []).filter((rule) => rule !== null))
|
||||||
setDeleteSeq(obj?.delete || [])
|
setDeleteSeq((obj?.delete || []).filter((rule) => rule !== null))
|
||||||
hasLoadedSeqConfigRef.current = true
|
hasLoadedSeqConfigRef.current = true
|
||||||
}, [property])
|
}, [property])
|
||||||
|
|
||||||
@@ -417,9 +417,9 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
|
|
||||||
hasLoadedSeqConfigRef.current = true
|
hasLoadedSeqConfigRef.current = true
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
setPrependSeq(obj?.prepend ?? [])
|
setPrependSeq((obj?.prepend ?? []).filter((rule) => rule !== null))
|
||||||
setAppendSeq(obj?.append ?? [])
|
setAppendSeq((obj?.append ?? []).filter((rule) => rule !== null))
|
||||||
setDeleteSeq(obj?.delete ?? [])
|
setDeleteSeq((obj?.delete ?? []).filter((rule) => rule !== null))
|
||||||
})
|
})
|
||||||
setVisualization(true)
|
setVisualization(true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -462,7 +462,7 @@ function NormalProxyGroups(props: { mode: string }) {
|
|||||||
}
|
}
|
||||||
}, [handleScroll])
|
}, [handleScroll])
|
||||||
|
|
||||||
const { handleProxyGroupChange } = useProxySelection({
|
const { select } = useProxySelection({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
onProxies()
|
onProxies()
|
||||||
},
|
},
|
||||||
@@ -477,9 +477,9 @@ function NormalProxyGroups(props: { mode: string }) {
|
|||||||
if (!['Selector', 'URLTest', 'Fallback'].includes(group.type)) return
|
if (!['Selector', 'URLTest', 'Fallback'].includes(group.type)) return
|
||||||
if (!isInteractableMember(member)) return
|
if (!isInteractableMember(member)) return
|
||||||
|
|
||||||
handleProxyGroupChange(group, { name: member.ref.name })
|
select(group.name, member.ref.name, group.now!, group.fixed!)
|
||||||
},
|
},
|
||||||
[handleProxyGroupChange],
|
[select],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleLocation = useStableCallback((group: ProxyGroupView) => {
|
const handleLocation = useStableCallback((group: ProxyGroupView) => {
|
||||||
@@ -608,7 +608,7 @@ function NormalProxyGroups(props: { mode: string }) {
|
|||||||
/>
|
/>
|
||||||
</DragDropProvider>
|
</DragDropProvider>
|
||||||
|
|
||||||
{mode === 'rule' && (
|
{(mode === 'rule' || mode === 'global') && (
|
||||||
<ProxyGroupNavigator
|
<ProxyGroupNavigator
|
||||||
proxyGroupNames={proxyGroupNames}
|
proxyGroupNames={proxyGroupNames}
|
||||||
onGroupLocation={handleGroupLocationByName}
|
onGroupLocation={handleGroupLocationByName}
|
||||||
|
|||||||
@@ -30,10 +30,8 @@ export const useProxyGroupHeaderLayout = () => {
|
|||||||
if (canceled || !operation.source || !operation.target) return
|
if (canceled || !operation.source || !operation.target) return
|
||||||
|
|
||||||
const position = toolsOnLeft ? 'right' : 'left'
|
const position = toolsOnLeft ? 'right' : 'left'
|
||||||
mutateVerge(
|
mutateVerge((prev) =>
|
||||||
(prev) =>
|
prev ? { ...prev, proxy_group_tools_position: position } : prev,
|
||||||
prev ? { ...prev, proxy_group_tools_position: position } : prev,
|
|
||||||
false,
|
|
||||||
)
|
)
|
||||||
try {
|
try {
|
||||||
await patchVerge({ proxy_group_tools_position: position })
|
await patchVerge({ proxy_group_tools_position: position })
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ export const useRenderList = (
|
|||||||
if (!proxyView) return []
|
if (!proxyView) return []
|
||||||
if (isChainMode)
|
if (isChainMode)
|
||||||
return selectedGroup ? [selectedGroup] : [CHAIN_DELAY_GROUP]
|
return selectedGroup ? [selectedGroup] : [CHAIN_DELAY_GROUP]
|
||||||
return mode === 'rule' || mode === 'script'
|
return mode === 'rule' || mode === 'global'
|
||||||
? proxyView.groups.map(({ name }) => name)
|
? proxyView.groups.map(({ name }) => name)
|
||||||
: proxyView.global
|
: proxyView.global
|
||||||
? [proxyView.global.name]
|
? [proxyView.global.name]
|
||||||
@@ -257,7 +257,7 @@ export const useRenderList = (
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const useRule = mode === 'rule' || mode === 'script'
|
const useRule = mode === 'rule' || mode === 'global'
|
||||||
const renderGroups = useRule
|
const renderGroups = useRule
|
||||||
? proxyView.groups
|
? proxyView.groups
|
||||||
: proxyView.global === null
|
: proxyView.global === null
|
||||||
|
|||||||
@@ -113,17 +113,15 @@ export const BackupConfigViewer = memo(
|
|||||||
errorNotice: false,
|
errorNotice: false,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await mutateVerge(
|
await mutateVerge((current) =>
|
||||||
(current) =>
|
current
|
||||||
current
|
? {
|
||||||
? {
|
...current,
|
||||||
...current,
|
webdav_url: trimmedUrl,
|
||||||
webdav_url: trimmedUrl,
|
webdav_username: trimmedUsername,
|
||||||
webdav_username: trimmedUsername,
|
webdav_password: data.password,
|
||||||
webdav_password: data.password,
|
}
|
||||||
}
|
: current,
|
||||||
: current,
|
|
||||||
false,
|
|
||||||
)
|
)
|
||||||
setWebdavStatus(signature, 'unknown')
|
setWebdavStatus(signature, 'unknown')
|
||||||
showNotice.success('settings.modals.backup.messages.webdavConfigSaved')
|
showNotice.success('settings.modals.backup.messages.webdavConfigSaved')
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Button, ButtonGroup } from '@mui/material'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value?: string
|
||||||
|
onChange?: (value: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DnsModeSwitch = (props: Props) => {
|
||||||
|
const { value, onChange } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ButtonGroup size="small" sx={{ my: '4px' }}>
|
||||||
|
<Button
|
||||||
|
variant={!value ? 'contained' : 'outlined'}
|
||||||
|
onClick={() => onChange?.('')}
|
||||||
|
sx={{ textTransform: 'capitalize' }}
|
||||||
|
>
|
||||||
|
Auto
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={value === 'disabled' ? 'contained' : 'outlined'}
|
||||||
|
onClick={() => onChange?.('disabled')}
|
||||||
|
sx={{ textTransform: 'capitalize' }}
|
||||||
|
>
|
||||||
|
Disabled
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={value === 'native' ? 'contained' : 'outlined'}
|
||||||
|
onClick={() => onChange?.('native')}
|
||||||
|
sx={{ textTransform: 'capitalize' }}
|
||||||
|
>
|
||||||
|
Native
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={value === 'hijack' ? 'contained' : 'outlined'}
|
||||||
|
onClick={() => onChange?.('hijack')}
|
||||||
|
sx={{ textTransform: 'capitalize' }}
|
||||||
|
>
|
||||||
|
Hijack
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -107,7 +107,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
|||||||
showNotice.error(err)
|
showNotice.error(err)
|
||||||
}
|
}
|
||||||
const onChangeData = (patch: Partial<IVergeConfig>) => {
|
const onChangeData = (patch: Partial<IVergeConfig>) => {
|
||||||
mutateVerge({ ...verge, ...patch }, false)
|
mutateVerge({ ...verge, ...patch })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -15,44 +15,33 @@ import { BaseDialog, DialogRef, Switch, TooltipIcon } from '@/components/base'
|
|||||||
import { useVerge } from '@/hooks/use-verge'
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
import { showNotice } from '@/services/notice-service'
|
import { showNotice } from '@/services/notice-service'
|
||||||
|
|
||||||
|
const buildInitialValues = (verge?: IVergeConfig | null) => ({
|
||||||
|
appLogLevel: verge?.app_log_level ?? 'warn',
|
||||||
|
appLogMaxSize: verge?.app_log_max_size ?? 128,
|
||||||
|
appLogMaxCount: verge?.app_log_max_count ?? 8,
|
||||||
|
autoCloseConnection: verge?.auto_close_connection ?? true,
|
||||||
|
autoCheckUpdate: verge?.auto_check_update ?? true,
|
||||||
|
enableBuiltinEnhanced: verge?.enable_builtin_enhanced ?? true,
|
||||||
|
proxyLayoutColumn: verge?.proxy_layout_column || 6,
|
||||||
|
enableAutoDelayDetection: verge?.enable_auto_delay_detection ?? false,
|
||||||
|
autoDelayDetectionIntervalMinutes:
|
||||||
|
verge?.auto_delay_detection_interval_minutes ?? 5,
|
||||||
|
defaultLatencyTest: verge?.default_latency_test || '',
|
||||||
|
autoLogClean: verge?.auto_log_clean || 0,
|
||||||
|
defaultLatencyTimeout: verge?.default_latency_timeout || 10000,
|
||||||
|
})
|
||||||
|
|
||||||
export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { verge, patchVerge } = useVerge()
|
const { verge, patchVerge } = useVerge()
|
||||||
|
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [values, setValues] = useState({
|
const [values, setValues] = useState(() => buildInitialValues(null))
|
||||||
appLogLevel: 'warn',
|
|
||||||
appLogMaxSize: 8,
|
|
||||||
appLogMaxCount: 12,
|
|
||||||
autoCloseConnection: true,
|
|
||||||
autoCheckUpdate: true,
|
|
||||||
enableBuiltinEnhanced: true,
|
|
||||||
proxyLayoutColumn: 6,
|
|
||||||
enableAutoDelayDetection: false,
|
|
||||||
autoDelayDetectionIntervalMinutes: 5,
|
|
||||||
defaultLatencyTest: '',
|
|
||||||
autoLogClean: 2,
|
|
||||||
defaultLatencyTimeout: 10000,
|
|
||||||
})
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
open: () => {
|
open: () => {
|
||||||
setOpen(true)
|
setOpen(true)
|
||||||
setValues({
|
setValues(buildInitialValues(verge))
|
||||||
appLogLevel: verge?.app_log_level ?? 'warn',
|
|
||||||
appLogMaxSize: verge?.app_log_max_size ?? 128,
|
|
||||||
appLogMaxCount: verge?.app_log_max_count ?? 8,
|
|
||||||
autoCloseConnection: verge?.auto_close_connection ?? true,
|
|
||||||
autoCheckUpdate: verge?.auto_check_update ?? true,
|
|
||||||
enableBuiltinEnhanced: verge?.enable_builtin_enhanced ?? true,
|
|
||||||
proxyLayoutColumn: verge?.proxy_layout_column || 6,
|
|
||||||
enableAutoDelayDetection: verge?.enable_auto_delay_detection ?? false,
|
|
||||||
autoDelayDetectionIntervalMinutes:
|
|
||||||
verge?.auto_delay_detection_interval_minutes ?? 5,
|
|
||||||
defaultLatencyTest: verge?.default_latency_test || '',
|
|
||||||
autoLogClean: verge?.auto_log_clean || 0,
|
|
||||||
defaultLatencyTimeout: verge?.default_latency_timeout || 10000,
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
close: () => setOpen(false),
|
close: () => setOpen(false),
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -87,6 +87,17 @@ const splitBypass = (value?: string) =>
|
|||||||
.map((item) => item.trim())
|
.map((item) => item.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
|
|
||||||
|
const buildInitialValues = (verge?: IVergeConfig | null) => ({
|
||||||
|
guard: verge?.enable_proxy_guard,
|
||||||
|
enable_bypass_check: verge?.enable_bypass_check ?? true,
|
||||||
|
bypass: verge?.system_proxy_bypass,
|
||||||
|
duration: verge?.proxy_guard_duration ?? 10,
|
||||||
|
use_default: verge?.use_default_bypass ?? true,
|
||||||
|
pac: verge?.proxy_auto_config,
|
||||||
|
pac_content: verge?.pac_file_content ?? DEFAULT_PAC,
|
||||||
|
proxy_host: verge?.proxy_host ?? '127.0.0.1',
|
||||||
|
})
|
||||||
|
|
||||||
export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const systemName = getSystem()
|
const systemName = getSystem()
|
||||||
@@ -116,16 +127,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
proxy_host,
|
proxy_host,
|
||||||
} = verge ?? {}
|
} = verge ?? {}
|
||||||
|
|
||||||
const [value, setValue] = useState({
|
const [value, setValue] = useState(() => buildInitialValues(null))
|
||||||
guard: enable_proxy_guard,
|
|
||||||
enable_bypass_check: enable_bypass_check ?? true,
|
|
||||||
bypass: system_proxy_bypass,
|
|
||||||
duration: proxy_guard_duration ?? 10,
|
|
||||||
use_default: use_default_bypass ?? true,
|
|
||||||
pac: proxy_auto_config,
|
|
||||||
pac_content: pac_file_content ?? DEFAULT_PAC,
|
|
||||||
proxy_host: proxy_host ?? '127.0.0.1',
|
|
||||||
})
|
|
||||||
const [embeddedServerPort, setEmbeddedServerPort] = useState<number | null>(
|
const [embeddedServerPort, setEmbeddedServerPort] = useState<number | null>(
|
||||||
null,
|
null,
|
||||||
)
|
)
|
||||||
@@ -192,16 +194,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
open: () => {
|
open: () => {
|
||||||
setOpen(true)
|
setOpen(true)
|
||||||
setValue({
|
setValue(buildInitialValues(verge))
|
||||||
guard: enable_proxy_guard,
|
|
||||||
enable_bypass_check: enable_bypass_check ?? true,
|
|
||||||
bypass: system_proxy_bypass,
|
|
||||||
duration: proxy_guard_duration ?? 10,
|
|
||||||
use_default: use_default_bypass ?? true,
|
|
||||||
pac: proxy_auto_config,
|
|
||||||
pac_content: pac_file_content ?? DEFAULT_PAC,
|
|
||||||
proxy_host: proxy_host ?? '127.0.0.1',
|
|
||||||
})
|
|
||||||
fetchNetworkInterfaces()
|
fetchNetworkInterfaces()
|
||||||
},
|
},
|
||||||
close: () => setOpen(false),
|
close: () => setOpen(false),
|
||||||
@@ -339,7 +332,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
Promise.resolve().then(async () => {
|
Promise.resolve().then(async () => {
|
||||||
try {
|
try {
|
||||||
if (Object.keys(patch).length > 0) {
|
if (Object.keys(patch).length > 0) {
|
||||||
mutateVerge({ ...verge, ...patch }, false)
|
mutateVerge({ ...verge, ...patch })
|
||||||
}
|
}
|
||||||
if (Object.keys(patch).length > 0) {
|
if (Object.keys(patch).length > 0) {
|
||||||
await patchVerge(patch)
|
await patchVerge(patch)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { showNotice } from '@/services/notice-service'
|
|||||||
import getSystem from '@/utils/get-system'
|
import getSystem from '@/utils/get-system'
|
||||||
import { areValidIpCidrs } from '@/utils/network'
|
import { areValidIpCidrs } from '@/utils/network'
|
||||||
|
|
||||||
|
import { DnsModeSwitch } from './dns-mode-switch'
|
||||||
import { StackModeSwitch } from './stack-mode-switch'
|
import { StackModeSwitch } from './stack-mode-switch'
|
||||||
|
|
||||||
const OS = getSystem()
|
const OS = getSystem()
|
||||||
@@ -52,6 +53,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
dnsHijack: ['any:53'],
|
dnsHijack: ['any:53'],
|
||||||
strictRoute: false,
|
strictRoute: false,
|
||||||
mtu: 1500,
|
mtu: 1500,
|
||||||
|
dnsMode: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const routeExcludeAddressItems = splitRouteExcludeAddress(
|
const routeExcludeAddressItems = splitRouteExcludeAddress(
|
||||||
@@ -84,6 +86,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
dnsHijack: clash?.tun['dns-hijack'] ?? ['any:53'],
|
dnsHijack: clash?.tun['dns-hijack'] ?? ['any:53'],
|
||||||
strictRoute: clash?.tun['strict-route'] ?? false,
|
strictRoute: clash?.tun['strict-route'] ?? false,
|
||||||
mtu: clash?.tun.mtu ?? 1500,
|
mtu: clash?.tun.mtu ?? 1500,
|
||||||
|
dnsMode: clash?.tun['dns-mode'] ?? '',
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
close: () => setOpen(false),
|
close: () => setOpen(false),
|
||||||
@@ -117,6 +120,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
: {}),
|
: {}),
|
||||||
'auto-detect-interface': values.autoDetectInterface,
|
'auto-detect-interface': values.autoDetectInterface,
|
||||||
'dns-hijack': values.dnsHijack[0] === '' ? [] : values.dnsHijack,
|
'dns-hijack': values.dnsHijack[0] === '' ? [] : values.dnsHijack,
|
||||||
|
'dns-mode': values.dnsMode,
|
||||||
'strict-route': values.strictRoute,
|
'strict-route': values.strictRoute,
|
||||||
mtu: values.mtu ?? 1500,
|
mtu: values.mtu ?? 1500,
|
||||||
}
|
}
|
||||||
@@ -163,6 +167,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
'auto-detect-interface': true,
|
'auto-detect-interface': true,
|
||||||
'dns-hijack': ['any:53'],
|
'dns-hijack': ['any:53'],
|
||||||
'route-exclude-address': [],
|
'route-exclude-address': [],
|
||||||
|
'dns-mode': '',
|
||||||
'strict-route': false,
|
'strict-route': false,
|
||||||
mtu: 1500,
|
mtu: 1500,
|
||||||
}
|
}
|
||||||
@@ -176,6 +181,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
dnsHijack: ['any:53'],
|
dnsHijack: ['any:53'],
|
||||||
strictRoute: false,
|
strictRoute: false,
|
||||||
mtu: 1500,
|
mtu: 1500,
|
||||||
|
dnsMode: '',
|
||||||
})
|
})
|
||||||
await patchClash({ tun })
|
await patchClash({ tun })
|
||||||
await mutateClash(
|
await mutateClash(
|
||||||
@@ -212,6 +218,25 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem sx={{ padding: '5px 2px' }}>
|
||||||
|
<ListItemText
|
||||||
|
primary={t('settings.modals.tun.fields.dnsMode')}
|
||||||
|
sx={{ maxWidth: 'fit-content' }}
|
||||||
|
/>
|
||||||
|
<TooltipIcon
|
||||||
|
title={t('settings.modals.tun.tooltips.dnsMode')}
|
||||||
|
sx={{ opacity: 0.7 }}
|
||||||
|
/>
|
||||||
|
<Box sx={{ marginLeft: 'auto' }}>
|
||||||
|
<DnsModeSwitch
|
||||||
|
value={values.dnsMode}
|
||||||
|
onChange={(value) => {
|
||||||
|
setValues((v) => ({ ...v, dnsMode: value }))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
<ListItem sx={{ padding: '5px 2px' }}>
|
<ListItem sx={{ padding: '5px 2px' }}>
|
||||||
<ListItemText primary={t('settings.modals.tun.fields.device')} />
|
<ListItemText primary={t('settings.modals.tun.fields.device')} />
|
||||||
<TextField
|
<TextField
|
||||||
|
|||||||
@@ -76,34 +76,28 @@ const proxyOptionToken = ({ memberIndex, member }: TunnelProxyOption) =>
|
|||||||
member.kind === 'node' ? member.node.recordId : member.ref.name
|
member.kind === 'node' ? member.node.recordId : member.ref.name
|
||||||
}`
|
}`
|
||||||
|
|
||||||
|
const buildInitialValues = (): TunnelFormValues => ({
|
||||||
|
localAddr: '',
|
||||||
|
localPort: '',
|
||||||
|
targetAddr: '',
|
||||||
|
targetPort: '',
|
||||||
|
network: 'tcp+udp',
|
||||||
|
group: '',
|
||||||
|
proxy: null,
|
||||||
|
})
|
||||||
|
|
||||||
export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
|
export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { clash, mutateClash, patchClash } = useClash()
|
const { clash, mutateClash, patchClash } = useClash()
|
||||||
|
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [expanded, setExpanded] = useState(false)
|
const [expanded, setExpanded] = useState(false)
|
||||||
const [values, setValues] = useState<TunnelFormValues>({
|
const [values, setValues] = useState<TunnelFormValues>(buildInitialValues)
|
||||||
localAddr: '',
|
|
||||||
localPort: '',
|
|
||||||
targetAddr: '',
|
|
||||||
targetPort: '',
|
|
||||||
network: 'tcp+udp',
|
|
||||||
group: '',
|
|
||||||
proxy: null,
|
|
||||||
})
|
|
||||||
const [draftTunnels, setDraftTunnels] = useState<TunnelEntry[]>([])
|
const [draftTunnels, setDraftTunnels] = useState<TunnelEntry[]>([])
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
open: () => {
|
open: () => {
|
||||||
setValues(() => ({
|
setValues(buildInitialValues())
|
||||||
localAddr: '',
|
|
||||||
localPort: '',
|
|
||||||
targetAddr: '',
|
|
||||||
targetPort: '',
|
|
||||||
network: 'tcp+udp',
|
|
||||||
group: '',
|
|
||||||
proxy: null,
|
|
||||||
}))
|
|
||||||
setDraftTunnels(() => clash?.tunnels ?? [])
|
setDraftTunnels(() => clash?.tunnels ?? [])
|
||||||
setOpen(true)
|
setOpen(true)
|
||||||
// 如果没有隧道,则自动展开
|
// 如果没有隧道,则自动展开
|
||||||
|
|||||||
@@ -50,21 +50,21 @@ export function WebUIViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
|
|
||||||
const handleAdd = useLockFn(async (value: string) => {
|
const handleAdd = useLockFn(async (value: string) => {
|
||||||
const newList = [...webUIList, value]
|
const newList = [...webUIList, value]
|
||||||
mutateVerge((old) => (old ? { ...old, web_ui_list: newList } : old), false)
|
mutateVerge((old) => (old ? { ...old, web_ui_list: newList } : old))
|
||||||
await patchVerge({ web_ui_list: newList })
|
await patchVerge({ web_ui_list: newList })
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleChange = useLockFn(async (index: number, value?: string) => {
|
const handleChange = useLockFn(async (index: number, value?: string) => {
|
||||||
const newList = [...webUIList]
|
const newList = [...webUIList]
|
||||||
newList[index] = value ?? ''
|
newList[index] = value ?? ''
|
||||||
mutateVerge((old) => (old ? { ...old, web_ui_list: newList } : old), false)
|
mutateVerge((old) => (old ? { ...old, web_ui_list: newList } : old))
|
||||||
await patchVerge({ web_ui_list: newList })
|
await patchVerge({ web_ui_list: newList })
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleDelete = useLockFn(async (index: number) => {
|
const handleDelete = useLockFn(async (index: number) => {
|
||||||
const newList = [...webUIList]
|
const newList = [...webUIList]
|
||||||
newList.splice(index, 1)
|
newList.splice(index, 1)
|
||||||
mutateVerge((old) => (old ? { ...old, web_ui_list: newList } : old), false)
|
mutateVerge((old) => (old ? { ...old, web_ui_list: newList } : old))
|
||||||
await patchVerge({ web_ui_list: newList })
|
await patchVerge({ web_ui_list: newList })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import React, { useRef } from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { DialogRef, Switch, TooltipIcon } from '@/components/base'
|
import { Switch, TooltipIcon } from '@/components/base'
|
||||||
import ProxyControlSwitches from '@/components/shared/proxy-control-switches'
|
import ProxyControlSwitches from '@/components/shared/proxy-control-switches'
|
||||||
import { useVerge } from '@/hooks/use-verge'
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
|
|
||||||
import { GuardState } from './mods/guard-state'
|
import { GuardState } from './mods/guard-state'
|
||||||
import { SettingList, SettingItem } from './mods/setting-comp'
|
import { SettingList, SettingItem } from './mods/setting-comp'
|
||||||
import { SysproxyViewer } from './mods/sysproxy-viewer'
|
|
||||||
import { TunViewer } from './mods/tun-viewer'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onError?: (err: Error) => void
|
onError?: (err: Error) => void
|
||||||
@@ -21,22 +19,16 @@ const SettingSystem = ({ onError }: Props) => {
|
|||||||
|
|
||||||
const { enable_auto_launch, enable_silent_start } = verge ?? {}
|
const { enable_auto_launch, enable_silent_start } = verge ?? {}
|
||||||
|
|
||||||
const sysproxyRef = useRef<DialogRef>(null)
|
|
||||||
const tunRef = useRef<DialogRef>(null)
|
|
||||||
|
|
||||||
const onSwitchFormat = (
|
const onSwitchFormat = (
|
||||||
_e: React.ChangeEvent<HTMLInputElement>,
|
_e: React.ChangeEvent<HTMLInputElement>,
|
||||||
value: boolean,
|
value: boolean,
|
||||||
) => value
|
) => value
|
||||||
const onChangeData = (patch: Partial<IVergeConfig>) => {
|
const onChangeData = (patch: Partial<IVergeConfig>) => {
|
||||||
mutateVerge({ ...verge, ...patch }, false)
|
mutateVerge({ ...verge, ...patch })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingList title={t('settings.sections.system.title')}>
|
<SettingList title={t('settings.sections.system.title')}>
|
||||||
<SysproxyViewer ref={sysproxyRef} />
|
|
||||||
<TunViewer ref={tunRef} />
|
|
||||||
|
|
||||||
<ProxyControlSwitches
|
<ProxyControlSwitches
|
||||||
label={t('settings.sections.system.toggles.tunMode')}
|
label={t('settings.sections.system.toggles.tunMode')}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
@@ -58,8 +50,6 @@ const SettingSystem = ({ onError }: Props) => {
|
|||||||
}}
|
}}
|
||||||
onGuard={async (e) => {
|
onGuard={async (e) => {
|
||||||
try {
|
try {
|
||||||
// 先触发UI更新立即看到反馈
|
|
||||||
onChangeData({ enable_auto_launch: e })
|
|
||||||
await patchVerge({ enable_auto_launch: e })
|
await patchVerge({ enable_auto_launch: e })
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ const SettingVergeBasic = ({ onError }: Props) => {
|
|||||||
const backupRef = useRef<DialogRef>(null)
|
const backupRef = useRef<DialogRef>(null)
|
||||||
|
|
||||||
const onChangeData = (patch: any) => {
|
const onChangeData = (patch: any) => {
|
||||||
mutateVerge({ ...verge, ...patch }, false)
|
mutateVerge({ ...verge, ...patch })
|
||||||
}
|
}
|
||||||
|
|
||||||
const onCopyClashEnv = useCallback(async () => {
|
const onCopyClashEnv = useCallback(async () => {
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ const ProxyControlSwitches = ({
|
|||||||
})
|
})
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
mutateVerge({ ...verge, enable_tun_mode: value }, false)
|
mutateVerge({ ...verge, enable_tun_mode: value })
|
||||||
await patchVerge({ enable_tun_mode: value })
|
await patchVerge({ enable_tun_mode: value })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,8 +229,8 @@ const ProxyControlSwitches = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SysproxyViewer ref={sysproxyRef} />
|
{isSystemProxyMode && <SysproxyViewer ref={sysproxyRef} />}
|
||||||
<TunViewer ref={tunRef} />
|
{isTunMode && <TunViewer ref={tunRef} />}
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,301 +0,0 @@
|
|||||||
import {
|
|
||||||
ErrorOutlineRounded,
|
|
||||||
RefreshRounded,
|
|
||||||
BugReportRounded,
|
|
||||||
} from '@mui/icons-material'
|
|
||||||
import { Box, Typography, Button, Alert, Collapse } from '@mui/material'
|
|
||||||
import React, { Component, ErrorInfo, ReactNode } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
children: ReactNode
|
|
||||||
fallbackComponent?: ReactNode
|
|
||||||
onError?: (error: Error, errorInfo: ErrorInfo) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
hasError: boolean
|
|
||||||
error: Error | null
|
|
||||||
errorInfo: ErrorInfo | null
|
|
||||||
showDetails: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TrafficErrorBoundary extends Component<Props, State> {
|
|
||||||
private retryCount = 0
|
|
||||||
private maxRetries = 3
|
|
||||||
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props)
|
|
||||||
this.state = {
|
|
||||||
hasError: false,
|
|
||||||
error: null,
|
|
||||||
errorInfo: null,
|
|
||||||
showDetails: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static getDerivedStateFromError(error: Error): Partial<State> {
|
|
||||||
return { hasError: true, error }
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
||||||
console.error('[TrafficErrorBoundary] 捕获到组件错误:', error, errorInfo)
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
error,
|
|
||||||
errorInfo,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (this.props.onError) {
|
|
||||||
this.props.onError(error, errorInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.reportError(error, errorInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
private reportError = (error: Error, errorInfo: ErrorInfo) => {
|
|
||||||
const errorReport = {
|
|
||||||
message: error.message,
|
|
||||||
stack: error.stack,
|
|
||||||
componentStack: errorInfo.componentStack,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
userAgent: navigator.userAgent,
|
|
||||||
url: window.location.href,
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('[TrafficErrorBoundary] 错误报告:', errorReport)
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleRetry = () => {
|
|
||||||
if (this.retryCount < this.maxRetries) {
|
|
||||||
this.retryCount++
|
|
||||||
console.log(
|
|
||||||
`[TrafficErrorBoundary] 尝试重试 (${this.retryCount}/${this.maxRetries})`,
|
|
||||||
)
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
hasError: false,
|
|
||||||
error: null,
|
|
||||||
errorInfo: null,
|
|
||||||
showDetails: false,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
console.warn('[TrafficErrorBoundary] 已达到最大重试次数')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleRefresh = () => {
|
|
||||||
window.location.reload()
|
|
||||||
}
|
|
||||||
|
|
||||||
private toggleDetails = () => {
|
|
||||||
this.setState((prev) => ({ showDetails: !prev.showDetails }))
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.state.hasError) {
|
|
||||||
if (this.props.fallbackComponent) {
|
|
||||||
return this.props.fallbackComponent
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TrafficErrorFallback
|
|
||||||
error={this.state.error}
|
|
||||||
errorInfo={this.state.errorInfo}
|
|
||||||
showDetails={this.state.showDetails}
|
|
||||||
canRetry={this.retryCount < this.maxRetries}
|
|
||||||
retryCount={this.retryCount}
|
|
||||||
maxRetries={this.maxRetries}
|
|
||||||
onRetry={this.handleRetry}
|
|
||||||
onRefresh={this.handleRefresh}
|
|
||||||
onToggleDetails={this.toggleDetails}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.props.children
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TrafficErrorFallbackProps {
|
|
||||||
error: Error | null
|
|
||||||
errorInfo: ErrorInfo | null
|
|
||||||
showDetails: boolean
|
|
||||||
canRetry: boolean
|
|
||||||
retryCount: number
|
|
||||||
maxRetries: number
|
|
||||||
onRetry: () => void
|
|
||||||
onRefresh: () => void
|
|
||||||
onToggleDetails: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const TrafficErrorFallback: React.FC<TrafficErrorFallbackProps> = ({
|
|
||||||
error,
|
|
||||||
errorInfo,
|
|
||||||
showDetails,
|
|
||||||
canRetry,
|
|
||||||
retryCount,
|
|
||||||
maxRetries,
|
|
||||||
onRetry,
|
|
||||||
onRefresh,
|
|
||||||
onToggleDetails,
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
p: 2,
|
|
||||||
minHeight: 200,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
border: '1px dashed',
|
|
||||||
borderColor: 'error.main',
|
|
||||||
borderRadius: 2,
|
|
||||||
bgcolor: 'error.light',
|
|
||||||
color: 'error.contrastText',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ErrorOutlineRounded sx={{ fontSize: 48, mb: 2, color: 'error.main' }} />
|
|
||||||
|
|
||||||
<Typography variant="h6" gutterBottom>
|
|
||||||
{t('shared.feedback.errors.trafficStats')}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{ mb: 2, textAlign: 'center' }}
|
|
||||||
>
|
|
||||||
{t('shared.feedback.errors.trafficStatsDescription')}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Alert severity="error" sx={{ mb: 2, maxWidth: 400 }}>
|
|
||||||
<Typography variant="body2">
|
|
||||||
<strong>{t('shared.feedback.errors.label')}:</strong>{' '}
|
|
||||||
{error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: t('shared.feedback.errors.unknown')}
|
|
||||||
</Typography>
|
|
||||||
{retryCount > 0 && (
|
|
||||||
<Typography variant="caption" sx={{ mt: 1, display: 'block' }}>
|
|
||||||
{t('shared.labels.retryAttempts')}: {retryCount}/{maxRetries}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
|
|
||||||
{canRetry && (
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
startIcon={<RefreshRounded />}
|
|
||||||
onClick={onRetry}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{t('shared.actions.retry')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button variant="outlined" onClick={onRefresh} size="small">
|
|
||||||
{t('shared.actions.refreshPage')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="text"
|
|
||||||
startIcon={<BugReportRounded />}
|
|
||||||
onClick={onToggleDetails}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{showDetails
|
|
||||||
? t('shared.actions.hideDetails')
|
|
||||||
: t('shared.actions.showDetails')}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Collapse in={showDetails} sx={{ width: '100%', maxWidth: 600 }}>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
p: 2,
|
|
||||||
bgcolor: 'background.paper',
|
|
||||||
borderRadius: 1,
|
|
||||||
border: '1px solid',
|
|
||||||
borderColor: 'divider',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="subtitle2" gutterBottom>
|
|
||||||
{t('shared.feedback.errors.details')}:
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
component="pre"
|
|
||||||
sx={{
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
fontSize: '0.75rem',
|
|
||||||
color: 'text.secondary',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{error?.stack}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{errorInfo?.componentStack && (
|
|
||||||
<>
|
|
||||||
<Typography variant="subtitle2" gutterBottom sx={{ mt: 2 }}>
|
|
||||||
{t('shared.feedback.errors.componentStack')}:
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
component="pre"
|
|
||||||
sx={{
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
fontSize: '0.75rem',
|
|
||||||
color: 'text.secondary',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{errorInfo.componentStack}
|
|
||||||
</Typography>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Collapse>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LightweightTrafficErrorBoundary: React.FC<{
|
|
||||||
children: ReactNode
|
|
||||||
}> = ({ children }) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TrafficErrorBoundary
|
|
||||||
fallbackComponent={
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
p: 1,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
minHeight: 60,
|
|
||||||
bgcolor: 'error.light',
|
|
||||||
borderRadius: 1,
|
|
||||||
color: 'error.contrastText',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ErrorOutlineRounded sx={{ mr: 1, fontSize: 20 }} />
|
|
||||||
<Typography variant="caption">
|
|
||||||
{t('shared.feedback.errors.trafficUnavailable')}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</TrafficErrorBoundary>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
import { useMemo } from 'react'
|
|
||||||
|
|
||||||
import { downloadIconCache } from '@/services/cmds'
|
import { downloadIconCache } from '@/services/cmds'
|
||||||
import { useQuery } from '@/services/query-client'
|
import { useQuery } from '@/services/query-client'
|
||||||
@@ -23,9 +22,8 @@ export const useIconCache = ({
|
|||||||
const iconValue = icon?.trim() ?? ''
|
const iconValue = icon?.trim() ?? ''
|
||||||
const cacheKeyValue = cacheKey?.trim() ?? ''
|
const cacheKeyValue = cacheKey?.trim() ?? ''
|
||||||
|
|
||||||
const isEnabled = useMemo(() => {
|
const isEnabled =
|
||||||
return enabled && iconValue.startsWith('http') && cacheKeyValue !== ''
|
enabled && iconValue.startsWith('http') && cacheKeyValue !== ''
|
||||||
}, [enabled, iconValue, cacheKeyValue])
|
|
||||||
|
|
||||||
const { data } = useQuery({
|
const { data } = useQuery({
|
||||||
queryKey: ['icon-cache', iconValue, cacheKeyValue],
|
queryKey: ['icon-cache', iconValue, cacheKeyValue],
|
||||||
|
|||||||
@@ -54,8 +54,7 @@ export const useLogData = () => {
|
|||||||
const hasLoadedInitialLogsRef = useRef(false)
|
const hasLoadedInitialLogsRef = useRef(false)
|
||||||
|
|
||||||
const { response, refresh, setData } = useMihomoWsSubscription<ILogItem[]>({
|
const { response, refresh, setData } = useMihomoWsSubscription<ILogItem[]>({
|
||||||
storageKey: 'mihomo_logs_date',
|
subscriptionKey: enableLog ? 'getClashLog' : null,
|
||||||
buildSubscriptKey: (date) => (enableLog ? `getClashLog-${date}` : null),
|
|
||||||
fallbackData: [],
|
fallbackData: [],
|
||||||
connect: () => MihomoWebSocket.connect_logs(logLevel),
|
connect: () => MihomoWebSocket.connect_logs(logLevel),
|
||||||
setupHandlers: ({ next, scheduleReconnect, isMounted }) => {
|
setupHandlers: ({ next, scheduleReconnect, isMounted }) => {
|
||||||
@@ -77,14 +76,13 @@ export const useLogData = () => {
|
|||||||
}
|
}
|
||||||
const pendingLogs = buffer.splice(0, buffer.length)
|
const pendingLogs = buffer.splice(0, buffer.length)
|
||||||
flushTimeStr = null
|
flushTimeStr = null
|
||||||
next(null, (current) => appendLogs(current, pendingLogs))
|
next((current) => appendLogs(current, pendingLogs))
|
||||||
flushTimer = null
|
flushTimer = null
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handleMessage: (data) => {
|
handleMessage: (data) => {
|
||||||
if (data.startsWith('Websocket error')) {
|
if (data.startsWith('Websocket error')) {
|
||||||
next(data)
|
|
||||||
void scheduleReconnect()
|
void scheduleReconnect()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -108,9 +106,7 @@ export const useLogData = () => {
|
|||||||
if (!flushTimer) {
|
if (!flushTimer) {
|
||||||
flushTimer = setTimeout(flush, FLUSH_DELAY_MS)
|
flushTimer = setTimeout(flush, FLUSH_DELAY_MS)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {}
|
||||||
next(error)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async onConnected() {
|
async onConnected() {
|
||||||
if (hasLoadedInitialLogsRef.current) {
|
if (hasLoadedInitialLogsRef.current) {
|
||||||
@@ -119,7 +115,7 @@ export const useLogData = () => {
|
|||||||
const logs = await getClashLogs()
|
const logs = await getClashLogs()
|
||||||
hasLoadedInitialLogsRef.current = true
|
hasLoadedInitialLogsRef.current = true
|
||||||
if (isMounted()) {
|
if (isMounted()) {
|
||||||
next(null, (current) => {
|
next((current) => {
|
||||||
if (!current || current.length === 0) {
|
if (!current || current.length === 0) {
|
||||||
return clampLogs(filterLogsByLevel(logs, allowedTypes))
|
return clampLogs(filterLogsByLevel(logs, allowedTypes))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,15 +33,13 @@ export const useMemoryData = (options?: { enabled?: boolean }) => {
|
|||||||
const enabled = options?.enabled ?? true
|
const enabled = options?.enabled ?? true
|
||||||
|
|
||||||
const { response, refresh } = useMihomoWsSubscription<IMemoryUsageItem>({
|
const { response, refresh } = useMihomoWsSubscription<IMemoryUsageItem>({
|
||||||
storageKey: 'mihomo_memory_date',
|
subscriptionKey: enabled ? 'getClashMemory' : null,
|
||||||
buildSubscriptKey: (date) => (enabled ? `getClashMemory-${date}` : null),
|
|
||||||
fallbackData: FALLBACK_MEMORY_USAGE,
|
fallbackData: FALLBACK_MEMORY_USAGE,
|
||||||
connect: () => MihomoWebSocket.connect_memory(),
|
connect: () => MihomoWebSocket.connect_memory(),
|
||||||
throttleMs: 500,
|
throttleMs: 500,
|
||||||
setupHandlers: ({ next, scheduleReconnect }) => ({
|
setupHandlers: ({ next, scheduleReconnect }) => ({
|
||||||
handleMessage: (data) => {
|
handleMessage: (data) => {
|
||||||
if (data.startsWith('Websocket error')) {
|
if (data.startsWith('Websocket error')) {
|
||||||
next(data, FALLBACK_MEMORY_USAGE)
|
|
||||||
void scheduleReconnect()
|
void scheduleReconnect()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -51,10 +49,8 @@ export const useMemoryData = (options?: { enabled?: boolean }) => {
|
|||||||
if (shouldSkipDuplicateMemory(parsed)) {
|
if (shouldSkipDuplicateMemory(parsed)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
next(null, parsed)
|
next(parsed)
|
||||||
} catch (error) {
|
} catch {}
|
||||||
next(error, FALLBACK_MEMORY_USAGE)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
import { useLocalStorage } from 'foxact/use-local-storage'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { useCallback, useEffect, useRef } from 'react'
|
|
||||||
import { type Message, type MihomoWebSocket } from 'tauri-plugin-mihomo-api'
|
import { type Message, type MihomoWebSocket } from 'tauri-plugin-mihomo-api'
|
||||||
|
|
||||||
import {
|
import { setCacheData, useQuery } from '@/services/query-client'
|
||||||
removeCacheData,
|
|
||||||
setCacheData,
|
|
||||||
useQuery,
|
|
||||||
} from '@/services/query-client'
|
|
||||||
|
|
||||||
const RECONNECT_DELAY_MS = 1000
|
const RECONNECT_DELAY_MS = 1000
|
||||||
|
|
||||||
@@ -31,7 +26,6 @@ interface SharedSubscriptionEntry {
|
|||||||
|
|
||||||
const sharedSubscriptions = new Map<string, SharedSubscriptionEntry>()
|
const sharedSubscriptions = new Map<string, SharedSubscriptionEntry>()
|
||||||
const subscriptionSnapshots = new Map<string, unknown>()
|
const subscriptionSnapshots = new Map<string, unknown>()
|
||||||
const initialSubscriptionDate = Date.now()
|
|
||||||
|
|
||||||
const getSubscriptionSnapshot = <T>(key: string) =>
|
const getSubscriptionSnapshot = <T>(key: string) =>
|
||||||
subscriptionSnapshots.get(key) as T | undefined
|
subscriptionSnapshots.get(key) as T | undefined
|
||||||
@@ -136,15 +130,13 @@ const createSharedSubscriptionEntry = (
|
|||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Updater<T> = (current?: T) => T
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mirrors SWR's MutatorCallback: consumers can pass either a plain value or a
|
* Pass either a plain value or a functional updater; the updater is resolved
|
||||||
* functional updater `(current?: T) => T`. The functional form is resolved
|
* against the current subscription snapshot before the cache is written.
|
||||||
* against the current subscription snapshot before updating SWR.
|
|
||||||
*/
|
*/
|
||||||
type NextFn<T> = (
|
type NextFn<T> = (data: T | Updater<T>) => void
|
||||||
error?: any,
|
|
||||||
data?: T | ((current?: T) => T | undefined),
|
|
||||||
) => void
|
|
||||||
|
|
||||||
interface HandlerContext<T> {
|
interface HandlerContext<T> {
|
||||||
next: NextFn<T>
|
next: NextFn<T>
|
||||||
@@ -159,8 +151,7 @@ interface HandlerResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface UseMihomoWsSubscriptionOptions<T> {
|
interface UseMihomoWsSubscriptionOptions<T> {
|
||||||
storageKey: string
|
subscriptionKey: string | null
|
||||||
buildSubscriptKey: (date: number) => string | null
|
|
||||||
fallbackData: T
|
fallbackData: T
|
||||||
connect: () => Promise<MihomoWebSocket>
|
connect: () => Promise<MihomoWebSocket>
|
||||||
/**
|
/**
|
||||||
@@ -178,18 +169,12 @@ interface UseMihomoWsSubscriptionOptions<T> {
|
|||||||
export const useMihomoWsSubscription = <T>(
|
export const useMihomoWsSubscription = <T>(
|
||||||
options: UseMihomoWsSubscriptionOptions<T>,
|
options: UseMihomoWsSubscriptionOptions<T>,
|
||||||
) => {
|
) => {
|
||||||
const {
|
const { subscriptionKey, fallbackData, connect, throttleMs, setupHandlers } =
|
||||||
storageKey,
|
options
|
||||||
buildSubscriptKey,
|
|
||||||
fallbackData,
|
|
||||||
connect,
|
|
||||||
throttleMs,
|
|
||||||
setupHandlers,
|
|
||||||
} = options
|
|
||||||
|
|
||||||
const [date, setDate] = useLocalStorage(storageKey, initialSubscriptionDate)
|
const subscriptionCacheKey = subscriptionKey
|
||||||
const subscriptKey = buildSubscriptKey(date)
|
? `$sub$${subscriptionKey}`
|
||||||
const subscriptionCacheKey = subscriptKey ? `$sub$${subscriptKey}` : null
|
: null
|
||||||
const lastSubscriptionCacheKeyRef = useRef<string | null>(null)
|
const lastSubscriptionCacheKeyRef = useRef<string | null>(null)
|
||||||
if (subscriptionCacheKey) {
|
if (subscriptionCacheKey) {
|
||||||
lastSubscriptionCacheKeyRef.current = subscriptionCacheKey
|
lastSubscriptionCacheKeyRef.current = subscriptionCacheKey
|
||||||
@@ -197,20 +182,9 @@ export const useMihomoWsSubscription = <T>(
|
|||||||
const responseCacheKey =
|
const responseCacheKey =
|
||||||
subscriptionCacheKey ?? lastSubscriptionCacheKeyRef.current
|
subscriptionCacheKey ?? lastSubscriptionCacheKeyRef.current
|
||||||
|
|
||||||
const resolveNextData = useCallback(
|
// Bumping the epoch re-runs the subscription effect, rebuilding the shared
|
||||||
(
|
// entry (and its captured `connect`/handler closures) under the same key.
|
||||||
data: T | ((current?: T) => T | undefined) | undefined,
|
const [refreshEpoch, setRefreshEpoch] = useState(0)
|
||||||
cacheKey: string,
|
|
||||||
): T => {
|
|
||||||
if (typeof data === 'function') {
|
|
||||||
const updater = data as (current?: T) => T | undefined
|
|
||||||
const current = getSubscriptionSnapshot<T>(cacheKey)
|
|
||||||
return updater(current) ?? fallbackData
|
|
||||||
}
|
|
||||||
return data ?? fallbackData
|
|
||||||
},
|
|
||||||
[fallbackData],
|
|
||||||
)
|
|
||||||
|
|
||||||
const response = useQuery<T>({
|
const response = useQuery<T>({
|
||||||
queryKey: responseCacheKey ? [responseCacheKey] : ['$sub$__disabled__'],
|
queryKey: responseCacheKey ? [responseCacheKey] : ['$sub$__disabled__'],
|
||||||
@@ -238,44 +212,35 @@ export const useMihomoWsSubscription = <T>(
|
|||||||
let throttleCleanup: (() => void) | undefined
|
let throttleCleanup: (() => void) | undefined
|
||||||
let wrappedNext: NextFn<T>
|
let wrappedNext: NextFn<T>
|
||||||
|
|
||||||
const baseNext: NextFn<T> = (error, data) => {
|
const baseNext: NextFn<T> = (data) => {
|
||||||
if (error !== undefined && error !== null) {
|
const resolved =
|
||||||
return
|
typeof data === 'function'
|
||||||
}
|
? (data as Updater<T>)(
|
||||||
if (data === undefined) return
|
getSubscriptionSnapshot<T>(subscriptionCacheKey),
|
||||||
const resolved = resolveNextData(data, subscriptionCacheKey)
|
)
|
||||||
|
: (data as T)
|
||||||
writeSubscriptionSnapshot(subscriptionCacheKey, resolved)
|
writeSubscriptionSnapshot(subscriptionCacheKey, resolved)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (throttleMs && throttleMs > 0) {
|
if (throttleMs && throttleMs > 0) {
|
||||||
let pendingData: T | ((current?: T) => T | undefined) | undefined
|
let pendingData: T | Updater<T> | undefined
|
||||||
let hasPending = false
|
|
||||||
let timerId: ReturnType<typeof setTimeout> | null = null
|
let timerId: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
const flush = () => {
|
const flush = () => {
|
||||||
timerId = null
|
timerId = null
|
||||||
if (hasPending) {
|
if (pendingData !== undefined) {
|
||||||
const data = pendingData
|
const data = pendingData
|
||||||
pendingData = undefined
|
pendingData = undefined
|
||||||
hasPending = false
|
baseNext(data)
|
||||||
baseNext(undefined, data)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wrappedNext = (
|
wrappedNext = (data) => {
|
||||||
error?: any,
|
|
||||||
data?: T | ((current?: T) => T | undefined),
|
|
||||||
) => {
|
|
||||||
if (error !== undefined && error !== null) {
|
|
||||||
baseNext(error, data)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!timerId) {
|
if (!timerId) {
|
||||||
baseNext(undefined, data)
|
baseNext(data)
|
||||||
timerId = setTimeout(flush, throttleMs)
|
timerId = setTimeout(flush, throttleMs)
|
||||||
} else {
|
} else {
|
||||||
pendingData = data
|
pendingData = data
|
||||||
hasPending = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,15 +306,14 @@ export const useMihomoWsSubscription = <T>(
|
|||||||
}
|
}
|
||||||
// eslint-disable-next-line react-compiler/react-compiler
|
// eslint-disable-next-line react-compiler/react-compiler
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps, @eslint-react/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps, @eslint-react/exhaustive-deps
|
||||||
}, [subscriptionCacheKey])
|
}, [subscriptionCacheKey, refreshEpoch])
|
||||||
|
|
||||||
const refresh = useCallback(() => {
|
const refresh = useCallback(() => {
|
||||||
if (subscriptionCacheKey) {
|
if (subscriptionCacheKey) {
|
||||||
subscriptionSnapshots.delete(subscriptionCacheKey)
|
writeSubscriptionSnapshot(subscriptionCacheKey, fallbackData)
|
||||||
void removeCacheData([subscriptionCacheKey])
|
|
||||||
}
|
}
|
||||||
setDate(Date.now())
|
setRefreshEpoch((epoch) => epoch + 1)
|
||||||
}, [subscriptionCacheKey, setDate])
|
}, [subscriptionCacheKey, fallbackData])
|
||||||
|
|
||||||
const setData = useCallback(
|
const setData = useCallback(
|
||||||
(data: T) => {
|
(data: T) => {
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ it('applies and records independent group selections while another group is pend
|
|||||||
const secondSuccess = vi.fn()
|
const secondSuccess = vi.fn()
|
||||||
const first = useProxySelection({ onSuccess: firstSuccess })
|
const first = useProxySelection({ onSuccess: firstSuccess })
|
||||||
const second = useProxySelection({ onSuccess: secondSuccess })
|
const second = useProxySelection({ onSuccess: secondSuccess })
|
||||||
first.changeProxy('Group A', 'Node A')
|
first.select('Group A', 'Node A')
|
||||||
second.changeProxy('Group B', 'Node B')
|
second.select('Group B', 'Node B')
|
||||||
await vi.waitFor(() =>
|
await vi.waitFor(() =>
|
||||||
expect(record).toHaveBeenCalledWith('Group B', 'Node B'),
|
expect(record).toHaveBeenCalledWith('Group B', 'Node B'),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ const cleanupConnections = async (previousProxy: string) => {
|
|||||||
interface ProxySelectionOptions {
|
interface ProxySelectionOptions {
|
||||||
onSuccess?: () => void
|
onSuccess?: () => void
|
||||||
onError?: (error: any) => void
|
onError?: (error: any) => void
|
||||||
enableConnectionCleanup?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProxyChangeRequest {
|
interface ProxyChangeRequest {
|
||||||
@@ -53,7 +52,7 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
|
|||||||
const pendingRequestRef = useRef<ProxyChangeRequest | null>(null)
|
const pendingRequestRef = useRef<ProxyChangeRequest | null>(null)
|
||||||
const isProcessingRef = useRef(false)
|
const isProcessingRef = useRef(false)
|
||||||
|
|
||||||
const { onSuccess, onError, enableConnectionCleanup = true } = options
|
const { onSuccess, onError } = options
|
||||||
|
|
||||||
const autoCloseConnection = verge?.auto_close_connection ?? false
|
const autoCloseConnection = verge?.auto_close_connection ?? false
|
||||||
|
|
||||||
@@ -98,7 +97,7 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enableConnectionCleanup && autoCloseConnection && previousProxy) {
|
if (autoCloseConnection && previousProxy) {
|
||||||
void cleanupConnections(previousProxy)
|
void cleanupConnections(previousProxy)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -112,7 +111,6 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
|
|||||||
[
|
[
|
||||||
autoCloseConnection,
|
autoCloseConnection,
|
||||||
forgetSelection,
|
forgetSelection,
|
||||||
enableConnectionCleanup,
|
|
||||||
onError,
|
onError,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
recordSelection,
|
recordSelection,
|
||||||
@@ -138,7 +136,7 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
|
|||||||
}
|
}
|
||||||
}, [executeChange])
|
}, [executeChange])
|
||||||
|
|
||||||
const changeProxy = useCallback(
|
const select = useCallback(
|
||||||
(
|
(
|
||||||
groupName: string,
|
groupName: string,
|
||||||
proxyName: string,
|
proxyName: string,
|
||||||
@@ -156,27 +154,5 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
|
|||||||
[flushChangeQueue],
|
[flushChangeQueue],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleSelectChange = useCallback(
|
return { select }
|
||||||
(groupName: string, previousProxy?: string, fixed?: string) =>
|
|
||||||
(event: { target: { value: string } }) => {
|
|
||||||
changeProxy(groupName, event.target.value, previousProxy, fixed)
|
|
||||||
},
|
|
||||||
[changeProxy],
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleProxyGroupChange = useCallback(
|
|
||||||
(
|
|
||||||
group: { name: string; now?: string | null; fixed?: string | null },
|
|
||||||
proxy: { name: string },
|
|
||||||
) => {
|
|
||||||
changeProxy(group.name, proxy.name, group.now!, group.fixed!)
|
|
||||||
},
|
|
||||||
[changeProxy],
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
changeProxy,
|
|
||||||
handleSelectChange,
|
|
||||||
handleProxyGroupChange,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,9 +52,8 @@ export const useSystemProxyState = () => {
|
|||||||
const toggleSystemProxy = async (enabled: boolean) => {
|
const toggleSystemProxy = async (enabled: boolean) => {
|
||||||
// Roll failed optimistic writes back to the latest confirmed state.
|
// Roll failed optimistic writes back to the latest confirmed state.
|
||||||
let confirmed = verge?.enable_system_proxy ?? false
|
let confirmed = verge?.enable_system_proxy ?? false
|
||||||
mutateVerge(
|
mutateVerge((prev) =>
|
||||||
(prev) => (prev ? { ...prev, enable_system_proxy: enabled } : prev),
|
prev ? { ...prev, enable_system_proxy: enabled } : prev,
|
||||||
false,
|
|
||||||
)
|
)
|
||||||
pendingRef.current = enabled
|
pendingRef.current = enabled
|
||||||
|
|
||||||
@@ -78,9 +77,8 @@ export const useSystemProxyState = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
mutateVerge(
|
mutateVerge((prev) =>
|
||||||
(prev) => (prev ? { ...prev, enable_system_proxy: confirmed } : prev),
|
prev ? { ...prev, enable_system_proxy: confirmed } : prev,
|
||||||
false,
|
|
||||||
)
|
)
|
||||||
// Queued requests were based on a state that never landed.
|
// Queued requests were based on a state that never landed.
|
||||||
pendingRef.current = null
|
pendingRef.current = null
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ export function useAppUptime() {
|
|||||||
const { data: uptime = 0 } = useQuery({
|
const { data: uptime = 0 } = useQuery({
|
||||||
queryKey: appUptimeQueryKey,
|
queryKey: appUptimeQueryKey,
|
||||||
queryFn: getAppUptime,
|
queryFn: getAppUptime,
|
||||||
staleTime: 5000,
|
// The wrapper maps staleTime to SWR's dedupingInterval, which drops
|
||||||
|
// refetchInterval ticks while it exceeds the interval.
|
||||||
refetchInterval: 3000,
|
refetchInterval: 3000,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
|
|||||||
@@ -32,15 +32,13 @@ export const useTrafficData = (options?: { enabled?: boolean }) => {
|
|||||||
graphData: { appendData },
|
graphData: { appendData },
|
||||||
} = useTrafficMonitorEnhanced({ subscribe: false, enabled })
|
} = useTrafficMonitorEnhanced({ subscribe: false, enabled })
|
||||||
const { response, refresh } = useMihomoWsSubscription<ITrafficItem>({
|
const { response, refresh } = useMihomoWsSubscription<ITrafficItem>({
|
||||||
storageKey: 'mihomo_traffic_date',
|
subscriptionKey: enabled ? 'getClashTraffic' : null,
|
||||||
buildSubscriptKey: (date) => (enabled ? `getClashTraffic-${date}` : null),
|
|
||||||
fallbackData: FALLBACK_TRAFFIC,
|
fallbackData: FALLBACK_TRAFFIC,
|
||||||
connect: () => MihomoWebSocket.connect_traffic(),
|
connect: () => MihomoWebSocket.connect_traffic(),
|
||||||
throttleMs: 200,
|
throttleMs: 200,
|
||||||
setupHandlers: ({ next, scheduleReconnect }) => ({
|
setupHandlers: ({ next, scheduleReconnect }) => ({
|
||||||
handleMessage: (data) => {
|
handleMessage: (data) => {
|
||||||
if (data.startsWith('Websocket error')) {
|
if (data.startsWith('Websocket error')) {
|
||||||
next(data, FALLBACK_TRAFFIC)
|
|
||||||
void scheduleReconnect()
|
void scheduleReconnect()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -51,10 +49,8 @@ export const useTrafficData = (options?: { enabled?: boolean }) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
appendData(parsed)
|
appendData(parsed)
|
||||||
next(null, parsed)
|
next(parsed)
|
||||||
} catch (error) {
|
} catch {}
|
||||||
next(error, FALLBACK_TRAFFIC)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Traffic } from 'tauri-plugin-mihomo-api'
|
|||||||
|
|
||||||
import { useVisibility } from '@/hooks/use-visibility'
|
import { useVisibility } from '@/hooks/use-visibility'
|
||||||
import { debugLog } from '@/utils/debug'
|
import { debugLog } from '@/utils/debug'
|
||||||
import { TrafficDataSampler, isSameTrafficData } from '@/utils/traffic-sampler'
|
|
||||||
|
|
||||||
// 引用计数管理器
|
// 引用计数管理器
|
||||||
class ReferenceCounter {
|
class ReferenceCounter {
|
||||||
@@ -32,119 +31,11 @@ const WORKER_CONFIG = {
|
|||||||
defaultRangeMinutes: 10,
|
defaultRangeMinutes: 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
class InlineTrafficMonitor {
|
|
||||||
private config = { ...WORKER_CONFIG }
|
|
||||||
private sampler = new TrafficDataSampler(this.config)
|
|
||||||
private throttleTimer: ReturnType<typeof setTimeout> | null = null
|
|
||||||
private agingTimer: ReturnType<typeof setInterval> | null = null
|
|
||||||
private currentRange = this.config.defaultRangeMinutes
|
|
||||||
private lastTimestamp: number | undefined
|
|
||||||
private lastBroadcast: ITrafficDataPoint[] = []
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private emit: (snapshot: ITrafficWorkerSnapshotMessage) => void,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
start(rangeMinutes?: number) {
|
|
||||||
this.currentRange = rangeMinutes ?? this.currentRange
|
|
||||||
this.handle({
|
|
||||||
type: 'init',
|
|
||||||
config: {
|
|
||||||
...this.config,
|
|
||||||
defaultRangeMinutes: this.currentRange,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
if (this.throttleTimer !== null) {
|
|
||||||
clearTimeout(this.throttleTimer)
|
|
||||||
this.throttleTimer = null
|
|
||||||
}
|
|
||||||
if (this.agingTimer !== null) {
|
|
||||||
clearInterval(this.agingTimer)
|
|
||||||
this.agingTimer = null
|
|
||||||
}
|
|
||||||
this.sampler.clear()
|
|
||||||
this.lastTimestamp = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
handle(message: TrafficWorkerRequestMessage) {
|
|
||||||
switch (message.type) {
|
|
||||||
case 'init': {
|
|
||||||
this.config = { ...message.config }
|
|
||||||
this.sampler = new TrafficDataSampler(this.config)
|
|
||||||
this.currentRange = message.config.defaultRangeMinutes
|
|
||||||
// Re-broadcast when points age out of the range, not on every tick
|
|
||||||
this.agingTimer = setInterval(() => {
|
|
||||||
if (this.throttleTimer !== null) return
|
|
||||||
const slice = this.sampler.getDataForTimeRange(this.currentRange)
|
|
||||||
if (!isSameTrafficData(this.lastBroadcast, slice)) {
|
|
||||||
this.emitSnapshot()
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
this.emitSnapshot()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'append': {
|
|
||||||
const timestamp = message.payload.timestamp ?? Date.now()
|
|
||||||
const dataPoint: ITrafficDataPoint = {
|
|
||||||
up: message.payload.up || 0,
|
|
||||||
down: message.payload.down || 0,
|
|
||||||
timestamp,
|
|
||||||
}
|
|
||||||
|
|
||||||
this.lastTimestamp = timestamp
|
|
||||||
this.sampler.addDataPoint(dataPoint)
|
|
||||||
this.scheduleSnapshot()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'setRange': {
|
|
||||||
if (this.currentRange !== message.minutes) {
|
|
||||||
this.currentRange = message.minutes
|
|
||||||
this.emitSnapshot()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'requestSnapshot': {
|
|
||||||
this.emitSnapshot()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private emitSnapshot() {
|
|
||||||
const dataPoints = this.sampler.getDataForTimeRange(this.currentRange)
|
|
||||||
|
|
||||||
this.emit({
|
|
||||||
type: 'snapshot',
|
|
||||||
dataPoints,
|
|
||||||
samplerStats: this.sampler.getStats(),
|
|
||||||
lastTimestamp: this.lastTimestamp,
|
|
||||||
})
|
|
||||||
this.lastBroadcast = dataPoints
|
|
||||||
}
|
|
||||||
|
|
||||||
private scheduleSnapshot() {
|
|
||||||
if (this.throttleTimer !== null) return
|
|
||||||
this.throttleTimer = setTimeout(() => {
|
|
||||||
this.throttleTimer = null
|
|
||||||
this.emitSnapshot()
|
|
||||||
}, this.config.snapshotIntervalMs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TrafficWorkerClient {
|
class TrafficWorkerClient {
|
||||||
private worker: Worker | null = null
|
private worker: Worker | null = null
|
||||||
private inlineMonitor: InlineTrafficMonitor | null = null
|
|
||||||
private mode: 'worker' | 'inline' | null = null
|
|
||||||
private listeners = new Set<
|
private listeners = new Set<
|
||||||
(snapshot: ITrafficWorkerSnapshotMessage) => void
|
(snapshot: ITrafficWorkerSnapshotMessage) => void
|
||||||
>()
|
>()
|
||||||
private pendingMessages: TrafficWorkerRequestMessage[] = []
|
|
||||||
private ready = false
|
|
||||||
private currentRange = WORKER_CONFIG.defaultRangeMinutes
|
private currentRange = WORKER_CONFIG.defaultRangeMinutes
|
||||||
|
|
||||||
start(rangeMinutes?: number) {
|
start(rangeMinutes?: number) {
|
||||||
@@ -155,101 +46,52 @@ class TrafficWorkerClient {
|
|||||||
|
|
||||||
this.currentRange = rangeMinutes ?? this.currentRange
|
this.currentRange = rangeMinutes ?? this.currentRange
|
||||||
|
|
||||||
if (this.ready) return
|
if (this.worker) return
|
||||||
|
|
||||||
const initMessage: TrafficWorkerRequestMessage = {
|
const initMessage: TrafficWorkerRequestMessage = {
|
||||||
type: 'init',
|
type: 'init',
|
||||||
config: {
|
config: { ...WORKER_CONFIG, defaultRangeMinutes: this.currentRange },
|
||||||
rawDataMinutes: WORKER_CONFIG.rawDataMinutes,
|
|
||||||
compressedDataMinutes: WORKER_CONFIG.compressedDataMinutes,
|
|
||||||
compressionRatio: WORKER_CONFIG.compressionRatio,
|
|
||||||
snapshotIntervalMs: WORKER_CONFIG.snapshotIntervalMs,
|
|
||||||
defaultRangeMinutes: this.currentRange,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof Worker !== 'undefined') {
|
this.worker = new Worker(
|
||||||
try {
|
new URL('../services/traffic-monitor-worker.ts', import.meta.url),
|
||||||
this.worker = new Worker(
|
{ type: 'module' },
|
||||||
new URL('../services/traffic-monitor-worker.ts', import.meta.url),
|
)
|
||||||
{ type: 'module' },
|
|
||||||
)
|
|
||||||
this.mode = 'worker'
|
|
||||||
|
|
||||||
this.worker.onmessage = (
|
this.worker.onmessage = (
|
||||||
event: MessageEvent<TrafficWorkerResponseMessage>,
|
event: MessageEvent<TrafficWorkerResponseMessage>,
|
||||||
) => {
|
) => {
|
||||||
const message = event.data
|
const message = event.data
|
||||||
if (message.type === 'snapshot') {
|
if (message.type === 'snapshot') {
|
||||||
if (import.meta.env.MODE === 'perf') {
|
if (import.meta.env.MODE === 'perf') {
|
||||||
document.documentElement.dataset.perfWorker = String(
|
document.documentElement.dataset.perfWorker = String(
|
||||||
message.lastTimestamp ?? 0,
|
message.lastTimestamp ?? 0,
|
||||||
)
|
)
|
||||||
document.documentElement.dataset.perfPoints = String(
|
document.documentElement.dataset.perfPoints = String(
|
||||||
message.dataPoints.length,
|
message.dataPoints.length,
|
||||||
)
|
)
|
||||||
const last = message.dataPoints.at(-1)
|
const last = message.dataPoints.at(-1)
|
||||||
document.documentElement.dataset.perfLastUp = String(
|
document.documentElement.dataset.perfLastUp = String(last?.up ?? 0)
|
||||||
last?.up ?? 0,
|
document.documentElement.dataset.perfLastDown = String(
|
||||||
)
|
last?.down ?? 0,
|
||||||
document.documentElement.dataset.perfLastDown = String(
|
)
|
||||||
last?.down ?? 0,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
this.listeners.forEach((listener) => {
|
|
||||||
listener(message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
this.listeners.forEach((listener) => {
|
||||||
this.worker.onerror = (error) => {
|
listener(message)
|
||||||
debugLog(`[TrafficWorkerClient] Worker error: ${String(error)}`)
|
})
|
||||||
}
|
|
||||||
|
|
||||||
this.ready = true
|
|
||||||
this.post(initMessage)
|
|
||||||
this.flushQueue()
|
|
||||||
return
|
|
||||||
} catch (error) {
|
|
||||||
debugLog(
|
|
||||||
`[TrafficWorkerClient] Worker initialization failed, falling back to inline sampler: ${String(error)}`,
|
|
||||||
)
|
|
||||||
this.worker = null
|
|
||||||
this.mode = null
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
debugLog(
|
|
||||||
'[TrafficWorkerClient] Worker not supported, using inline sampler',
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.startInline(initMessage)
|
this.worker.onerror = (error) => {
|
||||||
}
|
debugLog(`[TrafficWorkerClient] Worker error: ${String(error)}`)
|
||||||
|
}
|
||||||
|
|
||||||
private startInline(initMessage: TrafficWorkerRequestMessage) {
|
|
||||||
this.inlineMonitor = new InlineTrafficMonitor((snapshot) => {
|
|
||||||
this.listeners.forEach((listener) => {
|
|
||||||
listener(snapshot)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
this.mode = 'inline'
|
|
||||||
this.ready = true
|
|
||||||
this.post(initMessage)
|
this.post(initMessage)
|
||||||
this.flushQueue()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
if (this.worker) {
|
this.worker?.terminate()
|
||||||
this.worker.terminate()
|
|
||||||
}
|
|
||||||
if (this.inlineMonitor) {
|
|
||||||
this.inlineMonitor.stop()
|
|
||||||
}
|
|
||||||
this.worker = null
|
this.worker = null
|
||||||
this.inlineMonitor = null
|
|
||||||
this.mode = null
|
|
||||||
this.ready = false
|
|
||||||
this.pendingMessages = []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSnapshot(listener: (snapshot: ITrafficWorkerSnapshotMessage) => void) {
|
onSnapshot(listener: (snapshot: ITrafficWorkerSnapshotMessage) => void) {
|
||||||
@@ -260,37 +102,11 @@ class TrafficWorkerClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private post(message: TrafficWorkerRequestMessage) {
|
private post(message: TrafficWorkerRequestMessage) {
|
||||||
if (!this.ready) {
|
this.worker?.postMessage(message)
|
||||||
this.pendingMessages.push(message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.mode === 'worker' && this.worker) {
|
|
||||||
this.worker.postMessage(message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.mode === 'inline' && this.inlineMonitor) {
|
|
||||||
this.inlineMonitor.handle(message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pendingMessages.push(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
private flushQueue() {
|
|
||||||
if (!this.ready || this.pendingMessages.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const queued = [...this.pendingMessages]
|
|
||||||
this.pendingMessages = []
|
|
||||||
queued.forEach((message) => {
|
|
||||||
this.post(message)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ensureStarted() {
|
private ensureStarted() {
|
||||||
if (!this.ready) {
|
if (!this.worker) {
|
||||||
this.start(this.currentRange)
|
this.start(this.currentRange)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ export const useVerge = () => {
|
|||||||
| IVergeConfig
|
| IVergeConfig
|
||||||
| ((prev: IVergeConfig | undefined) => IVergeConfig | undefined)
|
| ((prev: IVergeConfig | undefined) => IVergeConfig | undefined)
|
||||||
| undefined,
|
| undefined,
|
||||||
_revalidate?: boolean,
|
|
||||||
) => {
|
) => {
|
||||||
if (updaterOrData === undefined) {
|
if (updaterOrData === undefined) {
|
||||||
void refetch()
|
void refetch()
|
||||||
|
|||||||
+6
-18
@@ -11,36 +11,24 @@ const useWindow = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useWindowControls = () => {
|
export const useWindowControls = () => {
|
||||||
const {
|
const { maximized, minimize, toggleMaximize, close, currentWindow } =
|
||||||
maximized,
|
useWindow()
|
||||||
minimize,
|
|
||||||
toggleMaximize,
|
|
||||||
close,
|
|
||||||
toggleFullscreen,
|
|
||||||
currentWindow,
|
|
||||||
} = useWindow()
|
|
||||||
return {
|
return {
|
||||||
maximized,
|
maximized,
|
||||||
minimize,
|
minimize,
|
||||||
toggleMaximize,
|
toggleMaximize,
|
||||||
close,
|
close,
|
||||||
toggleFullscreen,
|
|
||||||
currentWindow,
|
currentWindow,
|
||||||
} satisfies Pick<
|
} satisfies Pick<
|
||||||
WindowContextType,
|
WindowContextType,
|
||||||
| 'maximized'
|
'maximized' | 'minimize' | 'toggleMaximize' | 'close' | 'currentWindow'
|
||||||
| 'minimize'
|
|
||||||
| 'toggleMaximize'
|
|
||||||
| 'close'
|
|
||||||
| 'toggleFullscreen'
|
|
||||||
| 'currentWindow'
|
|
||||||
>
|
>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useWindowDecorations = () => {
|
export const useWindowDecorations = () => {
|
||||||
const { decorated, toggleDecorations, refreshDecorated } = useWindow()
|
const { decorated, toggleDecorations } = useWindow()
|
||||||
return { decorated, toggleDecorations, refreshDecorated } satisfies Pick<
|
return { decorated, toggleDecorations } satisfies Pick<
|
||||||
WindowContextType,
|
WindowContextType,
|
||||||
'decorated' | 'toggleDecorations' | 'refreshDecorated'
|
'decorated' | 'toggleDecorations'
|
||||||
>
|
>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -495,11 +495,13 @@
|
|||||||
"strictRoute": "توجيه صارم",
|
"strictRoute": "توجيه صارم",
|
||||||
"autoDetectInterface": "الكشف التلقائي عن الواجهة",
|
"autoDetectInterface": "الكشف التلقائي عن الواجهة",
|
||||||
"dnsHijack": "اختطاف DNS",
|
"dnsHijack": "اختطاف DNS",
|
||||||
|
"dnsMode": "وضع DNS",
|
||||||
"mtu": "وحدة الإرسال القصوى",
|
"mtu": "وحدة الإرسال القصوى",
|
||||||
"autoRedirect": "Auto Redirect"
|
"autoRedirect": "Auto Redirect"
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"dnsHijack": "Please use , to separate multiple DNS servers",
|
"dnsHijack": "Please use , to separate multiple DNS servers",
|
||||||
|
"dnsMode": "يحافظ Auto على السلوك القديم؛ يتطلب hijack تفعيل توجيه صارم على Windows أو Auto Redirect على Linux",
|
||||||
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
"no": "No",
|
"no": "No",
|
||||||
"failed": "Failed",
|
"failed": "Failed",
|
||||||
|
"risky": "غير مؤكد",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
"disallowedIsp": "Disallowed ISP",
|
"disallowedIsp": "Disallowed ISP",
|
||||||
"originalsOnly": "Originals Only",
|
"originalsOnly": "Originals Only",
|
||||||
|
|||||||
@@ -495,11 +495,13 @@
|
|||||||
"strictRoute": "Strenges Routing",
|
"strictRoute": "Strenges Routing",
|
||||||
"autoDetectInterface": "Netzwerkschnittstelle automatisch auswählen",
|
"autoDetectInterface": "Netzwerkschnittstelle automatisch auswählen",
|
||||||
"dnsHijack": "DNS-Hijacking",
|
"dnsHijack": "DNS-Hijacking",
|
||||||
|
"dnsMode": "DNS-Modus",
|
||||||
"mtu": "Maximale Übertragungseinheit",
|
"mtu": "Maximale Übertragungseinheit",
|
||||||
"autoRedirect": "Automatische Umleitung"
|
"autoRedirect": "Automatische Umleitung"
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"dnsHijack": "Mehrere DNS-Server durch Kommas trennen",
|
"dnsHijack": "Mehrere DNS-Server durch Kommas trennen",
|
||||||
|
"dnsMode": "Auto behält das alte Verhalten bei; hijack erfordert Strenges Routing unter Windows oder Automatische Umleitung unter Linux",
|
||||||
"autoRedirect": "Richtet TCP-Umleitungen per nftables/iptables automatisch ein"
|
"autoRedirect": "Richtet TCP-Umleitungen per nftables/iptables automatisch ein"
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"yes": "Unterstützt",
|
"yes": "Unterstützt",
|
||||||
"no": "Nicht unterstützt",
|
"no": "Nicht unterstützt",
|
||||||
"failed": "Test fehlgeschlagen",
|
"failed": "Test fehlgeschlagen",
|
||||||
|
"risky": "Ungewiss",
|
||||||
"completed": "Prüfung abgeschlossen",
|
"completed": "Prüfung abgeschlossen",
|
||||||
"disallowedIsp": "Nicht zugelassener Internetdienstanbieter",
|
"disallowedIsp": "Nicht zugelassener Internetdienstanbieter",
|
||||||
"originalsOnly": "Nur Original",
|
"originalsOnly": "Nur Original",
|
||||||
|
|||||||
@@ -495,11 +495,13 @@
|
|||||||
"strictRoute": "Strict Route",
|
"strictRoute": "Strict Route",
|
||||||
"autoDetectInterface": "Auto Detect Interface",
|
"autoDetectInterface": "Auto Detect Interface",
|
||||||
"dnsHijack": "DNS Hijack",
|
"dnsHijack": "DNS Hijack",
|
||||||
|
"dnsMode": "DNS Mode",
|
||||||
"mtu": "Max Transmission Unit",
|
"mtu": "Max Transmission Unit",
|
||||||
"autoRedirect": "Auto Redirect"
|
"autoRedirect": "Auto Redirect"
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"dnsHijack": "Please use , to separate multiple DNS servers",
|
"dnsHijack": "Please use , to separate multiple DNS servers",
|
||||||
|
"dnsMode": "Auto keeps the legacy behavior; hijack requires Strict Route on Windows or Auto Redirect on Linux",
|
||||||
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
"no": "No",
|
"no": "No",
|
||||||
"failed": "Failed",
|
"failed": "Failed",
|
||||||
|
"risky": "Risky",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
"disallowedIsp": "Disallowed ISP",
|
"disallowedIsp": "Disallowed ISP",
|
||||||
"originalsOnly": "Originals Only",
|
"originalsOnly": "Originals Only",
|
||||||
|
|||||||
@@ -495,11 +495,13 @@
|
|||||||
"strictRoute": "Enrutamiento estricto",
|
"strictRoute": "Enrutamiento estricto",
|
||||||
"autoDetectInterface": "Detectar automáticamente la interfaz de salida del tráfico",
|
"autoDetectInterface": "Detectar automáticamente la interfaz de salida del tráfico",
|
||||||
"dnsHijack": "Secuestro de DNS",
|
"dnsHijack": "Secuestro de DNS",
|
||||||
|
"dnsMode": "Modo DNS",
|
||||||
"mtu": "Unidad máxima de transmisión",
|
"mtu": "Unidad máxima de transmisión",
|
||||||
"autoRedirect": "Auto Redirect"
|
"autoRedirect": "Auto Redirect"
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"dnsHijack": "Please use , to separate multiple DNS servers",
|
"dnsHijack": "Please use , to separate multiple DNS servers",
|
||||||
|
"dnsMode": "Auto mantiene el comportamiento anterior; hijack requiere Enrutamiento estricto en Windows o Auto Redirect en Linux",
|
||||||
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"yes": "Soportado",
|
"yes": "Soportado",
|
||||||
"no": "No soportado",
|
"no": "No soportado",
|
||||||
"failed": "Prueba fallida",
|
"failed": "Prueba fallida",
|
||||||
|
"risky": "Dudoso",
|
||||||
"completed": "Detección completada",
|
"completed": "Detección completada",
|
||||||
"disallowedIsp": "Proveedor de servicios de Internet no permitido",
|
"disallowedIsp": "Proveedor de servicios de Internet no permitido",
|
||||||
"originalsOnly": "Solo originales",
|
"originalsOnly": "Solo originales",
|
||||||
|
|||||||
@@ -495,11 +495,13 @@
|
|||||||
"strictRoute": "مسیر دقیق",
|
"strictRoute": "مسیر دقیق",
|
||||||
"autoDetectInterface": "تشخیص خودکار رابط",
|
"autoDetectInterface": "تشخیص خودکار رابط",
|
||||||
"dnsHijack": "ربایش DNS",
|
"dnsHijack": "ربایش DNS",
|
||||||
|
"dnsMode": "حالت DNS",
|
||||||
"mtu": "واحد حداکثر انتقال",
|
"mtu": "واحد حداکثر انتقال",
|
||||||
"autoRedirect": "Auto Redirect"
|
"autoRedirect": "Auto Redirect"
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"dnsHijack": "لطفا برای جدا کردن چندین سرور DNS از , استفاده کنید.",
|
"dnsHijack": "لطفا برای جدا کردن چندین سرور DNS از , استفاده کنید.",
|
||||||
|
"dnsMode": "Auto رفتار قدیمی را حفظ میکند؛ hijack در ویندوز نیاز به مسیر دقیق یا در لینوکس نیاز به Auto Redirect دارد",
|
||||||
"autoRedirect": "پیکربندی خودکار ریدایرکتهای TCP در nftables/iptables"
|
"autoRedirect": "پیکربندی خودکار ریدایرکتهای TCP در nftables/iptables"
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
"no": "No",
|
"no": "No",
|
||||||
"failed": "Failed",
|
"failed": "Failed",
|
||||||
|
"risky": "نامشخص",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
"disallowedIsp": "Disallowed ISP",
|
"disallowedIsp": "Disallowed ISP",
|
||||||
"originalsOnly": "Originals Only",
|
"originalsOnly": "Originals Only",
|
||||||
|
|||||||
@@ -495,11 +495,13 @@
|
|||||||
"strictRoute": "Rute Ketat",
|
"strictRoute": "Rute Ketat",
|
||||||
"autoDetectInterface": "Deteksi Antarmuka Otomatis",
|
"autoDetectInterface": "Deteksi Antarmuka Otomatis",
|
||||||
"dnsHijack": "Pembajakan DNS",
|
"dnsHijack": "Pembajakan DNS",
|
||||||
|
"dnsMode": "Mode DNS",
|
||||||
"mtu": "Unit Transmisi Maksimum",
|
"mtu": "Unit Transmisi Maksimum",
|
||||||
"autoRedirect": "Auto Redirect"
|
"autoRedirect": "Auto Redirect"
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"dnsHijack": "Please use , to separate multiple DNS servers",
|
"dnsHijack": "Please use , to separate multiple DNS servers",
|
||||||
|
"dnsMode": "Auto mempertahankan perilaku lama; hijack memerlukan Rute Ketat di Windows atau Auto Redirect di Linux",
|
||||||
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
"no": "No",
|
"no": "No",
|
||||||
"failed": "Failed",
|
"failed": "Failed",
|
||||||
|
"risky": "Tidak pasti",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
"disallowedIsp": "Disallowed ISP",
|
"disallowedIsp": "Disallowed ISP",
|
||||||
"originalsOnly": "Originals Only",
|
"originalsOnly": "Originals Only",
|
||||||
|
|||||||
@@ -495,11 +495,13 @@
|
|||||||
"strictRoute": "厳格なルート",
|
"strictRoute": "厳格なルート",
|
||||||
"autoDetectInterface": "トラフィックの出口インターフェースを自動選択",
|
"autoDetectInterface": "トラフィックの出口インターフェースを自動選択",
|
||||||
"dnsHijack": "DNSハイジャック",
|
"dnsHijack": "DNSハイジャック",
|
||||||
|
"dnsMode": "DNSモード",
|
||||||
"mtu": "最大転送単位",
|
"mtu": "最大転送単位",
|
||||||
"autoRedirect": "Auto Redirect"
|
"autoRedirect": "Auto Redirect"
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"dnsHijack": "Please use , to separate multiple DNS servers",
|
"dnsHijack": "Please use , to separate multiple DNS servers",
|
||||||
|
"dnsMode": "Auto は従来の動作を維持します。hijack には Windows で厳格なルートまたは Linux で Auto Redirect が必要です",
|
||||||
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"yes": "サポートする",
|
"yes": "サポートする",
|
||||||
"no": "サポートしない",
|
"no": "サポートしない",
|
||||||
"failed": "テストに失敗しました。",
|
"failed": "テストに失敗しました。",
|
||||||
|
"risky": "要確認",
|
||||||
"completed": "検査完了",
|
"completed": "検査完了",
|
||||||
"disallowedIsp": "許可されていないインターネットサービスプロバイダー",
|
"disallowedIsp": "許可されていないインターネットサービスプロバイダー",
|
||||||
"originalsOnly": "オリジナルのみ",
|
"originalsOnly": "オリジナルのみ",
|
||||||
|
|||||||
@@ -495,11 +495,13 @@
|
|||||||
"strictRoute": "엄격 라우팅",
|
"strictRoute": "엄격 라우팅",
|
||||||
"autoDetectInterface": "인터페이스 자동 감지",
|
"autoDetectInterface": "인터페이스 자동 감지",
|
||||||
"dnsHijack": "DNS 하이재킹",
|
"dnsHijack": "DNS 하이재킹",
|
||||||
|
"dnsMode": "DNS 모드",
|
||||||
"mtu": "MTU",
|
"mtu": "MTU",
|
||||||
"autoRedirect": "Auto Redirect"
|
"autoRedirect": "Auto Redirect"
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"dnsHijack": "여러 DNS 서버는 쉼표(,)로 구분하세요",
|
"dnsHijack": "여러 DNS 서버는 쉼표(,)로 구분하세요",
|
||||||
|
"dnsMode": "Auto는 기존 동작을 유지합니다. hijack은 Windows에서 엄격 라우팅 또는 Linux에서 Auto Redirect가 필요합니다",
|
||||||
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"yes": "예",
|
"yes": "예",
|
||||||
"no": "아니오",
|
"no": "아니오",
|
||||||
"failed": "실패",
|
"failed": "실패",
|
||||||
|
"risky": "확인 필요",
|
||||||
"completed": "완료",
|
"completed": "완료",
|
||||||
"disallowedIsp": "허용되지 않는 ISP",
|
"disallowedIsp": "허용되지 않는 ISP",
|
||||||
"originalsOnly": "오리지널만",
|
"originalsOnly": "오리지널만",
|
||||||
|
|||||||
@@ -495,11 +495,13 @@
|
|||||||
"strictRoute": "Rota Estrita",
|
"strictRoute": "Rota Estrita",
|
||||||
"autoDetectInterface": "Detectar Interface Automaticamente",
|
"autoDetectInterface": "Detectar Interface Automaticamente",
|
||||||
"dnsHijack": "Sequestro de DNS",
|
"dnsHijack": "Sequestro de DNS",
|
||||||
|
"dnsMode": "Modo DNS",
|
||||||
"mtu": "Unidade Máxima de Transmissão",
|
"mtu": "Unidade Máxima de Transmissão",
|
||||||
"autoRedirect": "Redirecionamento Automático"
|
"autoRedirect": "Redirecionamento Automático"
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"dnsHijack": "Use , para separar vários servidores DNS",
|
"dnsHijack": "Use , para separar vários servidores DNS",
|
||||||
|
"dnsMode": "Auto mantém o comportamento antigo; hijack requer Rota Estrita no Windows ou Redirecionamento Automático no Linux",
|
||||||
"autoRedirect": "Configura automaticamente os redirecionamentos TCP do nftables/iptables"
|
"autoRedirect": "Configura automaticamente os redirecionamentos TCP do nftables/iptables"
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"yes": "Sim",
|
"yes": "Sim",
|
||||||
"no": "Não",
|
"no": "Não",
|
||||||
"failed": "Falhou",
|
"failed": "Falhou",
|
||||||
|
"risky": "Incerto",
|
||||||
"completed": "Concluído",
|
"completed": "Concluído",
|
||||||
"disallowedIsp": "ISP não permitido",
|
"disallowedIsp": "ISP não permitido",
|
||||||
"originalsOnly": "Somente originais",
|
"originalsOnly": "Somente originais",
|
||||||
|
|||||||
@@ -495,11 +495,13 @@
|
|||||||
"strictRoute": "Строгая маршрутизация",
|
"strictRoute": "Строгая маршрутизация",
|
||||||
"autoDetectInterface": "Автоопределение интерфейса",
|
"autoDetectInterface": "Автоопределение интерфейса",
|
||||||
"dnsHijack": "Перехват DNS",
|
"dnsHijack": "Перехват DNS",
|
||||||
|
"dnsMode": "Режим DNS",
|
||||||
"mtu": "MTU",
|
"mtu": "MTU",
|
||||||
"autoRedirect": "Автоматическое перенаправление"
|
"autoRedirect": "Автоматическое перенаправление"
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"dnsHijack": "Используйте запятую для разделения нескольких DNS-серверов",
|
"dnsHijack": "Используйте запятую для разделения нескольких DNS-серверов",
|
||||||
|
"dnsMode": "Auto сохраняет прежнее поведение; для hijack нужна Строгая маршрутизация в Windows или Автоматическое перенаправление в Linux",
|
||||||
"autoRedirect": "Автоматически настраивает перенаправление TCP через nftables/iptables"
|
"autoRedirect": "Автоматически настраивает перенаправление TCP через nftables/iptables"
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"yes": "Да",
|
"yes": "Да",
|
||||||
"no": "Нет",
|
"no": "Нет",
|
||||||
"failed": "Ошибка",
|
"failed": "Ошибка",
|
||||||
|
"risky": "Неясно",
|
||||||
"completed": "Завершено",
|
"completed": "Завершено",
|
||||||
"disallowedIsp": "Провайдер заблокирован",
|
"disallowedIsp": "Провайдер заблокирован",
|
||||||
"originalsOnly": "Только Originals",
|
"originalsOnly": "Только Originals",
|
||||||
|
|||||||
@@ -495,11 +495,13 @@
|
|||||||
"strictRoute": "Katı Yönlendirme",
|
"strictRoute": "Katı Yönlendirme",
|
||||||
"autoDetectInterface": "Arayüzü Otomatik Algıla",
|
"autoDetectInterface": "Arayüzü Otomatik Algıla",
|
||||||
"dnsHijack": "DNS Ele Geçirme",
|
"dnsHijack": "DNS Ele Geçirme",
|
||||||
|
"dnsMode": "DNS Modu",
|
||||||
"mtu": "Maksimum İletim Birimi",
|
"mtu": "Maksimum İletim Birimi",
|
||||||
"autoRedirect": "Otomatik Yönlendirme"
|
"autoRedirect": "Otomatik Yönlendirme"
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"dnsHijack": "Birden fazla DNS sunucusunu ayırmak için lütfen , kullanın",
|
"dnsHijack": "Birden fazla DNS sunucusunu ayırmak için lütfen , kullanın",
|
||||||
|
"dnsMode": "Auto eski davranışı korur; hijack için Windows'ta Katı Yönlendirme veya Linux'ta Otomatik Yönlendirme gerekir",
|
||||||
"autoRedirect": "nftables/iptables TCP yönlendirmelerini otomatik olarak yapılandırır"
|
"autoRedirect": "nftables/iptables TCP yönlendirmelerini otomatik olarak yapılandırır"
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"yes": "Evet",
|
"yes": "Evet",
|
||||||
"no": "Hayır",
|
"no": "Hayır",
|
||||||
"failed": "Başarısız",
|
"failed": "Başarısız",
|
||||||
|
"risky": "Belirsiz",
|
||||||
"completed": "Tamamlandı",
|
"completed": "Tamamlandı",
|
||||||
"disallowedIsp": "İzin Verilmeyen ISP",
|
"disallowedIsp": "İzin Verilmeyen ISP",
|
||||||
"originalsOnly": "Yalnızca Orijinaller",
|
"originalsOnly": "Yalnızca Orijinaller",
|
||||||
|
|||||||
@@ -495,11 +495,13 @@
|
|||||||
"strictRoute": "Катгый маршрутлау",
|
"strictRoute": "Катгый маршрутлау",
|
||||||
"autoDetectInterface": "Интерфейсны автоматик ачыклау",
|
"autoDetectInterface": "Интерфейсны автоматик ачыклау",
|
||||||
"dnsHijack": "DNS'ны үзгәртеп тоту (hijack)",
|
"dnsHijack": "DNS'ны үзгәртеп тоту (hijack)",
|
||||||
|
"dnsMode": "DNS режиме",
|
||||||
"mtu": "MTU (макс. тапшыру берәмлеге)",
|
"mtu": "MTU (макс. тапшыру берәмлеге)",
|
||||||
"autoRedirect": "Auto Redirect"
|
"autoRedirect": "Auto Redirect"
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"dnsHijack": "Please use , to separate multiple DNS servers",
|
"dnsHijack": "Please use , to separate multiple DNS servers",
|
||||||
|
"dnsMode": "Auto иске тәртипне саклый; hijack өчен Windows'ta Катгый маршрутлау яки Linux'ta Auto Redirect кирәк",
|
||||||
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
"no": "No",
|
"no": "No",
|
||||||
"failed": "Failed",
|
"failed": "Failed",
|
||||||
|
"risky": "Билгесез",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
"disallowedIsp": "Disallowed ISP",
|
"disallowedIsp": "Disallowed ISP",
|
||||||
"originalsOnly": "Originals Only",
|
"originalsOnly": "Originals Only",
|
||||||
|
|||||||
@@ -495,11 +495,13 @@
|
|||||||
"strictRoute": "严格路由",
|
"strictRoute": "严格路由",
|
||||||
"autoDetectInterface": "自动选择流量出口接口",
|
"autoDetectInterface": "自动选择流量出口接口",
|
||||||
"dnsHijack": "DNS 劫持",
|
"dnsHijack": "DNS 劫持",
|
||||||
|
"dnsMode": "DNS 模式",
|
||||||
"mtu": "最大传输单元",
|
"mtu": "最大传输单元",
|
||||||
"autoRedirect": "自动重定向"
|
"autoRedirect": "自动重定向"
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"dnsHijack": "多个 DNS 服务器请使用 , 分隔",
|
"dnsHijack": "多个 DNS 服务器请使用 , 分隔",
|
||||||
|
"dnsMode": "Auto 保持旧行为;hijack 需 Windows 开启严格路由或 Linux 开启自动重定向",
|
||||||
"autoRedirect": "自动配置 nftables/iptables 的 TCP 重定向"
|
"autoRedirect": "自动配置 nftables/iptables 的 TCP 重定向"
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"yes": "支持",
|
"yes": "支持",
|
||||||
"no": "不支持",
|
"no": "不支持",
|
||||||
"failed": "测试失败",
|
"failed": "测试失败",
|
||||||
|
"risky": "存疑",
|
||||||
"completed": "检测完成",
|
"completed": "检测完成",
|
||||||
"disallowedIsp": "不允许的 ISP",
|
"disallowedIsp": "不允许的 ISP",
|
||||||
"originalsOnly": "仅限原创",
|
"originalsOnly": "仅限原创",
|
||||||
|
|||||||
@@ -495,11 +495,13 @@
|
|||||||
"strictRoute": "嚴格路由",
|
"strictRoute": "嚴格路由",
|
||||||
"autoDetectInterface": "自動偵測流量輸出介面",
|
"autoDetectInterface": "自動偵測流量輸出介面",
|
||||||
"dnsHijack": "DNS 綁架",
|
"dnsHijack": "DNS 綁架",
|
||||||
|
"dnsMode": "DNS 模式",
|
||||||
"mtu": "最大傳輸單位",
|
"mtu": "最大傳輸單位",
|
||||||
"autoRedirect": "自動重導"
|
"autoRedirect": "自動重導"
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"dnsHijack": "請用半形逗號來區隔多個 DNS 伺服器",
|
"dnsHijack": "請用半形逗號來區隔多個 DNS 伺服器",
|
||||||
|
"dnsMode": "Auto 保持舊行為;hijack 需 Windows 開啟嚴格路由或 Linux 開啟自動重導",
|
||||||
"autoRedirect": "自動配置 nftables/iptables 的 TCP 重導"
|
"autoRedirect": "自動配置 nftables/iptables 的 TCP 重導"
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"yes": "支援",
|
"yes": "支援",
|
||||||
"no": "不支援",
|
"no": "不支援",
|
||||||
"failed": "測試失敗",
|
"failed": "測試失敗",
|
||||||
|
"risky": "存疑",
|
||||||
"completed": "檢測完成",
|
"completed": "檢測完成",
|
||||||
"disallowedIsp": "不允許的網際網路服務供應商",
|
"disallowedIsp": "不允許的網際網路服務供應商",
|
||||||
"originalsOnly": "僅限原創",
|
"originalsOnly": "僅限原創",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user