mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-08 11:02:08 +08:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da050d3e7e | ||
|
|
9a7b15eb15 | ||
|
|
98072a17a3 | ||
|
|
32a093e44b | ||
|
|
7ecec42f4d |
@@ -4,11 +4,11 @@
|
|||||||
|
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/node:2": {
|
"ghcr.io/devcontainers/features/node:2": {
|
||||||
"version": "26.13.0",
|
"version": "24.21.0",
|
||||||
"pnpmVersion": "12.16.1"
|
"pnpmVersion": "12.11.2"
|
||||||
},
|
},
|
||||||
"ghcr.io/devcontainers/features/rust:1": {
|
"ghcr.io/devcontainers/features/rust:1": {
|
||||||
"version": "1.99.1",
|
"version": "1.99.0",
|
||||||
"profile": "default"
|
"profile": "default"
|
||||||
},
|
},
|
||||||
"ghcr.io/devcontainers/features/git:1": {},
|
"ghcr.io/devcontainers/features/git:1": {},
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
# 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,8 +10,6 @@ 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 填写一个简洁明了的标题,以便他人快速检索
|
||||||
@@ -21,8 +19,6 @@ 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,16 +9,12 @@ 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,12 +1,6 @@
|
|||||||
## Problem
|
## Problem
|
||||||
|
|
||||||
What breaks or what is missing. Product bug fixes and features require a
|
What breaks or what is missing. Link the issue if one exists (`Fixes #123`).
|
||||||
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: '26.13.0'
|
node-version: '24.21.0'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -154,7 +154,7 @@ jobs:
|
|||||||
- name: Install Rust Stable
|
- name: Install Rust Stable
|
||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: '1.99.1'
|
toolchain: '1.99.0'
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
@@ -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: '26.13.0'
|
node-version: '24.21.0'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Pnpm Cache
|
- name: Pnpm Cache
|
||||||
@@ -206,7 +206,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
run: |
|
run: |
|
||||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||||
rustup target list --installed
|
rustup target list --installed
|
||||||
echo "Rust target ${{ matrix.target }} installed."
|
echo "Rust target ${{ matrix.target }} installed."
|
||||||
|
|
||||||
@@ -257,7 +257,7 @@ jobs:
|
|||||||
- name: Install Rust Stable
|
- name: Install Rust Stable
|
||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: '1.99.1'
|
toolchain: '1.99.0'
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
@@ -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: '26.13.0'
|
node-version: '24.21.0'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Pnpm Cache
|
- name: Pnpm Cache
|
||||||
@@ -347,7 +347,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
run: |
|
run: |
|
||||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||||
rustup target list --installed
|
rustup target list --installed
|
||||||
echo "Rust target ${{ matrix.target }} installed."
|
echo "Rust target ${{ matrix.target }} installed."
|
||||||
|
|
||||||
@@ -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: '26.13.0'
|
node-version: '24.21.0'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Pnpm Cache
|
- name: Pnpm Cache
|
||||||
@@ -454,7 +454,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
run: |
|
run: |
|
||||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||||
rustup target list --installed
|
rustup target list --installed
|
||||||
echo "Rust target ${{ matrix.target }} installed."
|
echo "Rust target ${{ matrix.target }} installed."
|
||||||
|
|
||||||
@@ -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: '26.13.0'
|
node-version: '24.21.0'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6.1.0
|
- uses: pnpm/action-setup@v6.1.0
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
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: '26.13.0'
|
|
||||||
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: '26.13.0'
|
node-version: '24.21.0'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install Rust Stable
|
- name: Install Rust Stable
|
||||||
if: github.event.inputs[matrix.input] == 'true'
|
if: github.event.inputs[matrix.input] == 'true'
|
||||||
uses: dtolnay/rust-toolchain@1.99.1
|
uses: dtolnay/rust-toolchain@1.99.0
|
||||||
|
|
||||||
- name: Rust Cache
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
@@ -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: '26.13.0'
|
node-version: '24.21.0'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Pnpm Cache
|
- name: Pnpm Cache
|
||||||
@@ -129,7 +129,7 @@ jobs:
|
|||||||
if: github.event.inputs[matrix.input] == 'true'
|
if: github.event.inputs[matrix.input] == 'true'
|
||||||
run: |
|
run: |
|
||||||
# Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed
|
# Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed
|
||||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||||
rustup target list --installed
|
rustup target list --installed
|
||||||
echo "Rust target ${{ matrix.target }} installed."
|
echo "Rust target ${{ matrix.target }} installed."
|
||||||
|
|
||||||
|
|||||||
@@ -2,23 +2,11 @@ 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
|
||||||
@@ -29,17 +17,46 @@ 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: '26.13.0'
|
node-version: '24.21.0'
|
||||||
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
|
||||||
@@ -48,21 +65,94 @@ 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.21.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,14 +2,6 @@ name: Clippy Lint
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
|
||||||
- "src-tauri/**"
|
|
||||||
- "crates/**"
|
|
||||||
- "Cargo.toml"
|
|
||||||
- "Cargo.lock"
|
|
||||||
- "rust-toolchain.toml"
|
|
||||||
- ".cargo/config.toml"
|
|
||||||
- ".github/workflows/lint-clippy.yml"
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
env:
|
env:
|
||||||
HUSKY: 0
|
HUSKY: 0
|
||||||
@@ -31,6 +23,28 @@ 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
|
||||||
|
|
||||||
@@ -65,7 +79,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'
|
if: matrix.os == 'windows-latest' && (github.event_name == 'workflow_dispatch' || steps.check_changes.outputs.rust == 'true')
|
||||||
working-directory: ./src-tauri
|
working-directory: ./src-tauri
|
||||||
run: cargo test --all-features --target ${{ matrix.target }}
|
run: cargo test --all-features --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
name: Prepare Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [edited, closed]
|
|
||||||
branches: [dev]
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
channel:
|
|
||||||
description: Release channel
|
|
||||||
required: true
|
|
||||||
type: choice
|
|
||||||
options: [rc, stable]
|
|
||||||
default: rc
|
|
||||||
release_tag:
|
|
||||||
description: 'Optional tag: vX.Y.Z-rc[.N] for RC, vX.Y.Z for stable'
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: prepare-release
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
prepare:
|
|
||||||
if: >-
|
|
||||||
${{ github.event_name == 'workflow_dispatch' ||
|
|
||||||
(github.event.pull_request.head.repo.full_name == github.repository &&
|
|
||||||
github.event.pull_request.user.login == 'github-actions[bot]' &&
|
|
||||||
((startsWith(github.event.pull_request.head.ref, 'release/v') &&
|
|
||||||
((github.event.action == 'closed' && github.event.pull_request.merged == true) ||
|
|
||||||
(github.event.changes.body && github.event.pull_request.state == 'open' &&
|
|
||||||
contains(github.event.pull_request.body, '[x] Refresh this PR from latest dev')))) ||
|
|
||||||
(startsWith(github.event.pull_request.head.ref, 'next-dev/v') &&
|
|
||||||
github.event.changes.body && github.event.pull_request.state == 'open' &&
|
|
||||||
(contains(github.event.pull_request.body, '[x] Refresh this PR from latest dev') ||
|
|
||||||
contains(github.event.pull_request.body, '[x] Use the next minor version') !=
|
|
||||||
contains(github.event.changes.body.from, '[x] Use the next minor version'))))) }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
steps:
|
|
||||||
- name: Require the dev branch
|
|
||||||
if: github.event_name == 'workflow_dispatch'
|
|
||||||
env:
|
|
||||||
SOURCE_REF: ${{ github.ref }}
|
|
||||||
run: |
|
|
||||||
if [[ "$SOURCE_REF" != 'refs/heads/dev' ]]; then
|
|
||||||
echo '::error::Run Prepare Release from the dev branch.'
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
with:
|
|
||||||
ref: refs/heads/dev
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: '26.13.0'
|
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
|
||||||
if: github.event.action != 'closed'
|
|
||||||
with:
|
|
||||||
run_install: false
|
|
||||||
|
|
||||||
- run: pnpm install --frozen-lockfile --ignore-scripts
|
|
||||||
if: github.event.action != 'closed'
|
|
||||||
|
|
||||||
- name: Create or refresh version or publication PR
|
|
||||||
env:
|
|
||||||
RELEASE_TAG: ${{ inputs.release_tag }}
|
|
||||||
RELEASE_CHANNEL: ${{ inputs.channel }}
|
|
||||||
RELEASE_COMMAND: ${{ github.event.action == 'closed' && 'publish-pr' || startsWith(github.event.pull_request.head.ref, 'next-dev/') && 'next-dev' || 'prepare' }}
|
|
||||||
run: node scripts-workflow/release.mjs "$RELEASE_COMMAND"
|
|
||||||
+82
-129
@@ -1,9 +1,6 @@
|
|||||||
name: Release Build
|
name: Release Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
|
||||||
types: [edited]
|
|
||||||
branches: [main]
|
|
||||||
push:
|
push:
|
||||||
# -rc tag 时预览发布, 跳过 telegram 通知、跳过 winget 提交、跳过 latest.json 文件更新
|
# -rc tag 时预览发布, 跳过 telegram 通知、跳过 winget 提交、跳过 latest.json 文件更新
|
||||||
tags:
|
tags:
|
||||||
@@ -14,59 +11,75 @@ env:
|
|||||||
RUST_BACKTRACE: short
|
RUST_BACKTRACE: short
|
||||||
HUSKY: 0
|
HUSKY: 0
|
||||||
concurrency:
|
concurrency:
|
||||||
group: release-publication
|
group: '${{ github.workflow }} - ${{ github.head_ref || github.ref }}'
|
||||||
cancel-in-progress: false
|
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
prepare_release:
|
check_tag_version:
|
||||||
name: Approve Snapshot and Prepare Draft Release
|
name: Check Release Tag and package.json Version Consistency
|
||||||
if: >-
|
|
||||||
${{ github.event_name == 'push' ||
|
|
||||||
(github.event.changes.body &&
|
|
||||||
github.event.pull_request.head.repo.full_name == github.repository &&
|
|
||||||
github.event.pull_request.user.login == 'github-actions[bot]' &&
|
|
||||||
startsWith(github.event.pull_request.head.ref, 'publish/v') &&
|
|
||||||
contains(github.event.pull_request.body, '[x] Publish this approved release')) }}
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: read
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
outputs:
|
|
||||||
release_tag: ${{ steps.source.outputs.release_tag }}
|
|
||||||
source_sha: ${{ steps.source.outputs.source_sha }}
|
|
||||||
channel: ${{ steps.source.outputs.channel }}
|
|
||||||
release_id: ${{ steps.draft.outputs.id }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout trusted release tooling
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
ref: refs/heads/dev
|
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: '26.13.0'
|
|
||||||
- name: Validate source and publish the approved snapshot
|
|
||||||
id: source
|
|
||||||
env:
|
|
||||||
RELEASE_COMMAND: ${{ github.event_name == 'push' && 'validate-tag' || 'publish' }}
|
|
||||||
run: node scripts-workflow/release.mjs "$RELEASE_COMMAND"
|
|
||||||
|
|
||||||
- name: Create or reuse draft release
|
- name: Check if tag is from main branch
|
||||||
id: draft
|
run: |
|
||||||
env:
|
TAG_REF="${GITHUB_REF##*/}"
|
||||||
RELEASE_TAG: ${{ steps.source.outputs.release_tag }}
|
echo "Checking if tag $TAG_REF is from main branch..."
|
||||||
SOURCE_SHA: ${{ steps.source.outputs.source_sha }}
|
|
||||||
run: node scripts-workflow/release.mjs prepare-draft
|
TAG_COMMIT=$(git rev-list -n 1 $TAG_REF)
|
||||||
|
MAIN_COMMITS=$(git rev-list origin/main)
|
||||||
|
|
||||||
|
if echo "$MAIN_COMMITS" | grep -q "$TAG_COMMIT"; then
|
||||||
|
echo "✅ Tag $TAG_REF is from main branch"
|
||||||
|
else
|
||||||
|
echo "❌ Tag $TAG_REF is not from main branch"
|
||||||
|
echo "This release workflow only accepts tags from main branch."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check tag and package.json version
|
||||||
|
run: |
|
||||||
|
TAG_REF="${GITHUB_REF_NAME:-${GITHUB_REF##*/}}"
|
||||||
|
echo "Current tag: $TAG_REF"
|
||||||
|
|
||||||
|
PKG_VERSION=$(jq -r .version package.json)
|
||||||
|
echo "package.json version: $PKG_VERSION"
|
||||||
|
|
||||||
|
EXPECTED_TAG="v$PKG_VERSION"
|
||||||
|
|
||||||
|
if [[ "$TAG_REF" != "$EXPECTED_TAG" ]]; then
|
||||||
|
echo "❌ Version mismatch:"
|
||||||
|
echo " Git tag : $TAG_REF"
|
||||||
|
echo " package.json : $EXPECTED_TAG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Tag and package.json version are consistent."
|
||||||
|
|
||||||
|
prepare_release:
|
||||||
|
name: Prepare Draft Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [check_tag_version]
|
||||||
|
outputs:
|
||||||
|
release_id: ${{ steps.prepare_release.outputs.id }}
|
||||||
|
steps:
|
||||||
|
- name: Create or update draft release
|
||||||
|
id: prepare_release
|
||||||
|
uses: softprops/action-gh-release@v3
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
target_commitish: ${{ github.sha }}
|
||||||
|
name: 'Clash Verge Rev ${{ github.ref_name }}'
|
||||||
|
body: 'Draft release, will be updated later.'
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
draft: true
|
||||||
|
prerelease: ${{ contains(github.ref_name, '-rc') }}
|
||||||
|
|
||||||
update_tag:
|
update_tag:
|
||||||
name: Publish Completed Release
|
name: Update tag
|
||||||
env:
|
|
||||||
RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }}
|
|
||||||
outputs:
|
|
||||||
source_sha: ${{ needs.prepare_release.outputs.source_sha }}
|
|
||||||
channel: ${{ needs.prepare_release.outputs.channel }}
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
[
|
[
|
||||||
@@ -78,8 +91,6 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7
|
||||||
with:
|
|
||||||
ref: ${{ needs.prepare_release.outputs.source_sha }}
|
|
||||||
|
|
||||||
- name: Group changelog platforms
|
- name: Group changelog platforms
|
||||||
run: bash ./scripts-workflow/group_platforms.sh
|
run: bash ./scripts-workflow/group_platforms.sh
|
||||||
@@ -92,7 +103,7 @@ jobs:
|
|||||||
- name: Set Env
|
- name: Set Env
|
||||||
run: |
|
run: |
|
||||||
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||||
TAG_REF="$RELEASE_TAG"
|
TAG_REF="${GITHUB_REF##*/}"
|
||||||
echo "TAG_NAME=$TAG_REF" >> $GITHUB_ENV
|
echo "TAG_NAME=$TAG_REF" >> $GITHUB_ENV
|
||||||
VERSION=$(echo "$TAG_REF" | sed 's/^v//')
|
VERSION=$(echo "$TAG_REF" | sed 's/^v//')
|
||||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||||
@@ -138,16 +149,10 @@ jobs:
|
|||||||
Created at ${{ env.BUILDTIME }}.
|
Created at ${{ env.BUILDTIME }}.
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
- name: Verify release tag still points to the approved source
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
SOURCE_SHA: ${{ needs.prepare_release.outputs.source_sha }}
|
|
||||||
run: node scripts-workflow/release.mjs verify-target
|
|
||||||
|
|
||||||
- name: Publish Release
|
- name: Publish Release
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
PRERELEASE: ${{ contains(env.RELEASE_TAG, '-rc') }}
|
PRERELEASE: ${{ contains(github.ref_name, '-rc') }}
|
||||||
run: |
|
run: |
|
||||||
release_id="${{ needs.prepare_release.outputs.release_id }}"
|
release_id="${{ needs.prepare_release.outputs.release_id }}"
|
||||||
|
|
||||||
@@ -176,8 +181,6 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
name: Release Build
|
name: Release Build
|
||||||
needs: [prepare_release]
|
needs: [prepare_release]
|
||||||
env:
|
|
||||||
RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }}
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -197,13 +200,11 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7
|
||||||
with:
|
|
||||||
ref: ${{ needs.prepare_release.outputs.source_sha }}
|
|
||||||
|
|
||||||
- name: Install Rust Stable
|
- name: Install Rust Stable
|
||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: '1.99.1'
|
toolchain: '1.99.0'
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
@@ -229,7 +230,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '24.21.0'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -243,11 +244,12 @@ jobs:
|
|||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
run: |
|
run: |
|
||||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||||
rustup target list --installed
|
rustup target list --installed
|
||||||
echo "Rust target ${{ matrix.target }} installed."
|
echo "Rust target ${{ matrix.target }} installed."
|
||||||
|
|
||||||
- name: Tauri build
|
- name: Tauri build
|
||||||
|
# 上游 5.24 修改了 latest.json 的生成逻辑,且依赖 tauri-plugin-update 2.10.0 暂未发布,故锁定在 0.5.23 版本
|
||||||
uses: tauri-apps/tauri-action@v1.0.0
|
uses: tauri-apps/tauri-action@v1.0.0
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: '--max_old_space_size=4096'
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
@@ -262,14 +264,11 @@ jobs:
|
|||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
with:
|
with:
|
||||||
releaseId: ${{ needs.prepare_release.outputs.release_id }}
|
releaseId: ${{ needs.prepare_release.outputs.release_id }}
|
||||||
tagName: ${{ env.RELEASE_TAG }}
|
tagName: ${{ github.ref_name }}
|
||||||
releaseBody: 'Draft release, will be updated later.'
|
releaseBody: 'Draft release, will be updated later.'
|
||||||
releaseDraft: true
|
|
||||||
prerelease: ${{ needs.prepare_release.outputs.channel == 'rc' }}
|
|
||||||
releaseCommitish: ${{ needs.prepare_release.outputs.source_sha }}
|
|
||||||
tauriScript: pnpm
|
tauriScript: pnpm
|
||||||
args: --target ${{ matrix.target }}
|
args: --target ${{ matrix.target }}
|
||||||
uploadUpdaterJson: ${{ needs.prepare_release.outputs.channel == 'stable' }}
|
includeUpdaterJson: true
|
||||||
|
|
||||||
- name: Attest Windows bundles
|
- name: Attest Windows bundles
|
||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
@@ -294,8 +293,6 @@ jobs:
|
|||||||
release-for-linux-arm:
|
release-for-linux-arm:
|
||||||
name: Release Build for Linux ARM
|
name: Release Build for Linux ARM
|
||||||
needs: [prepare_release]
|
needs: [prepare_release]
|
||||||
env:
|
|
||||||
RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }}
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -310,13 +307,11 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7
|
||||||
with:
|
|
||||||
ref: ${{ needs.prepare_release.outputs.source_sha }}
|
|
||||||
|
|
||||||
- name: Install Rust Stable
|
- name: Install Rust Stable
|
||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: '1.99.1'
|
toolchain: '1.99.0'
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
@@ -336,7 +331,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '24.21.0'
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v6
|
uses: pnpm/action-setup@v6
|
||||||
@@ -394,7 +389,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
run: |
|
run: |
|
||||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||||
rustup target list --installed
|
rustup target list --installed
|
||||||
echo "Rust target ${{ matrix.target }} installed."
|
echo "Rust target ${{ matrix.target }} installed."
|
||||||
|
|
||||||
@@ -425,7 +420,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
gh release upload "$RELEASE_TAG" \
|
gh release upload "${{ github.ref_name }}" \
|
||||||
target/${{ matrix.target }}/release/bundle/deb/*.deb \
|
target/${{ matrix.target }}/release/bundle/deb/*.deb \
|
||||||
target/${{ matrix.target }}/release/bundle/rpm/*.rpm \
|
target/${{ matrix.target }}/release/bundle/rpm/*.rpm \
|
||||||
--clobber
|
--clobber
|
||||||
@@ -433,8 +428,6 @@ jobs:
|
|||||||
release-for-fixed-webview2:
|
release-for-fixed-webview2:
|
||||||
name: Release Build for Fixed WebView2
|
name: Release Build for Fixed WebView2
|
||||||
needs: [prepare_release]
|
needs: [prepare_release]
|
||||||
env:
|
|
||||||
RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }}
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -449,13 +442,11 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7
|
||||||
with:
|
|
||||||
ref: ${{ needs.prepare_release.outputs.source_sha }}
|
|
||||||
|
|
||||||
- name: Install Rust Stable
|
- name: Install Rust Stable
|
||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: '1.99.1'
|
toolchain: '1.99.0'
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
@@ -475,7 +466,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '24.21.0'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -496,7 +487,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
run: |
|
run: |
|
||||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||||
rustup target list --installed
|
rustup target list --installed
|
||||||
echo "Rust target ${{ matrix.target }} installed."
|
echo "Rust target ${{ matrix.target }} installed."
|
||||||
|
|
||||||
@@ -541,55 +532,22 @@ jobs:
|
|||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
$files = Get-ChildItem "target\${{ matrix.target }}\release\bundle\nsis\*setup*" | Select-Object -ExpandProperty FullName
|
$files = Get-ChildItem "target\${{ matrix.target }}\release\bundle\nsis\*setup*" | Select-Object -ExpandProperty FullName
|
||||||
gh release upload "$env:RELEASE_TAG" $files --clobber
|
gh release upload "${{ github.ref_name }}" $files --clobber
|
||||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||||
|
|
||||||
prepare-next-dev:
|
|
||||||
name: Prepare Next Dev Version
|
|
||||||
if: ${{ needs.update_tag.outputs.channel == 'stable' }}
|
|
||||||
needs: [prepare_release, update_tag]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
concurrency:
|
|
||||||
group: prepare-release
|
|
||||||
cancel-in-progress: false
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }}
|
|
||||||
SOURCE_SHA: ${{ needs.prepare_release.outputs.source_sha }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
with:
|
|
||||||
ref: refs/heads/dev
|
|
||||||
fetch-depth: 0
|
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: '26.13.0'
|
|
||||||
- uses: pnpm/action-setup@v6
|
|
||||||
with:
|
|
||||||
run_install: false
|
|
||||||
- name: Prepare version and changelog PR
|
|
||||||
run: |
|
|
||||||
pnpm install --frozen-lockfile --ignore-scripts
|
|
||||||
node scripts-workflow/release.mjs next-dev
|
|
||||||
|
|
||||||
release-update:
|
release-update:
|
||||||
if: ${{ needs.update_tag.outputs.channel == 'stable' }}
|
if: ${{ !contains(github.ref_name, '-rc') }}
|
||||||
name: Release Update
|
name: Release Update
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [update_tag]
|
needs: [update_tag]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7
|
||||||
with:
|
|
||||||
ref: ${{ needs.update_tag.outputs.source_sha }}
|
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '24.21.0'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -605,19 +563,17 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
release-update-for-fixed-webview2:
|
release-update-for-fixed-webview2:
|
||||||
if: ${{ needs.update_tag.outputs.channel == 'stable' }}
|
if: ${{ !contains(github.ref_name, '-rc') }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [update_tag]
|
needs: [update_tag]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7
|
||||||
with:
|
|
||||||
ref: ${{ needs.update_tag.outputs.source_sha }}
|
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '24.21.0'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -633,7 +589,7 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
submit-to-winget:
|
submit-to-winget:
|
||||||
if: ${{ needs.update_tag.outputs.channel == 'stable' }}
|
if: ${{ !contains(github.ref_name, '-rc') }}
|
||||||
name: Submit to Winget
|
name: Submit to Winget
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [update_tag, release-update]
|
needs: [update_tag, release-update]
|
||||||
@@ -641,7 +597,6 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
ref: ${{ needs.update_tag.outputs.source_sha }}
|
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Get Version
|
- name: Get Version
|
||||||
run: |
|
run: |
|
||||||
@@ -658,7 +613,7 @@ jobs:
|
|||||||
token: ${{ secrets.WINGET_TOKEN }}
|
token: ${{ secrets.WINGET_TOKEN }}
|
||||||
|
|
||||||
notify-telegram:
|
notify-telegram:
|
||||||
if: ${{ needs.update_tag.outputs.channel == 'stable' }}
|
if: ${{ !contains(github.ref_name, '-rc') }}
|
||||||
name: Notify Telegram
|
name: Notify Telegram
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
@@ -671,8 +626,6 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7
|
||||||
with:
|
|
||||||
ref: ${{ needs.update_tag.outputs.source_sha }}
|
|
||||||
|
|
||||||
- name: Group changelog platforms
|
- name: Group changelog platforms
|
||||||
run: bash ./scripts-workflow/group_platforms.sh
|
run: bash ./scripts-workflow/group_platforms.sh
|
||||||
@@ -685,7 +638,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '24.21.0'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
|
|||||||
@@ -6,12 +6,6 @@ name: Check Formatting
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
|
||||||
- "src-tauri/**"
|
|
||||||
- "**/*.rs"
|
|
||||||
- "rustfmt.toml"
|
|
||||||
- "rust-toolchain.toml"
|
|
||||||
- ".github/workflows/rustfmt.yml"
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -25,10 +19,25 @@ 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
|
||||||
|
|||||||
@@ -22,13 +22,13 @@ jobs:
|
|||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: '1.99.1'
|
toolchain: '1.99.0'
|
||||||
targets: x86_64-pc-windows-msvc
|
targets: x86_64-pc-windows-msvc
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '24.21.0'
|
||||||
|
|
||||||
- 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: '26.13.0'
|
node-version: '24.21.0'
|
||||||
|
|
||||||
- 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: '26.13.0'
|
node-version: '24.21.0'
|
||||||
|
|
||||||
- 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: '26.13.0'
|
node-version: '24.21.0'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
|
|||||||
+3
-3
@@ -1,3 +1,3 @@
|
|||||||
nodejs 26.13.0
|
nodejs 24.21.0
|
||||||
pnpm 12.16.1
|
pnpm 12.11.2
|
||||||
rust 1.99.1
|
rust 1.99.0
|
||||||
|
|||||||
@@ -18,25 +18,22 @@ 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.** Product bug fixes and features require a pre-existing issue;
|
1. **Issue first.** Non-trivial changes require a pre-existing issue describing
|
||||||
self-scoped maintenance work does not. Follow the PR template for linkage and
|
the problem. If none exists, ask the maintainers to open or approve one
|
||||||
revert references. If a required issue is missing, ask maintainers to open or
|
before implementing.
|
||||||
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 or the stated PR scope. No drive-by refactors, renames, formatting
|
issue. No drive-by refactors, renames, formatting churn, or dependency bumps
|
||||||
churn, or dependency bumps unrelated to the work.
|
unrelated to the problem being fixed.
|
||||||
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 or stated task demands
|
speculative defensive code unless the linked issue demands them. When a test
|
||||||
them. When a test is genuinely necessary — it reproduces the reported
|
is genuinely necessary — it reproduces the reported regression or guards
|
||||||
regression or guards behavior whose breakage would otherwise go unnoticed —
|
behavior whose breakage would otherwise go unnoticed — keep it minimal and
|
||||||
keep it minimal and state in the PR body why it is needed. Bulk test files
|
state in the PR body why it is needed. Bulk test files and defensive
|
||||||
and defensive programming for hypothetical failure modes are PR bloat, not
|
programming for hypothetical failure modes are PR bloat, not rigor.
|
||||||
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.
|
||||||
@@ -47,12 +44,13 @@ 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 linked issue or stated task demands it.
|
the issue demands it.
|
||||||
9. **Disclose AI automation.** When an agent creates or co-creates a PR or Issue,
|
9. **Disclose AI automation.** When an agent produces or co-produces a change,
|
||||||
append a final body line with the model and effort used (e.g.
|
append a footer line to the PR body with the model and effort used (e.g.
|
||||||
`Assisted by: GPT-5.6 High`). Effort may be omitted when the runtime does not
|
`Assisted by: GPT-5.6 High`). The PR template intentionally omits this line —
|
||||||
report it. Human contributors are not asked to disclose AI assistance.
|
the agent adds it itself, humans are not asked to declare anything. Effort may
|
||||||
Disclosure is transparency only and carries no positive or negative weight.
|
be omitted when the runtime does not report it. Disclosure is transparency
|
||||||
|
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
|
||||||
@@ -61,10 +59,8 @@ 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.
|
||||||
|
|
||||||
## Issue and PR Workflows
|
## Pull Request Shape
|
||||||
|
|
||||||
When drafting, triaging, splitting, or implementing from a community issue, use
|
Describe three things, briefly: the problem (with issue link), why this approach
|
||||||
the conditional checklist in [`.github/ISSUE_GUIDELINES.md`](.github/ISSUE_GUIDELINES.md).
|
solves it, and what changed. See
|
||||||
Do not make GitHub issue changes without explicit authorization. For PRs, follow
|
[`PULL_REQUEST_TEMPLATE.md`](.github/PULL_REQUEST_TEMPLATE.md).
|
||||||
the directly loaded [PR template](.github/PULL_REQUEST_TEMPLATE.md); do not
|
|
||||||
duplicate its prompts here.
|
|
||||||
|
|||||||
+6
-19
@@ -28,7 +28,7 @@ Before contributing, you need to set up your development environment. Follow the
|
|||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
1. **Install Rust and Node.js**
|
1. **Install Rust and Node.js**
|
||||||
Follow the [Tauri prerequisites](https://tauri.app/start/prerequisites/) for your operating system. Use the Node.js version in [`.tool-versions`](.tool-versions); [`rust-toolchain.toml`](rust-toolchain.toml) selects the Rust version and required components when you run Cargo from this repository.
|
Our project requires both Rust and Node.js. Follow the official installation instructions [here](https://tauri.app/start/prerequisites/).
|
||||||
|
|
||||||
### Windows Users
|
### Windows Users
|
||||||
|
|
||||||
@@ -51,21 +51,18 @@ rustup set default-host x86_64-pc-windows-msvc
|
|||||||
|
|
||||||
### Install Node.js Package Manager
|
### Install Node.js Package Manager
|
||||||
|
|
||||||
[Corepack is no longer bundled with Node.js starting with Node.js 25](https://nodejs.org/api/corepack.html). Install it globally if it is not already available, then enable it:
|
Enable `corepack`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install --global corepack
|
|
||||||
corepack enable
|
corepack enable
|
||||||
```
|
```
|
||||||
|
|
||||||
Run pnpm from this repository so Corepack selects the version pinned by `packageManager` in [`package.json`](package.json).
|
|
||||||
|
|
||||||
### Install Project Dependencies
|
### Install Project Dependencies
|
||||||
|
|
||||||
Node.js dependencies:
|
Node.js dependencies:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm install --frozen-lockfile
|
pnpm install
|
||||||
```
|
```
|
||||||
|
|
||||||
Ubuntu-only system packages:
|
Ubuntu-only system packages:
|
||||||
@@ -81,24 +78,14 @@ pnpm run prebuild
|
|||||||
pnpm run prebuild --force # Re-download and overwrite Mihomo core and service binaries
|
pnpm run prebuild --force # Re-download and overwrite Mihomo core and service binaries
|
||||||
```
|
```
|
||||||
|
|
||||||
### Prepare the Development Service Source
|
|
||||||
|
|
||||||
`pnpm dev` builds the development service from a sibling checkout of [`clash-verge-service-ipc`](https://github.com/clash-verge-rev/clash-verge-service-ipc). The binaries downloaded by `pnpm prebuild` do not replace this source checkout.
|
|
||||||
|
|
||||||
Check the `clash_verge_service_ipc` dependency in [`src-tauri/Cargo.toml`](src-tauri/Cargo.toml), then clone the service repository alongside `clash-verge-rev` and check out the matching tag. If the sibling directory already exists, reuse it and check its version rather than overwriting it. Without this checkout, `pnpm dev` fails when it tries to access `../clash-verge-service-ipc/Cargo.toml`.
|
|
||||||
|
|
||||||
### Run the Development Server
|
### Run the Development Server
|
||||||
|
|
||||||
Run these commands from the root of `clash-verge-rev`:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm dev # Build the development service and start the desktop app
|
pnpm dev # Standard
|
||||||
pnpm dev:diff # Alias of pnpm dev
|
pnpm dev:diff # If an app instance already exists
|
||||||
pnpm dev:tauri # Run Tauri directly with the tauri-dev feature
|
pnpm dev:tauri # Run Tauri development mode
|
||||||
```
|
```
|
||||||
|
|
||||||
For frontend-only development, use `pnpm web:dev`. It does not start the Rust backend or build the development service.
|
|
||||||
|
|
||||||
### Build the Project
|
### Build the Project
|
||||||
|
|
||||||
Standard build:
|
Standard build:
|
||||||
|
|||||||
Generated
+253
-138
@@ -33,7 +33,6 @@ dependencies = [
|
|||||||
"cipher",
|
"cipher",
|
||||||
"cpubits",
|
"cpubits",
|
||||||
"cpufeatures 0.3.0",
|
"cpufeatures 0.3.0",
|
||||||
"zeroize",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -136,6 +135,15 @@ version = "1.0.104"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arbitrary"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||||
|
dependencies = [
|
||||||
|
"derive_arbitrary",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arboard"
|
name = "arboard"
|
||||||
version = "3.6.1"
|
version = "3.6.1"
|
||||||
@@ -1111,7 +1119,7 @@ dependencies = [
|
|||||||
"getrandom 0.4.3",
|
"getrandom 0.4.3",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"minisign-verify",
|
"minisign-verify 0.3.0",
|
||||||
"nanoid",
|
"nanoid",
|
||||||
"network-interface",
|
"network-interface",
|
||||||
"objc2",
|
"objc2",
|
||||||
@@ -1130,7 +1138,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml_ng",
|
"serde_yaml_ng",
|
||||||
"sha2 0.11.1",
|
"sha2 0.11.0",
|
||||||
"smartstring",
|
"smartstring",
|
||||||
"socket2",
|
"socket2",
|
||||||
"specta",
|
"specta",
|
||||||
@@ -1167,7 +1175,7 @@ dependencies = [
|
|||||||
"windows-service",
|
"windows-service",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
"winreg 0.56.0",
|
"winreg 0.56.0",
|
||||||
"zip",
|
"zip 9.0.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1239,7 +1247,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2 0.11.1",
|
"sha2 0.11.0",
|
||||||
"strum",
|
"strum",
|
||||||
"strum_macros",
|
"strum_macros",
|
||||||
"sysproxy",
|
"sysproxy",
|
||||||
@@ -1334,7 +1342,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1454,12 +1462,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "constant_time_eq"
|
name = "constant_time_eq"
|
||||||
version = "0.6.1"
|
version = "0.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9eb6feb175df65d0a3ccd3e73484e940d0ac664e8034f0519fd49a93db3071d9"
|
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
|
||||||
dependencies = [
|
|
||||||
"rustversion",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cookie"
|
name = "cookie"
|
||||||
@@ -1862,6 +1867,17 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_arbitrary"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_more"
|
name = "derive_more"
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
@@ -2015,7 +2031,7 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"option-ext",
|
"option-ext",
|
||||||
"redox_users 0.5.2",
|
"redox_users 0.5.2",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2274,7 +2290,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2790,7 +2806,7 @@ dependencies = [
|
|||||||
"gobject-sys 0.21.5",
|
"gobject-sys 0.21.5",
|
||||||
"libc",
|
"libc",
|
||||||
"system-deps 7.0.8",
|
"system-deps 7.0.8",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3344,7 +3360,7 @@ dependencies = [
|
|||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-core 0.61.2",
|
"windows-core 0.62.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3935,9 +3951,9 @@ checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.191"
|
version = "0.2.190"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
|
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libdbus-sys"
|
name = "libdbus-sys"
|
||||||
@@ -4032,11 +4048,11 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lzma-rust2"
|
name = "lzma-rust2"
|
||||||
version = "0.23.0"
|
version = "0.21.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f1c46f3fcd2e888afdb01a07efc071cb21300e07c68d32c3e8b01b5c8cb1fcaa"
|
checksum = "fde178a3caf126c440fa15628147772d8ee590a39477d711353fbe2e58a73a5b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sha2 0.11.1",
|
"sha2 0.11.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4151,6 +4167,12 @@ 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"
|
||||||
@@ -4215,7 +4237,7 @@ dependencies = [
|
|||||||
"png 0.18.1",
|
"png 0.18.1",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4382,7 +4404,7 @@ version = "0.50.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4859,7 +4881,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5321,9 +5343,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppmd-rust"
|
name = "ppmd-rust"
|
||||||
version = "1.5.0"
|
version = "1.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "196a7c80b9a7652aba7cc070827516c2abe4ccdf53d128e1944003cf5726cff1"
|
checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
@@ -5524,7 +5546,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5974,13 +5996,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-ini"
|
name = "rust-ini"
|
||||||
version = "0.21.1"
|
version = "0.21.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f"
|
checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"ordered-multimap",
|
"ordered-multimap",
|
||||||
"trim-in-place",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6028,7 +6049,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.4.15",
|
"linux-raw-sys 0.4.15",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6041,14 +6062,14 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.12.1",
|
"linux-raw-sys 0.12.1",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.23.46"
|
version = "0.23.45"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf"
|
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
"log",
|
"log",
|
||||||
@@ -6100,7 +6121,7 @@ dependencies = [
|
|||||||
"security-framework",
|
"security-framework",
|
||||||
"security-framework-sys",
|
"security-framework-sys",
|
||||||
"webpki-root-certs",
|
"webpki-root-certs",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6389,9 +6410,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.154"
|
version = "1.0.151"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
|
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -6576,9 +6597,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.11.1"
|
version = "0.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19"
|
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures 0.3.0",
|
"cpufeatures 0.3.0",
|
||||||
@@ -6601,6 +6622,7 @@ 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",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -6620,6 +6642,17 @@ 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"
|
||||||
@@ -6630,6 +6663,16 @@ 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"
|
||||||
@@ -6720,7 +6763,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6835,12 +6878,6 @@ 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"
|
||||||
@@ -6900,9 +6937,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "swift-rs"
|
name = "swift-rs"
|
||||||
version = "1.0.9"
|
version = "1.0.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cefd87076fd385308ee4aff597256c902f1fbb4d031de1558efc12f8ffefadd9"
|
checksum = "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -7069,9 +7106,9 @@ checksum = "c0e973b34477b7823833469eb0f5a3a60370fef7a453e02d751b59180d0a5a05"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tao"
|
name = "tao"
|
||||||
version = "0.37.2"
|
version = "0.37.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d5777b06fe6435126ae8d816eba269e050e1e10d302cf5461e9c49eccabc3548"
|
checksum = "f37f381f4e048e6cdf038b5705f8cf14ad108279d46eb968140a7b291aba9400"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.2",
|
"bitflags 2.13.2",
|
||||||
"block2",
|
"block2",
|
||||||
@@ -7149,9 +7186,9 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri"
|
name = "tauri"
|
||||||
version = "2.12.4"
|
version = "2.12.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f316751027fc2644221d1a448564d519efb07067fda584879d9cda966391d212"
|
checksum = "ed99ee9694a2deb776d91cae48ac7411ddfc89ecae2f9b5041111d8c88f2ace9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -7197,16 +7234,16 @@ dependencies = [
|
|||||||
"tray-icon",
|
"tray-icon",
|
||||||
"url",
|
"url",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com",
|
"webview2-com 0.39.1",
|
||||||
"window-vibrancy",
|
"window-vibrancy",
|
||||||
"windows 0.62.2",
|
"windows 0.62.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-build"
|
name = "tauri-build"
|
||||||
version = "2.7.3"
|
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 = "641dd5fd297c99ab92717b0428cca5b9c6f960b2ac96ca131b3eebc9d70b8ddc"
|
checksum = "59563ca5b331fd97f27ee9672ec8d36d33bb3c9e2c9f6710a1a5687256052e45"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cargo_toml",
|
"cargo_toml",
|
||||||
@@ -7218,8 +7255,6 @@ 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",
|
||||||
@@ -7227,11 +7262,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-codegen"
|
name = "tauri-codegen"
|
||||||
version = "2.7.3"
|
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 = "a6585c5b536c00f039d12ce70a9755172d5423bbcfd3969863baa70314c3e02f"
|
checksum = "e50213e463086ecc283d7e9f51c30cc0fd304ad457425158018be26dbf0d0a80"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.23.1",
|
"base64 0.22.1",
|
||||||
"brotli",
|
"brotli",
|
||||||
"ico",
|
"ico",
|
||||||
"json-patch",
|
"json-patch",
|
||||||
@@ -7254,9 +7289,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-macros"
|
name = "tauri-macros"
|
||||||
version = "2.7.3"
|
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 = "e2e83a0cfa1bae376e8fbbdc0b1692707aed50cd6072ce85cae4789af5d6c19f"
|
checksum = "8129fb43bb6e0d6b73ad3315cd172beaab17cd28899c594b1b32c80edbbfd99c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -7312,9 +7347,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-clipboard-manager"
|
name = "tauri-plugin-clipboard-manager"
|
||||||
version = "2.5.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 = "9be5ed8d94c33afe8377e885119069f6508780659271d0a25cfc54d441f2bc81"
|
checksum = "333d3b6beeb1c2c7fc040233d4ea7b6c03a9f8f039592b7400833c43fb5b568a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arboard",
|
"arboard",
|
||||||
"log",
|
"log",
|
||||||
@@ -7327,9 +7362,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-deep-link"
|
name = "tauri-plugin-deep-link"
|
||||||
version = "2.6.2"
|
version = "2.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1b3c64e24ebcbc63aaa2d88a9c51e2507084b46620baf39ecb0baa0738a08bb4"
|
checksum = "4ac7a92a46ab5c88f44532ca50906d6e448a948d4ddf8c5376ab125e3260f736"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dunce",
|
"dunce",
|
||||||
"plist",
|
"plist",
|
||||||
@@ -7375,9 +7410,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-dialog"
|
name = "tauri-plugin-dialog"
|
||||||
version = "2.8.3"
|
version = "2.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b7461aaacf1901915ea7b9f5cd74f8224a351ed96af0a62c07fce934003cb34b"
|
checksum = "daf9a5c92e39bdd84f22f6be9230e39d0c4e140345f71ffe6058dca4f71127cf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
@@ -7393,9 +7428,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-fs"
|
name = "tauri-plugin-fs"
|
||||||
version = "2.7.1"
|
version = "2.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5d58eb1d4984e535f47a642ffec09a2da6f5c18a12b5902d12126cb7626d628b"
|
checksum = "466eb32466fe5021b6728b06862f9ff14a9f7e1af890047654e03a96d7c0e1bb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"dunce",
|
"dunce",
|
||||||
@@ -7417,9 +7452,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-global-shortcut"
|
name = "tauri-plugin-global-shortcut"
|
||||||
version = "2.4.1"
|
version = "2.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "93ff17919fe09852d269bd37b1d3d2e993b9dbb514afe7acbf3346c1d3627e2d"
|
checksum = "0bd2e1f725891a1613af8f25bf918ba3357654b0fd3f2d794c0ce76cbdd92ed2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"global-hotkey",
|
"global-hotkey",
|
||||||
"log",
|
"log",
|
||||||
@@ -7432,9 +7467,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-http"
|
name = "tauri-plugin-http"
|
||||||
version = "2.8.3"
|
version = "2.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2c57e3451553f95a1db3bfed6a358a77ce5080b6399a242c6eadac113f97abd2"
|
checksum = "93e9d6b3319e101bc1be27cfa0fb4054d7784a12509c67ead0033b82180e5ed1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"cookie_store",
|
"cookie_store",
|
||||||
@@ -7451,13 +7486,13 @@ dependencies = [
|
|||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
"tokio",
|
"tokio",
|
||||||
"url",
|
"url",
|
||||||
"urlpattern",
|
"urlpattern 0.3.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-mihomo"
|
name = "tauri-plugin-mihomo"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo?rev=7a03d16f4d39b1bb885b7babe66b9e153e34b615#7a03d16f4d39b1bb885b7babe66b9e153e34b615"
|
source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo?branch=main#7a03d16f4d39b1bb885b7babe66b9e153e34b615"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"clashmap",
|
"clashmap",
|
||||||
@@ -7481,9 +7516,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-notification"
|
name = "tauri-plugin-notification"
|
||||||
version = "2.7.0"
|
version = "2.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b939d7b19c8af1381a72ef8474b3aba2b56c1ddac2c23f0c8e96abbcf86b198b"
|
checksum = "533770889abb60a0407d5a32096182f0e6463f5a3dfaa675be94580aa14070e1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"notify-rust",
|
"notify-rust",
|
||||||
@@ -7496,14 +7531,13 @@ 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.2"
|
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 = "53810be1b4fed22bb6ac3f4cab5a2b80d9391d1af436bda145607532bc0bb219"
|
checksum = "d39404b2572927e0d3ed30a7ca226037f7508d22e79c96ee6ef0d42b6a4b3345"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dunce",
|
"dunce",
|
||||||
"glob",
|
"glob",
|
||||||
@@ -7533,12 +7567,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-shell"
|
name = "tauri-plugin-shell"
|
||||||
version = "2.5.0"
|
version = "2.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8567d42b2c7264355daa1df415ec77f52aa42e885924c86a969be2e920e05521"
|
checksum = "094bdbc47e5efe36544ba21b3a53ab5c724150236cf55e6c9be46191deb6eab6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"libc",
|
|
||||||
"log",
|
"log",
|
||||||
"open",
|
"open",
|
||||||
"os_pipe",
|
"os_pipe",
|
||||||
@@ -7551,24 +7584,22 @@ 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.14.1"
|
version = "2.13.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d957abbcb3f9f6c76d22a3a0f012f30880640d0bccb5634d2b12a9937e4c872b"
|
checksum = "3cb0b2ea3e85ca287990d3859a29cc5cb18024240fd78f62e027fb7963670f18"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.23.1",
|
"base64 0.22.1",
|
||||||
"dirs 7.0.0",
|
"dirs 7.0.0",
|
||||||
"flate2",
|
"flate2",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"infer",
|
"infer",
|
||||||
"libc",
|
|
||||||
"log",
|
"log",
|
||||||
"minisign-verify",
|
"minisign-verify 0.2.5",
|
||||||
"osakit",
|
"osakit",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"reqwest 0.13.5",
|
"reqwest 0.13.5",
|
||||||
@@ -7585,14 +7616,14 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"url",
|
"url",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
"zip",
|
"zip 4.6.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-wdio-webdriver"
|
name = "tauri-plugin-wdio-webdriver"
|
||||||
version = "1.5.0"
|
version = "1.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4f76676d510d271d80abcf8b0552987b5b4f0eae5e3f906e9b5660d43c97c62e"
|
checksum = "52c5e97428174a52b7f4357bbc4535fcce52c6a06c8ee7a379502f7cda1b6045"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -7616,9 +7647,9 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
"uuid",
|
"uuid",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com",
|
"webview2-com 0.38.2",
|
||||||
"windows 0.62.2",
|
"windows 0.61.3",
|
||||||
"windows-core 0.62.2",
|
"windows-core 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7638,9 +7669,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "2.12.2"
|
version = "2.12.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c55766d99cea69055904d1735b93c2049302b2e09effd462120deb5581d78e9"
|
checksum = "c2a57a2a9b8c8b7fc00d35b510fe808ded7f9f1b44fc1cdda6aee38632ea5413"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cookie",
|
"cookie",
|
||||||
"dpi",
|
"dpi",
|
||||||
@@ -7657,15 +7688,15 @@ dependencies = [
|
|||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
"url",
|
"url",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com",
|
"webview2-com 0.39.1",
|
||||||
"windows 0.62.2",
|
"windows 0.62.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime-wry"
|
name = "tauri-runtime-wry"
|
||||||
version = "2.12.2"
|
version = "2.12.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8d61892587aea2a85245bcaec98c6fe46268874c0a0deae5535fb71ca7b93eae"
|
checksum = "7f9e7e0c9f80a5130773cfabb6df4dc924518fb1b2c923726966804634b57d22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gtk",
|
"gtk",
|
||||||
"http",
|
"http",
|
||||||
@@ -7683,7 +7714,7 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com",
|
"webview2-com 0.39.1",
|
||||||
"windows 0.62.2",
|
"windows 0.62.2",
|
||||||
"wry",
|
"wry",
|
||||||
]
|
]
|
||||||
@@ -7721,9 +7752,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-utils"
|
name = "tauri-utils"
|
||||||
version = "2.10.2"
|
version = "2.10.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "362ab21cd4cc713f2a02b1a9b5688198b1541cfb6907c4281e9190e0f0ef4aa3"
|
checksum = "2ff55a614843b9a3f010f211df637c94df2607175b8d0f6e0f4a7c19e18621ee"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"brotli",
|
"brotli",
|
||||||
@@ -7752,7 +7783,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",
|
"urlpattern 0.6.0",
|
||||||
"uuid",
|
"uuid",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
@@ -7786,10 +7817,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.4.3",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix 1.1.4",
|
"rustix 1.1.4",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7871,7 +7902,7 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
"semver 0.11.0",
|
"semver 0.11.0",
|
||||||
"sha2 0.9.9",
|
"sha2 0.9.9",
|
||||||
"signal-hook",
|
"signal-hook 0.1.17",
|
||||||
"terminfo",
|
"terminfo",
|
||||||
"termios",
|
"termios",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
@@ -8042,9 +8073,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.53.3"
|
version = "1.53.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e"
|
checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -8104,9 +8135,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.20"
|
version = "0.7.19"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b"
|
checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@@ -8455,7 +8486,7 @@ dependencies = [
|
|||||||
"png 0.18.1",
|
"png 0.18.1",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -8469,12 +8500,6 @@ 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"
|
||||||
@@ -8534,9 +8559,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typed-path"
|
name = "typed-path"
|
||||||
version = "0.13.0"
|
version = "0.12.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9ce898823d8cd0f028662d2f349694b1af0a5cc9be83f1d213a00d3147445e1e"
|
checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typeid"
|
name = "typeid"
|
||||||
@@ -8564,7 +8589,48 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"memoffset",
|
"memoffset",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"windows-sys 0.60.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]]
|
||||||
@@ -8626,6 +8692,18 @@ 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"
|
||||||
@@ -9017,6 +9095,20 @@ 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"
|
||||||
@@ -9024,7 +9116,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",
|
"webview2-com-sys 0.39.1",
|
||||||
"windows 0.62.2",
|
"windows 0.62.2",
|
||||||
"windows-core 0.62.2",
|
"windows-core 0.62.2",
|
||||||
]
|
]
|
||||||
@@ -9040,6 +9132,17 @@ 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"
|
||||||
@@ -9106,7 +9209,7 @@ version = "0.1.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -9127,7 +9230,7 @@ dependencies = [
|
|||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
"objc2-quartz-core",
|
"objc2-quartz-core",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
"windows-version",
|
"windows-version",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -9702,7 +9805,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
|
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -9743,11 +9846,11 @@ checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wry"
|
name = "wry"
|
||||||
version = "0.57.1"
|
version = "0.57.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "759c50ddad0ef9913bb5496cb9d509a50433a3b78cb0238d61105526589cf0e9"
|
checksum = "a819957a01b3119af85e638a38d242af76dbc87d130dca67bfd0441072e21ff0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.23.1",
|
"base64 0.22.1",
|
||||||
"block2",
|
"block2",
|
||||||
"cookie",
|
"cookie",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
@@ -9768,6 +9871,7 @@ dependencies = [
|
|||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
"objc2-ui-kit",
|
"objc2-ui-kit",
|
||||||
"objc2-web-kit",
|
"objc2-web-kit",
|
||||||
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"sha2 0.10.9",
|
"sha2 0.10.9",
|
||||||
@@ -9778,7 +9882,7 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webkit2gtk-sys",
|
"webkit2gtk-sys",
|
||||||
"webview2-com",
|
"webview2-com 0.39.1",
|
||||||
"windows 0.62.2",
|
"windows 0.62.2",
|
||||||
"windows-core 0.62.2",
|
"windows-core 0.62.2",
|
||||||
"windows-version",
|
"windows-version",
|
||||||
@@ -10034,9 +10138,21 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zip"
|
name = "zip"
|
||||||
version = "9.1.3"
|
version = "4.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1156b39d142a34b999c2310a50455e0cb3760bf7efad0122f796d813bee26ccb"
|
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
|
||||||
|
dependencies = [
|
||||||
|
"arbitrary",
|
||||||
|
"crc32fast",
|
||||||
|
"indexmap 2.14.0",
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zip"
|
||||||
|
version = "9.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e49ade69c4434c55dd651d36fe05f38e0578db948b773cbc01c48c4b5e560692"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"bzip2",
|
"bzip2",
|
||||||
@@ -10055,7 +10171,6 @@ dependencies = [
|
|||||||
"sha1 0.11.0",
|
"sha1 0.11.0",
|
||||||
"time",
|
"time",
|
||||||
"typed-path",
|
"typed-path",
|
||||||
"windows-sys 0.61.2",
|
|
||||||
"zeroize",
|
"zeroize",
|
||||||
"zopfli",
|
"zopfli",
|
||||||
"zstd",
|
"zstd",
|
||||||
@@ -10087,27 +10202,27 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zstd"
|
name = "zstd"
|
||||||
version = "0.14.3"
|
version = "0.13.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "db3ae9bf90af6e40197bef767eeefc46ad6397420fe873830bd85e5fbbfb3c70"
|
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zstd-safe",
|
"zstd-safe",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zstd-safe"
|
name = "zstd-safe"
|
||||||
version = "8.2.0"
|
version = "7.2.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ded520f16b29e1a9b771c722221b2b0a0398c2316d9bb3c42b458a5414d7434b"
|
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zstd-sys",
|
"zstd-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zstd-sys"
|
name = "zstd-sys"
|
||||||
version = "2.1.2+zstd.1.5.7"
|
version = "2.0.16+zstd.1.5.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "481ada9c543b2323cbb7bbdbb5249177fbf0188ed959ed2c15e4b22421c84ed3"
|
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
|
|||||||
+4
-4
@@ -61,13 +61,13 @@ tracing-estuary = { git = "https://github.com/Tunglies/tracing-estuary.git", bra
|
|||||||
tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" }
|
tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" }
|
||||||
tracing = "0.1.44"
|
tracing = "0.1.44"
|
||||||
|
|
||||||
tauri = { version = "2.12.4" }
|
tauri = { version = "2.12.1" }
|
||||||
tauri-plugin-clipboard-manager = "2.5.0"
|
tauri-plugin-clipboard-manager = "2.4.1"
|
||||||
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"
|
||||||
criterion = { version = "0.8.2", features = ["async_tokio"] }
|
criterion = { version = "0.8.2", features = ["async_tokio"] }
|
||||||
tokio = { version = "1.53.3", features = [
|
tokio = { version = "1.53.2", features = [
|
||||||
"rt-multi-thread",
|
"rt-multi-thread",
|
||||||
"macros",
|
"macros",
|
||||||
"time",
|
"time",
|
||||||
@@ -88,7 +88,7 @@ rust_iso3166 = "0.2.0"
|
|||||||
smartstring = { version = "1.0.1" }
|
smartstring = { version = "1.0.1" }
|
||||||
|
|
||||||
serde = { version = "1.0.229" }
|
serde = { version = "1.0.229" }
|
||||||
serde_json = { version = "1.0.154" }
|
serde_json = { version = "1.0.151" }
|
||||||
serde_yaml_ng = { version = "0.10.0" }
|
serde_yaml_ng = { version = "0.10.0" }
|
||||||
bitflags = { version = "2.13.2" }
|
bitflags = { version = "2.13.2" }
|
||||||
socket2 = "0.6.5"
|
socket2 = "0.6.5"
|
||||||
|
|||||||
+3
-23
@@ -1,43 +1,23 @@
|
|||||||
## v2.5.9
|
## v2.5.9
|
||||||
|
|
||||||
- **Mihomo(Meta) 内核升级至 v1.19.33**
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong> 🐞 修复问题 </strong></summary>
|
<summary><strong> 🐞 修复问题 </strong></summary>
|
||||||
|
|
||||||
- 修复规则扩展包含空列表项时,「编辑规则」界面报错的问题
|
- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置的问题
|
||||||
- 修复 重启应用后定时自动备份不执行的问题
|
- 修复「首页」网站测速失败后卡片一直显示测试中的问题
|
||||||
- 修复托盘测速与代理页行为不一致的问题,移除托盘「测试延迟」入口
|
|
||||||
- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置
|
|
||||||
- 修复 首页 网站测速失败后卡片一直显示测试中
|
|
||||||
- 修复 代理 页展开分组后不显示最新测速结果,需切换节点才刷新
|
|
||||||
- 修复 代理 在全局模式下无法展开节点组
|
|
||||||
- 修复 自定义 CSS 无法修改编辑器字体和字号的问题
|
|
||||||
- 修复 WebDAV 设置清空后无法保存
|
|
||||||
|
|
||||||
**🍎 macOS**
|
|
||||||
|
|
||||||
- macOS:修复 Bigsur 上首页测速和流量图无法显示的问题
|
|
||||||
|
|
||||||
**🐧 Linux**
|
|
||||||
|
|
||||||
- Linux:修复 deb/rpm 安装包错误包含多余的 `frontend-contract` 可执行文件,导致下游重新打包失败的问题
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||||
|
|
||||||
- 新增虚拟网卡设置的「DNS 模式」选项,支持配置 mihomo 系统 DNS 行为
|
|
||||||
- 新增「IP 信息」自动刷新开关,关闭后仍可手动刷新
|
|
||||||
- 新增 Muse AI 解锁检测,针对 Muse AI 特有的 Cloudflare 人机验证增加「Risky」状态标记
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong> 🚀 优化改进 </strong></summary>
|
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||||
|
|
||||||
- 优化「链式代理」:开启前需确认网速影响提示,空面板新增「关闭」按钮
|
- 记住「启用系统标题栏」设置,Windows/macOS 即时生效,Linux 重启后生效
|
||||||
|
|
||||||
**🖥️ Windows**
|
**🖥️ Windows**
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ tray:
|
|||||||
global: عام
|
global: عام
|
||||||
profiles: الملفات التعريفية
|
profiles: الملفات التعريفية
|
||||||
proxies: وكلاء
|
proxies: وكلاء
|
||||||
|
testLatency: اختبار التأخير
|
||||||
|
testingLatency: جارٍ اختبار التأخير…
|
||||||
systemProxy: وكيل النظام
|
systemProxy: وكيل النظام
|
||||||
tunMode: وضع TUN
|
tunMode: وضع TUN
|
||||||
closeAllConnections: إغلاق كل الاتصالات
|
closeAllConnections: إغلاق كل الاتصالات
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ tray:
|
|||||||
global: Global
|
global: Global
|
||||||
profiles: Profile
|
profiles: Profile
|
||||||
proxies: Proxy
|
proxies: Proxy
|
||||||
|
testLatency: Latenz testen
|
||||||
|
testingLatency: Latenz wird getestet…
|
||||||
systemProxy: Systemproxy
|
systemProxy: Systemproxy
|
||||||
tunMode: TUN-Modus
|
tunMode: TUN-Modus
|
||||||
closeAllConnections: Alle Verbindungen schließen
|
closeAllConnections: Alle Verbindungen schließen
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ tray:
|
|||||||
global: Global
|
global: Global
|
||||||
profiles: Profiles
|
profiles: Profiles
|
||||||
proxies: Proxies
|
proxies: Proxies
|
||||||
|
testLatency: Test Latency
|
||||||
|
testingLatency: Testing latency…
|
||||||
systemProxy: System Proxy
|
systemProxy: System Proxy
|
||||||
tunMode: TUN Mode
|
tunMode: TUN Mode
|
||||||
closeAllConnections: Close All Connections
|
closeAllConnections: Close All Connections
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ tray:
|
|||||||
global: Global
|
global: Global
|
||||||
profiles: Perfiles
|
profiles: Perfiles
|
||||||
proxies: Proxies
|
proxies: Proxies
|
||||||
|
testLatency: Probar latencia
|
||||||
|
testingLatency: Probando latencia…
|
||||||
systemProxy: Proxy del sistema
|
systemProxy: Proxy del sistema
|
||||||
tunMode: Modo TUN
|
tunMode: Modo TUN
|
||||||
closeAllConnections: Cerrar todas las conexiones
|
closeAllConnections: Cerrar todas las conexiones
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ tray:
|
|||||||
global: سراسری
|
global: سراسری
|
||||||
profiles: پروفایلها
|
profiles: پروفایلها
|
||||||
proxies: پروکسیها
|
proxies: پروکسیها
|
||||||
|
testLatency: آزمایش تأخیر
|
||||||
|
testingLatency: در حال آزمایش تأخیر…
|
||||||
systemProxy: پروکسی سیستم
|
systemProxy: پروکسی سیستم
|
||||||
tunMode: حالت TUN
|
tunMode: حالت TUN
|
||||||
closeAllConnections: بستن همه اتصالها
|
closeAllConnections: بستن همه اتصالها
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ tray:
|
|||||||
global: Global
|
global: Global
|
||||||
profiles: Profil
|
profiles: Profil
|
||||||
proxies: Proksi
|
proxies: Proksi
|
||||||
|
testLatency: Uji Latensi
|
||||||
|
testingLatency: Menguji latensi…
|
||||||
systemProxy: Proksi Sistem
|
systemProxy: Proksi Sistem
|
||||||
tunMode: Mode TUN
|
tunMode: Mode TUN
|
||||||
closeAllConnections: Tutup Semua Koneksi
|
closeAllConnections: Tutup Semua Koneksi
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ tray:
|
|||||||
global: グローバル
|
global: グローバル
|
||||||
profiles: プロファイル
|
profiles: プロファイル
|
||||||
proxies: プロキシ
|
proxies: プロキシ
|
||||||
|
testLatency: 遅延テスト
|
||||||
|
testingLatency: 遅延をテスト中…
|
||||||
systemProxy: システムプロキシ
|
systemProxy: システムプロキシ
|
||||||
tunMode: TUN モード
|
tunMode: TUN モード
|
||||||
closeAllConnections: すべての接続を閉じる
|
closeAllConnections: すべての接続を閉じる
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ tray:
|
|||||||
global: 글로벌
|
global: 글로벌
|
||||||
profiles: 프로필
|
profiles: 프로필
|
||||||
proxies: 프록시
|
proxies: 프록시
|
||||||
|
testLatency: 지연 테스트
|
||||||
|
testingLatency: 지연 테스트 중…
|
||||||
systemProxy: 시스템 프록시
|
systemProxy: 시스템 프록시
|
||||||
tunMode: TUN 모드
|
tunMode: TUN 모드
|
||||||
closeAllConnections: 모든 연결 닫기
|
closeAllConnections: 모든 연결 닫기
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ tray:
|
|||||||
global: Global
|
global: Global
|
||||||
profiles: Perfis
|
profiles: Perfis
|
||||||
proxies: Proxies
|
proxies: Proxies
|
||||||
|
testLatency: Test Latency
|
||||||
|
testingLatency: Testing latency…
|
||||||
systemProxy: Proxy do sistema
|
systemProxy: Proxy do sistema
|
||||||
tunMode: Modo TUN
|
tunMode: Modo TUN
|
||||||
closeAllConnections: Fechar todas as conexões
|
closeAllConnections: Fechar todas as conexões
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ tray:
|
|||||||
global: Глобальный
|
global: Глобальный
|
||||||
profiles: Профили
|
profiles: Профили
|
||||||
proxies: Прокси
|
proxies: Прокси
|
||||||
|
testLatency: Проверить задержку
|
||||||
|
testingLatency: Проверка задержки…
|
||||||
systemProxy: Системный прокси
|
systemProxy: Системный прокси
|
||||||
tunMode: Режим TUN
|
tunMode: Режим TUN
|
||||||
closeAllConnections: Закрыть все соединения
|
closeAllConnections: Закрыть все соединения
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ tray:
|
|||||||
global: Küresel
|
global: Küresel
|
||||||
profiles: Profiller
|
profiles: Profiller
|
||||||
proxies: Vekil'ler
|
proxies: Vekil'ler
|
||||||
|
testLatency: Gecikmeyi Test Et
|
||||||
|
testingLatency: Gecikme test ediliyor…
|
||||||
systemProxy: Sistem Vekil'i
|
systemProxy: Sistem Vekil'i
|
||||||
tunMode: TUN Modu
|
tunMode: TUN Modu
|
||||||
closeAllConnections: Tüm Bağlantıları Kapat
|
closeAllConnections: Tüm Bağlantıları Kapat
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ tray:
|
|||||||
global: Глобаль
|
global: Глобаль
|
||||||
profiles: Профильләр
|
profiles: Профильләр
|
||||||
proxies: Проксилар
|
proxies: Проксилар
|
||||||
|
testLatency: Кичектерүне тикшерү
|
||||||
|
testingLatency: Кичектерү тикшерелә…
|
||||||
systemProxy: Системалы прокси
|
systemProxy: Системалы прокси
|
||||||
tunMode: TUN режимы
|
tunMode: TUN режимы
|
||||||
closeAllConnections: Барлык тоташуларны ябу
|
closeAllConnections: Барлык тоташуларны ябу
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ tray:
|
|||||||
global: 全局
|
global: 全局
|
||||||
profiles: 订阅
|
profiles: 订阅
|
||||||
proxies: 代理
|
proxies: 代理
|
||||||
|
testLatency: 测试延迟
|
||||||
|
testingLatency: 正在测试延迟…
|
||||||
systemProxy: 系统代理
|
systemProxy: 系统代理
|
||||||
tunMode: TUN 模式
|
tunMode: TUN 模式
|
||||||
closeAllConnections: 关闭所有连接
|
closeAllConnections: 关闭所有连接
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ tray:
|
|||||||
global: 全域
|
global: 全域
|
||||||
profiles: 訂閱
|
profiles: 訂閱
|
||||||
proxies: 代理
|
proxies: 代理
|
||||||
|
testLatency: 測試延遲
|
||||||
|
testingLatency: 正在測試延遲…
|
||||||
systemProxy: 系統代理
|
systemProxy: 系統代理
|
||||||
tunMode: 虛擬網路介面卡模式
|
tunMode: 虛擬網路介面卡模式
|
||||||
closeAllConnections: 關閉所有連線
|
closeAllConnections: 關閉所有連線
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ 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;
|
||||||
@@ -31,7 +30,6 @@ enum UnlockCheck {
|
|||||||
ChatGptWeb,
|
ChatGptWeb,
|
||||||
Claude,
|
Claude,
|
||||||
Gemini,
|
Gemini,
|
||||||
MuseAi,
|
|
||||||
YouTubePremium,
|
YouTubePremium,
|
||||||
BahamutAnime,
|
BahamutAnime,
|
||||||
Netflix,
|
Netflix,
|
||||||
@@ -48,7 +46,6 @@ 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,
|
||||||
@@ -69,7 +66,6 @@ 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,
|
||||||
@@ -87,7 +83,6 @@ 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,
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,3 @@
|
|||||||
//! Linux-only termination signal bridge.
|
|
||||||
//!
|
|
||||||
//! Upstream Tauri delivers session-end and termination events to the app on
|
|
||||||
//! Windows and macOS, but has no equivalent on Linux: a desktop session may
|
|
||||||
//! end without any window or run event reaching the process. Linux builds
|
|
||||||
//! keep this crate to listen for SIGTERM, SIGINT and SIGHUP and forward them
|
|
||||||
//! to the app quit flow, so system proxy and TUN cleanup still runs before
|
|
||||||
//! the process dies. Non-Linux builds compile this crate to nothing and must
|
|
||||||
//! not depend on it.
|
|
||||||
|
|
||||||
#![cfg(target_os = "linux")]
|
|
||||||
|
|
||||||
use std::sync::{
|
use std::sync::{
|
||||||
OnceLock,
|
OnceLock,
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
@@ -17,13 +5,20 @@ use std::sync::{
|
|||||||
|
|
||||||
use clash_verge_logging::{Type, logging};
|
use clash_verge_logging::{Type, logging};
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
mod unix;
|
mod unix;
|
||||||
|
#[cfg(windows)]
|
||||||
|
mod windows;
|
||||||
|
|
||||||
pub(crate) static RUNTIME: OnceLock<Option<tokio::runtime::Runtime>> = OnceLock::new();
|
pub(crate) static RUNTIME: OnceLock<Option<tokio::runtime::Runtime>> = OnceLock::new();
|
||||||
pub(crate) static SHUTDOWN_LATCH: ShutdownLatch = ShutdownLatch::new();
|
pub(crate) static SHUTDOWN_LATCH: ShutdownLatch = ShutdownLatch::new();
|
||||||
|
|
||||||
/// Serializes signal-triggered cleanups: a signal arriving while a quit
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
/// attempt is still in flight is dropped instead of overlapping it.
|
pub enum ShutdownOutcome {
|
||||||
|
Committed,
|
||||||
|
Canceled,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct ShutdownLatch {
|
pub(crate) struct ShutdownLatch {
|
||||||
is_cleaning_up: AtomicBool,
|
is_cleaning_up: AtomicBool,
|
||||||
}
|
}
|
||||||
@@ -41,18 +36,17 @@ impl ShutdownLatch {
|
|||||||
.is_ok()
|
.is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish(&self) {
|
fn finish(&self, outcome: ShutdownOutcome) {
|
||||||
self.is_cleaning_up.store(false, Ordering::Release);
|
if matches!(outcome, ShutdownOutcome::Canceled) {
|
||||||
|
self.is_cleaning_up.store(false, Ordering::Release);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawns the Linux signal loop on a dedicated runtime so it survives the
|
|
||||||
/// teardown of any app-owned runtime. The handler runs once per accepted
|
|
||||||
/// signal; the quit flow it invokes owns the exit-or-cancel decision.
|
|
||||||
pub fn register<F, Fut>(f: F)
|
pub fn register<F, Fut>(f: F)
|
||||||
where
|
where
|
||||||
F: Fn() -> Fut + Send + Sync + 'static,
|
F: Fn() -> Fut + Send + Sync + 'static,
|
||||||
Fut: Future<Output = ()> + Send + 'static,
|
Fut: Future<Output = ShutdownOutcome> + Send + 'static,
|
||||||
{
|
{
|
||||||
RUNTIME.get_or_init(|| match tokio::runtime::Runtime::new() {
|
RUNTIME.get_or_init(|| match tokio::runtime::Runtime::new() {
|
||||||
Ok(rt) => Some(rt),
|
Ok(rt) => Some(rt),
|
||||||
@@ -67,22 +61,36 @@ where
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
unix::register(f);
|
unix::register(f);
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
windows::register(f);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::ShutdownLatch;
|
use super::{ShutdownLatch, ShutdownOutcome};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn finished_attempt_releases_latch_for_the_next_signal() {
|
fn canceled_shutdown_releases_latch_and_permits_next_attempt() {
|
||||||
let latch = ShutdownLatch::new();
|
let latch = ShutdownLatch::new();
|
||||||
|
|
||||||
assert!(latch.try_begin());
|
assert!(latch.try_begin());
|
||||||
assert!(!latch.try_begin());
|
assert!(!latch.try_begin());
|
||||||
|
|
||||||
latch.finish();
|
latch.finish(ShutdownOutcome::Canceled);
|
||||||
|
|
||||||
assert!(latch.try_begin());
|
assert!(latch.try_begin());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn committed_shutdown_retains_latch_and_suppresses_repeats() {
|
||||||
|
let latch = ShutdownLatch::new();
|
||||||
|
|
||||||
|
assert!(latch.try_begin());
|
||||||
|
latch.finish(ShutdownOutcome::Committed);
|
||||||
|
|
||||||
|
assert!(!latch.try_begin());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
use clash_verge_logging::{Type, logging};
|
use clash_verge_logging::{Type, logging};
|
||||||
use tokio::signal::unix::{SignalKind, signal};
|
use tokio::signal::unix::{SignalKind, signal};
|
||||||
|
|
||||||
use crate::{RUNTIME, SHUTDOWN_LATCH};
|
use crate::{RUNTIME, SHUTDOWN_LATCH, ShutdownOutcome};
|
||||||
|
|
||||||
pub fn register<F, Fut>(f: F)
|
pub fn register<F, Fut>(f: F)
|
||||||
where
|
where
|
||||||
F: Fn() -> Fut + Send + Sync + 'static,
|
F: Fn() -> Fut + Send + Sync + 'static,
|
||||||
Fut: Future<Output = ()> + Send + 'static,
|
Fut: Future<Output = ShutdownOutcome> + Send + 'static,
|
||||||
{
|
{
|
||||||
if let Some(Some(rt)) = RUNTIME.get() {
|
if let Some(Some(rt)) = RUNTIME.get() {
|
||||||
rt.spawn(async move {
|
rt.spawn(async move {
|
||||||
@@ -60,8 +60,7 @@ where
|
|||||||
}
|
}
|
||||||
logging!(info, Type::SystemSignal, "Caught signal {}", signal_name);
|
logging!(info, Type::SystemSignal, "Caught signal {}", signal_name);
|
||||||
|
|
||||||
f().await;
|
SHUTDOWN_LATCH.finish(f().await);
|
||||||
SHUTDOWN_LATCH.finish();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
use clash_verge_logging::{Type, logging};
|
||||||
|
use tokio::signal::windows;
|
||||||
|
|
||||||
|
use crate::{RUNTIME, SHUTDOWN_LATCH, ShutdownOutcome};
|
||||||
|
|
||||||
|
pub fn register<F, Fut>(f: F)
|
||||||
|
where
|
||||||
|
F: Fn() -> Fut + Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = ShutdownOutcome> + Send + 'static,
|
||||||
|
{
|
||||||
|
if let Some(Some(rt)) = RUNTIME.get() {
|
||||||
|
rt.spawn(async move {
|
||||||
|
let mut ctrl_c = match windows::ctrl_c() {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(error, Type::SystemSignal, "Failed to register Ctrl+C: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut ctrl_close = match windows::ctrl_close() {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(error, Type::SystemSignal, "Failed to register Ctrl+Close: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut ctrl_shutdown = match windows::ctrl_shutdown() {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(error, Type::SystemSignal, "Failed to register Ctrl+Shutdown: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut ctrl_logoff = match windows::ctrl_logoff() {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(error, Type::SystemSignal, "Failed to register Ctrl+Logoff: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let signal_name;
|
||||||
|
tokio::select! {
|
||||||
|
_ = ctrl_c.recv() => {
|
||||||
|
signal_name = "Ctrl+C";
|
||||||
|
}
|
||||||
|
_ = ctrl_close.recv() => {
|
||||||
|
signal_name = "Ctrl+Close";
|
||||||
|
}
|
||||||
|
_ = ctrl_shutdown.recv() => {
|
||||||
|
signal_name = "Ctrl+Shutdown";
|
||||||
|
}
|
||||||
|
_ = ctrl_logoff.recv() => {
|
||||||
|
signal_name = "Ctrl+Logoff";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !SHUTDOWN_LATCH.try_begin() {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::SystemSignal,
|
||||||
|
"Already shutting down, ignoring repeated signal: {}",
|
||||||
|
signal_name
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
logging!(info, Type::SystemSignal, "Caught Windows signal: {}", signal_name);
|
||||||
|
|
||||||
|
SHUTDOWN_LATCH.finish(f().await);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::SystemSignal,
|
||||||
|
"register shutdown signal failed, RUNTIME is not available"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ serde = { workspace = true, features = ["derive"] }
|
|||||||
sysinfo = { version = "0.39.6", features = ["network", "system"] }
|
sysinfo = { version = "0.39.6", features = ["network", "system"] }
|
||||||
|
|
||||||
[target.'cfg(not(windows))'.dependencies]
|
[target.'cfg(not(windows))'.dependencies]
|
||||||
libc = "0.2.191"
|
libc = "0.2.190"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
deelevate = { workspace = true }
|
deelevate = { workspace = true }
|
||||||
|
|||||||
@@ -129,7 +129,6 @@ export default defineConfig([
|
|||||||
'eslint.config.ts',
|
'eslint.config.ts',
|
||||||
`vite.config.mts`,
|
`vite.config.mts`,
|
||||||
'scripts/*.mjs',
|
'scripts/*.mjs',
|
||||||
'scripts-workflow/*.mjs',
|
|
||||||
'tests/*.ts',
|
'tests/*.ts',
|
||||||
'src/polyfills/*.js',
|
'src/polyfills/*.js',
|
||||||
'tests/*.mjs',
|
'tests/*.mjs',
|
||||||
@@ -239,13 +238,6 @@ export default defineConfig([
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
files: ['scripts-workflow/*.mjs'],
|
|
||||||
languageOptions: {
|
|
||||||
globals: globals.node,
|
|
||||||
},
|
|
||||||
rules: { 'es-x/no-object-groupby': 'off' },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
files: ['scripts/perf/*.{mjs,mts}'],
|
files: ['scripts/perf/*.{mjs,mts}'],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
|
|||||||
+21
-21
@@ -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": "tsrs -p . --noEmit && vite build",
|
"web:build": "tsc --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": "tsrs -p . --noEmit",
|
"typecheck": "tsc --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.4",
|
"@tauri-apps/api": "2.12.1",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "2.5.0",
|
"@tauri-apps/plugin-clipboard-manager": "2.4.0",
|
||||||
"@tauri-apps/plugin-dialog": "2.8.3",
|
"@tauri-apps/plugin-dialog": "2.8.0",
|
||||||
"@tauri-apps/plugin-fs": "2.7.1",
|
"@tauri-apps/plugin-fs": "2.6.0",
|
||||||
"@tauri-apps/plugin-http": "2.8.3",
|
"@tauri-apps/plugin-http": "2.8.0",
|
||||||
"@tauri-apps/plugin-opener": "2.7.2",
|
"@tauri-apps/plugin-opener": "2.7.0",
|
||||||
"@tauri-apps/plugin-updater": "2.14.1",
|
"@tauri-apps/plugin-updater": "2.13.1",
|
||||||
"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",
|
||||||
@@ -75,15 +75,16 @@
|
|||||||
"monaco-yaml": "^5.5.1",
|
"monaco-yaml": "^5.5.1",
|
||||||
"nanoid": "^6.0.1",
|
"nanoid": "^6.0.1",
|
||||||
"qrcode.react": "^4.2.0",
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "19.3.1",
|
"react": "19.3.0",
|
||||||
"react-dom": "19.3.1",
|
"react-dom": "19.3.0",
|
||||||
"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.1.9",
|
"react-i18next": "17.0.15",
|
||||||
"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",
|
||||||
"rehype-sanitize": "^6.0.0",
|
"rehype-sanitize": "^6.0.0",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
"swr": "^2.5.1",
|
"swr": "^2.5.1",
|
||||||
"tauri-plugin-mihomo-api": "github:clash-verge-rev/tauri-plugin-mihomo#main",
|
"tauri-plugin-mihomo-api": "github:clash-verge-rev/tauri-plugin-mihomo#main",
|
||||||
"types-pac": "^1.0.3",
|
"types-pac": "^1.0.3",
|
||||||
@@ -94,15 +95,15 @@
|
|||||||
"@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.3",
|
"@tauri-apps/cli": "2.12.1",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^26.0.0",
|
"@types/node": "^24.13.6",
|
||||||
"@types/react": "19.3.0",
|
"@types/react": "19.3.0",
|
||||||
"@types/react-dom": "19.3.0",
|
"@types/react-dom": "19.3.0",
|
||||||
"@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.5.0",
|
"@wdio/tauri-service": "1.4.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",
|
||||||
@@ -117,24 +118,23 @@
|
|||||||
"eslint-plugin-react-refresh": "^0.5.7",
|
"eslint-plugin-react-refresh": "^0.5.7",
|
||||||
"eslint-plugin-sukka": "^10.2.0",
|
"eslint-plugin-sukka": "^10.2.0",
|
||||||
"eslint-plugin-unused-imports": "^4.4.1",
|
"eslint-plugin-unused-imports": "^4.4.1",
|
||||||
"glob": "^14.0.0",
|
"glob": "^13.0.6",
|
||||||
"globals": "^17.12.0",
|
"globals": "^17.12.0",
|
||||||
"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.44.1",
|
"knip": "6.39.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.1.0",
|
"webdriverio": "10.0.0",
|
||||||
"ws": "8.22.1"
|
"ws": "8.22.0"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{ts,tsx,js,mjs}": [
|
"*.{ts,tsx,js,mjs}": [
|
||||||
@@ -146,5 +146,5 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@12.16.1"
|
"packageManager": "pnpm@12.11.2"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+965
-682
File diff suppressed because it is too large
Load Diff
@@ -10,11 +10,3 @@ 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
|
|
||||||
|
|||||||
@@ -45,17 +45,6 @@
|
|||||||
"matchManagers": ["npm"],
|
"matchManagers": ["npm"],
|
||||||
"groupName": "npm dependencies"
|
"groupName": "npm dependencies"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"description": "Update Tauri npm packages and Cargo crates together so frontend and backend stay in sync",
|
|
||||||
"matchPackageNames": [
|
|
||||||
"@tauri-apps/**",
|
|
||||||
"@wdio/tauri-service",
|
|
||||||
"tauri",
|
|
||||||
"tauri-*"
|
|
||||||
],
|
|
||||||
"groupName": "tauri",
|
|
||||||
"addLabels": ["tauri"]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"description": "Group all GitHub Actions updates into a single PR",
|
"description": "Group all GitHub Actions updates into a single PR",
|
||||||
"matchManagers": ["github-actions"],
|
"matchManagers": ["github-actions"],
|
||||||
@@ -79,13 +68,6 @@
|
|||||||
"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"],
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.99.1"
|
channel = "1.99.0"
|
||||||
components = ["rustfmt", "clippy"]
|
components = ["rustfmt", "clippy"]
|
||||||
|
|||||||
@@ -1,796 +0,0 @@
|
|||||||
import { execFileSync } from 'node:child_process'
|
|
||||||
import { appendFileSync, readFileSync, writeFileSync } from 'node:fs'
|
|
||||||
|
|
||||||
const env = process.env
|
|
||||||
const repository = env.GITHUB_REPOSITORY
|
|
||||||
const refreshLabel = 'Refresh this PR from latest dev'
|
|
||||||
const publishLabel = 'Publish this approved release'
|
|
||||||
const minorLabel = 'Use the next minor version'
|
|
||||||
const checked = (body, label = refreshLabel) =>
|
|
||||||
(body || '')
|
|
||||||
.split('\n')
|
|
||||||
.some((line) => line.toLowerCase() === `- [x] ${label}`.toLowerCase())
|
|
||||||
const versionPattern = '(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)'
|
|
||||||
const rcPattern = new RegExp(`^v${versionPattern}-rc(?:\\.(0|[1-9][0-9]*))?$`)
|
|
||||||
const stablePattern = new RegExp(`^v${versionPattern}$`)
|
|
||||||
const versionFiles = [
|
|
||||||
'package.json',
|
|
||||||
'src-tauri/tauri.conf.json',
|
|
||||||
'src-tauri/Cargo.toml',
|
|
||||||
'Cargo.lock',
|
|
||||||
]
|
|
||||||
const read = (path) => readFileSync(path, 'utf8')
|
|
||||||
const json = (path) => JSON.parse(read(path))
|
|
||||||
const run = (command, args, options = {}) =>
|
|
||||||
execFileSync(command, args, { encoding: 'utf8', ...options }).trim()
|
|
||||||
const git = (...args) => run('git', args)
|
|
||||||
const gitFile = (sha, path) =>
|
|
||||||
execFileSync('git', ['show', `${sha}:${path}`], { encoding: 'utf8' })
|
|
||||||
const isRc = (tag) => tag.match(rcPattern)?.[0] === tag
|
|
||||||
const isRelease = (tag) => isRc(tag) || stablePattern.test(tag)
|
|
||||||
const titleFor = (tag) => `${isRc(tag) ? 'Pre-Release' : 'Release'}: ${tag}`
|
|
||||||
const branchTag = (branch, prefix = 'release/') =>
|
|
||||||
branch.startsWith(prefix) && isRelease(branch.slice(prefix.length))
|
|
||||||
? branch.slice(prefix.length)
|
|
||||||
: undefined
|
|
||||||
const latest = (tags) =>
|
|
||||||
tags.sort((a, b) => a.localeCompare(b, 'en', { numeric: true })).at(-1)
|
|
||||||
const summary = (text) => appendFileSync(env.GITHUB_STEP_SUMMARY, `${text}\n`)
|
|
||||||
const endpoint = (path) =>
|
|
||||||
path === 'graphql' ? path : `repos/${repository}/${path}`
|
|
||||||
const list = (path) =>
|
|
||||||
JSON.parse(run('gh', ['api', '--paginate', '--slurp', endpoint(path)])).flat()
|
|
||||||
|
|
||||||
function api(path, body, method = 'POST') {
|
|
||||||
const response = run(
|
|
||||||
'gh',
|
|
||||||
['api', '--method', method, endpoint(path), '--input', '-'],
|
|
||||||
{
|
|
||||||
input: JSON.stringify(body),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
const result = response ? JSON.parse(response) : null
|
|
||||||
if (result?.errors) throw new Error(JSON.stringify(result.errors))
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
const get = (path) => JSON.parse(run('gh', ['api', endpoint(path)]))
|
|
||||||
const canWrite = (login) =>
|
|
||||||
['write', 'maintain', 'admin'].includes(
|
|
||||||
get(`collaborators/${login}/permission`).permission,
|
|
||||||
)
|
|
||||||
|
|
||||||
function output(values) {
|
|
||||||
appendFileSync(
|
|
||||||
env.GITHUB_OUTPUT,
|
|
||||||
Object.entries(values)
|
|
||||||
.map(([key, value]) => `${key}=${value}\n`)
|
|
||||||
.join(''),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function versionPatternFor(path) {
|
|
||||||
if (path.endsWith('.json')) return /("version"\s*:\s*")[^"]+/
|
|
||||||
return path === 'Cargo.lock'
|
|
||||||
? /(\[\[package\]\]\r?\nname = "clash-verge"\r?\nversion = ")[^"]+/
|
|
||||||
: /(^version\s*=\s*")[^"]+/m
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateVersions(tag, sha) {
|
|
||||||
for (const path of versionFiles) {
|
|
||||||
const content = sha ? gitFile(sha, path) : read(path)
|
|
||||||
const version = path.endsWith('.json')
|
|
||||||
? JSON.parse(content).version
|
|
||||||
: content.match(versionPatternFor(path))?.[0].split('"').at(-1)
|
|
||||||
if (`v${version}` !== tag)
|
|
||||||
throw new Error(`${path} version does not match ${tag}.`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateVersionCommit(tag, sha) {
|
|
||||||
validateVersions(tag, sha)
|
|
||||||
const commit = get(`commits/${sha}`)
|
|
||||||
if (!commit.commit.verification.verified || commit.parents.length !== 1)
|
|
||||||
throw new Error('The version commit must be signed and have one parent.')
|
|
||||||
const title = commit.commit.message.split('\n')[0]
|
|
||||||
if (title.replace(/ \(#\d+\)$/, '') !== titleFor(tag))
|
|
||||||
throw new Error('The final version commit has an unexpected title.')
|
|
||||||
const parent = commit.parents[0].sha
|
|
||||||
const changed = git('diff', '--name-only', parent, sha)
|
|
||||||
.split('\n')
|
|
||||||
.filter(Boolean)
|
|
||||||
if (
|
|
||||||
changed.some((path) => !versionFiles.includes(path)) ||
|
|
||||||
git('diff', '--summary', parent, sha)
|
|
||||||
)
|
|
||||||
throw new Error(
|
|
||||||
'The version commit changes files outside the four version files.',
|
|
||||||
)
|
|
||||||
for (const path of versionFiles) {
|
|
||||||
const before = gitFile(parent, path)
|
|
||||||
const after = gitFile(sha, path)
|
|
||||||
if (
|
|
||||||
before.replace(versionPatternFor(path), '$1VERSION') !==
|
|
||||||
after.replace(versionPatternFor(path), '$1VERSION')
|
|
||||||
)
|
|
||||||
throw new Error(`${path} contains changes other than its version.`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function remoteRefs() {
|
|
||||||
return git('ls-remote', '--heads', '--tags', '--refs', 'origin')
|
|
||||||
.split('\n')
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((line) => line.split('\t'))
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchBranches() {
|
|
||||||
git(
|
|
||||||
'fetch',
|
|
||||||
'--no-tags',
|
|
||||||
'origin',
|
|
||||||
'+refs/heads/dev:refs/remotes/origin/dev',
|
|
||||||
'+refs/heads/main:refs/remotes/origin/main',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function discardClosedCandidate(branch, base, prefix) {
|
|
||||||
const candidates = list(
|
|
||||||
`pulls?state=all&base=${base}&head=${repository.split('/')[0]}:${branch}&per_page=100`,
|
|
||||||
)
|
|
||||||
const latest = candidates[0]
|
|
||||||
if (!latest || candidates.some((pr) => pr.state === 'open'))
|
|
||||||
throw new Error(
|
|
||||||
`Branch ${branch} is reserved by an active or unknown candidate.`,
|
|
||||||
)
|
|
||||||
const pr = get(`pulls/${latest.number}`)
|
|
||||||
requireBotPr(pr, base, prefix)
|
|
||||||
if (pr.state !== 'closed' || pr.merged)
|
|
||||||
throw new Error(
|
|
||||||
`Close the unmerged candidate before retrying: ${pr.html_url}`,
|
|
||||||
)
|
|
||||||
updateRefs(pr.base.repo.node_id, [
|
|
||||||
{
|
|
||||||
name: `refs/heads/${branch}`,
|
|
||||||
beforeOid: pr.head.sha,
|
|
||||||
afterOid: '0'.repeat(40),
|
|
||||||
},
|
|
||||||
])
|
|
||||||
summary(`Released the unused branch from closed PR: ${pr.html_url}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function prepare() {
|
|
||||||
let refresh
|
|
||||||
if (env.GITHUB_EVENT_NAME === 'pull_request_target') {
|
|
||||||
const event = json(env.GITHUB_EVENT_PATH)
|
|
||||||
if (checked(event.changes?.body?.from) || !checked(event.pull_request.body))
|
|
||||||
return
|
|
||||||
if (!canWrite(event.sender.login))
|
|
||||||
throw new Error('Refreshing a version PR requires write access.')
|
|
||||||
refresh = get(`pulls/${event.pull_request.number}`)
|
|
||||||
if (refresh.state !== 'open' || !checked(refresh.body)) return
|
|
||||||
requireBotPr(refresh, 'dev', 'release/')
|
|
||||||
}
|
|
||||||
fetchBranches()
|
|
||||||
git('checkout', '--detach', 'origin/dev')
|
|
||||||
const channel = refresh
|
|
||||||
? isRc(branchTag(refresh.head.ref))
|
|
||||||
? 'rc'
|
|
||||||
: 'stable'
|
|
||||||
: env.RELEASE_CHANNEL || 'rc'
|
|
||||||
if (!['rc', 'stable'].includes(channel))
|
|
||||||
throw new Error('Unknown release channel.')
|
|
||||||
const requested = refresh
|
|
||||||
? branchTag(refresh.head.ref)
|
|
||||||
: env.RELEASE_TAG || ''
|
|
||||||
if (
|
|
||||||
requested &&
|
|
||||||
!(channel === 'rc' ? isRc(requested) : stablePattern.test(requested))
|
|
||||||
)
|
|
||||||
throw new Error(
|
|
||||||
channel === 'rc'
|
|
||||||
? 'Expected vX.Y.Z-rc or vX.Y.Z-rc.N.'
|
|
||||||
: 'Expected vX.Y.Z.',
|
|
||||||
)
|
|
||||||
const existing =
|
|
||||||
!refresh &&
|
|
||||||
list('pulls?state=open&base=dev&per_page=100').find(
|
|
||||||
(pr) =>
|
|
||||||
pr.head.repo?.full_name === repository &&
|
|
||||||
pr.user.login === 'github-actions[bot]' &&
|
|
||||||
branchTag(pr.head.ref) &&
|
|
||||||
isRc(branchTag(pr.head.ref)) === (channel === 'rc') &&
|
|
||||||
(!requested || branchTag(pr.head.ref) === requested),
|
|
||||||
)
|
|
||||||
if (existing) {
|
|
||||||
api(`issues/${existing.number}/labels`, { labels: ['release'] })
|
|
||||||
output({ existing_pr: existing.html_url })
|
|
||||||
summary(
|
|
||||||
`Existing version PR: ${existing.html_url}\nClose it before preparing a replacement; its candidate is unchanged.`,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const base = (requested || `v${json('package.json').version}`).split(
|
|
||||||
/[-+]/,
|
|
||||||
)[0]
|
|
||||||
if (!stablePattern.test(base))
|
|
||||||
throw new Error(`Invalid base version: ${base}`)
|
|
||||||
const refs = remoteRefs().map(([, ref]) => ref)
|
|
||||||
const tags = refs
|
|
||||||
.filter((ref) => ref.startsWith('refs/tags/'))
|
|
||||||
.map((ref) => ref.slice(10))
|
|
||||||
const reserved = refs
|
|
||||||
.filter((ref) => ref.startsWith('refs/heads/'))
|
|
||||||
.filter((ref) => !refresh || ref !== `refs/heads/${refresh.head.ref}`)
|
|
||||||
.map(
|
|
||||||
(ref) => branchTag(ref.slice(11)) || branchTag(ref.slice(11), 'publish/'),
|
|
||||||
)
|
|
||||||
.filter(Boolean)
|
|
||||||
const sameBase = (tag) => isRc(tag) && tag.startsWith(`${base}-rc`)
|
|
||||||
const occupied = [...tags, ...reserved]
|
|
||||||
const last = latest(occupied.filter(sameBase))
|
|
||||||
const tag =
|
|
||||||
requested ||
|
|
||||||
(channel === 'stable'
|
|
||||||
? base
|
|
||||||
: last
|
|
||||||
? `${base}-rc.${BigInt(last.match(rcPattern)[4] ?? 0) + 1n}`
|
|
||||||
: `${base}-rc`)
|
|
||||||
if (tags.includes(tag)) throw new Error(`Tag ${tag} already exists.`)
|
|
||||||
for (const [prefix, target] of [
|
|
||||||
['release/', 'dev'],
|
|
||||||
['publish/', 'main'],
|
|
||||||
]) {
|
|
||||||
const branch = `${prefix}${tag}`
|
|
||||||
if (refs.includes(`refs/heads/${branch}`) && branch !== refresh?.head.ref)
|
|
||||||
discardClosedCandidate(branch, target, prefix)
|
|
||||||
}
|
|
||||||
const sha = git('rev-parse', 'HEAD')
|
|
||||||
const body = releaseBody(tag, sha, tags, false)
|
|
||||||
summary(body)
|
|
||||||
if (
|
|
||||||
remoteRefs().some(
|
|
||||||
([, ref]) =>
|
|
||||||
ref === `refs/tags/${tag}` || ref === `refs/heads/publish/${tag}`,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
throw new Error(`Tag ${tag} already exists or is reserved for publication.`)
|
|
||||||
if (json('package.json').version !== tag.slice(1))
|
|
||||||
run('node', ['scripts/release-version.mjs', tag])
|
|
||||||
validateVersions(tag)
|
|
||||||
const title = titleFor(tag)
|
|
||||||
const branch = refresh
|
|
||||||
? `release-refresh/${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT}`
|
|
||||||
: `release/${tag}`
|
|
||||||
writeVersionPr({ sha, body, refresh, title, branch, files: versionFiles })
|
|
||||||
}
|
|
||||||
|
|
||||||
function releaseBody(tag, sha, tags, publishing) {
|
|
||||||
const previous =
|
|
||||||
(isRc(tag) &&
|
|
||||||
latest(
|
|
||||||
tags.filter(
|
|
||||||
(value) =>
|
|
||||||
isRc(value) &&
|
|
||||||
value.startsWith(`${tag.split('-')[0]}-rc`) &&
|
|
||||||
value !== tag,
|
|
||||||
),
|
|
||||||
)) ||
|
|
||||||
latest(tags.filter((value) => stablePattern.test(value) && value !== tag))
|
|
||||||
const url = `${env.GITHUB_SERVER_URL}/${repository}`
|
|
||||||
let range = sha
|
|
||||||
let comparison = 'No previous release tag'
|
|
||||||
if (previous) {
|
|
||||||
git('fetch', '--no-tags', 'origin', `refs/tags/${previous}`)
|
|
||||||
range = `${git('rev-parse', 'FETCH_HEAD^{commit}')}..${sha}`
|
|
||||||
comparison = `Changes since **${previous}** · [Compare changes](${url}/compare/${previous}...${sha})`
|
|
||||||
}
|
|
||||||
comparison += ` · **${git('rev-list', '--count', range)} commits**`
|
|
||||||
const commits = git('log', '--format=%h %s', range)
|
|
||||||
const groups = Object.groupBy(commits ? commits.split('\n') : [], (line) => {
|
|
||||||
if (/^\S+ chore\(deps\)!?: /i.test(line)) return 'deps'
|
|
||||||
const type = line.match(/^\S+ (\w+)(?:\([^)]*\))?!?: /)?.[1]?.toLowerCase()
|
|
||||||
return type === 'doc' ? 'docs' : type || 'other'
|
|
||||||
})
|
|
||||||
const labels = {
|
|
||||||
fix: 'Fixes',
|
|
||||||
feat: 'Features',
|
|
||||||
perf: 'Performance',
|
|
||||||
revert: 'Reverts',
|
|
||||||
refactor: 'Refactoring',
|
|
||||||
build: 'Build',
|
|
||||||
deps: 'Dependencies',
|
|
||||||
ci: 'CI',
|
|
||||||
docs: 'Documentation',
|
|
||||||
test: 'Tests',
|
|
||||||
chore: 'Maintenance',
|
|
||||||
style: 'Style',
|
|
||||||
other: 'Other',
|
|
||||||
}
|
|
||||||
const names = {
|
|
||||||
macos: 'macOS',
|
|
||||||
windows: 'Windows',
|
|
||||||
ui: 'UI',
|
|
||||||
ci: 'CI',
|
|
||||||
tun: 'TUN',
|
|
||||||
}
|
|
||||||
const order = Object.keys(labels)
|
|
||||||
const rank = (type) =>
|
|
||||||
order.includes(type) ? order.indexOf(type) : order.length
|
|
||||||
const scope =
|
|
||||||
Object.entries(groups)
|
|
||||||
.sort(([a], [b]) => rank(a) - rank(b) || a.localeCompare(b))
|
|
||||||
.map(([type, lines]) => {
|
|
||||||
const entries = lines.map((line) => {
|
|
||||||
const hash = line.slice(0, line.indexOf(' '))
|
|
||||||
const title = line
|
|
||||||
.slice(hash.length + 1)
|
|
||||||
.replace(/\s*\((?:[\w.-]+\/[\w.-]+)?#\d+\)/g, '')
|
|
||||||
.replace(/(?:[\w.-]+\/[\w.-]+)?#\d+\b/g, '')
|
|
||||||
.replace(/\s{2,}/g, ' ')
|
|
||||||
.trim()
|
|
||||||
.replace(
|
|
||||||
/^\w+(?:\(([^)]*)\))?(!)?:\s*/,
|
|
||||||
(_, scope, breaking) =>
|
|
||||||
`${breaking ? 'Breaking: ' : ''}${scope ? `${Object.hasOwn(names, scope) ? names[scope] : scope}: ` : ''}`,
|
|
||||||
)
|
|
||||||
.replaceAll('&', '&')
|
|
||||||
.replaceAll('<', '<')
|
|
||||||
.replaceAll('>', '>')
|
|
||||||
.replace(/[\\`*_[\]]/g, '\\$&')
|
|
||||||
return `- ${title} [#${hash}](${url}/commit/${hash})`
|
|
||||||
})
|
|
||||||
return `### ${Object.hasOwn(labels, type) ? labels[type] : type} · ${lines.length}\n\n${entries.join('\n')}`
|
|
||||||
})
|
|
||||||
.join('\n\n') || 'No new commits.'
|
|
||||||
return `## ${titleFor(tag)}
|
|
||||||
|
|
||||||
Source: \`dev\` · [${sha}](${url}/commit/${sha}) \n${comparison}
|
|
||||||
${
|
|
||||||
publishing
|
|
||||||
? `<!-- release-source:${sha} -->
|
|
||||||
|
|
||||||
After approval, select the checkbox below to publish.
|
|
||||||
Do not use the normal Squash or Rebase merge buttons.
|
|
||||||
|
|
||||||
- [ ] ${publishLabel}`
|
|
||||||
: `
|
|
||||||
- [ ] ${refreshLabel}
|
|
||||||
|
|
||||||
Refreshing requires a new approval.`
|
|
||||||
}
|
|
||||||
|
|
||||||
## Changes
|
|
||||||
|
|
||||||
${scope}
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeVersionPr({ sha, body, refresh, title, branch, files }) {
|
|
||||||
// Create-only ref and expectedHeadOid prevent replacing another candidate.
|
|
||||||
api('git/refs', { ref: `refs/heads/${branch}`, sha })
|
|
||||||
let pr
|
|
||||||
try {
|
|
||||||
const commit = api('graphql', {
|
|
||||||
query: `mutation($input: CreateCommitOnBranchInput!) {
|
|
||||||
createCommitOnBranch(input: $input) { commit { oid } }
|
|
||||||
}`,
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
branch: { repositoryNameWithOwner: repository, branchName: branch },
|
|
||||||
expectedHeadOid: sha,
|
|
||||||
message: { headline: title },
|
|
||||||
fileChanges: {
|
|
||||||
additions: files.map((path) => ({
|
|
||||||
path,
|
|
||||||
contents: readFileSync(path).toString('base64'),
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if (!commit.data?.createCommitOnBranch?.commit?.oid)
|
|
||||||
throw new Error('Signed version commit failed.')
|
|
||||||
if (refresh) {
|
|
||||||
const current = get(`pulls/${refresh.number}`)
|
|
||||||
if (
|
|
||||||
current.state !== 'open' ||
|
|
||||||
current.head.sha !== refresh.head.sha ||
|
|
||||||
current.base.ref !== refresh.base.ref ||
|
|
||||||
current.body !== refresh.body
|
|
||||||
)
|
|
||||||
throw new Error(
|
|
||||||
'The version pull request changed while preparing the update.',
|
|
||||||
)
|
|
||||||
const oid = commit.data.createCommitOnBranch.commit.oid
|
|
||||||
updateRefs(refresh.base.repo.node_id, [
|
|
||||||
{
|
|
||||||
name: `refs/heads/${refresh.head.ref}`,
|
|
||||||
beforeOid: refresh.head.sha,
|
|
||||||
afterOid: oid,
|
|
||||||
force: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: `refs/heads/${branch}`,
|
|
||||||
beforeOid: oid,
|
|
||||||
afterOid: '0'.repeat(40),
|
|
||||||
},
|
|
||||||
])
|
|
||||||
for (const review of list(
|
|
||||||
`pulls/${refresh.number}/reviews?per_page=100`,
|
|
||||||
)) {
|
|
||||||
if (review.state === 'APPROVED')
|
|
||||||
api(
|
|
||||||
`pulls/${refresh.number}/reviews/${review.id}/dismissals`,
|
|
||||||
{
|
|
||||||
message:
|
|
||||||
'The version PR was refreshed from dev; please review again.',
|
|
||||||
},
|
|
||||||
'PUT',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
pr = api(`pulls/${refresh.number}`, { title, body }, 'PATCH')
|
|
||||||
} else {
|
|
||||||
pr = api('pulls', {
|
|
||||||
head: branch,
|
|
||||||
base: 'dev',
|
|
||||||
title,
|
|
||||||
body,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (refresh) {
|
|
||||||
try {
|
|
||||||
api(`git/refs/heads/${branch}`, {}, 'DELETE')
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
api(`issues/${pr.number}/labels`, { labels: ['release'] })
|
|
||||||
summary(`Version pull request: ${pr.html_url}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateRefs(repositoryId, refUpdates) {
|
|
||||||
api('graphql', {
|
|
||||||
query: `mutation($input: UpdateRefsInput!) { updateRefs(input: $input) { clientMutationId } }`,
|
|
||||||
variables: { input: { repositoryId, refUpdates } },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function requireBotPr(pr, base, prefix) {
|
|
||||||
const tag = branchTag(pr.head.ref, prefix)
|
|
||||||
if (
|
|
||||||
!tag ||
|
|
||||||
pr.base.ref !== base ||
|
|
||||||
pr.base.repo.full_name !== repository ||
|
|
||||||
pr.head.repo?.full_name !== repository ||
|
|
||||||
pr.user.login !== 'github-actions[bot]'
|
|
||||||
)
|
|
||||||
throw new Error(
|
|
||||||
'Expected a release PR created by this repository workflow.',
|
|
||||||
)
|
|
||||||
return tag
|
|
||||||
}
|
|
||||||
|
|
||||||
function publishPr() {
|
|
||||||
const event = json(env.GITHUB_EVENT_PATH)
|
|
||||||
const pr = get(`pulls/${event.pull_request.number}`)
|
|
||||||
const tag = requireBotPr(pr, 'dev', 'release/')
|
|
||||||
if (!pr.merged || pr.merge_commit_sha !== event.pull_request.merge_commit_sha)
|
|
||||||
throw new Error('Expected the final merged version commit on dev.')
|
|
||||||
const sha = pr.merge_commit_sha
|
|
||||||
fetchBranches()
|
|
||||||
git('fetch', '--no-tags', 'origin', sha)
|
|
||||||
git('merge-base', '--is-ancestor', sha, 'origin/dev')
|
|
||||||
validateVersionCommit(tag, sha)
|
|
||||||
const branch = `publish/${tag}`
|
|
||||||
const existing = list(
|
|
||||||
`pulls?state=open&base=main&head=${repository.split('/')[0]}:${branch}&per_page=100`,
|
|
||||||
)
|
|
||||||
if (existing.length) {
|
|
||||||
const publication = existing[0]
|
|
||||||
requireBotPr(publication, 'main', 'publish/')
|
|
||||||
if (publication.head.sha !== sha)
|
|
||||||
throw new Error(
|
|
||||||
'This tag is already reserved by a different publication snapshot.',
|
|
||||||
)
|
|
||||||
summary(`Existing publication PR: ${publication.html_url}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const refs = remoteRefs()
|
|
||||||
if (refs.some(([, ref]) => ref === `refs/tags/${tag}`))
|
|
||||||
throw new Error(`Tag ${tag} already exists.`)
|
|
||||||
git('merge-base', '--is-ancestor', 'origin/main', sha)
|
|
||||||
const head = refs.find(([, ref]) => ref === `refs/heads/${branch}`)?.[0]
|
|
||||||
if (head && head !== sha)
|
|
||||||
throw new Error('Publication branch already points to another commit.')
|
|
||||||
if (!head) api('git/refs', { ref: `refs/heads/${branch}`, sha })
|
|
||||||
const body = releaseBody(
|
|
||||||
tag,
|
|
||||||
sha,
|
|
||||||
refs
|
|
||||||
.filter(([, ref]) => ref.startsWith('refs/tags/'))
|
|
||||||
.map(([, ref]) => ref.slice(10)),
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
const publication = api('pulls', {
|
|
||||||
head: branch,
|
|
||||||
base: 'main',
|
|
||||||
title: titleFor(tag),
|
|
||||||
body,
|
|
||||||
})
|
|
||||||
api(`issues/${publication.number}/labels`, { labels: ['release'] })
|
|
||||||
summary(`Publication PR: ${publication.html_url}\n\n${body}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function requireApproval(pr, sha) {
|
|
||||||
const reviews = new Map()
|
|
||||||
for (const review of list(`pulls/${pr.number}/reviews?per_page=100`)) {
|
|
||||||
if (['APPROVED', 'CHANGES_REQUESTED', 'DISMISSED'].includes(review.state))
|
|
||||||
reviews.set(review.user.login, review)
|
|
||||||
}
|
|
||||||
let approved = false
|
|
||||||
for (const review of reviews.values()) {
|
|
||||||
if (!canWrite(review.user.login)) continue
|
|
||||||
if (review.state === 'CHANGES_REQUESTED')
|
|
||||||
throw new Error('A reviewer with write access has requested changes.')
|
|
||||||
if (review.state === 'APPROVED' && review.commit_id === sha) approved = true
|
|
||||||
}
|
|
||||||
if (!approved)
|
|
||||||
throw new Error(
|
|
||||||
'The current PR head needs approval from a reviewer with write access.',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function releaseOutput(tag, sha) {
|
|
||||||
output({
|
|
||||||
release_tag: tag,
|
|
||||||
source_sha: sha,
|
|
||||||
channel: isRc(tag) ? 'rc' : 'stable',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function publish() {
|
|
||||||
const event = json(env.GITHUB_EVENT_PATH)
|
|
||||||
const tag = requireBotPr(event.pull_request, 'main', 'publish/')
|
|
||||||
if (
|
|
||||||
checked(event.changes?.body?.from, publishLabel) ||
|
|
||||||
!checked(event.pull_request.body, publishLabel)
|
|
||||||
)
|
|
||||||
throw new Error('Publishing requires a new checkbox selection.')
|
|
||||||
if (!canWrite(event.sender.login))
|
|
||||||
throw new Error('Publishing requires write access.')
|
|
||||||
const sha = event.pull_request.head.sha
|
|
||||||
const pr = get(`pulls/${event.pull_request.number}`)
|
|
||||||
requireBotPr(pr, 'main', 'publish/')
|
|
||||||
if (
|
|
||||||
pr.head.sha !== sha ||
|
|
||||||
!checked(pr.body, publishLabel) ||
|
|
||||||
!pr.body.includes(`<!-- release-source:${sha} -->`)
|
|
||||||
)
|
|
||||||
throw new Error(
|
|
||||||
'The publication snapshot changed after the checkbox selection.',
|
|
||||||
)
|
|
||||||
fetchBranches()
|
|
||||||
git('fetch', '--no-tags', 'origin', sha)
|
|
||||||
git('merge-base', '--is-ancestor', sha, 'origin/dev')
|
|
||||||
validateVersionCommit(tag, sha)
|
|
||||||
const marker = `Release workflow run: ${env.GITHUB_RUN_ID}\nSource: ${sha}\nPR: ${pr.number}`
|
|
||||||
const existing = remoteRefs().find(([, ref]) => ref === `refs/tags/${tag}`)
|
|
||||||
if (existing) {
|
|
||||||
const ref = get(`git/ref/tags/${tag}`)
|
|
||||||
const object =
|
|
||||||
ref.object.type === 'tag' && get(`git/tags/${ref.object.sha}`)
|
|
||||||
if (
|
|
||||||
!object ||
|
|
||||||
object.object.type !== 'commit' ||
|
|
||||||
object.object.sha !== sha ||
|
|
||||||
object.message.trim() !== marker ||
|
|
||||||
Number(env.GITHUB_RUN_ATTEMPT) <= 1
|
|
||||||
)
|
|
||||||
throw new Error(
|
|
||||||
'An existing tag can only be recovered by its original release run.',
|
|
||||||
)
|
|
||||||
git('merge-base', '--is-ancestor', sha, 'origin/main')
|
|
||||||
summary(`Resuming ${tag} at ${sha} from this release run.`)
|
|
||||||
} else {
|
|
||||||
if (pr.state !== 'open' || pr.draft)
|
|
||||||
throw new Error('Expected an open, ready publication PR.')
|
|
||||||
requireApproval(pr, sha)
|
|
||||||
const main = git('rev-parse', 'origin/main')
|
|
||||||
git('merge-base', '--is-ancestor', main, sha)
|
|
||||||
git(
|
|
||||||
'-c',
|
|
||||||
'user.name=github-actions[bot]',
|
|
||||||
'-c',
|
|
||||||
'user.email=41898282+github-actions[bot]@users.noreply.github.com',
|
|
||||||
'tag',
|
|
||||||
'--annotate',
|
|
||||||
'--no-sign',
|
|
||||||
'--message',
|
|
||||||
marker,
|
|
||||||
tag,
|
|
||||||
sha,
|
|
||||||
)
|
|
||||||
// GraphQL updateRefs rejects annotated tag objects; Git preserves atomicity.
|
|
||||||
git(
|
|
||||||
'push',
|
|
||||||
'--atomic',
|
|
||||||
`--force-with-lease=refs/heads/main:${main}`,
|
|
||||||
`--force-with-lease=refs/heads/${pr.head.ref}:${sha}`,
|
|
||||||
`--force-with-lease=refs/tags/${tag}:`,
|
|
||||||
'origin',
|
|
||||||
`${sha}:refs/heads/main`,
|
|
||||||
`${sha}:refs/heads/${pr.head.ref}`,
|
|
||||||
`refs/tags/${tag}:refs/tags/${tag}`,
|
|
||||||
)
|
|
||||||
summary(`Fast-forwarded main and tagged ${tag} at approved commit ${sha}.`)
|
|
||||||
}
|
|
||||||
releaseOutput(tag, sha)
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateTag() {
|
|
||||||
const tag = env.GITHUB_REF_NAME
|
|
||||||
if (!isRelease(tag)) throw new Error('Expected a stable or RC release tag.')
|
|
||||||
fetchBranches()
|
|
||||||
const sha = git('rev-parse', `${env.GITHUB_SHA}^{commit}`)
|
|
||||||
const ref = get(`git/ref/tags/${tag}`)
|
|
||||||
const object = ref.object.type === 'tag' && get(`git/tags/${ref.object.sha}`)
|
|
||||||
if (object && object.message.includes('Release workflow run:'))
|
|
||||||
throw new Error(
|
|
||||||
'Resume the original publication run for workflow-owned tags.',
|
|
||||||
)
|
|
||||||
git('fetch', '--no-tags', 'origin', `refs/tags/${tag}`)
|
|
||||||
if (git('rev-parse', 'FETCH_HEAD^{commit}') !== sha)
|
|
||||||
throw new Error('The tag moved after this run started.')
|
|
||||||
for (const branch of ['main', 'dev'])
|
|
||||||
git('merge-base', '--is-ancestor', sha, `origin/${branch}`)
|
|
||||||
validateVersions(tag, sha)
|
|
||||||
releaseOutput(tag, sha)
|
|
||||||
}
|
|
||||||
|
|
||||||
function prepareDraft() {
|
|
||||||
const tag = env.RELEASE_TAG
|
|
||||||
verifyTarget()
|
|
||||||
const release = list('releases?per_page=100').find(
|
|
||||||
(release) => release.tag_name === tag,
|
|
||||||
)
|
|
||||||
if (release && !release.draft)
|
|
||||||
throw new Error('This release is already public.')
|
|
||||||
const draft =
|
|
||||||
release ||
|
|
||||||
api('releases', {
|
|
||||||
tag_name: tag,
|
|
||||||
target_commitish: env.SOURCE_SHA,
|
|
||||||
name: `Clash Verge Rev ${tag}`,
|
|
||||||
body: 'Draft release, will be updated later.',
|
|
||||||
draft: true,
|
|
||||||
prerelease: isRc(tag),
|
|
||||||
})
|
|
||||||
output({ id: draft.id })
|
|
||||||
}
|
|
||||||
|
|
||||||
function verifyTarget(tag = env.RELEASE_TAG, sha = env.SOURCE_SHA) {
|
|
||||||
const ref = get(`git/ref/tags/${tag}`)
|
|
||||||
const target =
|
|
||||||
ref.object.type === 'tag'
|
|
||||||
? get(`git/tags/${ref.object.sha}`).object
|
|
||||||
: ref.object
|
|
||||||
if (target.type !== 'commit' || target.sha !== sha)
|
|
||||||
throw new Error('The release tag no longer points to the validated source.')
|
|
||||||
}
|
|
||||||
|
|
||||||
function nextDev() {
|
|
||||||
let refresh
|
|
||||||
let tag = env.RELEASE_TAG
|
|
||||||
let source = env.SOURCE_SHA
|
|
||||||
let minor = false
|
|
||||||
if (env.GITHUB_EVENT_NAME === 'pull_request_target') {
|
|
||||||
const event = json(env.GITHUB_EVENT_PATH)
|
|
||||||
const oldBody = event.changes?.body?.from
|
|
||||||
const body = event.pull_request.body
|
|
||||||
if (
|
|
||||||
checked(oldBody, minorLabel) === checked(body, minorLabel) &&
|
|
||||||
(checked(oldBody) || !checked(body))
|
|
||||||
)
|
|
||||||
return
|
|
||||||
if (!canWrite(event.sender.login))
|
|
||||||
throw new Error('Changing the next dev version requires write access.')
|
|
||||||
refresh = get(`pulls/${event.pull_request.number}`)
|
|
||||||
if (refresh.state !== 'open') return
|
|
||||||
tag = requireBotPr(refresh, 'dev', 'next-dev/')
|
|
||||||
if (
|
|
||||||
refresh.head.sha !== event.pull_request.head.sha ||
|
|
||||||
refresh.body !== body
|
|
||||||
)
|
|
||||||
throw new Error(
|
|
||||||
'The version update PR changed after the checkbox selection.',
|
|
||||||
)
|
|
||||||
minor = checked(body, minorLabel)
|
|
||||||
source = body.match(/<!-- next-dev-source:([a-f0-9]{40}) -->/)?.[1]
|
|
||||||
}
|
|
||||||
if (!stablePattern.test(tag) || !/^[a-f0-9]{40}$/.test(source))
|
|
||||||
throw new Error('The next dev version requires a stable release snapshot.')
|
|
||||||
verifyTarget(tag, source)
|
|
||||||
const release = get(`releases/tags/${tag}`)
|
|
||||||
if (release.draft || release.prerelease)
|
|
||||||
throw new Error('The stable release must be public before preparing dev.')
|
|
||||||
const branch = `next-dev/${tag}`
|
|
||||||
if (!refresh) {
|
|
||||||
const existing = list(
|
|
||||||
`pulls?state=all&base=dev&head=${repository.split('/')[0]}:${branch}&per_page=100`,
|
|
||||||
)[0]
|
|
||||||
if (existing) {
|
|
||||||
requireBotPr(existing, 'dev', 'next-dev/')
|
|
||||||
summary(`Existing dev version PR: ${existing.html_url}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchBranches()
|
|
||||||
git('fetch', '--no-tags', 'origin', source)
|
|
||||||
git('merge-base', '--is-ancestor', source, 'origin/main')
|
|
||||||
git('merge-base', '--is-ancestor', source, 'origin/dev')
|
|
||||||
git('checkout', '--detach', 'origin/dev')
|
|
||||||
const sha = git('rev-parse', 'HEAD')
|
|
||||||
validateVersions(tag, source)
|
|
||||||
validateVersions(tag, sha)
|
|
||||||
if (read('Changelog.md') !== gitFile(source, 'Changelog.md'))
|
|
||||||
throw new Error(
|
|
||||||
'Changelog.md changed after this release. Preserve the unreleased entries and prepare the dev update manually.',
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
!read('Changelog.md').split('\n').includes(`## ${tag}`) ||
|
|
||||||
read('docs/Changelog.history.md').split('\n').includes(`## ${tag}`)
|
|
||||||
)
|
|
||||||
throw new Error('The released changelog is missing or already archived.')
|
|
||||||
const [major, minorVersion, patch] = tag.slice(1).split('.').map(BigInt)
|
|
||||||
const patchTag = `v${major}.${minorVersion}.${patch + 1n}`
|
|
||||||
const minorTag = `v${major}.${minorVersion + 1n}.0`
|
|
||||||
const nextTag = minor ? minorTag : patchTag
|
|
||||||
run('pnpm', ['release-version', nextTag])
|
|
||||||
run('bash', ['scripts-workflow/bump_changelog.sh'])
|
|
||||||
const changelog = read('Changelog.md')
|
|
||||||
if (!changelog.includes('## v(Version Goes Here)'))
|
|
||||||
throw new Error('The changelog template has no version placeholder.')
|
|
||||||
writeFileSync(
|
|
||||||
'Changelog.md',
|
|
||||||
changelog.replace('## v(Version Goes Here)', `## ${nextTag}`),
|
|
||||||
)
|
|
||||||
validateVersions(nextTag)
|
|
||||||
const title = `Dev: ${nextTag}`
|
|
||||||
const body = `## ${title}
|
|
||||||
|
|
||||||
After [${tag}](${env.GITHUB_SERVER_URL}/${repository}/releases/tag/${tag}).
|
|
||||||
<!-- next-dev-source:${source} -->
|
|
||||||
|
|
||||||
Patch: \`${patchTag}\` · Minor: \`${minorTag}\`
|
|
||||||
|
|
||||||
- [${minor ? 'x' : ' '}] ${minorLabel}
|
|
||||||
- [ ] ${refreshLabel}
|
|
||||||
|
|
||||||
Changing the version or refreshing requires a new approval.
|
|
||||||
`
|
|
||||||
writeVersionPr({
|
|
||||||
sha,
|
|
||||||
body,
|
|
||||||
refresh,
|
|
||||||
title,
|
|
||||||
branch: refresh
|
|
||||||
? `next-dev-refresh/${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT}`
|
|
||||||
: branch,
|
|
||||||
files: [...versionFiles, 'Changelog.md', 'docs/Changelog.history.md'],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const commands = {
|
|
||||||
prepare,
|
|
||||||
'next-dev': nextDev,
|
|
||||||
'publish-pr': publishPr,
|
|
||||||
publish,
|
|
||||||
'validate-tag': validateTag,
|
|
||||||
'prepare-draft': prepareDraft,
|
|
||||||
'verify-target': verifyTarget,
|
|
||||||
}
|
|
||||||
const command = commands[process.argv[2]]
|
|
||||||
if (!command) throw new Error('Unknown release command.')
|
|
||||||
command()
|
|
||||||
@@ -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/contract-parity.yml",
|
".github/workflows/frontend-check.yml",
|
||||||
".husky/pre-push"
|
".husky/pre-push"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ try {
|
|||||||
'run',
|
'run',
|
||||||
'-p',
|
'-p',
|
||||||
'clash-verge',
|
'clash-verge',
|
||||||
'--example',
|
'--bin',
|
||||||
'frontend-contract',
|
'frontend-contract',
|
||||||
'--locked',
|
'--locked',
|
||||||
'-q',
|
'-q',
|
||||||
|
|||||||
@@ -434,13 +434,12 @@ 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(
|
||||||
viewport[0] === 1000 &&
|
run.viewport.driver.width / viewport[2] === 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(
|
||||||
|
|||||||
+18
-21
@@ -29,12 +29,13 @@ 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.3", features = [] }
|
tauri-build = { version = "2.7.1", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clash-verge-draft = { workspace = true }
|
clash-verge-draft = { workspace = true }
|
||||||
clash-verge-media-unlock = { workspace = true }
|
clash-verge-media-unlock = { workspace = true }
|
||||||
clash-verge-logging = { workspace = true }
|
clash-verge-logging = { workspace = true }
|
||||||
|
clash-verge-signal = { workspace = true }
|
||||||
clash-verge-i18n = { workspace = true }
|
clash-verge-i18n = { workspace = true }
|
||||||
clash-verge-limiter = { workspace = true }
|
clash-verge-limiter = { workspace = true }
|
||||||
tauri-plugin-clash-verge-sysinfo = { workspace = true }
|
tauri-plugin-clash-verge-sysinfo = { workspace = true }
|
||||||
@@ -72,55 +73,51 @@ 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.5.0"
|
tauri-plugin-shell = "2.4.0"
|
||||||
tauri-plugin-opener = "2.7.2"
|
tauri-plugin-opener = "2.7.0"
|
||||||
tauri-plugin-dialog = "2.8.3"
|
tauri-plugin-dialog = "2.8.1"
|
||||||
tauri-plugin-fs = "2.7.1"
|
tauri-plugin-fs = "2.6.0"
|
||||||
tauri-plugin-process = "2.4.0"
|
tauri-plugin-process = "2.4.0"
|
||||||
tauri-plugin-deep-link = "2.6.2"
|
tauri-plugin-deep-link = "2.6.1"
|
||||||
tauri-plugin-window-state = "2.5.0"
|
tauri-plugin-window-state = "2.5.0"
|
||||||
zip = "9.1.3"
|
zip = "9.0.1"
|
||||||
flate2 = "1.1.10"
|
flate2 = "1.1.10"
|
||||||
reqwest_dav = "0.3.3"
|
reqwest_dav = "0.3.3"
|
||||||
aes-gcm = { version = "0.11.1" }
|
aes-gcm = { version = "0.11.1" }
|
||||||
base64 = "0.23.1"
|
base64 = "0.23.1"
|
||||||
minisign-verify = "0.3.0"
|
minisign-verify = "0.3.0"
|
||||||
sha2 = "0.11.1"
|
sha2 = "0.11"
|
||||||
getrandom = "0.4.3"
|
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.7.0"
|
tauri-plugin-notification = "2.5.1"
|
||||||
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.3"
|
tauri-plugin-http = "2.8.0"
|
||||||
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", branch = "main" }
|
||||||
async-trait = "0.1.92"
|
async-trait = "0.1.92"
|
||||||
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.6", version = "2.7.6", features = [
|
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.6", version = "2.7.6", features = [
|
||||||
"client",
|
"client",
|
||||||
] }
|
] }
|
||||||
arc-swap = "1.9.2"
|
arc-swap = "1.9.2"
|
||||||
tokio-rustls = "0.26.6"
|
tokio-rustls = "0.26.6"
|
||||||
rustls = { version = "0.23.46", features = ["ring"] }
|
rustls = { version = "0.23.45", features = ["ring"] }
|
||||||
webpki-roots = "1.0.9"
|
webpki-roots = "1.0.9"
|
||||||
dark-light = "3.0.0"
|
dark-light = "3.0.0"
|
||||||
bytes = "1.12.1"
|
bytes = "1.12.1"
|
||||||
tokio-util = { version = "0.7.20", features = ["time"] }
|
tokio-util = { version = "0.7.19", features = ["time"] }
|
||||||
specta = { version = "=2.0.0-rc.25", features = ["derive", "function", "serde_json"] }
|
specta = { version = "=2.0.0-rc.25", features = ["derive", "function", "serde_json"] }
|
||||||
tauri-specta = { version = "=2.0.0-rc.25", features = ["derive", "typescript"] }
|
tauri-specta = { version = "=2.0.0-rc.25", features = ["derive", "typescript"] }
|
||||||
specta-typescript = "=0.0.12"
|
specta-typescript = "=0.0.12"
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
libc = "0.2.191"
|
libc = "0.2.190"
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
|
||||||
# Upstream has no session-end signal support on Linux; see crates/clash-verge-signal.
|
|
||||||
clash-verge-signal = { workspace = true }
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
tauri-plugin-wdio-webdriver = { version = "=1.5.0", optional = true }
|
tauri-plugin-wdio-webdriver = { version = "=1.4.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 +150,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.1"
|
tauri-plugin-global-shortcut = "2.4.0"
|
||||||
tauri-plugin-updater = "2.14.1"
|
tauri-plugin-updater = "2.13.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = { workspace = true }
|
criterion = { workspace = true }
|
||||||
|
|||||||
+5
-10
@@ -1,9 +1,10 @@
|
|||||||
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 or examples.
|
// Tauri's resource is linked to the app binary, but not to Rust unit-test harnesses.
|
||||||
// Give non-app targets their own manifest so tests and examples can resolve TaskDialogIndirect.
|
// Give mock-context builds their own manifest so tests 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(
|
||||||
@@ -24,14 +25,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
</assembly>
|
</assembly>
|
||||||
"#,
|
"#,
|
||||||
)?;
|
)?;
|
||||||
let manifest_display = manifest_path.display();
|
println!("cargo:rustc-link-arg=/MANIFEST:EMBED");
|
||||||
if std::env::var_os("CARGO_FEATURE_CLIPPY").is_some() {
|
println!("cargo:rustc-link-arg=/MANIFESTINPUT:{}", manifest_path.display());
|
||||||
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")]
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Dev-only target: Tauri ships everything under src/bin/ to /usr/bin in deb/rpm
|
|
||||||
// bundles (PR #8131), so this generator must stay an example, never a bin.
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut args = std::env::args_os().skip(1);
|
let mut args = std::env::args_os().skip(1);
|
||||||
let output = args
|
let output = args
|
||||||
@@ -282,9 +282,7 @@ async fn handle_validation_failure(
|
|||||||
) -> CmdResult<ValidationOutcome> {
|
) -> CmdResult<ValidationOutcome> {
|
||||||
logging!(warn, Type::Cmd, "配置验证失败: {}", outcome);
|
logging!(warn, Type::Cmd, "配置验证失败: {}", outcome);
|
||||||
discard_and_restore(current_profile).await?;
|
discard_and_restore(current_profile).await?;
|
||||||
if !matches!(outcome, ValidationOutcome::Busy) {
|
handle_validation_notice(&outcome, ValidationNoticeTarget::Runtime, "运行时配置");
|
||||||
handle_validation_notice(&outcome, ValidationNoticeTarget::Runtime, "运行时配置");
|
|
||||||
}
|
|
||||||
Ok(outcome)
|
Ok(outcome)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,6 +311,9 @@ async fn perform_config_update(
|
|||||||
current_value: Option<&String>,
|
current_value: Option<&String>,
|
||||||
current_profile: Option<&String>,
|
current_profile: Option<&String>,
|
||||||
) -> CmdResult<ValidationOutcome> {
|
) -> CmdResult<ValidationOutcome> {
|
||||||
|
defer! {
|
||||||
|
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
|
||||||
|
}
|
||||||
let update_result = run_profile_config_update_transition(|| CoreManager::global().update_config_forced()).await;
|
let update_result = run_profile_config_update_transition(|| CoreManager::global().update_config_forced()).await;
|
||||||
|
|
||||||
match update_result {
|
match update_result {
|
||||||
@@ -372,6 +373,8 @@ pub struct ProfilePatch {
|
|||||||
#[specta(type = Option<std::string::String>)]
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub url: Option<String>,
|
pub url: Option<String>,
|
||||||
pub selected: Option<Vec<crate::config::PrfSelected>>,
|
pub selected: Option<Vec<crate::config::PrfSelected>>,
|
||||||
|
#[specta(type = Option<std::collections::BTreeMap<std::string::String, std::string::String>>)]
|
||||||
|
pub latency_test_urls: Option<std::collections::BTreeMap<String, String>>,
|
||||||
pub extra: Option<crate::config::PrfExtra>,
|
pub extra: Option<crate::config::PrfExtra>,
|
||||||
pub updated: Option<usize>,
|
pub updated: Option<usize>,
|
||||||
pub option: Option<PrfOption>,
|
pub option: Option<PrfOption>,
|
||||||
@@ -386,6 +389,7 @@ impl From<ProfilePatch> for PrfItem {
|
|||||||
file: patch.file,
|
file: patch.file,
|
||||||
url: patch.url,
|
url: patch.url,
|
||||||
selected: patch.selected,
|
selected: patch.selected,
|
||||||
|
latency_test_urls: patch.latency_test_urls,
|
||||||
extra: patch.extra,
|
extra: patch.extra,
|
||||||
updated: patch.updated,
|
updated: patch.updated,
|
||||||
option: patch.option,
|
option: patch.option,
|
||||||
@@ -405,9 +409,6 @@ pub async fn patch_profiles_config(profiles: ProfileSelection) -> CmdResult<Vali
|
|||||||
logging!(info, Type::Cmd, "当前正在切换配置,放弃请求");
|
logging!(info, Type::Cmd, "当前正在切换配置,放弃请求");
|
||||||
return Ok(ValidationOutcome::Busy);
|
return Ok(ValidationOutcome::Busy);
|
||||||
}
|
}
|
||||||
defer! {
|
|
||||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
|
|
||||||
}
|
|
||||||
let _profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
|
let _profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
|
||||||
|
|
||||||
let profiles = IProfiles {
|
let profiles = IProfiles {
|
||||||
|
|||||||
@@ -108,20 +108,32 @@ fn runtime_group_scopes(config: Option<&Mapping>) -> BTreeMap<String, GroupScope
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn get_proxy_view() -> CmdResult<ProxyViewV1> {
|
pub async fn get_proxy_view() -> CmdResult<ProxyViewV1> {
|
||||||
|
proxy_view(None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn proxy_view(provider_timeout: Option<std::time::Duration>) -> CmdResult<ProxyViewV1> {
|
||||||
let runtime = Config::runtime().await;
|
let runtime = Config::runtime().await;
|
||||||
let latest_runtime = runtime.latest_arc();
|
let latest_runtime = runtime.latest_arc();
|
||||||
let runtime_group_order = runtime_group_order(latest_runtime.config.as_ref());
|
let runtime_group_order = runtime_group_order(latest_runtime.config.as_ref());
|
||||||
let group_scopes = runtime_group_scopes(latest_runtime.config.as_ref());
|
let group_scopes = runtime_group_scopes(latest_runtime.config.as_ref());
|
||||||
|
|
||||||
let mihomo = Handle::mihomo();
|
let mihomo = Handle::mihomo();
|
||||||
let (proxies, providers) = tokio::join!(mihomo.get_proxies(), mihomo.get_proxy_providers(),);
|
let (proxies, providers) = tokio::join!(mihomo.get_proxies(), async {
|
||||||
|
match provider_timeout {
|
||||||
|
Some(timeout) => tokio::time::timeout(timeout, mihomo.get_proxy_providers())
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.and_then(Result::ok),
|
||||||
|
None => mihomo.get_proxy_providers().await.ok(),
|
||||||
|
}
|
||||||
|
});
|
||||||
let proxies = proxies.stringify_err()?;
|
let proxies = proxies.stringify_err()?;
|
||||||
|
|
||||||
Ok(ProxyViewBuilder::build(ProxyViewInput {
|
Ok(ProxyViewBuilder::build(ProxyViewInput {
|
||||||
runtime_group_order,
|
runtime_group_order,
|
||||||
group_scopes,
|
group_scopes,
|
||||||
proxies,
|
proxies,
|
||||||
providers: providers.ok(),
|
providers,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ pub struct PrfItem {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub selected: Option<Vec<PrfSelected>>,
|
pub selected: Option<Vec<PrfSelected>>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[specta(type = Option<std::collections::BTreeMap<std::string::String, std::string::String>>)]
|
||||||
|
pub latency_test_urls: Option<std::collections::BTreeMap<String, String>>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub extra: Option<PrfExtra>,
|
pub extra: Option<PrfExtra>,
|
||||||
|
|
||||||
@@ -229,6 +233,7 @@ impl PrfItem {
|
|||||||
desc: Some(desc),
|
desc: Some(desc),
|
||||||
file: Some(file),
|
file: Some(file),
|
||||||
url: None,
|
url: None,
|
||||||
|
latency_test_urls: None,
|
||||||
selected: None,
|
selected: None,
|
||||||
extra: None,
|
extra: None,
|
||||||
option: Some(PrfOption {
|
option: Some(PrfOption {
|
||||||
@@ -408,6 +413,7 @@ impl PrfItem {
|
|||||||
desc: desc.cloned(),
|
desc: desc.cloned(),
|
||||||
file: Some(file),
|
file: Some(file),
|
||||||
url: Some(url.as_str().into()),
|
url: Some(url.as_str().into()),
|
||||||
|
latency_test_urls: None,
|
||||||
selected: None,
|
selected: None,
|
||||||
extra,
|
extra,
|
||||||
option: Some(PrfOption {
|
option: Some(PrfOption {
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ impl IProfiles {
|
|||||||
patch!(each, item, file);
|
patch!(each, item, file);
|
||||||
patch!(each, item, url);
|
patch!(each, item, url);
|
||||||
patch!(each, item, selected);
|
patch!(each, item, selected);
|
||||||
|
patch!(each, item, latency_test_urls);
|
||||||
patch!(each, item, extra);
|
patch!(each, item, extra);
|
||||||
patch!(each, item, updated);
|
patch!(each, item, updated);
|
||||||
patch!(each, item, option);
|
patch!(each, item, option);
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ pub struct IVerge {
|
|||||||
|
|
||||||
pub collapse_navbar: Option<bool>,
|
pub collapse_navbar: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub enable_system_titlebar: Option<bool>,
|
||||||
|
|
||||||
pub sysproxy_tray_icon: Option<bool>,
|
pub sysproxy_tray_icon: Option<bool>,
|
||||||
|
|
||||||
pub tun_tray_icon: Option<bool>,
|
pub tun_tray_icon: Option<bool>,
|
||||||
@@ -374,6 +377,7 @@ impl IVerge {
|
|||||||
menu_icon: Some("monochrome".into()),
|
menu_icon: Some("monochrome".into()),
|
||||||
notice_position: Some("top-right".into()),
|
notice_position: Some("top-right".into()),
|
||||||
collapse_navbar: Some(false),
|
collapse_navbar: Some(false),
|
||||||
|
enable_system_titlebar: Some(true),
|
||||||
common_tray_icon: Some(false),
|
common_tray_icon: Some(false),
|
||||||
sysproxy_tray_icon: Some(false),
|
sysproxy_tray_icon: Some(false),
|
||||||
tun_tray_icon: Some(false),
|
tun_tray_icon: Some(false),
|
||||||
@@ -461,6 +465,7 @@ impl IVerge {
|
|||||||
patch!(proxy_group_tools_position);
|
patch!(proxy_group_tools_position);
|
||||||
patch!(notice_position);
|
patch!(notice_position);
|
||||||
patch!(collapse_navbar);
|
patch!(collapse_navbar);
|
||||||
|
patch!(enable_system_titlebar);
|
||||||
patch!(common_tray_icon);
|
patch!(common_tray_icon);
|
||||||
patch!(sysproxy_tray_icon);
|
patch!(sysproxy_tray_icon);
|
||||||
patch!(tun_tray_icon);
|
patch!(tun_tray_icon);
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ 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",
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ impl AutoBackupManager {
|
|||||||
{
|
{
|
||||||
*self.settings.write() = settings;
|
*self.settings.write() = settings;
|
||||||
}
|
}
|
||||||
let _ = self.settings_tx.send_replace(settings);
|
let _ = self.settings_tx.send(settings);
|
||||||
self.maybe_start_runner(settings);
|
self.maybe_start_runner(settings);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -119,7 +119,7 @@ impl AutoBackupManager {
|
|||||||
{
|
{
|
||||||
*self.settings.write() = settings;
|
*self.settings.write() = settings;
|
||||||
}
|
}
|
||||||
let _ = self.settings_tx.send_replace(settings);
|
let _ = self.settings_tx.send(settings);
|
||||||
self.maybe_start_runner(settings);
|
self.maybe_start_runner(settings);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::{Serialize, Serializer, ser::SerializeMap as _};
|
||||||
use tauri_plugin_mihomo::models::{
|
use tauri_plugin_mihomo::models::{
|
||||||
DelayHistory, Proxies, Proxy, ProxyProvider, ProxyProviders, ProxyType, VehicleType,
|
DelayHistory, Extra, Proxies, Proxy, ProxyProvider, ProxyProviders, ProxyType, VehicleType,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct ProxyViewInput {
|
pub struct ProxyViewInput {
|
||||||
@@ -57,6 +57,28 @@ pub struct ProxyCapabilities {
|
|||||||
pub smux: bool,
|
pub smux: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, specta::Type)]
|
||||||
|
struct ProxyLatencyHistory<'a> {
|
||||||
|
#[specta(type = Vec<crate::ipc::DelayHistory>)]
|
||||||
|
history: &'a [DelayHistory],
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize_proxy_latency_history<S: Serializer>(
|
||||||
|
extra: &HashMap<String, Extra>,
|
||||||
|
serializer: S,
|
||||||
|
) -> Result<S::Ok, S::Error> {
|
||||||
|
let mut map = serializer.serialize_map(Some(extra.len()))?;
|
||||||
|
for (url, extra) in extra {
|
||||||
|
map.serialize_entry(
|
||||||
|
url,
|
||||||
|
&ProxyLatencyHistory {
|
||||||
|
history: &extra.history,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
map.end()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
|
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ProxyGroupView {
|
pub struct ProxyGroupView {
|
||||||
@@ -76,6 +98,9 @@ pub struct ProxyGroupView {
|
|||||||
pub test_url: Option<String>,
|
pub test_url: Option<String>,
|
||||||
#[specta(type = Vec<crate::ipc::DelayHistory>)]
|
#[specta(type = Vec<crate::ipc::DelayHistory>)]
|
||||||
pub history: Vec<DelayHistory>,
|
pub history: Vec<DelayHistory>,
|
||||||
|
#[serde(serialize_with = "serialize_proxy_latency_history")]
|
||||||
|
#[specta(type = HashMap<String, ProxyLatencyHistory<'static>>)]
|
||||||
|
pub extra: HashMap<String, Extra>,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub capabilities: ProxyCapabilities,
|
pub capabilities: ProxyCapabilities,
|
||||||
pub members: Vec<ProxyMemberRef>,
|
pub members: Vec<ProxyMemberRef>,
|
||||||
@@ -91,6 +116,9 @@ pub struct ProxyNodeView {
|
|||||||
pub proxy_type: ProxyType,
|
pub proxy_type: ProxyType,
|
||||||
#[specta(type = Vec<crate::ipc::DelayHistory>)]
|
#[specta(type = Vec<crate::ipc::DelayHistory>)]
|
||||||
pub history: Vec<DelayHistory>,
|
pub history: Vec<DelayHistory>,
|
||||||
|
#[serde(serialize_with = "serialize_proxy_latency_history")]
|
||||||
|
#[specta(type = HashMap<String, ProxyLatencyHistory<'static>>)]
|
||||||
|
pub extra: HashMap<String, Extra>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub id: Option<String>,
|
pub id: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -256,7 +284,7 @@ impl ProxyViewBuilder {
|
|||||||
} else {
|
} else {
|
||||||
ProxyViewProviderState::Unavailable
|
ProxyViewProviderState::Unavailable
|
||||||
};
|
};
|
||||||
let (mut core_groups, core_nodes) = partition_core(proxies);
|
let (mut core_groups, core_nodes) = partition_core(proxies, providers.is_some());
|
||||||
let (mut records, core_node_ids) = build_core_records(core_nodes);
|
let (mut records, core_node_ids) = build_core_records(core_nodes);
|
||||||
let (providers, provider_candidates) = build_provider_records(providers, &mut records);
|
let (providers, provider_candidates) = build_provider_records(providers, &mut records);
|
||||||
let resolver = MemberResolver {
|
let resolver = MemberResolver {
|
||||||
@@ -289,14 +317,14 @@ impl ProxyViewBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn partition_core(proxies: Proxies) -> (BTreeMap<String, Proxy>, BTreeMap<String, Proxy>) {
|
fn partition_core(proxies: Proxies, providers_available: bool) -> (BTreeMap<String, Proxy>, BTreeMap<String, Proxy>) {
|
||||||
let mut groups = BTreeMap::new();
|
let mut groups = BTreeMap::new();
|
||||||
let mut nodes = BTreeMap::new();
|
let mut nodes = BTreeMap::new();
|
||||||
|
|
||||||
for (name, proxy) in proxies.proxies {
|
for (name, proxy) in proxies.proxies {
|
||||||
if proxy.all.is_some() {
|
if proxy.all.is_some() {
|
||||||
groups.insert(name, proxy);
|
groups.insert(name, proxy);
|
||||||
} else {
|
} else if !providers_available || proxy.provider_name.is_empty() {
|
||||||
nodes.insert(name, proxy);
|
nodes.insert(name, proxy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -403,6 +431,7 @@ fn build_group(name: String, proxy: Proxy, resolver: &MemberResolver<'_>) -> Pro
|
|||||||
now,
|
now,
|
||||||
test_url,
|
test_url,
|
||||||
history,
|
history,
|
||||||
|
extra,
|
||||||
udp,
|
udp,
|
||||||
xudp,
|
xudp,
|
||||||
tfo,
|
tfo,
|
||||||
@@ -435,6 +464,7 @@ fn build_group(name: String, proxy: Proxy, resolver: &MemberResolver<'_>) -> Pro
|
|||||||
icon,
|
icon,
|
||||||
test_url,
|
test_url,
|
||||||
history,
|
history,
|
||||||
|
extra,
|
||||||
capabilities: ProxyCapabilities {
|
capabilities: ProxyCapabilities {
|
||||||
udp,
|
udp,
|
||||||
xudp,
|
xudp,
|
||||||
@@ -453,6 +483,7 @@ fn build_node(record_id: String, name: String, proxy: Proxy, source: ProxyNodeSo
|
|||||||
icon,
|
icon,
|
||||||
test_url,
|
test_url,
|
||||||
history,
|
history,
|
||||||
|
extra,
|
||||||
udp,
|
udp,
|
||||||
xudp,
|
xudp,
|
||||||
tfo,
|
tfo,
|
||||||
@@ -467,6 +498,7 @@ fn build_node(record_id: String, name: String, proxy: Proxy, source: ProxyNodeSo
|
|||||||
name,
|
name,
|
||||||
proxy_type,
|
proxy_type,
|
||||||
history,
|
history,
|
||||||
|
extra,
|
||||||
id,
|
id,
|
||||||
hidden,
|
hidden,
|
||||||
icon,
|
icon,
|
||||||
@@ -520,3 +552,62 @@ fn build_standalone(core_node_ids: &BTreeMap<String, String>) -> Vec<String> {
|
|||||||
);
|
);
|
||||||
standalone
|
standalone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn provider_echo_in_proxies_does_not_override_group_membership() -> anyhow::Result<()> {
|
||||||
|
let view = ProxyViewBuilder::build(ProxyViewInput {
|
||||||
|
runtime_group_order: vec!["A".into(), "B".into()],
|
||||||
|
group_scopes: ["A", "B"]
|
||||||
|
.into_iter()
|
||||||
|
.map(|name| {
|
||||||
|
(
|
||||||
|
name.into(),
|
||||||
|
GroupScope {
|
||||||
|
providers: vec![name.into()],
|
||||||
|
exclude_types: vec![],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
proxies: serde_json::from_value(json!({"proxies": {
|
||||||
|
"A": {"all": ["node"], "type": "Selector"},
|
||||||
|
"B": {"all": ["node"], "type": "Selector"},
|
||||||
|
"node": {"name": "node", "provider-name": "A", "history": [{"time": "2026-10-08T00:00:00Z", "delay": 32}]}
|
||||||
|
}}))?,
|
||||||
|
providers: Some(serde_json::from_value(json!({"providers": {
|
||||||
|
"A": {"vehicleType": "Inline", "proxies": [{"name": "node", "history": [{"time": "2026-10-08T00:00:00Z", "delay": 32}]}]},
|
||||||
|
"B": {"vehicleType": "Inline", "proxies": [{"name": "node", "history": [{"time": "2026-10-08T00:00:00Z", "delay": 123}]}]}
|
||||||
|
}}))?),
|
||||||
|
});
|
||||||
|
for (group, expected) in view.groups.iter().zip([32, 123]) {
|
||||||
|
let ProxyMemberRef::Node { record_id, .. } = &group.members[0] else {
|
||||||
|
anyhow::bail!("Expected node")
|
||||||
|
};
|
||||||
|
let record = &view.records[record_id];
|
||||||
|
assert_eq!(record.history.last().map(|entry| entry.delay), Some(expected));
|
||||||
|
assert!(
|
||||||
|
matches!(&record.source, ProxyNodeSource::Provider { provider_name, .. } if provider_name == &group.name)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn unavailable_provider_keeps_core_echo_interactable() -> anyhow::Result<()> {
|
||||||
|
let view = ProxyViewBuilder::build(ProxyViewInput {
|
||||||
|
runtime_group_order: vec!["A".into()],
|
||||||
|
group_scopes: BTreeMap::new(),
|
||||||
|
proxies: serde_json::from_value(json!({"proxies": {
|
||||||
|
"A": {"all": ["node"], "type": "Selector"},
|
||||||
|
"node": {"name": "node", "provider-name": "provider"}
|
||||||
|
}}))?,
|
||||||
|
providers: None,
|
||||||
|
});
|
||||||
|
assert!(matches!(&view.groups[0].members[0], ProxyMemberRef::Node { .. }));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
use super::Tray;
|
||||||
|
use crate::{
|
||||||
|
Type, cmd,
|
||||||
|
config::Config,
|
||||||
|
core::{
|
||||||
|
handle,
|
||||||
|
notify::{Refresh, announce},
|
||||||
|
proxy_view::{ProxyMemberRef, ProxyNodeSource},
|
||||||
|
},
|
||||||
|
logging,
|
||||||
|
};
|
||||||
|
|
||||||
|
const TRAY_DELAY_TEST_CONCURRENCY: usize = 10;
|
||||||
|
const TRAY_DEFAULT_LATENCY_URL: &str = "http://cp.cloudflare.com/generate_204";
|
||||||
|
|
||||||
|
fn is_latency_test_url(raw: &str) -> bool {
|
||||||
|
let raw = raw.trim();
|
||||||
|
raw.split_once("://")
|
||||||
|
.is_some_and(|(scheme, _)| scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https"))
|
||||||
|
&& tauri::Url::parse(raw).is_ok_and(|url| url.has_host() && matches!(url.scheme(), "http" | "https"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn test_url(
|
||||||
|
group: &crate::core::proxy_view::ProxyGroupView,
|
||||||
|
verge: &crate::config::IVerge,
|
||||||
|
profiles: &crate::config::IProfiles,
|
||||||
|
) -> String {
|
||||||
|
let custom_url = profiles
|
||||||
|
.current
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|uid| profiles.get_item(uid).ok())
|
||||||
|
.and_then(|profile| profile.latency_test_urls.as_ref())
|
||||||
|
.and_then(|urls| urls.get(group.name.as_str()))
|
||||||
|
.map(|url| url.as_str())
|
||||||
|
.filter(|url| !url.trim().is_empty());
|
||||||
|
let group_url = group.test_url.as_deref().filter(|url| !url.trim().is_empty());
|
||||||
|
let default_url = verge
|
||||||
|
.default_latency_test
|
||||||
|
.as_deref()
|
||||||
|
.filter(|url| !url.trim().is_empty());
|
||||||
|
custom_url
|
||||||
|
.or(group_url)
|
||||||
|
.or(default_url)
|
||||||
|
.filter(|&url| is_latency_test_url(url))
|
||||||
|
.unwrap_or(TRAY_DEFAULT_LATENCY_URL)
|
||||||
|
.trim()
|
||||||
|
.to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
static RUNNING: parking_lot::Mutex<std::collections::BTreeSet<String>> =
|
||||||
|
parking_lot::Mutex::new(std::collections::BTreeSet::new());
|
||||||
|
|
||||||
|
pub(super) fn is_running(group_name: &str) -> bool {
|
||||||
|
RUNNING.lock().contains(group_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn test_proxy_group_delay(group_name: &str) {
|
||||||
|
if !RUNNING.lock().insert(group_name.to_owned()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Tray::global().refresh_latency_item(group_name);
|
||||||
|
let _running = scopeguard::guard(group_name, |name| {
|
||||||
|
RUNNING.lock().remove(name);
|
||||||
|
Tray::global().refresh_latency_item(name);
|
||||||
|
});
|
||||||
|
use futures::{StreamExt as _, stream};
|
||||||
|
|
||||||
|
let view = match cmd::proxy::get_proxy_view().await {
|
||||||
|
Ok(view) => view,
|
||||||
|
Err(err) => {
|
||||||
|
logging!(warn, Type::Tray, "Failed to read proxies for tray latency test: {err}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let verge = Config::verge().await.latest_arc();
|
||||||
|
|
||||||
|
let group = view
|
||||||
|
.groups
|
||||||
|
.iter()
|
||||||
|
.chain(view.global.iter())
|
||||||
|
.find(|group| group.name.as_str() == group_name);
|
||||||
|
let Some(group) = group else {
|
||||||
|
logging!(warn, Type::Tray, "Tray latency test group not found: {group_name}");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let profiles = Config::profiles().await.latest_arc();
|
||||||
|
let url = test_url(group, &verge, &profiles);
|
||||||
|
let timeout = verge
|
||||||
|
.default_latency_timeout
|
||||||
|
.filter(|timeout| *timeout > 0)
|
||||||
|
.unwrap_or(10000) as u32;
|
||||||
|
|
||||||
|
let mut targets: Vec<(std::string::String, Option<std::string::String>)> = Vec::with_capacity(group.members.len());
|
||||||
|
for member in &group.members {
|
||||||
|
match member {
|
||||||
|
ProxyMemberRef::Node { name, record_id } => match view.records.get(record_id).map(|node| &node.source) {
|
||||||
|
Some(ProxyNodeSource::Provider {
|
||||||
|
provider_name,
|
||||||
|
proxy_name,
|
||||||
|
}) => targets.push((proxy_name.clone(), Some(provider_name.clone()))),
|
||||||
|
_ => targets.push((name.clone(), None)),
|
||||||
|
},
|
||||||
|
ProxyMemberRef::Group { name } => targets.push((name.clone(), None)),
|
||||||
|
ProxyMemberRef::Unresolved { .. } => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mihomo = handle::Handle::mihomo();
|
||||||
|
|
||||||
|
stream::iter(targets.into_iter().map(move |(member, provider)| {
|
||||||
|
let url = url.clone();
|
||||||
|
async move {
|
||||||
|
let result = match provider {
|
||||||
|
Some(provider_name) => {
|
||||||
|
mihomo
|
||||||
|
.healthcheck_node_in_provider(&provider_name, &member, &url, timeout)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
None => mihomo.delay_proxy_by_name(&member, &url, timeout).await,
|
||||||
|
};
|
||||||
|
if let Err(err) = result {
|
||||||
|
logging!(debug, Type::Tray, "Tray latency test failed: {member} - {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.buffer_unordered(TRAY_DELAY_TEST_CONCURRENCY)
|
||||||
|
.for_each(|()| std::future::ready(()))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Err(err) = cmd::proxy::sync_tray_proxy_selection().await {
|
||||||
|
logging!(warn, Type::Tray, "Failed to refresh tray after latency test: {err}");
|
||||||
|
}
|
||||||
|
|
||||||
|
announce(Refresh::Proxies);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn latency_urls_require_a_valid_http_host_and_port() {
|
||||||
|
for invalid in [
|
||||||
|
"http://?",
|
||||||
|
"http://host:invalid",
|
||||||
|
"http://bad host/",
|
||||||
|
"ftp://localhost",
|
||||||
|
"http:localhost",
|
||||||
|
] {
|
||||||
|
assert!(!is_latency_test_url(invalid), "{invalid}");
|
||||||
|
}
|
||||||
|
for valid in ["http://localhost/", " https://example.com/ ", "HTTP://localhost:8080/"] {
|
||||||
|
assert!(is_latency_test_url(valid), "{valid}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ define_menu! {
|
|||||||
outbound_modes => OUTBOUND_MODES, "tray_outbound_modes", "tray.outboundModes",
|
outbound_modes => OUTBOUND_MODES, "tray_outbound_modes", "tray.outboundModes",
|
||||||
profiles => PROFILES, "tray_profiles", "tray.profiles",
|
profiles => PROFILES, "tray_profiles", "tray.profiles",
|
||||||
proxies => PROXIES, "tray_proxies", "tray.proxies",
|
proxies => PROXIES, "tray_proxies", "tray.proxies",
|
||||||
|
test_latency => TEST_LATENCY, "tray_test_latency", "tray.testLatency",
|
||||||
system_proxy => SYSTEM_PROXY, "tray_system_proxy", "tray.systemProxy",
|
system_proxy => SYSTEM_PROXY, "tray_system_proxy", "tray.systemProxy",
|
||||||
tun_mode => TUN_MODE, "tray_tun_mode", "tray.tunMode",
|
tun_mode => TUN_MODE, "tray_tun_mode", "tray.tunMode",
|
||||||
close_all_connections => CLOSE_ALL_CONNECTIONS, "tray_close_all_connections", "tray.closeAllConnections",
|
close_all_connections => CLOSE_ALL_CONNECTIONS, "tray_close_all_connections", "tray.closeAllConnections",
|
||||||
|
|||||||
+363
-141
@@ -1,5 +1,6 @@
|
|||||||
use crate::config::{IProfilePreview, IVerge};
|
use crate::config::{IProfilePreview, IVerge};
|
||||||
use crate::core::lightweight;
|
use crate::core::lightweight;
|
||||||
|
use crate::core::proxy_view::{ProxyGroupView, ProxyMemberRef, ProxyNodeSource, ProxyViewProviderState, ProxyViewV1};
|
||||||
use crate::core::tray::menu_def::TrayAction;
|
use crate::core::tray::menu_def::TrayAction;
|
||||||
use crate::process::AsyncHandler;
|
use crate::process::AsyncHandler;
|
||||||
use crate::singleton;
|
use crate::singleton;
|
||||||
@@ -14,7 +15,6 @@ use crate::{
|
|||||||
use clash_verge_limiter::{Limiter, SystemClock, SystemLimiter};
|
use clash_verge_limiter::{Limiter, SystemClock, SystemLimiter};
|
||||||
use clash_verge_logging::logging_error;
|
use clash_verge_logging::logging_error;
|
||||||
use tauri::tray::{MouseButton, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent};
|
use tauri::tray::{MouseButton, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent};
|
||||||
use tauri_plugin_mihomo::models::Proxies;
|
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
use super::handle;
|
use super::handle;
|
||||||
@@ -28,6 +28,7 @@ use tauri::{
|
|||||||
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
|
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mod latency;
|
||||||
mod menu_def;
|
mod menu_def;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
mod speed_task;
|
mod speed_task;
|
||||||
@@ -55,8 +56,25 @@ enum IconKind {
|
|||||||
Tun,
|
Tun,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct LatencyMenuItem {
|
||||||
|
item: MenuItem<Wry>,
|
||||||
|
running: bool,
|
||||||
|
proxies: Vec<ProxyLatencyItem>,
|
||||||
|
provider_state: ProxyViewProviderState,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ProxyLatencyItem {
|
||||||
|
item: CheckMenuItem<Wry>,
|
||||||
|
name: std::string::String,
|
||||||
|
provider_name: Option<std::string::String>,
|
||||||
|
text: std::string::String,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Tray {
|
pub struct Tray {
|
||||||
limiter: SystemLimiter,
|
limiter: SystemLimiter,
|
||||||
|
proxy_hover_limiter: SystemLimiter,
|
||||||
|
menu_update: tokio::sync::Mutex<()>,
|
||||||
|
latency_items: parking_lot::Mutex<HashMap<std::string::String, LatencyMenuItem>>,
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
speed_controller: speed_task::TraySpeedController,
|
speed_controller: speed_task::TraySpeedController,
|
||||||
}
|
}
|
||||||
@@ -130,6 +148,9 @@ impl Default for Tray {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
limiter: Limiter::new(Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS), SystemClock),
|
limiter: Limiter::new(Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS), SystemClock),
|
||||||
|
proxy_hover_limiter: Limiter::new(Duration::from_secs(1), SystemClock),
|
||||||
|
menu_update: tokio::sync::Mutex::new(()),
|
||||||
|
latency_items: parking_lot::Mutex::new(HashMap::new()),
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
speed_controller: speed_task::TraySpeedController::new(),
|
speed_controller: speed_task::TraySpeedController::new(),
|
||||||
}
|
}
|
||||||
@@ -195,6 +216,7 @@ impl Tray {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn update_menu_internal(&self, app_handle: &AppHandle, include_proxy_groups: bool) -> Result<()> {
|
async fn update_menu_internal(&self, app_handle: &AppHandle, include_proxy_groups: bool) -> Result<()> {
|
||||||
|
let _update = self.menu_update.lock().await;
|
||||||
let Some(tray) = app_handle.tray_by_id(TRAY_ID) else {
|
let Some(tray) = app_handle.tray_by_id(TRAY_ID) else {
|
||||||
logging!(warn, Type::Tray, "Failed to update tray menu: tray not found");
|
logging!(warn, Type::Tray, "Failed to update tray menu: tray not found");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -219,29 +241,133 @@ impl Tray {
|
|||||||
let profiles_preview = profiles_arc.profiles_preview().unwrap_or_default();
|
let profiles_preview = profiles_arc.profiles_preview().unwrap_or_default();
|
||||||
let is_lightweight_mode = is_in_lightweight_mode();
|
let is_lightweight_mode = is_in_lightweight_mode();
|
||||||
|
|
||||||
logging_error!(
|
let (menu, latency_items) = create_tray_menu(
|
||||||
Type::Tray,
|
app_handle,
|
||||||
tray.set_menu(Some(
|
Some(mode.as_str()),
|
||||||
create_tray_menu(
|
*system_proxy,
|
||||||
app_handle,
|
*tun_mode,
|
||||||
Some(mode.as_str()),
|
tun_mode_available,
|
||||||
*system_proxy,
|
profiles_preview,
|
||||||
*tun_mode,
|
TrayMenuOptions {
|
||||||
tun_mode_available,
|
is_lightweight_mode,
|
||||||
profiles_preview,
|
include_proxy_groups,
|
||||||
TrayMenuOptions {
|
},
|
||||||
is_lightweight_mode,
|
)
|
||||||
include_proxy_groups,
|
.await?;
|
||||||
},
|
tray.set_menu(Some(menu))?;
|
||||||
)
|
let groups: Vec<_> = latency_items.keys().cloned().collect();
|
||||||
.await?,
|
*self.latency_items.lock() = latency_items;
|
||||||
))
|
for group in groups {
|
||||||
);
|
self.refresh_latency_item(&group);
|
||||||
|
}
|
||||||
|
|
||||||
logging!(debug, Type::Tray, "托盘菜单更新成功");
|
logging!(debug, Type::Tray, "托盘菜单更新成功");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn refresh_proxy_items(&'static self) {
|
||||||
|
let Ok(update) = self.menu_update.try_lock() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if self.latency_items.lock().is_empty() || handle::Handle::global().is_exiting() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Ok(Ok(view)) = tokio::time::timeout(
|
||||||
|
Duration::from_secs(1),
|
||||||
|
cmd::proxy::proxy_view(Some(Duration::from_millis(750))),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if self
|
||||||
|
.latency_items
|
||||||
|
.lock()
|
||||||
|
.values()
|
||||||
|
.any(|entry| entry.provider_state != view.provider_state)
|
||||||
|
{
|
||||||
|
// Provider fallback changes member identities; rebuild through the existing queue.
|
||||||
|
drop(update);
|
||||||
|
logging_error!(Type::Tray, cmd::proxy::sync_tray_proxy_selection().await);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let verge = Config::verge().await.latest_arc();
|
||||||
|
let profiles = Config::profiles().await.latest_arc();
|
||||||
|
logging_error!(
|
||||||
|
Type::Tray,
|
||||||
|
handle::Handle::app_handle().run_on_main_thread(move || {
|
||||||
|
// Keep rebuilding serialized until these handles have received their snapshot.
|
||||||
|
let _update = update;
|
||||||
|
let mut items = self.latency_items.lock();
|
||||||
|
for (group_name, entry) in items.iter_mut() {
|
||||||
|
let Some(group) = find_group(&view, group_name) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let url = latency::test_url(group, &verge, &profiles);
|
||||||
|
let mut members = HashMap::with_capacity(group.members.len());
|
||||||
|
for member in &group.members {
|
||||||
|
members
|
||||||
|
.entry((member_name(member), member_provider(&view, member)))
|
||||||
|
.or_insert(member);
|
||||||
|
}
|
||||||
|
for proxy in &mut entry.proxies {
|
||||||
|
let Some(member) = members.get(&(proxy.name.as_str(), proxy.provider_name.as_deref())) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let text = proxy_item_text(&view, member, &url, &verge);
|
||||||
|
let checked = group.now.as_ref() == Some(&proxy.name);
|
||||||
|
if text != proxy.text {
|
||||||
|
match proxy.item.set_text(&text) {
|
||||||
|
Ok(()) => proxy.text = text,
|
||||||
|
Err(err) => logging!(warn, Type::Tray, "Failed to refresh proxy latency: {err}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match proxy.item.is_checked() {
|
||||||
|
Ok(current) if current != checked => {
|
||||||
|
logging_error!(Type::Tray, proxy.item.set_checked(checked))
|
||||||
|
}
|
||||||
|
Err(err) => logging!(warn, Type::Tray, "Failed to read proxy selection: {err}"),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_latency_item(&self, group_name: &str) {
|
||||||
|
let group_name = group_name.to_owned();
|
||||||
|
logging_error!(
|
||||||
|
Type::Tray,
|
||||||
|
handle::Handle::app_handle().run_on_main_thread(move || {
|
||||||
|
Self::global().refresh_latency_item_on_main_thread(&group_name);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_latency_item_on_main_thread(&self, group_name: &str) {
|
||||||
|
let running = latency::is_running(group_name);
|
||||||
|
let item = {
|
||||||
|
let mut items = self.latency_items.lock();
|
||||||
|
items.get_mut(group_name).and_then(|entry| {
|
||||||
|
if entry.running == running {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
entry.running = running;
|
||||||
|
Some(entry.item.clone())
|
||||||
|
})
|
||||||
|
};
|
||||||
|
if let Some(item) = item {
|
||||||
|
let label = if running {
|
||||||
|
clash_verge_i18n::t!("tray.testingLatency")
|
||||||
|
} else {
|
||||||
|
clash_verge_i18n::t!("tray.testLatency")
|
||||||
|
};
|
||||||
|
logging_error!(Type::Tray, item.set_text(label));
|
||||||
|
logging_error!(Type::Tray, item.set_enabled(!running));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn update_icon(&self, verge: &IVerge) -> Result<()> {
|
pub async fn update_icon(&self, verge: &IVerge) -> Result<()> {
|
||||||
if handle::Handle::global().is_exiting() {
|
if handle::Handle::global().is_exiting() {
|
||||||
logging!(debug, Type::Tray, "应用正在退出,跳过托盘图标更新");
|
logging!(debug, Type::Tray, "应用正在退出,跳过托盘图标更新");
|
||||||
@@ -422,11 +548,12 @@ 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 下不支持小键盘按键,Windows 下也不支持 Win 键(前端上报为 `CMD`)
|
// 托盘菜单中的 `accelerator` 属性,在 Linux/Windows 中都不支持小键盘按键的解析
|
||||||
let uppercase_key = key.to_uppercase();
|
if key.to_uppercase().contains("NUMPAD") {
|
||||||
let unsupported =
|
None
|
||||||
uppercase_key.contains("NUMPAD") || (cfg!(windows) && uppercase_key.contains("CMD"));
|
} else {
|
||||||
if unsupported { None } else { Some((func, key)) }
|
Some((func, key))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
@@ -456,96 +583,175 @@ fn create_profile_menu_item(
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UNTESTED_DELAY_TEXT: &str = "-ms";
|
||||||
|
const TIMEOUT_DELAY_TEXT: &str = "T/O";
|
||||||
|
const TRAY_GROUP_DEPTH_LIMIT: usize = 8;
|
||||||
|
|
||||||
|
const fn member_name(member: &ProxyMemberRef) -> &str {
|
||||||
|
match member {
|
||||||
|
ProxyMemberRef::Node { name, .. }
|
||||||
|
| ProxyMemberRef::Group { name }
|
||||||
|
| ProxyMemberRef::Unresolved { name, .. } => name.as_str(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_group<'a>(view: &'a ProxyViewV1, name: &str) -> Option<&'a ProxyGroupView> {
|
||||||
|
view.groups
|
||||||
|
.iter()
|
||||||
|
.chain(view.global.iter())
|
||||||
|
.find(|group| group.name.as_str() == name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The delay recorded for `url`, falling back to the node's latest measurement under any URL, so a
|
||||||
|
/// node the core tested on its own still shows a value.
|
||||||
|
fn node_delay(view: &ProxyViewV1, record_id: &str, url: &str) -> Option<u16> {
|
||||||
|
view.records
|
||||||
|
.get(record_id)
|
||||||
|
.map(|node| node.extra.get(url).map_or(&node.history, |extra| &extra.history))
|
||||||
|
.and_then(|history| history.last())
|
||||||
|
.map(|history| history.delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A group row shows the delay of the node the group currently selects. The core never writes a
|
||||||
|
/// history for the group itself, so `now` is followed until the chain reaches a node.
|
||||||
|
fn selected_member_delay(view: &ProxyViewV1, group_name: &str, url: &str) -> Option<u16> {
|
||||||
|
let mut current = group_name;
|
||||||
|
for _ in 0..TRAY_GROUP_DEPTH_LIMIT {
|
||||||
|
let Some(group) = find_group(view, current) else { break };
|
||||||
|
let Some(now) = group.now.as_deref().filter(|now| !now.is_empty() && *now != current) else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let Some(member) = group.members.iter().find(|member| member_name(member) == now) else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
match member {
|
||||||
|
ProxyMemberRef::Node { record_id, .. } => return node_delay(view, record_id, url),
|
||||||
|
ProxyMemberRef::Group { name } => current = name.as_str(),
|
||||||
|
ProxyMemberRef::Unresolved { .. } => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
find_group(view, group_name)
|
||||||
|
.map(|group| group.extra.get(url).map_or(&group.history, |extra| &extra.history))
|
||||||
|
.and_then(|history| history.last())
|
||||||
|
.map(|history| history.delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn member_provider<'a>(view: &'a ProxyViewV1, member: &ProxyMemberRef) -> Option<&'a str> {
|
||||||
|
let ProxyMemberRef::Node { record_id, .. } = member else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
match &view.records.get(record_id)?.source {
|
||||||
|
ProxyNodeSource::Provider { provider_name, .. } => Some(provider_name),
|
||||||
|
ProxyNodeSource::Core { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn proxy_item_text(view: &ProxyViewV1, member: &ProxyMemberRef, url: &str, verge: &IVerge) -> std::string::String {
|
||||||
|
let timeout = verge
|
||||||
|
.default_latency_timeout
|
||||||
|
.filter(|timeout| *timeout > 0)
|
||||||
|
.unwrap_or(10000);
|
||||||
|
let delay = match member {
|
||||||
|
ProxyMemberRef::Node { record_id, .. } => node_delay(view, record_id, url),
|
||||||
|
ProxyMemberRef::Group { name } => selected_member_delay(view, name, url),
|
||||||
|
ProxyMemberRef::Unresolved { .. } => None,
|
||||||
|
};
|
||||||
|
let delay_text = delay.map_or_else(
|
||||||
|
|| UNTESTED_DELAY_TEXT.to_owned(),
|
||||||
|
|delay| {
|
||||||
|
if delay == 0 || u32::from(delay) >= timeout as u32 {
|
||||||
|
TIMEOUT_DELAY_TEXT.to_owned()
|
||||||
|
} else {
|
||||||
|
format!("{delay}ms")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
format!("{} | {delay_text}", member_name(member))
|
||||||
|
}
|
||||||
|
|
||||||
fn create_subcreate_proxy_menu_item(
|
fn create_subcreate_proxy_menu_item(
|
||||||
app_handle: &AppHandle,
|
app_handle: &AppHandle,
|
||||||
proxy_mode: &str,
|
proxy_mode: &str,
|
||||||
proxy_group_order_map: Option<HashMap<String, usize>>,
|
view: Option<ProxyViewV1>,
|
||||||
proxy_nodes_data: Option<Proxies>,
|
latency_items: &mut HashMap<std::string::String, LatencyMenuItem>,
|
||||||
|
texts: &MenuTexts,
|
||||||
|
verge: &IVerge,
|
||||||
|
profiles: &crate::config::IProfiles,
|
||||||
) -> Vec<Submenu<Wry>> {
|
) -> Vec<Submenu<Wry>> {
|
||||||
let proxy_submenus: Vec<Submenu<Wry>> = {
|
let Some(view) = view else { return Vec::new() };
|
||||||
let mut submenus: Vec<(String, usize, Submenu<Wry>)> = Vec::new();
|
view.groups
|
||||||
|
.iter()
|
||||||
// TODO: 应用启动时,内核还未启动完全,无法获取代理节点信息
|
.chain(view.global.iter())
|
||||||
if let Some(proxy_nodes_data) = proxy_nodes_data {
|
.filter_map(|group| {
|
||||||
for (group_name, group_data) in proxy_nodes_data.proxies.iter() {
|
if group.hidden.unwrap_or_default() || (proxy_mode == "global") != (group.name == "GLOBAL") {
|
||||||
let should_show = match proxy_mode {
|
return None;
|
||||||
"global" => group_name == "GLOBAL",
|
|
||||||
_ => group_name != "GLOBAL",
|
|
||||||
} && !group_data.hidden.unwrap_or_default();
|
|
||||||
|
|
||||||
if !should_show {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(all_proxies) = group_data.all.as_ref() else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let now_proxy = group_data.now.as_deref().unwrap_or_default();
|
|
||||||
|
|
||||||
let group_items: Vec<CheckMenuItem<Wry>> = all_proxies
|
|
||||||
.iter()
|
|
||||||
.filter_map(|proxy_str| {
|
|
||||||
let is_selected = *proxy_str == now_proxy;
|
|
||||||
let item_id = format!("proxy_{}_{}", group_name, proxy_str);
|
|
||||||
|
|
||||||
let delay_text = proxy_nodes_data
|
|
||||||
.proxies
|
|
||||||
.get(proxy_str)
|
|
||||||
.and_then(|h| h.history.last())
|
|
||||||
.map(|h| match h.delay {
|
|
||||||
0 => "-ms".into(),
|
|
||||||
delay if delay >= 10000 => "-ms".into(),
|
|
||||||
_ => format!("{}ms", h.delay),
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| "-ms".into());
|
|
||||||
|
|
||||||
let display_text = format!("{} | {}", proxy_str, delay_text);
|
|
||||||
|
|
||||||
CheckMenuItem::with_id(app_handle, item_id, display_text, true, is_selected, None::<&str>)
|
|
||||||
.map_err(|e| logging!(warn, Type::Tray, "Failed to create proxy menu item: {}", e))
|
|
||||||
.ok()
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if group_items.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let group_display_name = group_name.to_string();
|
|
||||||
|
|
||||||
let group_items_refs: Vec<&dyn IsMenuItem<Wry>> =
|
|
||||||
group_items.iter().map(|item| item as &dyn IsMenuItem<Wry>).collect();
|
|
||||||
|
|
||||||
if let Ok(submenu) = Submenu::with_id_and_items(
|
|
||||||
app_handle,
|
|
||||||
format!("proxy_group_{}", group_name),
|
|
||||||
group_display_name,
|
|
||||||
true,
|
|
||||||
&group_items_refs,
|
|
||||||
) {
|
|
||||||
let insertion_index = submenus.len();
|
|
||||||
submenus.push((group_name.into(), insertion_index, submenu));
|
|
||||||
} else {
|
|
||||||
logging!(warn, Type::Tray, "Failed to create proxy group submenu: {}", group_name);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
let url = latency::test_url(group, verge, profiles);
|
||||||
|
let proxies: Vec<ProxyLatencyItem> = group
|
||||||
if let Some(order_map) = proxy_group_order_map.as_ref() {
|
.members
|
||||||
submenus.sort_by(|(name_a, original_index_a, _), (name_b, original_index_b, _)| {
|
.iter()
|
||||||
match (order_map.get(name_a), order_map.get(name_b)) {
|
.filter_map(|member| {
|
||||||
(Some(index_a), Some(index_b)) => index_a.cmp(index_b),
|
let name = member_name(member);
|
||||||
(Some(_), None) => std::cmp::Ordering::Less,
|
let text = proxy_item_text(&view, member, &url, verge);
|
||||||
(None, Some(_)) => std::cmp::Ordering::Greater,
|
let checked = group.now.as_deref() == Some(name);
|
||||||
(None, None) => original_index_a.cmp(original_index_b),
|
CheckMenuItem::with_id(
|
||||||
}
|
app_handle,
|
||||||
});
|
format!("proxy_{}_{}", group.name, name),
|
||||||
}
|
&text,
|
||||||
|
true,
|
||||||
submenus.into_iter().map(|(_, _, submenu)| submenu).collect()
|
checked,
|
||||||
};
|
None::<&str>,
|
||||||
proxy_submenus
|
)
|
||||||
|
.map(|item| ProxyLatencyItem {
|
||||||
|
item,
|
||||||
|
name: name.to_owned(),
|
||||||
|
provider_name: member_provider(&view, member).map(str::to_owned),
|
||||||
|
text,
|
||||||
|
})
|
||||||
|
.map_err(|e| logging!(warn, Type::Tray, "Failed to create proxy menu item: {e}"))
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let group_items: Vec<_> = proxies.iter().map(|proxy| proxy.item.clone()).collect();
|
||||||
|
if group_items.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let running = latency::is_running(&group.name);
|
||||||
|
let test_item = MenuItem::with_id(
|
||||||
|
app_handle,
|
||||||
|
format!("{}_{}", MenuIds::TEST_LATENCY, group.name),
|
||||||
|
if running {
|
||||||
|
clash_verge_i18n::t!("tray.testingLatency")
|
||||||
|
} else {
|
||||||
|
texts.test_latency.clone()
|
||||||
|
},
|
||||||
|
!running,
|
||||||
|
None::<&str>,
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
latency_items.insert(
|
||||||
|
group.name.clone(),
|
||||||
|
LatencyMenuItem {
|
||||||
|
item: test_item.clone(),
|
||||||
|
running,
|
||||||
|
proxies,
|
||||||
|
provider_state: view.provider_state,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let separator = PredefinedMenuItem::separator(app_handle).ok()?;
|
||||||
|
let mut items: Vec<&dyn IsMenuItem<Wry>> = vec![&test_item, &separator];
|
||||||
|
items.extend(group_items.iter().map(|item| item as &dyn IsMenuItem<Wry>));
|
||||||
|
Submenu::with_id_and_items(
|
||||||
|
app_handle,
|
||||||
|
format!("proxy_group_{}", group.name),
|
||||||
|
&group.name,
|
||||||
|
true,
|
||||||
|
&items,
|
||||||
|
)
|
||||||
|
.map_err(|e| logging!(warn, Type::Tray, "Failed to create proxy group submenu: {e}"))
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_proxy_menu_item(
|
fn create_proxy_menu_item(
|
||||||
@@ -592,54 +798,38 @@ async fn create_tray_menu(
|
|||||||
tun_mode_available: bool,
|
tun_mode_available: bool,
|
||||||
profiles_preview: Vec<IProfilePreview<'_>>,
|
profiles_preview: Vec<IProfilePreview<'_>>,
|
||||||
options: TrayMenuOptions,
|
options: TrayMenuOptions,
|
||||||
) -> Result<tauri::menu::Menu<Wry>> {
|
) -> Result<(tauri::menu::Menu<Wry>, HashMap<std::string::String, LatencyMenuItem>)> {
|
||||||
let current_proxy_mode = mode.unwrap_or("");
|
let current_proxy_mode = mode.unwrap_or("");
|
||||||
|
|
||||||
let mut verge_settings = Config::verge().await.latest_arc();
|
let mut verge_settings = Config::verge().await.latest_arc();
|
||||||
let fetch_proxy_groups =
|
let fetch_proxy_groups =
|
||||||
options.include_proxy_groups && verge_settings.tray_proxy_groups_display_mode.as_deref() != Some("disable");
|
options.include_proxy_groups && verge_settings.tray_proxy_groups_display_mode.as_deref() != Some("disable");
|
||||||
|
|
||||||
// TODO: should update tray menu again when it was timeout error
|
// Proxy data must remain optional so a stopped Core cannot roll back unrelated settings.
|
||||||
let (proxy_nodes_data, runtime_proxy_groups_order) = if fetch_proxy_groups {
|
let proxy_view = if fetch_proxy_groups {
|
||||||
let proxy_nodes_data =
|
tokio::time::timeout(
|
||||||
tokio::time::timeout(Duration::from_millis(1000), handle::Handle::mihomo().get_proxies())
|
Duration::from_millis(1000),
|
||||||
.await
|
cmd::proxy::proxy_view(Some(Duration::from_millis(750))),
|
||||||
.map_or(None, |res| res.ok());
|
)
|
||||||
|
.await
|
||||||
let runtime = Config::runtime().await.latest_arc();
|
.ok()
|
||||||
let runtime_proxy_groups_order = runtime.config.as_ref().map(|config| {
|
.and_then(Result::ok)
|
||||||
config
|
|
||||||
.get("proxy-groups")
|
|
||||||
.and_then(|groups| groups.as_sequence())
|
|
||||||
.map(|groups| {
|
|
||||||
groups
|
|
||||||
.iter()
|
|
||||||
.filter_map(|group| group.get("name"))
|
|
||||||
.filter_map(|name| name.as_str())
|
|
||||||
.enumerate()
|
|
||||||
.map(|(index, name)| (name.into(), index))
|
|
||||||
.collect::<HashMap<String, usize>>()
|
|
||||||
})
|
|
||||||
.unwrap_or_default()
|
|
||||||
});
|
|
||||||
|
|
||||||
(proxy_nodes_data, runtime_proxy_groups_order)
|
|
||||||
} else {
|
} else {
|
||||||
(None, None)
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
if fetch_proxy_groups {
|
if fetch_proxy_groups {
|
||||||
verge_settings = Config::verge().await.latest_arc();
|
verge_settings = Config::verge().await.latest_arc();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let profiles_config = Config::profiles().await.latest_arc();
|
||||||
|
|
||||||
let tray_proxy_groups_display_mode = verge_settings
|
let tray_proxy_groups_display_mode = verge_settings
|
||||||
.tray_proxy_groups_display_mode
|
.tray_proxy_groups_display_mode
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.unwrap_or("default");
|
.unwrap_or("default");
|
||||||
let include_proxy_groups = options.include_proxy_groups && tray_proxy_groups_display_mode != "disable";
|
let include_proxy_groups = options.include_proxy_groups && tray_proxy_groups_display_mode != "disable";
|
||||||
|
|
||||||
let proxy_group_order_map = runtime_proxy_groups_order;
|
|
||||||
|
|
||||||
let show_outbound_modes_inline = verge_settings.tray_inline_outbound_modes.unwrap_or(false);
|
let show_outbound_modes_inline = verge_settings.tray_inline_outbound_modes.unwrap_or(false);
|
||||||
|
|
||||||
let version = env!("CARGO_PKG_VERSION");
|
let version = env!("CARGO_PKG_VERSION");
|
||||||
@@ -719,9 +909,17 @@ async fn create_tray_menu(
|
|||||||
&profile_menu_items_refs,
|
&profile_menu_items_refs,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
let mut latency_items = HashMap::new();
|
||||||
let (proxies_menu, inline_proxy_items) = if include_proxy_groups {
|
let (proxies_menu, inline_proxy_items) = if include_proxy_groups {
|
||||||
let proxy_sub_menus =
|
let proxy_sub_menus = create_subcreate_proxy_menu_item(
|
||||||
create_subcreate_proxy_menu_item(app_handle, current_proxy_mode, proxy_group_order_map, proxy_nodes_data);
|
app_handle,
|
||||||
|
current_proxy_mode,
|
||||||
|
proxy_view,
|
||||||
|
&mut latency_items,
|
||||||
|
&texts,
|
||||||
|
&verge_settings,
|
||||||
|
&profiles_config,
|
||||||
|
);
|
||||||
|
|
||||||
match tray_proxy_groups_display_mode {
|
match tray_proxy_groups_display_mode {
|
||||||
"default" => create_proxy_menu_item(app_handle, false, proxy_sub_menus, &texts.proxies)?,
|
"default" => create_proxy_menu_item(app_handle, false, proxy_sub_menus, &texts.proxies)?,
|
||||||
@@ -865,10 +1063,25 @@ async fn create_tray_menu(
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
let menu = tauri::menu::MenuBuilder::new(app_handle).items(&menu_items).build()?;
|
let menu = tauri::menu::MenuBuilder::new(app_handle).items(&menu_items).build()?;
|
||||||
Ok(menu)
|
Ok((menu, latency_items))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_tray_icon_event(_tray_icon: &TrayIcon, tray_event: TrayIconEvent) {
|
fn on_tray_icon_event(_tray_icon: &TrayIcon, tray_event: TrayIconEvent) {
|
||||||
|
let refresh_proxies = match tray_event {
|
||||||
|
TrayIconEvent::Enter { .. } => Tray::global().proxy_hover_limiter.check(),
|
||||||
|
TrayIconEvent::Click {
|
||||||
|
button: MouseButton::Left | MouseButton::Right,
|
||||||
|
button_state: MouseButtonState::Down,
|
||||||
|
..
|
||||||
|
} => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
if refresh_proxies {
|
||||||
|
AsyncHandler::spawn(|| async {
|
||||||
|
Tray::global().refresh_proxy_items().await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if matches!(
|
if matches!(
|
||||||
tray_event,
|
tray_event,
|
||||||
TrayIconEvent::Move { .. } | TrayIconEvent::Leave { .. } | TrayIconEvent::Enter { .. }
|
TrayIconEvent::Move { .. } | TrayIconEvent::Leave { .. } | TrayIconEvent::Enter { .. }
|
||||||
@@ -990,6 +1203,15 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
|||||||
};
|
};
|
||||||
feat::toggle_proxy_profile(profile_index.into()).await;
|
feat::toggle_proxy_profile(profile_index.into()).await;
|
||||||
}
|
}
|
||||||
|
id if id.starts_with(MenuIds::TEST_LATENCY) => {
|
||||||
|
let group_name = id
|
||||||
|
.strip_prefix(MenuIds::TEST_LATENCY)
|
||||||
|
.and_then(|rest| rest.strip_prefix('_'));
|
||||||
|
if let Some(group_name) = group_name {
|
||||||
|
logging!(info, Type::Tray, "Tray menu: test group latency {}", group_name);
|
||||||
|
latency::test_proxy_group_delay(group_name).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
id if id.starts_with("proxy_") => {
|
id if id.starts_with("proxy_") => {
|
||||||
let rest = match id.strip_prefix("proxy_") {
|
let rest = match id.strip_prefix("proxy_") {
|
||||||
Some(r) => r,
|
Some(r) => r,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ pub async fn patch_verge(patch: &IVerge, not_save_file: bool) -> Result<()> {
|
|||||||
|
|
||||||
/// Apply a patch without post-update reconciliation.
|
/// Apply a patch without post-update reconciliation.
|
||||||
pub(super) async fn apply_verge_patch(patch: &IVerge, not_save_file: bool) -> Result<()> {
|
pub(super) async fn apply_verge_patch(patch: &IVerge, not_save_file: bool) -> Result<()> {
|
||||||
let config_write = Config::lock_config_write().await;
|
let config_write = Config::try_lock_config_write()?;
|
||||||
apply_verge_patch_locked(&config_write, patch, not_save_file).await
|
apply_verge_patch_locked(&config_write, patch, not_save_file).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ verge_registry! {
|
|||||||
proxy_group_tools_position => NoEffect;
|
proxy_group_tools_position => NoEffect;
|
||||||
notice_position => NoEffect;
|
notice_position => NoEffect;
|
||||||
collapse_navbar => NoEffect;
|
collapse_navbar => NoEffect;
|
||||||
|
enable_system_titlebar => NoEffect;
|
||||||
sysproxy_tray_icon => [TrayIcon];
|
sysproxy_tray_icon => [TrayIcon];
|
||||||
tun_tray_icon => [TrayIcon];
|
tun_tray_icon => [TrayIcon];
|
||||||
enable_tun_mode => Tun;
|
enable_tun_mode => Tun;
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ pub async fn open_or_close_dashboard() {
|
|||||||
logging!(info, Type::Window, "Window toggle result: {result:?}");
|
logging!(info, Type::Window, "Window toggle result: {result:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn quit() {
|
pub async fn quit() -> clash_verge_signal::ShutdownOutcome {
|
||||||
logging!(debug, Type::System, "启动退出流程");
|
logging!(debug, Type::System, "启动退出流程");
|
||||||
// 设置退出标志
|
// 设置退出标志
|
||||||
handle::Handle::global().set_is_exiting();
|
handle::Handle::global().set_is_exiting();
|
||||||
@@ -165,11 +165,13 @@ pub async fn quit() {
|
|||||||
NoticeStatus::AppQuitCoreStopFailed,
|
NoticeStatus::AppQuitCoreStopFailed,
|
||||||
cleanup_result.stop_error.unwrap_or_default(),
|
cleanup_result.stop_error.unwrap_or_default(),
|
||||||
);
|
);
|
||||||
|
return clash_verge_signal::ShutdownOutcome::Canceled;
|
||||||
}
|
}
|
||||||
|
|
||||||
utils::server::shutdown_embedded_server();
|
utils::server::shutdown_embedded_server();
|
||||||
let app_handle = handle::Handle::app_handle();
|
let app_handle = handle::Handle::app_handle();
|
||||||
app_handle.exit(if cleanup_result.all_success { 0 } else { 1 });
|
app_handle.exit(if cleanup_result.all_success { 0 } else { 1 });
|
||||||
|
clash_verge_signal::ShutdownOutcome::Committed
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn clean_async() -> CleanupResult {
|
pub async fn clean_async() -> CleanupResult {
|
||||||
@@ -316,6 +318,7 @@ pub async fn hide() {
|
|||||||
{
|
{
|
||||||
let _ = window.hide();
|
let _ = window.hide();
|
||||||
}
|
}
|
||||||
|
handle::Handle::global().set_activation_policy_accessory();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -116,9 +116,13 @@ mod app_init {
|
|||||||
/// Setup window state management
|
/// Setup window state management
|
||||||
pub fn setup_window_state(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn setup_window_state(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
logging!(debug, Type::Setup, "初始化窗口状态管理...");
|
logging!(debug, Type::Setup, "初始化窗口状态管理...");
|
||||||
|
// Decorations are config-driven (`enable_system_titlebar`); restoring a stale
|
||||||
|
// value here would override the builder's `.decorations(...)` on window recreate.
|
||||||
|
let state_flags =
|
||||||
|
tauri_plugin_window_state::StateFlags::all() & !tauri_plugin_window_state::StateFlags::DECORATIONS;
|
||||||
let window_state_plugin = tauri_plugin_window_state::Builder::new()
|
let window_state_plugin = tauri_plugin_window_state::Builder::new()
|
||||||
.with_filename(files::WINDOW_STATE)
|
.with_filename(files::WINDOW_STATE)
|
||||||
.with_state_flags(tauri_plugin_window_state::StateFlags::default())
|
.with_state_flags(state_flags)
|
||||||
.build();
|
.build();
|
||||||
app.handle().plugin(window_state_plugin)?;
|
app.handle().plugin(window_state_plugin)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -230,7 +234,6 @@ pub fn run() -> std::process::ExitCode {
|
|||||||
if let Err(panic) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
if let Err(panic) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||||
resolve::resolve_setup_async();
|
resolve::resolve_setup_async();
|
||||||
resolve::resolve_setup_sync();
|
resolve::resolve_setup_sync();
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
resolve::init_signal();
|
resolve::init_signal();
|
||||||
})) {
|
})) {
|
||||||
log_setup_panic("window-core", panic);
|
log_setup_panic("window-core", panic);
|
||||||
@@ -289,6 +292,9 @@ pub fn run() -> std::process::ExitCode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_window_close(api: &tauri::WindowEvent) {
|
pub fn handle_window_close(api: &tauri::WindowEvent) {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
handle::Handle::global().set_activation_policy_accessory();
|
||||||
|
|
||||||
if core::handle::Handle::global().is_exiting() {
|
if core::handle::Handle::global().is_exiting() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ fn main() -> ExitCode {
|
|||||||
#[cfg(not(all(feature = "perf-harness", target_os = "macos")))]
|
#[cfg(not(all(feature = "perf-harness", target_os = "macos")))]
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
let default_parallelism = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
|
let default_parallelism = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
|
||||||
let worker_limit = std::cmp::min(default_parallelism, 4);
|
let worker_limit = std::cmp::min(default_parallelism, 8);
|
||||||
let blocking_limit = 2 * worker_limit;
|
let blocking_limit = 2 * worker_limit;
|
||||||
|
|
||||||
#[allow(clippy::unwrap_used)]
|
#[allow(clippy::unwrap_used)]
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ use crate::{
|
|||||||
utils::{init, server, window_manager::WindowManager},
|
utils::{init, server, window_manager::WindowManager},
|
||||||
};
|
};
|
||||||
use clash_verge_logging::{Type, logging, logging_error};
|
use clash_verge_logging::{Type, logging, logging_error};
|
||||||
|
use clash_verge_signal;
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub mod dns;
|
pub mod dns;
|
||||||
@@ -188,8 +189,6 @@ async fn init_silent_updater() {
|
|||||||
logging!(info, Type::Setup, "Silent updater initialized");
|
logging!(info, Type::Setup, "Silent updater initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upstream has no session-end signal support on Linux; see crates/clash-verge-signal.
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
pub(crate) fn init_signal() {
|
pub(crate) fn init_signal() {
|
||||||
logging!(debug, Type::Setup, "Initializing signal handlers...");
|
logging!(debug, Type::Setup, "Initializing signal handlers...");
|
||||||
clash_verge_signal::register(feat::quit);
|
clash_verge_signal::register(feat::quit);
|
||||||
|
|||||||
@@ -19,11 +19,6 @@ const DEFAULT_HEIGHT: f64 = 700.0;
|
|||||||
const MINIMAL_WIDTH: f64 = 520.0;
|
const MINIMAL_WIDTH: f64 = 520.0;
|
||||||
const MINIMAL_HEIGHT: f64 = 520.0;
|
const MINIMAL_HEIGHT: f64 = 520.0;
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
const DEFAULT_DECORATIONS: bool = false;
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
const DEFAULT_DECORATIONS: bool = true;
|
|
||||||
|
|
||||||
const fn restored_window_size_is_too_small(width: u32, height: u32) -> bool {
|
const fn restored_window_size_is_too_small(width: u32, height: u32) -> bool {
|
||||||
width < MINIMAL_WIDTH as u32 || height < MINIMAL_HEIGHT as u32
|
width < MINIMAL_WIDTH as u32 || height < MINIMAL_HEIGHT as u32
|
||||||
}
|
}
|
||||||
@@ -49,6 +44,7 @@ pub async fn build_new_window() -> Result<WebviewWindow, String> {
|
|||||||
|
|
||||||
let config = Config::verge().await;
|
let config = Config::verge().await;
|
||||||
let latest = config.latest_arc();
|
let latest = config.latest_arc();
|
||||||
|
let decorations = latest.enable_system_titlebar.unwrap_or(true);
|
||||||
let start_page = latest.start_page.as_deref().unwrap_or("/");
|
let start_page = latest.start_page.as_deref().unwrap_or("/");
|
||||||
let initial_theme_mode = match latest.theme_mode.as_deref() {
|
let initial_theme_mode = match latest.theme_mode.as_deref() {
|
||||||
Some("dark") => "dark",
|
Some("dark") => "dark",
|
||||||
@@ -83,7 +79,7 @@ pub async fn build_new_window() -> Result<WebviewWindow, String> {
|
|||||||
)
|
)
|
||||||
.title("Clash Verge")
|
.title("Clash Verge")
|
||||||
.center()
|
.center()
|
||||||
.decorations(DEFAULT_DECORATIONS)
|
.decorations(decorations)
|
||||||
.fullscreen(false)
|
.fullscreen(false)
|
||||||
.inner_size(DEFAULT_WIDTH, DEFAULT_HEIGHT)
|
.inner_size(DEFAULT_WIDTH, DEFAULT_HEIGHT)
|
||||||
.min_inner_size(MINIMAL_WIDTH, MINIMAL_HEIGHT)
|
.min_inner_size(MINIMAL_WIDTH, MINIMAL_HEIGHT)
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ static INSTANCE_LOCK: OnceLock<std::fs::File> = OnceLock::new();
|
|||||||
const PAC_INITIAL_AVAILABLE: bool = false;
|
const PAC_INITIAL_AVAILABLE: bool = false;
|
||||||
static PAC_AVAILABLE: AtomicBool = AtomicBool::new(PAC_INITIAL_AVAILABLE);
|
static PAC_AVAILABLE: AtomicBool = AtomicBool::new(PAC_INITIAL_AVAILABLE);
|
||||||
static COMMANDS_READY: AtomicBool = AtomicBool::new(false);
|
static COMMANDS_READY: AtomicBool = AtomicBool::new(false);
|
||||||
|
#[cfg(feature = "verge-dev")]
|
||||||
|
static DEV_QUIT_REQUESTED: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum SingletonDisposition {
|
pub enum SingletonDisposition {
|
||||||
@@ -162,12 +164,55 @@ fn instance_auth(token: String) -> impl warp::Filter<Extract = (), Error = warp:
|
|||||||
.untuple_one()
|
.untuple_one()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "verge-dev")]
|
||||||
|
fn dev_quit_route<F>(
|
||||||
|
token: String,
|
||||||
|
commands_ready: &'static AtomicBool,
|
||||||
|
quit_requested: &'static AtomicBool,
|
||||||
|
trigger: F,
|
||||||
|
) -> impl warp::Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone
|
||||||
|
where
|
||||||
|
F: Fn() + Clone + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
instance_auth(token)
|
||||||
|
.and(warp::path!("commands" / "dev" / "quit"))
|
||||||
|
.and(warp::post())
|
||||||
|
.and_then(move || {
|
||||||
|
let trigger = trigger.clone();
|
||||||
|
async move {
|
||||||
|
if !commands_ready.load(Ordering::Acquire) {
|
||||||
|
return Ok::<_, warp::Rejection>(warp::reply::with_status(
|
||||||
|
String::from("starting"),
|
||||||
|
warp::http::StatusCode::SERVICE_UNAVAILABLE,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if quit_requested
|
||||||
|
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
trigger();
|
||||||
|
}
|
||||||
|
Ok(warp::reply::with_status(
|
||||||
|
String::from("accepted"),
|
||||||
|
warp::http::StatusCode::ACCEPTED,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "verge-dev")]
|
||||||
|
fn release_dev_quit_latch(quit_requested: &AtomicBool, outcome: clash_verge_signal::ShutdownOutcome) {
|
||||||
|
if matches!(outcome, clash_verge_signal::ShutdownOutcome::Canceled) {
|
||||||
|
quit_requested.store(false, Ordering::Release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn start_embedded_server(listener: tokio::net::TcpListener, token: String) {
|
fn start_embedded_server(listener: tokio::net::TcpListener, token: String) {
|
||||||
let (shutdown_tx, shutdown_rx) = oneshot::channel();
|
let (shutdown_tx, shutdown_rx) = oneshot::channel();
|
||||||
let _ = SHUTDOWN_SENDER.set(Mutex::new(Some(shutdown_tx)));
|
let _ = SHUTDOWN_SENDER.set(Mutex::new(Some(shutdown_tx)));
|
||||||
|
|
||||||
#[cfg(feature = "verge-dev")]
|
#[cfg(feature = "verge-dev")]
|
||||||
let auth = instance_auth(token);
|
let auth = instance_auth(token.clone());
|
||||||
#[cfg(not(feature = "verge-dev"))]
|
#[cfg(not(feature = "verge-dev"))]
|
||||||
let auth = instance_auth(token);
|
let auth = instance_auth(token);
|
||||||
|
|
||||||
@@ -233,6 +278,12 @@ fn start_embedded_server(listener: tokio::net::TcpListener, token: String) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let commands = auth.clone().and(visible).or(auth.and(scheme)).or(pac);
|
let commands = auth.clone().and(visible).or(auth.and(scheme)).or(pac);
|
||||||
|
#[cfg(feature = "verge-dev")]
|
||||||
|
let commands = commands.or(dev_quit_route(token, &COMMANDS_READY, &DEV_QUIT_REQUESTED, || {
|
||||||
|
AsyncHandler::spawn(|| async {
|
||||||
|
release_dev_quit_latch(&DEV_QUIT_REQUESTED, crate::feat::quit().await);
|
||||||
|
});
|
||||||
|
}));
|
||||||
AsyncHandler::spawn(move || async move {
|
AsyncHandler::spawn(move || async move {
|
||||||
warp::serve(commands)
|
warp::serve(commands)
|
||||||
.incoming(listener)
|
.incoming(listener)
|
||||||
|
|||||||
@@ -27,12 +27,8 @@ const TRAY_STATUS_ITEM_EXTRA_WIDTH: f64 = 30.0;
|
|||||||
const TRAY_STATUS_ITEM_MIN_LENGTH: f64 = 58.0;
|
const TRAY_STATUS_ITEM_MIN_LENGTH: f64 = 58.0;
|
||||||
const NS_VARIABLE_STATUS_ITEM_LENGTH: f64 = -1.0;
|
const NS_VARIABLE_STATUS_ITEM_LENGTH: f64 = -1.0;
|
||||||
|
|
||||||
/// (button_height, mono_advance, attributes).
|
|
||||||
type CachedAttrs = (f64, f64, Retained<NSDictionary<NSString, AnyObject>>);
|
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
static LAST_DISPLAY_STR: RefCell<String> = const { RefCell::new(String::new()) };
|
static LAST_DISPLAY_STR: RefCell<String> = const { RefCell::new(String::new()) };
|
||||||
static CACHED_ATTRS: RefCell<Option<CachedAttrs>> = const { RefCell::new(None) };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_tray_speed(up: u64, down: u64) -> String {
|
fn format_tray_speed(up: u64, down: u64) -> String {
|
||||||
@@ -78,26 +74,8 @@ fn create_attributed_string(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attributes and per-glyph advance, rebuilt only when the button height changes.
|
fn status_item_length_for_speed(attr_str: &NSAttributedString) -> f64 {
|
||||||
fn cached_attributes(button_height: f64) -> (f64, Retained<NSDictionary<NSString, AnyObject>>) {
|
(attr_str.size().width.ceil() + TRAY_STATUS_ITEM_EXTRA_WIDTH).max(TRAY_STATUS_ITEM_MIN_LENGTH)
|
||||||
CACHED_ATTRS.with(|cache| {
|
|
||||||
if let Some((height, advance, attrs)) = cache.borrow().as_ref()
|
|
||||||
&& *height == button_height
|
|
||||||
{
|
|
||||||
return (*advance, attrs.clone());
|
|
||||||
}
|
|
||||||
let attrs = build_attributes(button_height);
|
|
||||||
let probe = NSString::from_str("0");
|
|
||||||
let advance = create_attributed_string(&probe, Some(&attrs)).size().width;
|
|
||||||
*cache.borrow_mut() = Some((button_height, advance, attrs.clone()));
|
|
||||||
(advance, attrs)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Longest line times the monospaced advance; matches `size().width` for the ASCII-only speed text.
|
|
||||||
fn arithmetic_width(text: &str, advance: f64) -> f64 {
|
|
||||||
let glyphs = text.split('\n').map(str::len).max().unwrap_or(0) as f64;
|
|
||||||
glyphs * advance
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_click_target_frame(button: &NSStatusBarButton) {
|
fn sync_click_target_frame(button: &NSStatusBarButton) {
|
||||||
@@ -111,7 +89,7 @@ fn sync_click_target_frame(button: &NSStatusBarButton) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// `with_inner_tray_icon` should run this on the main thread; fail safely if it does not.
|
/// `with_inner_tray_icon` should run this on the main thread; fail safely if it does not.
|
||||||
fn apply_status_item_attributed_title(status_item: &NSStatusItem, text: &str, show_speed: bool) {
|
fn apply_status_item_attributed_title(status_item: &NSStatusItem, text: &NSString, show_speed: bool) {
|
||||||
let Some(mtm) = MainThreadMarker::new() else {
|
let Some(mtm) = MainThreadMarker::new() else {
|
||||||
logging!(warn, Type::Tray, "托盘速率富文本设置跳过:非主线程调用");
|
logging!(warn, Type::Tray, "托盘速率富文本设置跳过:非主线程调用");
|
||||||
return;
|
return;
|
||||||
@@ -119,19 +97,17 @@ fn apply_status_item_attributed_title(status_item: &NSStatusItem, text: &str, sh
|
|||||||
let Some(button) = status_item.button(mtm) else {
|
let Some(button) = status_item.button(mtm) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let ns_string = NSString::from_str(text);
|
|
||||||
let attr_str = if show_speed {
|
let attr_str = if show_speed {
|
||||||
let (advance, attrs) = cached_attributes(button.bounds().size.height);
|
let attrs = build_attributes(button.bounds().size.height);
|
||||||
let attrs: &NSDictionary<NSString, AnyObject> = &attrs;
|
let attrs: &NSDictionary<NSString, AnyObject> = &attrs;
|
||||||
let attr_str = create_attributed_string(&ns_string, Some(attrs));
|
let attr_str = create_attributed_string(text, Some(attrs));
|
||||||
let width = arithmetic_width(text, advance);
|
let length = status_item_length_for_speed(&attr_str);
|
||||||
let length = (width.ceil() + TRAY_STATUS_ITEM_EXTRA_WIDTH).max(TRAY_STATUS_ITEM_MIN_LENGTH);
|
|
||||||
if status_item.length() != length {
|
if status_item.length() != length {
|
||||||
status_item.setLength(length);
|
status_item.setLength(length);
|
||||||
}
|
}
|
||||||
attr_str
|
attr_str
|
||||||
} else {
|
} else {
|
||||||
create_attributed_string(&ns_string, None)
|
create_attributed_string(text, None)
|
||||||
};
|
};
|
||||||
button.setAttributedTitle(&attr_str);
|
button.setAttributedTitle(&attr_str);
|
||||||
sync_click_target_frame(&button);
|
sync_click_target_frame(&button);
|
||||||
@@ -145,7 +121,8 @@ pub fn set_speed_attributed_title(status_item: &NSStatusItem, up: u64, down: u64
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
*last = speed_text;
|
*last = speed_text;
|
||||||
apply_status_item_attributed_title(status_item, last.as_str(), true);
|
let ns_string = NSString::from_str(last.as_str());
|
||||||
|
apply_status_item_attributed_title(status_item, &ns_string, true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +130,7 @@ pub fn clear_speed_attributed_title(status_item: &NSStatusItem) {
|
|||||||
LAST_DISPLAY_STR.with(|last| {
|
LAST_DISPLAY_STR.with(|last| {
|
||||||
last.borrow_mut().clear();
|
last.borrow_mut().clear();
|
||||||
});
|
});
|
||||||
|
let empty = NSString::from_str("");
|
||||||
status_item.setLength(NS_VARIABLE_STATUS_ITEM_LENGTH);
|
status_item.setLength(NS_VARIABLE_STATUS_ITEM_LENGTH);
|
||||||
apply_status_item_attributed_title(status_item, "", false);
|
apply_status_item_attributed_title(status_item, &empty, false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,15 +202,9 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
padding: 10px;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
border-bottom: 1px solid var(--divider-color);
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
|
||||||
&-drag-region {
|
|
||||||
align-self: stretch;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-content__left .the-logo {
|
.layout-content__left .the-logo {
|
||||||
@@ -235,16 +229,9 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
padding: 10px;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
border-bottom: 1px solid var(--divider-color);
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
|
||||||
&-drag-region {
|
|
||||||
align-self: stretch;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
order: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-content__left {
|
.layout-content__left {
|
||||||
|
|||||||
@@ -47,7 +47,10 @@ import {
|
|||||||
useCoreDataStatus,
|
useCoreDataStatus,
|
||||||
useProxiesData,
|
useProxiesData,
|
||||||
} from '@/providers/app-data-context'
|
} from '@/providers/app-data-context'
|
||||||
|
import { syncTrayProxySelection } from '@/services/cmds'
|
||||||
import delayManager from '@/services/delay'
|
import delayManager from '@/services/delay'
|
||||||
|
import { mutate } from '@/services/mutate'
|
||||||
|
import { profileTestUrls } from '@/services/proxy-test-url'
|
||||||
import {
|
import {
|
||||||
findCurrentGroupMember,
|
findCurrentGroupMember,
|
||||||
getRecord,
|
getRecord,
|
||||||
@@ -675,6 +678,20 @@ export const CurrentProxyCard = () => {
|
|||||||
[proxyView],
|
[proxyView],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedGroup) return
|
||||||
|
const customUrl = currentProfile
|
||||||
|
? profileTestUrls(currentProfile)[selectedGroup.name]
|
||||||
|
: undefined
|
||||||
|
delayManager.setUrl(
|
||||||
|
selectedGroup.name,
|
||||||
|
customUrl?.trim() ||
|
||||||
|
selectedGroup.testUrl?.trim() ||
|
||||||
|
verge?.default_latency_test?.trim() ||
|
||||||
|
'http://cp.cloudflare.com/generate_204',
|
||||||
|
)
|
||||||
|
}, [selectedGroup, currentProfile, verge?.default_latency_test])
|
||||||
|
|
||||||
const unsortedProxyOptions = useMemo(
|
const unsortedProxyOptions = useMemo(
|
||||||
() => optionsForGroup(selectedGroup),
|
() => optionsForGroup(selectedGroup),
|
||||||
[optionsForGroup, selectedGroup],
|
[optionsForGroup, selectedGroup],
|
||||||
@@ -907,6 +924,12 @@ export const CurrentProxyCard = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
refreshProxy()
|
refreshProxy()
|
||||||
|
mutate(() => syncTrayProxySelection(), {
|
||||||
|
id: 'sync-tray-selection',
|
||||||
|
errorNotice: false,
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('[CurrentProxyCard] Failed to sync tray latency:', error)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const proxyOptions = useMemo(
|
const proxyOptions = useMemo(
|
||||||
|
|||||||
@@ -4,19 +4,10 @@ import {
|
|||||||
VisibilityOffOutlined,
|
VisibilityOffOutlined,
|
||||||
VisibilityOutlined,
|
VisibilityOutlined,
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import {
|
import { Box, Button, IconButton, Skeleton, Typography } from '@mui/material'
|
||||||
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,
|
||||||
@@ -108,13 +99,9 @@ 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, the card stays eligible for refresh until unmount.
|
// Once intersected, refreshes stay enabled until unmount.
|
||||||
const [containerRef, hasIntersected, _resetIntersected] = useIntersection({
|
const [containerRef, hasIntersected, _resetIntersected] = useIntersection({
|
||||||
rootMargin: '0px',
|
rootMargin: '0px',
|
||||||
})
|
})
|
||||||
@@ -164,8 +151,6 @@ 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.
|
||||||
@@ -205,7 +190,7 @@ export const IpInfoCard = () => {
|
|||||||
if (timer != null) clearInterval(timer)
|
if (timer != null) clearInterval(timer)
|
||||||
document.removeEventListener('visibilitychange', onVisibilityChange)
|
document.removeEventListener('visibilitychange', onVisibilityChange)
|
||||||
}
|
}
|
||||||
}, [autoRefresh, hasIntersected])
|
}, [hasIntersected])
|
||||||
|
|
||||||
const toggleShowIp = useCallback(() => {
|
const toggleShowIp = useCallback(() => {
|
||||||
setShowIp((prev) => !prev)
|
setShowIp((prev) => !prev)
|
||||||
@@ -379,25 +364,12 @@ export const IpInfoCard = () => {
|
|||||||
fontSize: '0.7rem',
|
fontSize: '0.7rem',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FormControlLabel
|
<Typography variant="caption">
|
||||||
sx={{ m: 0 }}
|
{t('home.components.ipInfo.labels.autoRefresh')}
|
||||||
control={
|
{countdown.type === 'countdown'
|
||||||
<Switch
|
? `: ${countdown.remainingSeconds}s`
|
||||||
size="small"
|
: '...'}
|
||||||
checked={autoRefresh}
|
</Typography>
|
||||||
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={{
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
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 { getCurrentWindow } from '@tauri-apps/api/window'
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
type MouseEvent,
|
||||||
type PointerEvent,
|
type PointerEvent,
|
||||||
useCallback,
|
useCallback,
|
||||||
useImperativeHandle,
|
useEffect,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
|
|
||||||
import { useWindowControls } from '@/hooks/use-window'
|
|
||||||
import getSystem from '@/utils/get-system'
|
import getSystem from '@/utils/get-system'
|
||||||
|
|
||||||
const RESIZE_HANDLES = [
|
const RESIZE_HANDLES = [
|
||||||
@@ -21,9 +21,7 @@ const RESIZE_HANDLES = [
|
|||||||
{ direction: 'NorthWest', position: 'north-west' },
|
{ direction: 'NorthWest', position: 'north-west' },
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export const WindowResizeHandles = () => {
|
export const WindowResizeHandles = ({ maximized }: { maximized: boolean }) => {
|
||||||
const { currentWindow, maximized } = useWindowControls()
|
|
||||||
|
|
||||||
const startResizeDragging = useCallback(
|
const startResizeDragging = useCallback(
|
||||||
(event: PointerEvent<HTMLDivElement>) => {
|
(event: PointerEvent<HTMLDivElement>) => {
|
||||||
if (event.button !== 0) return
|
if (event.button !== 0) return
|
||||||
@@ -33,14 +31,14 @@ export const WindowResizeHandles = () => {
|
|||||||
const handle = RESIZE_HANDLES.find((item) => item.direction === direction)
|
const handle = RESIZE_HANDLES.find((item) => item.direction === direction)
|
||||||
|
|
||||||
if (handle) {
|
if (handle) {
|
||||||
void currentWindow
|
void getCurrentWindow()
|
||||||
.startResizeDragging(handle.direction)
|
.startResizeDragging(handle.direction)
|
||||||
.catch((error) =>
|
.catch((error) =>
|
||||||
console.warn('[WindowResizeHandles] 调整窗口大小失败:', error),
|
console.warn('[WindowResizeHandles] 调整窗口大小失败:', error),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[currentWindow],
|
[],
|
||||||
)
|
)
|
||||||
|
|
||||||
if (getSystem() !== 'linux' || maximized) return null
|
if (getSystem() !== 'linux' || maximized) return null
|
||||||
@@ -63,44 +61,77 @@ export const WindowResizeHandles = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WindowControls = forwardRef(function WindowControls(props, ref) {
|
export const WindowControls = ({
|
||||||
|
maximized,
|
||||||
|
setMaximized,
|
||||||
|
}: {
|
||||||
|
maximized: boolean
|
||||||
|
setMaximized: (value: boolean) => void
|
||||||
|
}) => {
|
||||||
const OS = getSystem()
|
const OS = getSystem()
|
||||||
const {
|
|
||||||
currentWindow,
|
|
||||||
maximized,
|
|
||||||
minimize,
|
|
||||||
close,
|
|
||||||
toggleFullscreen,
|
|
||||||
toggleMaximize,
|
|
||||||
} = useWindowControls()
|
|
||||||
|
|
||||||
useImperativeHandle(
|
const close = useCallback(async () => {
|
||||||
ref,
|
// Delay one frame so the UI can clear :hover before the window hides.
|
||||||
() => ({
|
await new Promise((resolve) => setTimeout(resolve, 20))
|
||||||
currentWindow,
|
await getCurrentWindow().close()
|
||||||
maximized,
|
}, [])
|
||||||
minimize,
|
|
||||||
close,
|
const minimize = useCallback(async () => {
|
||||||
toggleFullscreen,
|
// Delay one frame so the UI can clear :hover before the window hides.
|
||||||
toggleMaximize,
|
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||||
}),
|
await getCurrentWindow().minimize()
|
||||||
[
|
}, [])
|
||||||
currentWindow,
|
|
||||||
maximized,
|
const toggleMaximize = useCallback(async () => {
|
||||||
minimize,
|
const currentWindow = getCurrentWindow()
|
||||||
close,
|
if (await currentWindow.isMaximized()) {
|
||||||
toggleFullscreen,
|
await currentWindow.unmaximize()
|
||||||
toggleMaximize,
|
setMaximized(false)
|
||||||
],
|
} else {
|
||||||
|
await currentWindow.maximize()
|
||||||
|
setMaximized(true)
|
||||||
|
}
|
||||||
|
}, [setMaximized])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getCurrentWindow().setMinimizable(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleTitlebarMouseDown = useCallback(
|
||||||
|
(event: MouseEvent<HTMLDivElement>) => {
|
||||||
|
// Left button only, and only on the first press: detail 2 is the second
|
||||||
|
// press of a double-click, which `handleTitlebarDoubleClick` turns into maximize.
|
||||||
|
if (event.button !== 0 || event.detail !== 1) return
|
||||||
|
// Only the empty bar area drags; the control buttons keep their own clicks.
|
||||||
|
if (event.target !== event.currentTarget) return
|
||||||
|
event.preventDefault()
|
||||||
|
void getCurrentWindow()
|
||||||
|
.startDragging()
|
||||||
|
.catch((error) => console.warn('[WindowControls] 开始拖动失败:', error))
|
||||||
|
},
|
||||||
|
[],
|
||||||
)
|
)
|
||||||
|
|
||||||
// 通过前端对 tauri 窗口进行翻转全屏时会短暂地与系统图标重叠渲染。
|
const handleTitlebarDoubleClick = useCallback(
|
||||||
// 这可能是上游缺陷,保险起见跨平台以窗口的最大化翻转为准。
|
(event: MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (event.target !== event.currentTarget) return
|
||||||
|
void toggleMaximize().catch((error) =>
|
||||||
|
console.warn('[WindowControls] 切换最大化失败:', error),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[toggleMaximize],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
onMouseDown={handleTitlebarMouseDown}
|
||||||
|
onDoubleClick={handleTitlebarDoubleClick}
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
justifyContent: OS === 'macos' ? 'flex-start' : 'flex-end',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
px: 1,
|
||||||
gap: 1,
|
gap: 1,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
'> button': {
|
'> button': {
|
||||||
@@ -186,4 +217,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 || []).filter((rule) => rule !== null))
|
setPrependSeq(obj?.prepend || [])
|
||||||
setAppendSeq((obj?.append || []).filter((rule) => rule !== null))
|
setAppendSeq(obj?.append || [])
|
||||||
setDeleteSeq((obj?.delete || []).filter((rule) => rule !== null))
|
setDeleteSeq(obj?.delete || [])
|
||||||
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 ?? []).filter((rule) => rule !== null))
|
setPrependSeq(obj?.prepend ?? [])
|
||||||
setAppendSeq((obj?.append ?? []).filter((rule) => rule !== null))
|
setAppendSeq(obj?.append ?? [])
|
||||||
setDeleteSeq((obj?.delete ?? []).filter((rule) => rule !== null))
|
setDeleteSeq(obj?.delete ?? [])
|
||||||
})
|
})
|
||||||
setVisualization(true)
|
setVisualization(true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ interface ProxyChainProps {
|
|||||||
onMarkUnsavedChanges?: () => void
|
onMarkUnsavedChanges?: () => void
|
||||||
mode?: string
|
mode?: string
|
||||||
selectedGroup?: string | null
|
selectedGroup?: string | null
|
||||||
onExitChainMode: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProxyChainItemProps {
|
interface ProxyChainItemProps {
|
||||||
@@ -332,7 +331,6 @@ export const ProxyChain = ({
|
|||||||
onMarkUnsavedChanges,
|
onMarkUnsavedChanges,
|
||||||
mode,
|
mode,
|
||||||
selectedGroup,
|
selectedGroup,
|
||||||
onExitChainMode,
|
|
||||||
}: ProxyChainProps) => {
|
}: ProxyChainProps) => {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -698,18 +696,13 @@ export const ProxyChain = ({
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
gap: 2,
|
|
||||||
height: '100%',
|
height: '100%',
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography>{t('proxies.page.chain.empty')}</Typography>
|
<Typography>{t('proxies.page.chain.empty')}</Typography>
|
||||||
<Button size="small" variant="outlined" onClick={onExitChainMode}>
|
|
||||||
{t('shared.actions.close')}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<DragDropProvider
|
<DragDropProvider
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import VisibilityOffRounded from '@mui/icons-material/VisibilityOffRounded'
|
|||||||
import VisibilityRounded from '@mui/icons-material/VisibilityRounded'
|
import VisibilityRounded from '@mui/icons-material/VisibilityRounded'
|
||||||
import WifiTetheringOffRounded from '@mui/icons-material/WifiTetheringOffRounded'
|
import WifiTetheringOffRounded from '@mui/icons-material/WifiTetheringOffRounded'
|
||||||
import WifiTetheringRounded from '@mui/icons-material/WifiTetheringRounded'
|
import WifiTetheringRounded from '@mui/icons-material/WifiTetheringRounded'
|
||||||
import { Box, IconButton, type SxProps, TextField } from '@mui/material'
|
import { Box, IconButton, type SxProps } from '@mui/material'
|
||||||
import { useDebounceFn } from 'ahooks'
|
import { useDebounceFn } from 'ahooks'
|
||||||
import { memo, useEffect, useRef } from 'react'
|
import { memo, useEffect, useRef } from 'react'
|
||||||
import { flushSync } from 'react-dom'
|
import { flushSync } from 'react-dom'
|
||||||
@@ -18,10 +18,11 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { useVerge } from '@/hooks/use-verge'
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
import delayManager from '@/services/delay'
|
import delayManager from '@/services/delay'
|
||||||
import { showNotice } from '@/services/notice-service'
|
import { showNotice } from '@/services/notice-service'
|
||||||
import { isValidUrl } from '@/utils/network'
|
import { isValidLatencyTestUrl } from '@/utils/network'
|
||||||
|
|
||||||
import { BaseSearchBox, type SearchState } from '../base'
|
import { BaseSearchBox, type SearchState } from '../base'
|
||||||
|
|
||||||
|
import { ProxyTestUrlInput } from './proxy-test-url-input'
|
||||||
import type { ProxySortType } from './use-filter-sort'
|
import type { ProxySortType } from './use-filter-sort'
|
||||||
import type { HeadState } from './use-head-state'
|
import type { HeadState } from './use-head-state'
|
||||||
|
|
||||||
@@ -69,7 +70,10 @@ export const ProxyGroupTools = memo(function ProxyGroupTools(props: Props) {
|
|||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
delayManager.setUrl(groupName, testUrl?.trim() || url || defaultLatencyUrl)
|
delayManager.setUrl(
|
||||||
|
groupName,
|
||||||
|
testUrl?.trim() || url?.trim() || defaultLatencyUrl,
|
||||||
|
)
|
||||||
}, [groupName, testUrl, defaultLatencyUrl, url])
|
}, [groupName, testUrl, defaultLatencyUrl, url])
|
||||||
|
|
||||||
// 过滤输入是高频操作,且每次都会触发整组代理的重新过滤/排序与虚拟列表重渲染,
|
// 过滤输入是高频操作,且每次都会触发整组代理的重新过滤/排序与虚拟列表重渲染,
|
||||||
@@ -112,7 +116,8 @@ export const ProxyGroupTools = memo(function ProxyGroupTools(props: Props) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{textState === 'url' && (
|
{textState === 'url' && (
|
||||||
<TextField
|
<ProxyTestUrlInput
|
||||||
|
name={groupName}
|
||||||
inputRef={inputRef}
|
inputRef={inputRef}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
hiddenLabel
|
hiddenLabel
|
||||||
@@ -125,7 +130,7 @@ export const ProxyGroupTools = memo(function ProxyGroupTools(props: Props) {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
}}
|
}}
|
||||||
onChange={(e) => onHeadState({ testUrl: e.target.value })}
|
onCommit={(testUrl) => onHeadState({ testUrl })}
|
||||||
sx={{ flex: '1 1 auto', input: { py: 0.65, px: 1 } }}
|
sx={{ flex: '1 1 auto', input: { py: 0.65, px: 1 } }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -176,7 +181,7 @@ export const ProxyGroupTools = memo(function ProxyGroupTools(props: Props) {
|
|||||||
if (testUrl?.trim() && textState !== 'filter') {
|
if (testUrl?.trim() && textState !== 'filter') {
|
||||||
onHeadState({ textState: 'url' })
|
onHeadState({ textState: 'url' })
|
||||||
}
|
}
|
||||||
if (testUrl?.trim() && !isValidUrl(testUrl)) {
|
if (testUrl?.trim() && !isValidLatencyTestUrl(testUrl)) {
|
||||||
showNotice.warning('proxies.feedback.warnings.invalidTestUrl')
|
showNotice.warning('proxies.feedback.warnings.invalidTestUrl')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ interface GroupSelectMenuProps {
|
|||||||
interface ProxyGroupsChainProps {
|
interface ProxyGroupsChainProps {
|
||||||
mode: string
|
mode: string
|
||||||
chainConfigData?: string | null
|
chainConfigData?: string | null
|
||||||
onExitChainMode: () => void
|
|
||||||
availableGroups: any[]
|
availableGroups: any[]
|
||||||
activeSelectedGroup: string | null
|
activeSelectedGroup: string | null
|
||||||
showScrollTop: boolean
|
showScrollTop: boolean
|
||||||
@@ -306,7 +305,6 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
|
|||||||
const {
|
const {
|
||||||
mode,
|
mode,
|
||||||
chainConfigData,
|
chainConfigData,
|
||||||
onExitChainMode,
|
|
||||||
availableGroups,
|
availableGroups,
|
||||||
activeSelectedGroup,
|
activeSelectedGroup,
|
||||||
showScrollTop,
|
showScrollTop,
|
||||||
@@ -523,7 +521,6 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
|
|||||||
chainConfigData={chainConfigData}
|
chainConfigData={chainConfigData}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
selectedGroup={activeSelectedGroup}
|
selectedGroup={activeSelectedGroup}
|
||||||
onExitChainMode={onExitChainMode}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ import { useProfiles } from '@/hooks/use-profiles'
|
|||||||
import { useProxySelection } from '@/hooks/use-proxy-selection'
|
import { useProxySelection } from '@/hooks/use-proxy-selection'
|
||||||
import { useVerge } from '@/hooks/use-verge'
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
import { useProxiesData, useSystemData } from '@/providers/app-data-context'
|
import { useProxiesData, useSystemData } from '@/providers/app-data-context'
|
||||||
|
import { syncTrayProxySelection } from '@/services/cmds'
|
||||||
import delayManager from '@/services/delay'
|
import delayManager from '@/services/delay'
|
||||||
|
import { mutate } from '@/services/mutate'
|
||||||
import {
|
import {
|
||||||
isInteractableMember,
|
isInteractableMember,
|
||||||
resolveMember,
|
resolveMember,
|
||||||
@@ -42,7 +44,6 @@ import {
|
|||||||
ProxyGroupNavigator,
|
ProxyGroupNavigator,
|
||||||
} from './proxy-group-navigator'
|
} from './proxy-group-navigator'
|
||||||
import { ProxyRender } from './proxy-render'
|
import { ProxyRender } from './proxy-render'
|
||||||
import type { HeadState } from './use-head-state'
|
|
||||||
import {
|
import {
|
||||||
PROXY_GROUP_HEADER_SENSORS,
|
PROXY_GROUP_HEADER_SENSORS,
|
||||||
useProxyGroupHeaderLayout,
|
useProxyGroupHeaderLayout,
|
||||||
@@ -69,7 +70,6 @@ interface Props {
|
|||||||
mode: string
|
mode: string
|
||||||
isChainMode?: boolean
|
isChainMode?: boolean
|
||||||
chainConfigData?: string | null
|
chainConfigData?: string | null
|
||||||
onExitChainMode: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function useEmptyRenderList() {
|
function useEmptyRenderList() {
|
||||||
@@ -90,11 +90,11 @@ function useProxyRenderState(
|
|||||||
) {
|
) {
|
||||||
const { verge } = useVerge()
|
const { verge } = useVerge()
|
||||||
const { proxyView } = useProxiesData()
|
const { proxyView } = useProxiesData()
|
||||||
const {
|
const { renderList, onProxies, onHeadState } = useRenderList(
|
||||||
renderList,
|
mode,
|
||||||
onProxies,
|
isChainMode,
|
||||||
onHeadState: setListHeadState,
|
activeSelectedGroup,
|
||||||
} = useRenderList(mode, isChainMode, activeSelectedGroup)
|
)
|
||||||
const scrollPositionKey = useMemo(
|
const scrollPositionKey = useMemo(
|
||||||
() =>
|
() =>
|
||||||
isChainMode
|
isChainMode
|
||||||
@@ -103,14 +103,6 @@ function useProxyRenderState(
|
|||||||
[activeSelectedGroup, isChainMode, mode],
|
[activeSelectedGroup, isChainMode, mode],
|
||||||
)
|
)
|
||||||
|
|
||||||
const onHeadState = useCallback(
|
|
||||||
(groupName: string, patch: Partial<HeadState>) => {
|
|
||||||
if (patch.open) onProxies()
|
|
||||||
setListHeadState(groupName, patch)
|
|
||||||
},
|
|
||||||
[onProxies, setListHeadState],
|
|
||||||
)
|
|
||||||
|
|
||||||
const timeout = verge?.default_latency_timeout || 10000
|
const timeout = verge?.default_latency_timeout || 10000
|
||||||
|
|
||||||
const handleCheckAll = useStableCallback(
|
const handleCheckAll = useStableCallback(
|
||||||
@@ -143,6 +135,12 @@ function useProxyRenderState(
|
|||||||
console.error(`[ProxyGroups] 延迟测试出错,组: ${groupName}`, error)
|
console.error(`[ProxyGroups] 延迟测试出错,组: ${groupName}`, error)
|
||||||
} finally {
|
} finally {
|
||||||
onProxies()
|
onProxies()
|
||||||
|
mutate(() => syncTrayProxySelection(), {
|
||||||
|
id: 'sync-tray-selection',
|
||||||
|
errorNotice: false,
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('[ProxyGroups] Failed to sync tray latency:', error)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -192,9 +190,8 @@ function useProxyRenderState(
|
|||||||
function ChainProxyGroups(props: {
|
function ChainProxyGroups(props: {
|
||||||
mode: string
|
mode: string
|
||||||
chainConfigData?: string | null
|
chainConfigData?: string | null
|
||||||
onExitChainMode: () => void
|
|
||||||
}) {
|
}) {
|
||||||
const { mode, chainConfigData, onExitChainMode } = props
|
const { mode, chainConfigData } = props
|
||||||
const { proxyView } = useProxiesData()
|
const { proxyView } = useProxiesData()
|
||||||
const [selectedGroup, setSelectedGroup] = useState<string | null>(null)
|
const [selectedGroup, setSelectedGroup] = useState<string | null>(null)
|
||||||
|
|
||||||
@@ -349,7 +346,6 @@ function ChainProxyGroups(props: {
|
|||||||
<ProxyGroupsChain
|
<ProxyGroupsChain
|
||||||
mode={mode}
|
mode={mode}
|
||||||
chainConfigData={chainConfigData}
|
chainConfigData={chainConfigData}
|
||||||
onExitChainMode={onExitChainMode}
|
|
||||||
availableGroups={availableGroups}
|
availableGroups={availableGroups}
|
||||||
activeSelectedGroup={activeSelectedGroup}
|
activeSelectedGroup={activeSelectedGroup}
|
||||||
showScrollTop={showScrollTop}
|
showScrollTop={showScrollTop}
|
||||||
@@ -608,7 +604,7 @@ function NormalProxyGroups(props: { mode: string }) {
|
|||||||
/>
|
/>
|
||||||
</DragDropProvider>
|
</DragDropProvider>
|
||||||
|
|
||||||
{(mode === 'rule' || mode === 'global') && (
|
{mode === 'rule' && (
|
||||||
<ProxyGroupNavigator
|
<ProxyGroupNavigator
|
||||||
proxyGroupNames={proxyGroupNames}
|
proxyGroupNames={proxyGroupNames}
|
||||||
onGroupLocation={handleGroupLocationByName}
|
onGroupLocation={handleGroupLocationByName}
|
||||||
@@ -621,7 +617,7 @@ function NormalProxyGroups(props: { mode: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ProxyGroups = (props: Props) => {
|
export const ProxyGroups = (props: Props) => {
|
||||||
const { mode, isChainMode = false, chainConfigData, onExitChainMode } = props
|
const { mode, isChainMode = false, chainConfigData } = props
|
||||||
const { profiles, isLoading: isProfilesLoading } = useProfiles()
|
const { profiles, isLoading: isProfilesLoading } = useProfiles()
|
||||||
const { isProxyViewPending } = useProxiesData()
|
const { isProxyViewPending } = useProxiesData()
|
||||||
const { isRunningModePending } = useSystemData()
|
const { isRunningModePending } = useSystemData()
|
||||||
@@ -643,11 +639,7 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
return <ProxyEmptyState reason={listState.reason} />
|
return <ProxyEmptyState reason={listState.reason} />
|
||||||
case 'render':
|
case 'render':
|
||||||
return isChainMode ? (
|
return isChainMode ? (
|
||||||
<ChainProxyGroups
|
<ChainProxyGroups mode={mode} chainConfigData={chainConfigData} />
|
||||||
mode={mode}
|
|
||||||
chainConfigData={chainConfigData}
|
|
||||||
onExitChainMode={onExitChainMode}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<NormalProxyGroups mode={mode} />
|
<NormalProxyGroups mode={mode} />
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
SortByAlphaRounded,
|
SortByAlphaRounded,
|
||||||
SortRounded,
|
SortRounded,
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import { Box, IconButton, TextField, type SxProps } from '@mui/material'
|
import { Box, IconButton, type SxProps } from '@mui/material'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
@@ -20,8 +20,9 @@ import { useVerge } from '@/hooks/use-verge'
|
|||||||
import delayManager from '@/services/delay'
|
import delayManager from '@/services/delay'
|
||||||
import { showNotice } from '@/services/notice-service'
|
import { showNotice } from '@/services/notice-service'
|
||||||
import { debugLog } from '@/utils/debug'
|
import { debugLog } from '@/utils/debug'
|
||||||
import { isValidUrl } from '@/utils/network'
|
import { isValidLatencyTestUrl } from '@/utils/network'
|
||||||
|
|
||||||
|
import { ProxyTestUrlInput } from './proxy-test-url-input'
|
||||||
import type { ProxySortType } from './use-filter-sort'
|
import type { ProxySortType } from './use-filter-sort'
|
||||||
import type { HeadState } from './use-head-state'
|
import type { HeadState } from './use-head-state'
|
||||||
|
|
||||||
@@ -72,7 +73,10 @@ export const ProxyHead = ({
|
|||||||
'http://cp.cloudflare.com/generate_204'
|
'http://cp.cloudflare.com/generate_204'
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
delayManager.setUrl(groupName, testUrl?.trim() || url || defaultLatencyUrl)
|
delayManager.setUrl(
|
||||||
|
groupName,
|
||||||
|
testUrl?.trim() || url?.trim() || defaultLatencyUrl,
|
||||||
|
)
|
||||||
}, [groupName, testUrl, defaultLatencyUrl, url])
|
}, [groupName, testUrl, defaultLatencyUrl, url])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -97,7 +101,7 @@ export const ProxyHead = ({
|
|||||||
debugLog(`[ProxyHead] 使用自定义测试URL: ${testUrl}`)
|
debugLog(`[ProxyHead] 使用自定义测试URL: ${testUrl}`)
|
||||||
onHeadState({ textState: 'url' })
|
onHeadState({ textState: 'url' })
|
||||||
}
|
}
|
||||||
if (testUrl?.trim() && !isValidUrl(testUrl)) {
|
if (testUrl?.trim() && !isValidLatencyTestUrl(testUrl)) {
|
||||||
showNotice.warning('proxies.feedback.warnings.invalidTestUrl')
|
showNotice.warning('proxies.feedback.warnings.invalidTestUrl')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -188,7 +192,8 @@ export const ProxyHead = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{textState === 'url' && (
|
{textState === 'url' && (
|
||||||
<TextField
|
<ProxyTestUrlInput
|
||||||
|
name={groupName}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
hiddenLabel
|
hiddenLabel
|
||||||
@@ -197,7 +202,7 @@ export const ProxyHead = ({
|
|||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
placeholder={t('proxies.page.placeholders.delayCheckUrl')}
|
placeholder={t('proxies.page.placeholders.delayCheckUrl')}
|
||||||
onChange={(e) => onHeadState({ testUrl: e.target.value })}
|
onCommit={(testUrl) => onHeadState({ testUrl })}
|
||||||
sx={{ ml: 0.5, flex: '1 1 auto', input: { py: 0.65, px: 1 } }}
|
sx={{ ml: 0.5, flex: '1 1 auto', input: { py: 0.65, px: 1 } }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { TextField, type TextFieldProps } from '@mui/material'
|
||||||
|
import { useEffect, useEffectEvent, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
import { useProfiles } from '@/hooks/use-profiles'
|
||||||
|
|
||||||
|
type Props = Omit<TextFieldProps, 'value' | 'onChange'> & {
|
||||||
|
value: string
|
||||||
|
onCommit: (url: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProxyTestUrlInput = ({ value, name, ...props }: Props) => {
|
||||||
|
const { profiles } = useProfiles()
|
||||||
|
return (
|
||||||
|
<DraftTestUrlInput
|
||||||
|
key={JSON.stringify([profiles?.current, name])}
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const DraftTestUrlInput = ({ value, onCommit, ...props }: Props) => {
|
||||||
|
const [draft, setDraft] = useState<string | null>(null)
|
||||||
|
const pendingRef = useRef<string | null>(null)
|
||||||
|
const commitOnUnmount = useEffectEvent(() => {
|
||||||
|
if (pendingRef.current !== null && pendingRef.current !== value)
|
||||||
|
onCommit(pendingRef.current)
|
||||||
|
})
|
||||||
|
useEffect(() => () => commitOnUnmount(), [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
{...props}
|
||||||
|
value={draft ?? value}
|
||||||
|
onChange={(event) => {
|
||||||
|
pendingRef.current = event.target.value
|
||||||
|
setDraft(event.target.value)
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
const next = pendingRef.current
|
||||||
|
pendingRef.current = null
|
||||||
|
setDraft(null)
|
||||||
|
if (next !== null && next !== value) onCommit(next)
|
||||||
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault()
|
||||||
|
const input = event.target as HTMLInputElement
|
||||||
|
input.blur()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -13,11 +13,17 @@ const node = (memberIndex: number, delay: number, name = `${memberIndex}`) =>
|
|||||||
kind: 'node',
|
kind: 'node',
|
||||||
ref: { kind: 'node', name, recordId: `${memberIndex}` },
|
ref: { kind: 'node', name, recordId: `${memberIndex}` },
|
||||||
node: {
|
node: {
|
||||||
|
recordId: `${memberIndex}`,
|
||||||
history: [{ delay }],
|
history: [{ delay }],
|
||||||
|
extra: {
|
||||||
|
'http://cp.cloudflare.com/generate_204': {
|
||||||
|
history: [{ time: new Date(0).toISOString(), delay }],
|
||||||
|
},
|
||||||
|
},
|
||||||
source: { kind: 'provider', providerName: `${memberIndex}` },
|
source: { kind: 'provider', providerName: `${memberIndex}` },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}) as ResolvedMemberOccurrence
|
}) as unknown as ResolvedMemberOccurrence
|
||||||
|
|
||||||
afterEach(() => vi.restoreAllMocks())
|
afterEach(() => vi.restoreAllMocks())
|
||||||
|
|
||||||
@@ -36,7 +42,9 @@ test('matches the previous comparator for cached, fallback and sentinel delays',
|
|||||||
for (const cached of [false, true]) {
|
for (const cached of [false, true]) {
|
||||||
const group = `sort-${cached}`
|
const group = `sort-${cached}`
|
||||||
if (cached) {
|
if (cached) {
|
||||||
values.forEach((delay, i) => delayManager.setDelay(`${i}`, group, delay))
|
values.forEach((delay, i) =>
|
||||||
|
delayManager.setDelay(list[i].member, group, delay),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
for (const timeout of [10000, 20, 0, NaN]) {
|
for (const timeout of [10000, 20, 0, NaN]) {
|
||||||
const expected = list
|
const expected = list
|
||||||
@@ -66,7 +74,7 @@ test('reads each occurrence once and observes cache updates and expiry on the ne
|
|||||||
node(3, 25),
|
node(3, 25),
|
||||||
]
|
]
|
||||||
const cachedOrder = [list[0], list[2], list[1], list[3]]
|
const cachedOrder = [list[0], list[2], list[1], list[3]]
|
||||||
delayManager.setDelay('0', 'expiry', 1)
|
delayManager.setDelay(list[0].member, 'expiry', 1)
|
||||||
const get = vi.spyOn(delayManager, 'getDelayFix')
|
const get = vi.spyOn(delayManager, 'getDelayFix')
|
||||||
expect(filterSort(list, 'expiry', '', 1)).toEqual(cachedOrder)
|
expect(filterSort(list, 'expiry', '', 1)).toEqual(cachedOrder)
|
||||||
expect(get).toHaveBeenCalledTimes(list.length)
|
expect(get).toHaveBeenCalledTimes(list.length)
|
||||||
@@ -80,7 +88,7 @@ test('reads each occurrence once and observes cache updates and expiry on the ne
|
|||||||
list[3],
|
list[3],
|
||||||
list[0],
|
list[0],
|
||||||
])
|
])
|
||||||
delayManager.setDelay('0', 'expiry', 2)
|
delayManager.setDelay(list[0].member, 'expiry', 2)
|
||||||
expect(filterSort(list, 'expiry', '', 1)).toEqual(cachedOrder)
|
expect(filterSort(list, 'expiry', '', 1)).toEqual(cachedOrder)
|
||||||
get.mockClear()
|
get.mockClear()
|
||||||
filterSort([], 'expiry', '', 1)
|
filterSort([], 'expiry', '', 1)
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { useCallback, useEffect, useReducer } from 'react'
|
import { useCallback, useEffect, useReducer } from 'react'
|
||||||
|
|
||||||
import { useProfiles } from '@/hooks/use-profiles'
|
import { useProfiles } from '@/hooks/use-profiles'
|
||||||
|
import {
|
||||||
|
profileTestUrls,
|
||||||
|
commitProfileTestUrl,
|
||||||
|
} from '@/services/proxy-test-url'
|
||||||
|
|
||||||
import { ProxySortType } from './use-filter-sort'
|
import { ProxySortType } from './use-filter-sort'
|
||||||
|
|
||||||
@@ -32,22 +36,45 @@ export const DEFAULT_STATE: HeadState = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type HeadStateAction =
|
type HeadStateAction =
|
||||||
| { type: 'reset' }
|
| { type: 'reset'; profile: string }
|
||||||
| { type: 'replace'; payload: Record<string, HeadState> }
|
| { type: 'replace'; profile: string; payload: Record<string, HeadState> }
|
||||||
| { type: 'update'; groupName: string; patch: Partial<HeadState> }
|
| { type: 'update'; groupName: string; patch: Partial<HeadState> }
|
||||||
|
| { type: 'urls'; urls: Record<string, string> }
|
||||||
|
|
||||||
|
interface LoadedHeadState {
|
||||||
|
profile: string | null
|
||||||
|
heads: Record<string, HeadState>
|
||||||
|
}
|
||||||
|
|
||||||
function headStateReducer(
|
function headStateReducer(
|
||||||
state: Record<string, HeadState>,
|
state: LoadedHeadState,
|
||||||
action: HeadStateAction,
|
action: HeadStateAction,
|
||||||
): Record<string, HeadState> {
|
): LoadedHeadState {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'reset':
|
case 'reset':
|
||||||
return {}
|
return { profile: action.profile, heads: {} }
|
||||||
case 'replace':
|
case 'replace':
|
||||||
return action.payload
|
return { profile: action.profile, heads: action.payload }
|
||||||
|
case 'urls': {
|
||||||
|
let heads = state.heads
|
||||||
|
for (const [name, testUrl] of Object.entries(action.urls)) {
|
||||||
|
if (heads[name]?.testUrl === testUrl) continue
|
||||||
|
heads = {
|
||||||
|
...heads,
|
||||||
|
[name]: { ...DEFAULT_STATE, ...heads[name], testUrl },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return heads === state.heads ? state : { ...state, heads }
|
||||||
|
}
|
||||||
case 'update': {
|
case 'update': {
|
||||||
const prev = state[action.groupName] || DEFAULT_STATE
|
const prev = state.heads[action.groupName] || DEFAULT_STATE
|
||||||
return { ...state, [action.groupName]: { ...prev, ...action.patch } }
|
return {
|
||||||
|
...state,
|
||||||
|
heads: {
|
||||||
|
...state.heads,
|
||||||
|
[action.groupName]: { ...prev, ...action.patch },
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return state
|
return state
|
||||||
@@ -55,30 +82,42 @@ function headStateReducer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useHeadStateNew() {
|
export function useHeadStateNew() {
|
||||||
const { profiles } = useProfiles()
|
const { profiles, current: profile } = useProfiles()
|
||||||
const current = profiles?.current || ''
|
const current = profiles?.current || ''
|
||||||
|
|
||||||
const [state, dispatch] = useReducer(headStateReducer, {})
|
const [{ profile: loadedProfile, heads: state }, dispatch] = useReducer(
|
||||||
|
headStateReducer,
|
||||||
|
{ profile: null, heads: {} },
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const urls = profile ? profileTestUrls(profile) : {}
|
||||||
|
if (loadedProfile === current) {
|
||||||
|
dispatch({ type: 'urls', urls })
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(
|
const data = JSON.parse(
|
||||||
localStorage.getItem(HEAD_STATE_KEY)!,
|
localStorage.getItem(HEAD_STATE_KEY) || '{}',
|
||||||
) as HeadStateStorage
|
) as HeadStateStorage
|
||||||
|
|
||||||
const value = data[current] || {}
|
const value = data?.[current] || {}
|
||||||
|
for (const [name, testUrl] of Object.entries(urls)) {
|
||||||
|
value[name] = { ...DEFAULT_STATE, ...value[name], testUrl }
|
||||||
|
}
|
||||||
|
|
||||||
if (value && typeof value === 'object') {
|
if (value && typeof value === 'object') {
|
||||||
dispatch({ type: 'replace', payload: value })
|
dispatch({ type: 'replace', profile: current, payload: value })
|
||||||
} else {
|
} else {
|
||||||
dispatch({ type: 'reset' })
|
dispatch({ type: 'reset', profile: current })
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
dispatch({ type: 'reset' })
|
dispatch({ type: 'reset', profile: current })
|
||||||
}
|
}
|
||||||
}, [current])
|
}, [current, loadedProfile, profile])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (loadedProfile !== current) return
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
const item = localStorage.getItem(HEAD_STATE_KEY)
|
const item = localStorage.getItem(HEAD_STATE_KEY)
|
||||||
@@ -94,13 +133,16 @@ export function useHeadStateNew() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return () => clearTimeout(timer)
|
return () => clearTimeout(timer)
|
||||||
}, [state, current])
|
}, [state, current, loadedProfile])
|
||||||
|
|
||||||
const setHeadState = useCallback(
|
const setHeadState = useCallback(
|
||||||
(groupName: string, obj: Partial<HeadState>) => {
|
(groupName: string, obj: Partial<HeadState>) => {
|
||||||
dispatch({ type: 'update', groupName, patch: obj })
|
dispatch({ type: 'update', groupName, patch: obj })
|
||||||
|
if (current && obj.testUrl !== undefined) {
|
||||||
|
void commitProfileTestUrl(current, groupName, obj.testUrl)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[],
|
[current],
|
||||||
)
|
)
|
||||||
|
|
||||||
return [state, setHeadState] as const
|
return [state, setHeadState] as const
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ const virtualGroup = (members: ProxyGroupView['members']): ProxyGroupView => ({
|
|||||||
mptcp: false,
|
mptcp: false,
|
||||||
smux: false,
|
smux: false,
|
||||||
history: [],
|
history: [],
|
||||||
|
extra: {},
|
||||||
members,
|
members,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -211,7 +212,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 === 'global'
|
return mode === 'rule' || mode === 'script'
|
||||||
? proxyView.groups.map(({ name }) => name)
|
? proxyView.groups.map(({ name }) => name)
|
||||||
: proxyView.global
|
: proxyView.global
|
||||||
? [proxyView.global.name]
|
? [proxyView.global.name]
|
||||||
@@ -257,7 +258,7 @@ export const useRenderList = (
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const useRule = mode === 'rule' || mode === 'global'
|
const useRule = mode === 'rule' || mode === 'script'
|
||||||
const renderGroups = useRule
|
const renderGroups = useRule
|
||||||
? proxyView.groups
|
? proxyView.groups
|
||||||
: proxyView.global === null
|
: proxyView.global === null
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
import { Button, Grid, Stack, TextField } from '@mui/material'
|
import Visibility from '@mui/icons-material/Visibility'
|
||||||
|
import VisibilityOff from '@mui/icons-material/VisibilityOff'
|
||||||
|
import {
|
||||||
|
TextField,
|
||||||
|
Button,
|
||||||
|
Grid,
|
||||||
|
Stack,
|
||||||
|
IconButton,
|
||||||
|
InputAdornment,
|
||||||
|
} from '@mui/material'
|
||||||
import { useLockFn } from 'ahooks'
|
import { useLockFn } from 'ahooks'
|
||||||
import { memo, useEffect, useRef } from 'react'
|
import { useState, useRef, memo, useEffect } from 'react'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { useVerge } from '@/hooks/use-verge'
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
import { createWebdavBackup, saveWebdavConfig } from '@/services/cmds'
|
import { saveWebdavConfig, createWebdavBackup } from '@/services/cmds'
|
||||||
import { mutate } from '@/services/mutate'
|
import { mutate } from '@/services/mutate'
|
||||||
import { errorDetail, showNotice } from '@/services/notice-service'
|
import { errorDetail, showNotice } from '@/services/notice-service'
|
||||||
import {
|
import {
|
||||||
@@ -34,15 +43,16 @@ export const BackupConfigViewer = memo(
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { verge, mutateVerge } = useVerge()
|
const { verge, mutateVerge } = useVerge()
|
||||||
const { webdav_url, webdav_username, webdav_password } = verge || {}
|
const { webdav_url, webdav_username, webdav_password } = verge || {}
|
||||||
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
const usernameRef = useRef<HTMLInputElement>(null)
|
const usernameRef = useRef<HTMLInputElement>(null)
|
||||||
const passwordRef = useRef<HTMLInputElement>(null)
|
const passwordRef = useRef<HTMLInputElement>(null)
|
||||||
const urlRef = useRef<HTMLInputElement>(null)
|
const urlRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
const { register, handleSubmit, watch } = useForm<IWebDavConfig>({
|
const { register, handleSubmit, watch } = useForm<IWebDavConfig>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
url: webdav_url ?? '',
|
url: webdav_url!,
|
||||||
username: webdav_username ?? '',
|
username: webdav_username!,
|
||||||
password: webdav_password ?? '',
|
password: webdav_password!,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const url = watch('url')
|
const url = watch('url')
|
||||||
@@ -50,14 +60,18 @@ export const BackupConfigViewer = memo(
|
|||||||
const password = watch('password')
|
const password = watch('password')
|
||||||
|
|
||||||
const webdavChanged =
|
const webdavChanged =
|
||||||
(webdav_url ?? '') !== url ||
|
webdav_url !== url ||
|
||||||
(webdav_username ?? '') !== username ||
|
webdav_username !== username ||
|
||||||
(webdav_password ?? '') !== password
|
webdav_password !== password
|
||||||
|
|
||||||
const webdavSignature = buildWebdavSignature(verge)
|
const webdavSignature = buildWebdavSignature(verge)
|
||||||
const webdavStatus = getWebdavStatus(webdavSignature)
|
const webdavStatus = getWebdavStatus(webdavSignature)
|
||||||
const shouldAutoInit = webdavStatus !== 'failed'
|
const shouldAutoInit = webdavStatus !== 'failed'
|
||||||
|
|
||||||
|
const handleClickShowPassword = () => {
|
||||||
|
setShowPassword((prev) => !prev)
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!webdav_url || !webdav_username || !webdav_password) {
|
if (!webdav_url || !webdav_username || !webdav_password) {
|
||||||
return
|
return
|
||||||
@@ -69,9 +83,9 @@ export const BackupConfigViewer = memo(
|
|||||||
}, [webdav_url, webdav_username, webdav_password, onInit, shouldAutoInit])
|
}, [webdav_url, webdav_username, webdav_password, onInit, shouldAutoInit])
|
||||||
|
|
||||||
const checkForm = () => {
|
const checkForm = () => {
|
||||||
const username = usernameRef.current?.value.trim()
|
const username = usernameRef.current?.value
|
||||||
const password = passwordRef.current?.value
|
const password = passwordRef.current?.value
|
||||||
const url = urlRef.current?.value.trim()
|
const url = urlRef.current?.value
|
||||||
|
|
||||||
if (!url) {
|
if (!url) {
|
||||||
urlRef.current?.focus()
|
urlRef.current?.focus()
|
||||||
@@ -95,15 +109,14 @@ export const BackupConfigViewer = memo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const save = useLockFn(async (data: IWebDavConfig) => {
|
const save = useLockFn(async (data: IWebDavConfig) => {
|
||||||
const trimmedUrl = data.url.trim()
|
checkForm()
|
||||||
const trimmedUsername = data.username.trim()
|
|
||||||
const isClearing = !trimmedUrl && !trimmedUsername && !data.password
|
|
||||||
if (!isClearing) checkForm()
|
|
||||||
const signature = buildWebdavSignature({
|
const signature = buildWebdavSignature({
|
||||||
webdav_url: data.url,
|
webdav_url: data.url,
|
||||||
webdav_username: data.username,
|
webdav_username: data.username,
|
||||||
webdav_password: data.password,
|
webdav_password: data.password,
|
||||||
})
|
})
|
||||||
|
const trimmedUrl = data.url.trim()
|
||||||
|
const trimmedUsername = data.username.trim()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@@ -128,7 +141,7 @@ export const BackupConfigViewer = memo(
|
|||||||
)
|
)
|
||||||
setWebdavStatus(signature, 'unknown')
|
setWebdavStatus(signature, 'unknown')
|
||||||
showNotice.success('settings.modals.backup.messages.webdavConfigSaved')
|
showNotice.success('settings.modals.backup.messages.webdavConfigSaved')
|
||||||
if (!isClearing) await onSaveSuccess(signature)
|
await onSaveSuccess(signature)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showNotice.error(
|
showNotice.error(
|
||||||
'settings.modals.backup.messages.webdavConfigSaveFailed',
|
'settings.modals.backup.messages.webdavConfigSaveFailed',
|
||||||
@@ -202,7 +215,7 @@ export const BackupConfigViewer = memo(
|
|||||||
<Grid size={{ xs: 6 }}>
|
<Grid size={{ xs: 6 }}>
|
||||||
<TextField
|
<TextField
|
||||||
label={t('shared.labels.password')}
|
label={t('shared.labels.password')}
|
||||||
type="password"
|
type={showPassword ? 'text' : 'password'}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small"
|
size="small"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
@@ -210,6 +223,20 @@ export const BackupConfigViewer = memo(
|
|||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
inputRef={passwordRef}
|
inputRef={passwordRef}
|
||||||
{...register('password')}
|
{...register('password')}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton
|
||||||
|
onClick={handleClickShowPassword}
|
||||||
|
edge="end"
|
||||||
|
>
|
||||||
|
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -224,9 +251,9 @@ export const BackupConfigViewer = memo(
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{webdavChanged ||
|
{webdavChanged ||
|
||||||
!webdav_url ||
|
webdav_url === undefined ||
|
||||||
!webdav_username ||
|
webdav_username === undefined ||
|
||||||
!webdav_password ? (
|
webdav_password === undefined ? (
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color={'primary'}
|
color={'primary'}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -12,16 +12,21 @@ import {
|
|||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
import { join } from '@tauri-apps/api/path'
|
import { join } from '@tauri-apps/api/path'
|
||||||
|
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||||
import { open as openDialog } from '@tauri-apps/plugin-dialog'
|
import { open as openDialog } from '@tauri-apps/plugin-dialog'
|
||||||
import { exists } from '@tauri-apps/plugin-fs'
|
import { exists } from '@tauri-apps/plugin-fs'
|
||||||
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
|
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { BaseDialog, DialogRef, Switch, TooltipIcon } from '@/components/base'
|
import {
|
||||||
|
BaseDialog,
|
||||||
|
type DialogRef,
|
||||||
|
Switch,
|
||||||
|
TooltipIcon,
|
||||||
|
} from '@/components/base'
|
||||||
import { DEFAULT_HOVER_DELAY } from '@/components/proxy/proxy-group-navigator'
|
import { DEFAULT_HOVER_DELAY } from '@/components/proxy/proxy-group-navigator'
|
||||||
import { useVerge } from '@/hooks/use-verge'
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
import { useWindowDecorations } from '@/hooks/use-window'
|
import { copyIconFile, getAppDir, restartApp } from '@/services/cmds'
|
||||||
import { copyIconFile, getAppDir } from '@/services/cmds'
|
|
||||||
import { mutate } from '@/services/mutate'
|
import { mutate } from '@/services/mutate'
|
||||||
import { showNotice } from '@/services/notice-service'
|
import { showNotice } from '@/services/notice-service'
|
||||||
import getSystem from '@/utils/get-system'
|
import getSystem from '@/utils/get-system'
|
||||||
@@ -58,8 +63,6 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
|||||||
const [sysproxyIcon, setSysproxyIcon] = useState('')
|
const [sysproxyIcon, setSysproxyIcon] = useState('')
|
||||||
const [tunIcon, setTunIcon] = useState('')
|
const [tunIcon, setTunIcon] = useState('')
|
||||||
|
|
||||||
const { decorated, toggleDecorations } = useWindowDecorations()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initIconPath()
|
initIconPath()
|
||||||
}, [])
|
}, [])
|
||||||
@@ -123,17 +126,50 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
|
|||||||
<List>
|
<List>
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t(
|
primary={
|
||||||
'settings.components.verge.layout.fields.preferSystemTitlebar',
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||||
)}
|
<span>
|
||||||
|
{t(
|
||||||
|
'settings.components.verge.layout.fields.enableSystemTitlebar',
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<TooltipIcon
|
||||||
|
title={t(
|
||||||
|
'settings.components.verge.layout.tooltips.enableSystemTitlebar',
|
||||||
|
)}
|
||||||
|
sx={{ opacity: '0.7' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<GuardState
|
<GuardState
|
||||||
value={decorated}
|
value={verge?.enable_system_titlebar ?? true}
|
||||||
valueProps="checked"
|
valueProps="checked"
|
||||||
onCatch={onError}
|
onCatch={onError}
|
||||||
onFormat={onSwitchFormat}
|
onFormat={onSwitchFormat}
|
||||||
onChange={async () => {
|
onChange={(e) => onChangeData({ enable_system_titlebar: e })}
|
||||||
await toggleDecorations()
|
onGuard={async (e) => {
|
||||||
|
await patchVerge({ enable_system_titlebar: e })
|
||||||
|
if (OS === 'linux') {
|
||||||
|
void mutate(() => restartApp(), {
|
||||||
|
id: 'restart-app',
|
||||||
|
errorNotice: false,
|
||||||
|
}).catch((error) => {
|
||||||
|
console.warn('[LayoutViewer] failed to restart app:', error)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const currentWindow = getCurrentWindow()
|
||||||
|
try {
|
||||||
|
await currentWindow.setDecorations(e)
|
||||||
|
// macOS clears the Miniaturizable style bit when decorations are disabled; re-enable minimize.
|
||||||
|
await currentWindow.setMinimizable(true)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
'[LayoutViewer] failed to apply system titlebar setting:',
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Switch edge="end" />
|
<Switch edge="end" />
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ 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()
|
||||||
@@ -53,7 +52,6 @@ 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(
|
||||||
@@ -86,7 +84,6 @@ 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),
|
||||||
@@ -120,7 +117,6 @@ 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,
|
||||||
}
|
}
|
||||||
@@ -167,7 +163,6 @@ 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,
|
||||||
}
|
}
|
||||||
@@ -181,7 +176,6 @@ 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(
|
||||||
@@ -218,25 +212,6 @@ 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
|
||||||
|
|||||||
@@ -72,10 +72,12 @@ const LazyReactMarkdown = lazy(async () => {
|
|||||||
{ default: ReactMarkdown },
|
{ default: ReactMarkdown },
|
||||||
{ default: rehypeRaw },
|
{ default: rehypeRaw },
|
||||||
{ default: rehypeSanitize, defaultSchema },
|
{ default: rehypeSanitize, defaultSchema },
|
||||||
|
{ default: remarkGfm },
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
import('react-markdown'),
|
import('react-markdown'),
|
||||||
import('rehype-raw'),
|
import('rehype-raw'),
|
||||||
import('rehype-sanitize'),
|
import('rehype-sanitize'),
|
||||||
|
import('remark-gfm'),
|
||||||
])
|
])
|
||||||
|
|
||||||
const sanitizeSchema = {
|
const sanitizeSchema = {
|
||||||
@@ -102,6 +104,7 @@ const LazyReactMarkdown = lazy(async () => {
|
|||||||
default: (props: ReactMarkdownOptions) => (
|
default: (props: ReactMarkdownOptions) => (
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
{...props}
|
{...props}
|
||||||
|
remarkPlugins={[remarkGfm, ...(props.remarkPlugins ?? [])]}
|
||||||
rehypePlugins={[rehypeRaw, [rehypeSanitize, sanitizeSchema]]}
|
rehypePlugins={[rehypeRaw, [rehypeSanitize, sanitizeSchema]]}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useCallback, useRef } from 'react'
|
import { useCallback, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
import type { ProfilePatch, ProfileSelection } from '@/services/bindings'
|
import type { ProfilePatch, ProfileSelection } from '@/services/bindings'
|
||||||
import { getProfiles, patchProfile, patchProfilesConfig } from '@/services/cmds'
|
import { getProfiles, patchProfile, patchProfilesConfig } from '@/services/cmds'
|
||||||
import { mutate } from '@/services/mutate'
|
import { mutate } from '@/services/mutate'
|
||||||
|
import { migrateProfileTestUrls } from '@/services/proxy-test-url'
|
||||||
import { fetchCacheData, setCacheData, useQuery } from '@/services/query-client'
|
import { fetchCacheData, setCacheData, useQuery } from '@/services/query-client'
|
||||||
import { debugLog } from '@/utils/debug'
|
import { debugLog } from '@/utils/debug'
|
||||||
|
|
||||||
@@ -35,6 +36,10 @@ export const useProfiles = () => {
|
|||||||
refetchInterval: false,
|
refetchInterval: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (profiles) migrateProfileTestUrls(profiles)
|
||||||
|
}, [profiles])
|
||||||
|
|
||||||
const refetchRef = useRef(refetch)
|
const refetchRef = useRef(refetch)
|
||||||
refetchRef.current = refetch
|
refetchRef.current = refetch
|
||||||
const mutateProfiles = useCallback(async () => {
|
const mutateProfiles = useCallback(async () => {
|
||||||
@@ -46,7 +51,6 @@ export const useProfiles = () => {
|
|||||||
try {
|
try {
|
||||||
const result = await mutate(() => patchProfilesConfig(value), {
|
const result = await mutate(() => patchProfilesConfig(value), {
|
||||||
id: 'patch-profiles-config',
|
id: 'patch-profiles-config',
|
||||||
errorNotice: false,
|
|
||||||
onFulfilled: (outcome) => {
|
onFulfilled: (outcome) => {
|
||||||
if (outcome.status === 'valid') {
|
if (outcome.status === 'valid') {
|
||||||
void setCacheData<IProfilesConfig>(profilesQueryKey, (current) =>
|
void setCacheData<IProfilesConfig>(profilesQueryKey, (current) =>
|
||||||
@@ -56,8 +60,12 @@ export const useProfiles = () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
// Backend Busy keeps local state untouched, as before.
|
||||||
|
return result.value
|
||||||
|
}
|
||||||
const outcome = result.value
|
const outcome = result.value
|
||||||
if (outcome.status !== 'valid') {
|
if (outcome.status !== 'valid' && outcome.status !== 'busy') {
|
||||||
await mutateProfiles()
|
await mutateProfiles()
|
||||||
}
|
}
|
||||||
return outcome
|
return outcome
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ export function useProxyDelayState(
|
|||||||
groupName: string,
|
groupName: string,
|
||||||
): UseProxyDelayState {
|
): UseProxyDelayState {
|
||||||
const name = member.ref.name
|
const name = member.ref.name
|
||||||
const details = memberDetails(member)
|
|
||||||
const unresolved = member.kind === 'unresolved'
|
const unresolved = member.kind === 'unresolved'
|
||||||
const isPreset = unresolved || PRESET_PROXY_NAMES.includes(name)
|
const isPreset = unresolved || PRESET_PROXY_NAMES.includes(name)
|
||||||
const [delayState, setDelayState] = useReducer(identity, INITIAL_DELAY)
|
const [delayState, setDelayState] = useReducer(identity, INITIAL_DELAY)
|
||||||
@@ -43,18 +42,18 @@ export function useProxyDelayState(
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isPreset) return
|
if (isPreset) return
|
||||||
delayManager.setListener(name, groupName, setDelayState)
|
delayManager.setListener(member, groupName, setDelayState)
|
||||||
return () => {
|
return () => {
|
||||||
delayManager.removeListener(name, groupName)
|
delayManager.removeListener(member, groupName)
|
||||||
}
|
}
|
||||||
}, [name, groupName, isPreset])
|
}, [member, groupName, isPreset])
|
||||||
|
|
||||||
const updateDelay = useCallback(() => {
|
const updateDelay = useCallback(() => {
|
||||||
if (unresolved) {
|
if (unresolved) {
|
||||||
setDelayState(INITIAL_DELAY)
|
setDelayState(INITIAL_DELAY)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const cachedUpdate = delayManager.getDelayUpdate(name, groupName)
|
const cachedUpdate = delayManager.getDelayUpdate(member, groupName)
|
||||||
if (cachedUpdate) {
|
if (cachedUpdate) {
|
||||||
setDelayState({ ...cachedUpdate })
|
setDelayState({ ...cachedUpdate })
|
||||||
return
|
return
|
||||||
@@ -67,7 +66,9 @@ export function useProxyDelayState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let updatedAt = 0
|
let updatedAt = 0
|
||||||
const history = details?.history
|
const history =
|
||||||
|
delayManager.getHistory(member, groupName) ??
|
||||||
|
memberDetails(member)?.history
|
||||||
if (history && history.length > 0) {
|
if (history && history.length > 0) {
|
||||||
const lastRecord = history[history.length - 1]
|
const lastRecord = history[history.length - 1]
|
||||||
const parsed = Date.parse(lastRecord.time)
|
const parsed = Date.parse(lastRecord.time)
|
||||||
@@ -77,16 +78,18 @@ export function useProxyDelayState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
setDelayState({ delay: fallbackDelay, updatedAt })
|
setDelayState({ delay: fallbackDelay, updatedAt })
|
||||||
}, [details?.history, groupName, member, name, unresolved])
|
}, [groupName, member, unresolved])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateDelay()
|
updateDelay()
|
||||||
}, [updateDelay])
|
return delayManager.addGroupListener(groupName, updateDelay)
|
||||||
|
}, [groupName, updateDelay])
|
||||||
|
|
||||||
const onDelay = useLockFn(async () => {
|
const onDelay = useLockFn(async () => {
|
||||||
if (!isInteractableMember(member)) return
|
if (!isInteractableMember(member)) return
|
||||||
setDelayState({ delay: -2, updatedAt: Date.now() })
|
setDelayState({ delay: -2, updatedAt: Date.now() })
|
||||||
setDelayState(await delayManager.checkDelay(member, groupName, timeout))
|
await delayManager.checkDelay(member, groupName, timeout)
|
||||||
|
updateDelay()
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import debounce from '@/utils/debounce'
|
||||||
|
|
||||||
|
export const useWindowMaximized = (enabled: boolean) => {
|
||||||
|
const [maximized, setMaximized] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled) return
|
||||||
|
|
||||||
|
const currentWindow = getCurrentWindow()
|
||||||
|
let isUnmounted = false
|
||||||
|
|
||||||
|
const syncMaximized = async () => {
|
||||||
|
if (isUnmounted) return
|
||||||
|
try {
|
||||||
|
setMaximized(await currentWindow.isMaximized())
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[useWindowMaximized] 读取最大化状态失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void syncMaximized()
|
||||||
|
|
||||||
|
const unlistenPromise = currentWindow.onResized(
|
||||||
|
debounce(syncMaximized, 300),
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isUnmounted = true
|
||||||
|
unlistenPromise
|
||||||
|
.then((unlisten) => unlisten())
|
||||||
|
.catch((err) =>
|
||||||
|
console.warn('[useWindowMaximized] 清理监听器失败:', err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [enabled])
|
||||||
|
|
||||||
|
return { maximized, setMaximized }
|
||||||
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { use } from 'react'
|
|
||||||
|
|
||||||
import { WindowContext, type WindowContextType } from '@/providers/window'
|
|
||||||
|
|
||||||
const useWindow = () => {
|
|
||||||
const context = use(WindowContext)
|
|
||||||
if (context === undefined) {
|
|
||||||
throw new Error('useWindow must be used within WindowProvider')
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useWindowControls = () => {
|
|
||||||
const {
|
|
||||||
maximized,
|
|
||||||
minimize,
|
|
||||||
toggleMaximize,
|
|
||||||
close,
|
|
||||||
toggleFullscreen,
|
|
||||||
currentWindow,
|
|
||||||
} = useWindow()
|
|
||||||
return {
|
|
||||||
maximized,
|
|
||||||
minimize,
|
|
||||||
toggleMaximize,
|
|
||||||
close,
|
|
||||||
toggleFullscreen,
|
|
||||||
currentWindow,
|
|
||||||
} satisfies Pick<
|
|
||||||
WindowContextType,
|
|
||||||
| 'maximized'
|
|
||||||
| 'minimize'
|
|
||||||
| 'toggleMaximize'
|
|
||||||
| 'close'
|
|
||||||
| 'toggleFullscreen'
|
|
||||||
| 'currentWindow'
|
|
||||||
>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useWindowDecorations = () => {
|
|
||||||
const { decorated, toggleDecorations, refreshDecorated } = useWindow()
|
|
||||||
return { decorated, toggleDecorations, refreshDecorated } satisfies Pick<
|
|
||||||
WindowContextType,
|
|
||||||
'decorated' | 'toggleDecorations' | 'refreshDecorated'
|
|
||||||
>
|
|
||||||
}
|
|
||||||
@@ -70,8 +70,6 @@
|
|||||||
"chain": {
|
"chain": {
|
||||||
"header": "Chain Proxy Config",
|
"header": "Chain Proxy Config",
|
||||||
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||||
"confirmEnable": "I understand, enable",
|
|
||||||
"keepDisabled": "Keep disabled",
|
|
||||||
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||||
"instruction": "Click nodes in order to add to proxy chain",
|
"instruction": "Click nodes in order to add to proxy chain",
|
||||||
"minimumNodes": "Chain proxy requires at least 2 nodes",
|
"minimumNodes": "Chain proxy requires at least 2 nodes",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user