mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-14 21:20:48 +08:00
refactor: lint (#6511)
* refactor: lint * chore: remove eslint-plugin/config-prettier
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
name: 问题反馈 / Bug report
|
name: 问题反馈 / Bug report
|
||||||
title: "[BUG] "
|
title: '[BUG] '
|
||||||
description: 反馈你遇到的问题 / Report the issue you are experiencing
|
description: 反馈你遇到的问题 / Report the issue you are experiencing
|
||||||
labels: ["bug"]
|
labels: ['bug']
|
||||||
type: "Bug"
|
type: 'Bug'
|
||||||
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
name: 功能请求 / Feature request
|
name: 功能请求 / Feature request
|
||||||
title: "[Feature] "
|
title: '[Feature] '
|
||||||
description: 提出你的功能请求 / Propose your feature request
|
description: 提出你的功能请求 / Propose your feature request
|
||||||
labels: ["enhancement"]
|
labels: ['enhancement']
|
||||||
type: "Feature"
|
type: 'Feature'
|
||||||
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
name: I18N / 多语言相关
|
name: I18N / 多语言相关
|
||||||
title: "[I18N] "
|
title: '[I18N] '
|
||||||
description: 用于多语言翻译、国际化相关问题或建议 / For issues or suggestions related to translations and internationalization
|
description: 用于多语言翻译、国际化相关问题或建议 / For issues or suggestions related to translations and internationalization
|
||||||
labels: ["I18n"]
|
labels: ['I18n']
|
||||||
type: "Task"
|
type: 'Task'
|
||||||
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
|
|||||||
+12
-12
@@ -27,7 +27,7 @@ env:
|
|||||||
RUST_BACKTRACE: short
|
RUST_BACKTRACE: short
|
||||||
HUSKY: 0
|
HUSKY: 0
|
||||||
concurrency:
|
concurrency:
|
||||||
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
|
group: '${{ github.workflow }} - ${{ github.head_ref || github.ref }}'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check_alpha_tag:
|
check_alpha_tag:
|
||||||
@@ -249,7 +249,7 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
name: 'Clash Verge Rev ${{ env.TAG_CHANNEL }}'
|
||||||
body_path: release.txt
|
body_path: release.txt
|
||||||
prerelease: true
|
prerelease: true
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -307,7 +307,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "24.14.0"
|
node-version: '24.14.0'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -325,7 +325,7 @@ jobs:
|
|||||||
- name: Tauri build
|
- name: Tauri build
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
@@ -337,8 +337,8 @@ jobs:
|
|||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
with:
|
with:
|
||||||
tagName: ${{ env.TAG_NAME }}
|
tagName: ${{ env.TAG_NAME }}
|
||||||
releaseName: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
releaseName: 'Clash Verge Rev ${{ env.TAG_CHANNEL }}'
|
||||||
releaseBody: "More new features are now supported."
|
releaseBody: 'More new features are now supported.'
|
||||||
releaseDraft: false
|
releaseDraft: false
|
||||||
prerelease: true
|
prerelease: true
|
||||||
tauriScript: pnpm
|
tauriScript: pnpm
|
||||||
@@ -377,7 +377,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "24.14.0"
|
node-version: '24.14.0'
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
@@ -453,7 +453,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
pnpm build --target ${{ matrix.target }}
|
pnpm build --target ${{ matrix.target }}
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
|
||||||
@@ -468,7 +468,7 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
name: 'Clash Verge Rev ${{ env.TAG_CHANNEL }}'
|
||||||
prerelease: true
|
prerelease: true
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
files: |
|
files: |
|
||||||
@@ -505,7 +505,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "24.14.0"
|
node-version: '24.14.0'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -531,7 +531,7 @@ jobs:
|
|||||||
id: build
|
id: build
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
@@ -563,7 +563,7 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
name: 'Clash Verge Rev ${{ env.TAG_CHANNEL }}'
|
||||||
prerelease: true
|
prerelease: true
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
schedule:
|
schedule:
|
||||||
# UTC+8 12:00, 18:00 -> UTC 4:00, 10:00
|
# UTC+8 12:00, 18:00 -> UTC 4:00, 10:00
|
||||||
- cron: "0 4,10 * * *"
|
- cron: '0 4,10 * * *'
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
env:
|
env:
|
||||||
TAG_NAME: autobuild
|
TAG_NAME: autobuild
|
||||||
@@ -13,7 +13,7 @@ env:
|
|||||||
RUST_BACKTRACE: short
|
RUST_BACKTRACE: short
|
||||||
HUSKY: 0
|
HUSKY: 0
|
||||||
concurrency:
|
concurrency:
|
||||||
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
|
group: '${{ github.workflow }} - ${{ github.head_ref || github.ref }}'
|
||||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "24.14.0"
|
node-version: '24.14.0'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -105,7 +105,7 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
name: 'Clash Verge Rev ${{ env.TAG_CHANNEL }}'
|
||||||
body_path: release.txt
|
body_path: release.txt
|
||||||
prerelease: true
|
prerelease: true
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -147,7 +147,7 @@ jobs:
|
|||||||
- name: Install Rust Stable
|
- name: Install Rust Stable
|
||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: "1.91.0"
|
toolchain: '1.91.0'
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
@@ -157,8 +157,8 @@ jobs:
|
|||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||||
prefix-key: "v1-rust"
|
prefix-key: 'v1-rust'
|
||||||
key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
|
key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||||
workspaces: |
|
workspaces: |
|
||||||
. -> target
|
. -> target
|
||||||
cache-all-crates: true
|
cache-all-crates: true
|
||||||
@@ -196,14 +196,14 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "24.14.0"
|
node-version: '24.14.0'
|
||||||
cache: "pnpm"
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Pnpm Cache
|
- name: Pnpm Cache
|
||||||
uses: actions/cache@v5
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ~/.pnpm-store
|
path: ~/.pnpm-store
|
||||||
key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
|
key: 'pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}
|
pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}
|
||||||
|
|
||||||
@@ -225,7 +225,7 @@ jobs:
|
|||||||
- name: Tauri build for Windows-macOS-Linux
|
- name: Tauri build for Windows-macOS-Linux
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
@@ -237,8 +237,8 @@ jobs:
|
|||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
with:
|
with:
|
||||||
tagName: ${{ env.TAG_NAME }}
|
tagName: ${{ env.TAG_NAME }}
|
||||||
releaseName: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
releaseName: 'Clash Verge Rev ${{ env.TAG_CHANNEL }}'
|
||||||
releaseBody: "More new features are now supported."
|
releaseBody: 'More new features are now supported.'
|
||||||
releaseDraft: false
|
releaseDraft: false
|
||||||
prerelease: true
|
prerelease: true
|
||||||
tauriScript: pnpm
|
tauriScript: pnpm
|
||||||
@@ -269,7 +269,7 @@ jobs:
|
|||||||
- name: Install Rust Stable
|
- name: Install Rust Stable
|
||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: "1.91.0"
|
toolchain: '1.91.0'
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
@@ -279,8 +279,8 @@ jobs:
|
|||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||||
prefix-key: "v1-rust"
|
prefix-key: 'v1-rust'
|
||||||
key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
|
key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||||
workspaces: |
|
workspaces: |
|
||||||
. -> target
|
. -> target
|
||||||
cache-all-crates: true
|
cache-all-crates: true
|
||||||
@@ -294,14 +294,14 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "24.14.0"
|
node-version: '24.14.0'
|
||||||
cache: "pnpm"
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Pnpm Cache
|
- name: Pnpm Cache
|
||||||
uses: actions/cache@v5
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ~/.pnpm-store
|
path: ~/.pnpm-store
|
||||||
key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
|
key: 'pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}
|
pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}
|
||||||
|
|
||||||
@@ -313,7 +313,7 @@ jobs:
|
|||||||
- name: Release ${{ env.TAG_CHANNEL }} Version
|
- name: Release ${{ env.TAG_CHANNEL }} Version
|
||||||
run: pnpm release-version autobuild-latest
|
run: pnpm release-version autobuild-latest
|
||||||
|
|
||||||
- name: "Setup for linux"
|
- name: 'Setup for linux'
|
||||||
run: |-
|
run: |-
|
||||||
sudo ls -lR /etc/apt/
|
sudo ls -lR /etc/apt/
|
||||||
|
|
||||||
@@ -376,7 +376,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
pnpm build --target ${{ matrix.target }}
|
pnpm build --target ${{ matrix.target }}
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
|
||||||
@@ -391,7 +391,7 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
name: 'Clash Verge Rev ${{ env.TAG_CHANNEL }}'
|
||||||
prerelease: true
|
prerelease: true
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
files: |
|
files: |
|
||||||
@@ -424,8 +424,8 @@ jobs:
|
|||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||||
prefix-key: "v1-rust"
|
prefix-key: 'v1-rust'
|
||||||
key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
|
key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||||
workspaces: |
|
workspaces: |
|
||||||
. -> target
|
. -> target
|
||||||
cache-all-crates: true
|
cache-all-crates: true
|
||||||
@@ -439,14 +439,14 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "24.14.0"
|
node-version: '24.14.0'
|
||||||
cache: "pnpm"
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Pnpm Cache
|
- name: Pnpm Cache
|
||||||
uses: actions/cache@v5
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ~/.pnpm-store
|
path: ~/.pnpm-store
|
||||||
key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
|
key: 'pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}
|
pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}
|
||||||
|
|
||||||
@@ -476,7 +476,7 @@ jobs:
|
|||||||
id: build
|
id: build
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
@@ -509,7 +509,7 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
name: 'Clash Verge Rev ${{ env.TAG_CHANNEL }}'
|
||||||
prerelease: true
|
prerelease: true
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
files: target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
files: target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
||||||
@@ -541,7 +541,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "24.14.0"
|
node-version: '24.14.0'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4.4.0
|
- uses: pnpm/action-setup@v4.4.0
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
|
|||||||
@@ -4,36 +4,36 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
tag_name:
|
tag_name:
|
||||||
description: "Release tag name to check against (default: autobuild)"
|
description: 'Release tag name to check against (default: autobuild)'
|
||||||
required: false
|
required: false
|
||||||
default: "autobuild"
|
default: 'autobuild'
|
||||||
type: string
|
type: string
|
||||||
force_build:
|
force_build:
|
||||||
description: "Force build regardless of checks"
|
description: 'Force build regardless of checks'
|
||||||
required: false
|
required: false
|
||||||
default: false
|
default: false
|
||||||
type: boolean
|
type: boolean
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
tag_name:
|
tag_name:
|
||||||
description: "Release tag name to check against (default: autobuild)"
|
description: 'Release tag name to check against (default: autobuild)'
|
||||||
required: false
|
required: false
|
||||||
default: "autobuild"
|
default: 'autobuild'
|
||||||
type: string
|
type: string
|
||||||
force_build:
|
force_build:
|
||||||
description: "Force build regardless of checks"
|
description: 'Force build regardless of checks'
|
||||||
required: false
|
required: false
|
||||||
default: false
|
default: false
|
||||||
type: boolean
|
type: boolean
|
||||||
outputs:
|
outputs:
|
||||||
should_run:
|
should_run:
|
||||||
description: "Whether the build should run"
|
description: 'Whether the build should run'
|
||||||
value: ${{ jobs.check_commit.outputs.should_run }}
|
value: ${{ jobs.check_commit.outputs.should_run }}
|
||||||
last_tauri_commit:
|
last_tauri_commit:
|
||||||
description: "The last commit hash with Tauri-related changes"
|
description: 'The last commit hash with Tauri-related changes'
|
||||||
value: ${{ jobs.check_commit.outputs.last_tauri_commit }}
|
value: ${{ jobs.check_commit.outputs.last_tauri_commit }}
|
||||||
autobuild_version:
|
autobuild_version:
|
||||||
description: "The generated autobuild version string"
|
description: 'The generated autobuild version string'
|
||||||
value: ${{ jobs.check_commit.outputs.autobuild_version }}
|
value: ${{ jobs.check_commit.outputs.autobuild_version }}
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
@@ -4,24 +4,24 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
tag_name:
|
tag_name:
|
||||||
description: "Release tag name to clean (default: autobuild)"
|
description: 'Release tag name to clean (default: autobuild)'
|
||||||
required: false
|
required: false
|
||||||
default: "autobuild"
|
default: 'autobuild'
|
||||||
type: string
|
type: string
|
||||||
dry_run:
|
dry_run:
|
||||||
description: "Dry run mode (only show what would be deleted)"
|
description: 'Dry run mode (only show what would be deleted)'
|
||||||
required: false
|
required: false
|
||||||
default: false
|
default: false
|
||||||
type: boolean
|
type: boolean
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
tag_name:
|
tag_name:
|
||||||
description: "Release tag name to clean (default: autobuild)"
|
description: 'Release tag name to clean (default: autobuild)'
|
||||||
required: false
|
required: false
|
||||||
default: "autobuild"
|
default: 'autobuild'
|
||||||
type: string
|
type: string
|
||||||
dry_run:
|
dry_run:
|
||||||
description: "Dry run mode (only show what would be deleted)"
|
description: 'Dry run mode (only show what would be deleted)'
|
||||||
required: false
|
required: false
|
||||||
default: false
|
default: false
|
||||||
type: boolean
|
type: boolean
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
cargo-check:
|
cargo-check:
|
||||||
# Treat all Rust compiler warnings as errors
|
# Treat all Rust compiler warnings as errors
|
||||||
env:
|
env:
|
||||||
RUSTFLAGS: "-D warnings"
|
RUSTFLAGS: '-D warnings'
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "24.14.0"
|
node-version: '24.14.0'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
|
|||||||
+11
-11
@@ -4,22 +4,22 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
run_windows:
|
run_windows:
|
||||||
description: "运行 Windows"
|
description: '运行 Windows'
|
||||||
required: false
|
required: false
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
default: true
|
||||||
run_macos_aarch64:
|
run_macos_aarch64:
|
||||||
description: "运行 macOS aarch64"
|
description: '运行 macOS aarch64'
|
||||||
required: false
|
required: false
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
default: true
|
||||||
run_windows_arm64:
|
run_windows_arm64:
|
||||||
description: "运行 Windows ARM64"
|
description: '运行 Windows ARM64'
|
||||||
required: false
|
required: false
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
default: true
|
||||||
run_linux_amd64:
|
run_linux_amd64:
|
||||||
description: "运行 Linux amd64"
|
description: '运行 Linux amd64'
|
||||||
required: false
|
required: false
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
default: true
|
||||||
@@ -32,7 +32,7 @@ env:
|
|||||||
RUST_BACKTRACE: short
|
RUST_BACKTRACE: short
|
||||||
HUSKY: 0
|
HUSKY: 0
|
||||||
concurrency:
|
concurrency:
|
||||||
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
|
group: '${{ github.workflow }} - ${{ github.head_ref || github.ref }}'
|
||||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -80,8 +80,8 @@ jobs:
|
|||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||||
prefix-key: "v1-rust"
|
prefix-key: 'v1-rust'
|
||||||
key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
|
key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||||
workspaces: |
|
workspaces: |
|
||||||
. -> target
|
. -> target
|
||||||
cache-all-crates: true
|
cache-all-crates: true
|
||||||
@@ -103,14 +103,14 @@ jobs:
|
|||||||
if: github.event.inputs[matrix.input] == 'true'
|
if: github.event.inputs[matrix.input] == 'true'
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "24.14.0"
|
node-version: '24.14.0'
|
||||||
cache: "pnpm"
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Pnpm Cache
|
- name: Pnpm Cache
|
||||||
uses: actions/cache@v5
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ~/.pnpm-store
|
path: ~/.pnpm-store
|
||||||
key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
|
key: 'pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}
|
pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}
|
||||||
lookup-only: true
|
lookup-only: true
|
||||||
@@ -137,7 +137,7 @@ jobs:
|
|||||||
if: github.event.inputs[matrix.input] == 'true'
|
if: github.event.inputs[matrix.input] == 'true'
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ jobs:
|
|||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@v6
|
||||||
if: steps.check_frontend.outputs.frontend == 'true'
|
if: steps.check_frontend.outputs.frontend == 'true'
|
||||||
with:
|
with:
|
||||||
node-version: "24.14.0"
|
node-version: '24.14.0'
|
||||||
cache: "pnpm"
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Restore pnpm cache
|
- name: Restore pnpm cache
|
||||||
if: steps.check_frontend.outputs.frontend == 'true'
|
if: steps.check_frontend.outputs.frontend == 'true'
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ jobs:
|
|||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||||
prefix-key: "v1-rust"
|
prefix-key: 'v1-rust'
|
||||||
key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
|
key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||||
workspaces: |
|
workspaces: |
|
||||||
. -> target
|
. -> target
|
||||||
cache-all-crates: true
|
cache-all-crates: true
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
# -rc tag 时预览发布, 跳过 telegram 通知、跳过 winget 提交、跳过 latest.json 文件更新
|
# -rc tag 时预览发布, 跳过 telegram 通知、跳过 winget 提交、跳过 latest.json 文件更新
|
||||||
tags:
|
tags:
|
||||||
- "v*.*.*"
|
- 'v*.*.*'
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
env:
|
env:
|
||||||
CARGO_INCREMENTAL: 0
|
CARGO_INCREMENTAL: 0
|
||||||
@@ -15,7 +15,7 @@ env:
|
|||||||
HUSKY: 0
|
HUSKY: 0
|
||||||
concurrency:
|
concurrency:
|
||||||
# only allow per workflow per commit (and not pr) to run at a time
|
# only allow per workflow per commit (and not pr) to run at a time
|
||||||
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
|
group: '${{ github.workflow }} - ${{ github.head_ref || github.ref }}'
|
||||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -129,7 +129,7 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
name: "Clash Verge Rev ${{ env.TAG_NAME }}"
|
name: 'Clash Verge Rev ${{ env.TAG_NAME }}'
|
||||||
body_path: release.txt
|
body_path: release.txt
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: ${{ contains(github.ref_name, '-rc') }}
|
prerelease: ${{ contains(github.ref_name, '-rc') }}
|
||||||
@@ -162,7 +162,7 @@ jobs:
|
|||||||
- name: Install Rust Stable
|
- name: Install Rust Stable
|
||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: "1.91.0"
|
toolchain: '1.91.0'
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
@@ -172,8 +172,8 @@ jobs:
|
|||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||||
prefix-key: "v1-rust"
|
prefix-key: 'v1-rust'
|
||||||
key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
|
key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||||
workspaces: |
|
workspaces: |
|
||||||
. -> target
|
. -> target
|
||||||
cache-all-crates: true
|
cache-all-crates: true
|
||||||
@@ -197,7 +197,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "24.14.0"
|
node-version: '24.14.0'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -220,7 +220,7 @@ jobs:
|
|||||||
# 上游 5.24 修改了 latest.json 的生成逻辑,且依赖 tauri-plugin-update 2.10.0 暂未发布,故锁定在 0.5.23 版本
|
# 上游 5.24 修改了 latest.json 的生成逻辑,且依赖 tauri-plugin-update 2.10.0 暂未发布,故锁定在 0.5.23 版本
|
||||||
uses: tauri-apps/tauri-action@v0.6.2
|
uses: tauri-apps/tauri-action@v0.6.2
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
@@ -232,8 +232,8 @@ jobs:
|
|||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
with:
|
with:
|
||||||
tagName: ${{ github.ref_name }}
|
tagName: ${{ github.ref_name }}
|
||||||
releaseName: "Clash Verge Rev ${{ github.ref_name }}"
|
releaseName: 'Clash Verge Rev ${{ github.ref_name }}'
|
||||||
releaseBody: "Draft release, will be updated later."
|
releaseBody: 'Draft release, will be updated later.'
|
||||||
releaseDraft: true
|
releaseDraft: true
|
||||||
prerelease: ${{ contains(github.ref_name, '-rc') }}
|
prerelease: ${{ contains(github.ref_name, '-rc') }}
|
||||||
tauriScript: pnpm
|
tauriScript: pnpm
|
||||||
@@ -261,7 +261,7 @@ jobs:
|
|||||||
- name: Install Rust Stable
|
- name: Install Rust Stable
|
||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: "1.91.0"
|
toolchain: '1.91.0'
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
@@ -271,8 +271,8 @@ jobs:
|
|||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||||
prefix-key: "v1-rust"
|
prefix-key: 'v1-rust'
|
||||||
key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
|
key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||||
workspaces: |
|
workspaces: |
|
||||||
. -> target
|
. -> target
|
||||||
cache-all-crates: true
|
cache-all-crates: true
|
||||||
@@ -281,7 +281,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "24.14.0"
|
node-version: '24.14.0'
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
@@ -293,7 +293,7 @@ jobs:
|
|||||||
pnpm i
|
pnpm i
|
||||||
pnpm run prebuild ${{ matrix.target }}
|
pnpm run prebuild ${{ matrix.target }}
|
||||||
|
|
||||||
- name: "Setup for linux"
|
- name: 'Setup for linux'
|
||||||
run: |-
|
run: |-
|
||||||
sudo ls -lR /etc/apt/
|
sudo ls -lR /etc/apt/
|
||||||
|
|
||||||
@@ -323,14 +323,14 @@ jobs:
|
|||||||
patchelf:${{ matrix.arch }} \
|
patchelf:${{ matrix.arch }} \
|
||||||
librsvg2-dev:${{ matrix.arch }}
|
librsvg2-dev:${{ matrix.arch }}
|
||||||
|
|
||||||
- name: "Install aarch64 tools"
|
- name: 'Install aarch64 tools'
|
||||||
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
||||||
run: |
|
run: |
|
||||||
sudo apt install -y \
|
sudo apt install -y \
|
||||||
gcc-aarch64-linux-gnu \
|
gcc-aarch64-linux-gnu \
|
||||||
g++-aarch64-linux-gnu
|
g++-aarch64-linux-gnu
|
||||||
|
|
||||||
- name: "Install armv7 tools"
|
- name: 'Install armv7 tools'
|
||||||
if: matrix.target == 'armv7-unknown-linux-gnueabihf'
|
if: matrix.target == 'armv7-unknown-linux-gnueabihf'
|
||||||
run: |
|
run: |
|
||||||
sudo apt install -y \
|
sudo apt install -y \
|
||||||
@@ -356,7 +356,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
pnpm build --target ${{ matrix.target }}
|
pnpm build --target ${{ matrix.target }}
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
|
||||||
@@ -371,8 +371,8 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: v${{env.VERSION}}
|
tag_name: v${{env.VERSION}}
|
||||||
name: "Clash Verge Rev v${{env.VERSION}}"
|
name: 'Clash Verge Rev v${{env.VERSION}}'
|
||||||
body: "See release notes for detailed changelog."
|
body: 'See release notes for detailed changelog.'
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
prerelease: ${{ contains(github.ref_name, '-rc') }}
|
prerelease: ${{ contains(github.ref_name, '-rc') }}
|
||||||
files: |
|
files: |
|
||||||
@@ -400,7 +400,7 @@ jobs:
|
|||||||
- name: Install Rust Stable
|
- name: Install Rust Stable
|
||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: "1.91.0"
|
toolchain: '1.91.0'
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
@@ -410,8 +410,8 @@ jobs:
|
|||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||||
prefix-key: "v1-rust"
|
prefix-key: 'v1-rust'
|
||||||
key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
|
key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
|
||||||
workspaces: |
|
workspaces: |
|
||||||
. -> target
|
. -> target
|
||||||
cache-all-crates: true
|
cache-all-crates: true
|
||||||
@@ -420,7 +420,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "24.14.0"
|
node-version: '24.14.0'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -450,7 +450,7 @@ jobs:
|
|||||||
id: build
|
id: build
|
||||||
uses: tauri-apps/tauri-action@v0.6.2
|
uses: tauri-apps/tauri-action@v0.6.2
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
@@ -482,8 +482,8 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: v${{steps.build.outputs.appVersion}}
|
tag_name: v${{steps.build.outputs.appVersion}}
|
||||||
name: "Clash Verge Rev v${{steps.build.outputs.appVersion}}"
|
name: 'Clash Verge Rev v${{steps.build.outputs.appVersion}}'
|
||||||
body: "See release notes for detailed changelog."
|
body: 'See release notes for detailed changelog.'
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
prerelease: ${{ contains(github.ref_name, '-rc') }}
|
prerelease: ${{ contains(github.ref_name, '-rc') }}
|
||||||
files: target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
files: target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
||||||
@@ -505,7 +505,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "24.14.0"
|
node-version: '24.14.0'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -531,7 +531,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "24.14.0"
|
node-version: '24.14.0'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -593,7 +593,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "24.14.0"
|
node-version: '24.14.0'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "24.14.0"
|
node-version: '24.14.0'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "24.14.0"
|
node-version: '24.14.0'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ src-tauri/target/
|
|||||||
src-tauri/gen/
|
src-tauri/gen/
|
||||||
|
|
||||||
target
|
target
|
||||||
|
Cargo.lock
|
||||||
|
|||||||
+3
-14
@@ -1,16 +1,5 @@
|
|||||||
{
|
{
|
||||||
"printWidth": 80,
|
"semi": false,
|
||||||
"tabWidth": 2,
|
"singleQuote": true,
|
||||||
"useTabs": false,
|
"plugins": ["prettier-plugin-toml"]
|
||||||
"semi": true,
|
|
||||||
"singleQuote": false,
|
|
||||||
"jsxSingleQuote": false,
|
|
||||||
"trailingComma": "all",
|
|
||||||
"bracketSpacing": true,
|
|
||||||
"bracketSameLine": false,
|
|
||||||
"arrowParens": "always",
|
|
||||||
"proseWrap": "preserve",
|
|
||||||
"htmlWhitespaceSensitivity": "css",
|
|
||||||
"endOfLine": "auto",
|
|
||||||
"embeddedLanguageFormatting": "auto"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ members = [
|
|||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ notifications:
|
|||||||
body: ダッシュボードの表示状態が更新されました。
|
body: ダッシュボードの表示状態が更新されました。
|
||||||
clashModeChanged:
|
clashModeChanged:
|
||||||
title: モード切り替え
|
title: モード切り替え
|
||||||
body: "{mode} に切り替えました。"
|
body: '{mode} に切り替えました。'
|
||||||
systemProxyToggled:
|
systemProxyToggled:
|
||||||
title: システムプロキシ
|
title: システムプロキシ
|
||||||
body: システムプロキシの状態が更新されました。
|
body: システムプロキシの状態が更新されました。
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ notifications:
|
|||||||
body: 대시보드 표시 상태가 업데이트되었습니다.
|
body: 대시보드 표시 상태가 업데이트되었습니다.
|
||||||
clashModeChanged:
|
clashModeChanged:
|
||||||
title: 모드 전환
|
title: 모드 전환
|
||||||
body: "{mode}(으)로 전환되었습니다."
|
body: '{mode}(으)로 전환되었습니다.'
|
||||||
systemProxyToggled:
|
systemProxyToggled:
|
||||||
title: 시스템 프록시
|
title: 시스템 프록시
|
||||||
body: 시스템 프록시 상태가 업데이트되었습니다.
|
body: 시스템 프록시 상태가 업데이트되었습니다.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ notifications:
|
|||||||
body: Gösterge panelinin görünürlüğü güncellendi.
|
body: Gösterge panelinin görünürlüğü güncellendi.
|
||||||
clashModeChanged:
|
clashModeChanged:
|
||||||
title: Mod Değişimi
|
title: Mod Değişimi
|
||||||
body: "{mode} moduna geçildi."
|
body: '{mode} moduna geçildi.'
|
||||||
systemProxyToggled:
|
systemProxyToggled:
|
||||||
title: Sistem Vekil'i
|
title: Sistem Vekil'i
|
||||||
body: Sistem vekil'i durumu güncellendi.
|
body: Sistem vekil'i durumu güncellendi.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ notifications:
|
|||||||
body: Идарә панеленең күренеше яңартылды.
|
body: Идарә панеленең күренеше яңартылды.
|
||||||
clashModeChanged:
|
clashModeChanged:
|
||||||
title: Режим алыштыру
|
title: Режим алыштыру
|
||||||
body: "{mode} режимына күчтел."
|
body: '{mode} режимына күчтел.'
|
||||||
systemProxyToggled:
|
systemProxyToggled:
|
||||||
title: Системалы прокси
|
title: Системалы прокси
|
||||||
body: Системалы прокси хәле яңартылды.
|
body: Системалы прокси хәле яңартылды.
|
||||||
|
|||||||
+83
-86
@@ -1,148 +1,145 @@
|
|||||||
import eslintJS from "@eslint/js";
|
import eslintJS from '@eslint/js'
|
||||||
import eslintReact from "@eslint-react/eslint-plugin";
|
import eslintReact from '@eslint-react/eslint-plugin'
|
||||||
import { defineConfig } from "eslint/config";
|
import { defineConfig } from 'eslint/config'
|
||||||
import configPrettier from "eslint-config-prettier";
|
import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'
|
||||||
import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript";
|
import pluginImportX from 'eslint-plugin-import-x'
|
||||||
import pluginImportX from "eslint-plugin-import-x";
|
import pluginReactCompiler from 'eslint-plugin-react-compiler'
|
||||||
import pluginPrettier from "eslint-plugin-prettier";
|
import pluginReactHooks from 'eslint-plugin-react-hooks'
|
||||||
import pluginReactCompiler from "eslint-plugin-react-compiler";
|
import pluginReactRefresh from 'eslint-plugin-react-refresh'
|
||||||
import pluginReactHooks from "eslint-plugin-react-hooks";
|
import pluginUnusedImports from 'eslint-plugin-unused-imports'
|
||||||
import pluginReactRefresh from "eslint-plugin-react-refresh";
|
import globals from 'globals'
|
||||||
import pluginUnusedImports from "eslint-plugin-unused-imports";
|
import tseslint from 'typescript-eslint'
|
||||||
import globals from "globals";
|
|
||||||
import tseslint from "typescript-eslint";
|
|
||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
{
|
{
|
||||||
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
files: ['**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||||
|
|
||||||
plugins: {
|
plugins: {
|
||||||
js: eslintJS,
|
js: eslintJS,
|
||||||
// @ts-expect-error -- https://github.com/typescript-eslint/typescript-eslint/issues/11543
|
// @ts-expect-error -- https://github.com/typescript-eslint/typescript-eslint/issues/11543
|
||||||
"react-hooks": pluginReactHooks,
|
'react-hooks': pluginReactHooks,
|
||||||
"react-compiler": pluginReactCompiler,
|
'react-compiler': pluginReactCompiler,
|
||||||
// @ts-expect-error -- https://github.com/un-ts/eslint-plugin-import-x/issues/421
|
// @ts-expect-error -- https://github.com/un-ts/eslint-plugin-import-x/issues/421
|
||||||
"import-x": pluginImportX,
|
'import-x': pluginImportX,
|
||||||
"react-refresh": pluginReactRefresh,
|
'react-refresh': pluginReactRefresh,
|
||||||
"unused-imports": pluginUnusedImports,
|
'unused-imports': pluginUnusedImports,
|
||||||
prettier: pluginPrettier,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
extends: [
|
extends: [
|
||||||
eslintJS.configs.recommended,
|
eslintJS.configs.recommended,
|
||||||
tseslint.configs.recommended,
|
tseslint.configs.recommended,
|
||||||
eslintReact.configs["recommended-typescript"],
|
eslintReact.configs['recommended-typescript'],
|
||||||
configPrettier,
|
|
||||||
],
|
],
|
||||||
|
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
globals: globals.browser,
|
globals: globals.browser,
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
projectService: {
|
projectService: {
|
||||||
allowDefaultProject: ["src/polyfills/*.js"],
|
allowDefaultProject: [
|
||||||
|
'eslint.config.ts',
|
||||||
|
'src/polyfills/*.js',
|
||||||
|
'src-tauri/src/enhance/builtin/*.js',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
settings: {
|
settings: {
|
||||||
react: {
|
react: {
|
||||||
version: "detect",
|
version: 'detect',
|
||||||
},
|
},
|
||||||
"import-x/resolver-next": [
|
'import-x/resolver-next': [
|
||||||
createTypeScriptImportResolver({
|
createTypeScriptImportResolver({
|
||||||
project: "./tsconfig.json",
|
project: './tsconfig.json',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
rules: {
|
rules: {
|
||||||
// React
|
// React
|
||||||
"react-hooks/rules-of-hooks": "error",
|
'react-hooks/rules-of-hooks': 'error',
|
||||||
"react-hooks/exhaustive-deps": "error",
|
'react-hooks/exhaustive-deps': 'error',
|
||||||
"react-compiler/react-compiler": "error",
|
'react-compiler/react-compiler': 'error',
|
||||||
"react-refresh/only-export-components": [
|
'react-refresh/only-export-components': [
|
||||||
"warn",
|
'warn',
|
||||||
{ allowConstantExport: true },
|
{ allowConstantExport: true },
|
||||||
],
|
],
|
||||||
|
|
||||||
"@eslint-react/no-forward-ref": "off",
|
'@eslint-react/no-forward-ref': 'off',
|
||||||
|
|
||||||
// React performance and production quality rules
|
// React performance and production quality rules
|
||||||
"@eslint-react/no-array-index-key": "warn",
|
'@eslint-react/no-array-index-key': 'warn',
|
||||||
"@eslint-react/no-children-count": "error",
|
'@eslint-react/no-children-count': 'error',
|
||||||
"@eslint-react/no-children-for-each": "error",
|
'@eslint-react/no-children-for-each': 'error',
|
||||||
"@eslint-react/no-children-map": "error",
|
'@eslint-react/no-children-map': 'error',
|
||||||
"@eslint-react/no-children-only": "error",
|
'@eslint-react/no-children-only': 'error',
|
||||||
"@eslint-react/no-children-prop": "error",
|
'@eslint-react/no-children-prop': 'error',
|
||||||
"@eslint-react/no-children-to-array": "error",
|
'@eslint-react/no-children-to-array': 'error',
|
||||||
"@eslint-react/no-class-component": "error",
|
'@eslint-react/no-class-component': 'error',
|
||||||
"@eslint-react/no-clone-element": "error",
|
'@eslint-react/no-clone-element': 'error',
|
||||||
"@eslint-react/no-create-ref": "error",
|
'@eslint-react/no-create-ref': 'error',
|
||||||
"@eslint-react/no-default-props": "error",
|
'@eslint-react/no-default-props': 'error',
|
||||||
"@eslint-react/no-direct-mutation-state": "error",
|
'@eslint-react/no-direct-mutation-state': 'error',
|
||||||
"@eslint-react/no-implicit-key": "error",
|
'@eslint-react/no-implicit-key': 'error',
|
||||||
"@eslint-react/no-prop-types": "error",
|
'@eslint-react/no-prop-types': 'error',
|
||||||
"@eslint-react/no-set-state-in-component-did-mount": "error",
|
'@eslint-react/no-set-state-in-component-did-mount': 'error',
|
||||||
"@eslint-react/no-set-state-in-component-did-update": "error",
|
'@eslint-react/no-set-state-in-component-did-update': 'error',
|
||||||
"@eslint-react/no-set-state-in-component-will-update": "error",
|
'@eslint-react/no-set-state-in-component-will-update': 'error',
|
||||||
"@eslint-react/no-string-refs": "error",
|
'@eslint-react/no-string-refs': 'error',
|
||||||
"@eslint-react/no-unstable-context-value": "warn",
|
'@eslint-react/no-unstable-context-value': 'warn',
|
||||||
"@eslint-react/no-unstable-default-props": "warn",
|
'@eslint-react/no-unstable-default-props': 'warn',
|
||||||
"@eslint-react/no-unused-class-component-members": "error",
|
'@eslint-react/no-unused-class-component-members': 'error',
|
||||||
"@eslint-react/no-unused-state": "error",
|
'@eslint-react/no-unused-state': 'error',
|
||||||
"@eslint-react/no-useless-fragment": "warn",
|
'@eslint-react/no-useless-fragment': 'warn',
|
||||||
"@eslint-react/prefer-destructuring-assignment": "warn",
|
'@eslint-react/prefer-destructuring-assignment': 'warn',
|
||||||
|
|
||||||
// TypeScript
|
// TypeScript
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
|
||||||
// unused-imports 代替 no-unused-vars
|
// unused-imports 代替 no-unused-vars
|
||||||
"@typescript-eslint/no-unused-vars": "off",
|
'@typescript-eslint/no-unused-vars': 'off',
|
||||||
"unused-imports/no-unused-imports": "error",
|
'unused-imports/no-unused-imports': 'error',
|
||||||
"unused-imports/no-unused-vars": [
|
'unused-imports/no-unused-vars': [
|
||||||
"warn",
|
'warn',
|
||||||
{
|
{
|
||||||
vars: "all",
|
vars: 'all',
|
||||||
varsIgnorePattern: "^_",
|
varsIgnorePattern: '^_',
|
||||||
args: "after-used",
|
args: 'after-used',
|
||||||
argsIgnorePattern: "^_",
|
argsIgnorePattern: '^_',
|
||||||
caughtErrorsIgnorePattern: "^ignore",
|
caughtErrorsIgnorePattern: '^ignore',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
// Import
|
// Import
|
||||||
"import-x/no-unresolved": "error",
|
'import-x/no-unresolved': 'error',
|
||||||
"import-x/order": [
|
'import-x/order': [
|
||||||
"warn",
|
'warn',
|
||||||
{
|
{
|
||||||
groups: [
|
groups: [
|
||||||
"builtin",
|
'builtin',
|
||||||
"external",
|
'external',
|
||||||
"internal",
|
'internal',
|
||||||
"parent",
|
'parent',
|
||||||
"sibling",
|
'sibling',
|
||||||
"index",
|
'index',
|
||||||
],
|
],
|
||||||
"newlines-between": "always",
|
'newlines-between': 'always',
|
||||||
alphabetize: {
|
alphabetize: {
|
||||||
order: "asc",
|
order: 'asc',
|
||||||
caseInsensitive: true,
|
caseInsensitive: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
// 其他常见
|
// 其他常见
|
||||||
"prefer-const": "warn",
|
'prefer-const': 'warn',
|
||||||
"no-case-declarations": "error",
|
'no-case-declarations': 'error',
|
||||||
"no-fallthrough": "error",
|
'no-fallthrough': 'error',
|
||||||
"no-empty": ["warn", { allowEmptyCatch: true }],
|
'no-empty': ['warn', { allowEmptyCatch: true }],
|
||||||
|
|
||||||
// Prettier 格式化问题
|
|
||||||
"prettier/prettier": "warn",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ["scripts/**/*.{js,mjs,cjs}", "scripts-workflow/**/*.{js,mjs,cjs}"],
|
files: ['scripts/**/*.{js,mjs,cjs}'],
|
||||||
|
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
globals: {
|
globals: {
|
||||||
@@ -151,4 +148,4 @@ export default defineConfig([
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
])
|
||||||
|
|||||||
+1
-2
@@ -99,10 +99,8 @@
|
|||||||
"commander": "^14.0.3",
|
"commander": "^14.0.3",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"eslint": "^10.0.3",
|
"eslint": "^10.0.3",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
|
||||||
"eslint-import-resolver-typescript": "^4.4.4",
|
"eslint-import-resolver-typescript": "^4.4.4",
|
||||||
"eslint-plugin-import-x": "^4.16.1",
|
"eslint-plugin-import-x": "^4.16.1",
|
||||||
"eslint-plugin-prettier": "^5.5.5",
|
|
||||||
"eslint-plugin-react-compiler": "19.1.0-rc.2",
|
"eslint-plugin-react-compiler": "19.1.0-rc.2",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
@@ -115,6 +113,7 @@
|
|||||||
"lint-staged": "^16.3.2",
|
"lint-staged": "^16.3.2",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
|
"prettier-plugin-toml": "^2.0.6",
|
||||||
"sass": "^1.97.3",
|
"sass": "^1.97.3",
|
||||||
"tar": "^7.5.10",
|
"tar": "^7.5.10",
|
||||||
"terser": "^5.46.0",
|
"terser": "^5.46.0",
|
||||||
|
|||||||
Generated
+23
-63
@@ -198,18 +198,12 @@ importers:
|
|||||||
eslint:
|
eslint:
|
||||||
specifier: ^10.0.3
|
specifier: ^10.0.3
|
||||||
version: 10.0.3(jiti@2.6.1)
|
version: 10.0.3(jiti@2.6.1)
|
||||||
eslint-config-prettier:
|
|
||||||
specifier: ^10.1.8
|
|
||||||
version: 10.1.8(eslint@10.0.3(jiti@2.6.1))
|
|
||||||
eslint-import-resolver-typescript:
|
eslint-import-resolver-typescript:
|
||||||
specifier: ^4.4.4
|
specifier: ^4.4.4
|
||||||
version: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1)))(eslint@10.0.3(jiti@2.6.1))
|
version: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1)))(eslint@10.0.3(jiti@2.6.1))
|
||||||
eslint-plugin-import-x:
|
eslint-plugin-import-x:
|
||||||
specifier: ^4.16.1
|
specifier: ^4.16.1
|
||||||
version: 4.16.1(@typescript-eslint/utils@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))
|
version: 4.16.1(@typescript-eslint/utils@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))
|
||||||
eslint-plugin-prettier:
|
|
||||||
specifier: ^5.5.5
|
|
||||||
version: 5.5.5(eslint-config-prettier@10.1.8(eslint@10.0.3(jiti@2.6.1)))(eslint@10.0.3(jiti@2.6.1))(prettier@3.8.1)
|
|
||||||
eslint-plugin-react-compiler:
|
eslint-plugin-react-compiler:
|
||||||
specifier: 19.1.0-rc.2
|
specifier: 19.1.0-rc.2
|
||||||
version: 19.1.0-rc.2(eslint@10.0.3(jiti@2.6.1))
|
version: 19.1.0-rc.2(eslint@10.0.3(jiti@2.6.1))
|
||||||
@@ -246,6 +240,9 @@ importers:
|
|||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.8.1
|
specifier: ^3.8.1
|
||||||
version: 3.8.1
|
version: 3.8.1
|
||||||
|
prettier-plugin-toml:
|
||||||
|
specifier: ^2.0.6
|
||||||
|
version: 2.0.6(prettier@3.8.1)
|
||||||
sass:
|
sass:
|
||||||
specifier: ^1.97.3
|
specifier: ^1.97.3
|
||||||
version: 1.97.3
|
version: 1.97.3
|
||||||
@@ -1407,10 +1404,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==}
|
resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
|
|
||||||
'@pkgr/core@0.2.9':
|
|
||||||
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
|
|
||||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
|
||||||
|
|
||||||
'@popperjs/core@2.11.8':
|
'@popperjs/core@2.11.8':
|
||||||
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
||||||
|
|
||||||
@@ -1750,6 +1743,12 @@ packages:
|
|||||||
'@tanstack/virtual-core@3.13.21':
|
'@tanstack/virtual-core@3.13.21':
|
||||||
resolution: {integrity: sha512-ww+fmLHyCbPSf7JNbWZP3g7wl6SdNo3ah5Aiw+0e9FDErkVHLKprYUrwTm7dF646FtEkN/KkAKPYezxpmvOjxw==}
|
resolution: {integrity: sha512-ww+fmLHyCbPSf7JNbWZP3g7wl6SdNo3ah5Aiw+0e9FDErkVHLKprYUrwTm7dF646FtEkN/KkAKPYezxpmvOjxw==}
|
||||||
|
|
||||||
|
'@taplo/core@0.2.0':
|
||||||
|
resolution: {integrity: sha512-r8bl54Zj1In3QLkiW/ex694bVzpPJ9EhwqT9xkcUVODnVUGirdB1JTsmiIv0o1uwqZiwhi8xNnTOQBRQCpizrQ==}
|
||||||
|
|
||||||
|
'@taplo/lib@0.5.0':
|
||||||
|
resolution: {integrity: sha512-+xIqpQXJco3T+VGaTTwmhxLa51qpkQxCjRwezjFZgr+l21ExlywJFcDfTrNmL6lG6tqb0h8GyJKO3UPGPtSCWg==}
|
||||||
|
|
||||||
'@tauri-apps/api@2.10.1':
|
'@tauri-apps/api@2.10.1':
|
||||||
resolution: {integrity: sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==}
|
resolution: {integrity: sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==}
|
||||||
|
|
||||||
@@ -2460,12 +2459,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
eslint-config-prettier@10.1.8:
|
|
||||||
resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==}
|
|
||||||
hasBin: true
|
|
||||||
peerDependencies:
|
|
||||||
eslint: '>=7.0.0'
|
|
||||||
|
|
||||||
eslint-import-context@0.1.9:
|
eslint-import-context@0.1.9:
|
||||||
resolution: {integrity: sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==}
|
resolution: {integrity: sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==}
|
||||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||||
@@ -2501,20 +2494,6 @@ packages:
|
|||||||
eslint-import-resolver-node:
|
eslint-import-resolver-node:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
eslint-plugin-prettier@5.5.5:
|
|
||||||
resolution: {integrity: sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==}
|
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/eslint': '>=8.0.0'
|
|
||||||
eslint: '>=8.0.0'
|
|
||||||
eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0'
|
|
||||||
prettier: '>=3.0.0'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/eslint':
|
|
||||||
optional: true
|
|
||||||
eslint-config-prettier:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
eslint-plugin-react-compiler@19.1.0-rc.2:
|
eslint-plugin-react-compiler@19.1.0-rc.2:
|
||||||
resolution: {integrity: sha512-oKalwDGcD+RX9mf3NEO4zOoUMeLvjSvcbbEOpquzmzqEEM2MQdp7/FY/Hx9NzmUwFzH1W9SKTz5fihfMldpEYw==}
|
resolution: {integrity: sha512-oKalwDGcD+RX9mf3NEO4zOoUMeLvjSvcbbEOpquzmzqEEM2MQdp7/FY/Hx9NzmUwFzH1W9SKTz5fihfMldpEYw==}
|
||||||
engines: {node: ^14.17.0 || ^16.0.0 || >= 18.0.0}
|
engines: {node: ^14.17.0 || ^16.0.0 || >= 18.0.0}
|
||||||
@@ -2656,9 +2635,6 @@ packages:
|
|||||||
fast-deep-equal@3.1.3:
|
fast-deep-equal@3.1.3:
|
||||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||||
|
|
||||||
fast-diff@1.3.0:
|
|
||||||
resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
|
|
||||||
|
|
||||||
fast-json-stable-stringify@2.1.0:
|
fast-json-stable-stringify@2.1.0:
|
||||||
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
|
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
|
||||||
|
|
||||||
@@ -3373,9 +3349,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
prettier-linter-helpers@1.0.1:
|
prettier-plugin-toml@2.0.6:
|
||||||
resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==}
|
resolution: {integrity: sha512-12N/wBuHa9jd/KVy9pRP20NMKxQfQLMseQCt66lIbLaPLItvGUcSIryE1eZZMJ7loSws6Ig3M2Elc2EreNh76w==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
prettier: ^3.0.3
|
||||||
|
|
||||||
prettier@3.8.1:
|
prettier@3.8.1:
|
||||||
resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==}
|
resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==}
|
||||||
@@ -3652,10 +3630,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
synckit@0.11.12:
|
|
||||||
resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==}
|
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
|
||||||
|
|
||||||
systemjs@6.15.1:
|
systemjs@6.15.1:
|
||||||
resolution: {integrity: sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==}
|
resolution: {integrity: sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==}
|
||||||
|
|
||||||
@@ -5217,8 +5191,6 @@ snapshots:
|
|||||||
'@parcel/watcher-win32-x64': 2.5.6
|
'@parcel/watcher-win32-x64': 2.5.6
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@pkgr/core@0.2.9': {}
|
|
||||||
|
|
||||||
'@popperjs/core@2.11.8': {}
|
'@popperjs/core@2.11.8': {}
|
||||||
|
|
||||||
'@rolldown/binding-android-arm64@1.0.0-rc.9':
|
'@rolldown/binding-android-arm64@1.0.0-rc.9':
|
||||||
@@ -5441,6 +5413,12 @@ snapshots:
|
|||||||
|
|
||||||
'@tanstack/virtual-core@3.13.21': {}
|
'@tanstack/virtual-core@3.13.21': {}
|
||||||
|
|
||||||
|
'@taplo/core@0.2.0': {}
|
||||||
|
|
||||||
|
'@taplo/lib@0.5.0':
|
||||||
|
dependencies:
|
||||||
|
'@taplo/core': 0.2.0
|
||||||
|
|
||||||
'@tauri-apps/api@2.10.1': {}
|
'@tauri-apps/api@2.10.1': {}
|
||||||
|
|
||||||
'@tauri-apps/cli-darwin-arm64@2.10.1':
|
'@tauri-apps/cli-darwin-arm64@2.10.1':
|
||||||
@@ -6122,10 +6100,6 @@ snapshots:
|
|||||||
|
|
||||||
escape-string-regexp@4.0.0: {}
|
escape-string-regexp@4.0.0: {}
|
||||||
|
|
||||||
eslint-config-prettier@10.1.8(eslint@10.0.3(jiti@2.6.1)):
|
|
||||||
dependencies:
|
|
||||||
eslint: 10.0.3(jiti@2.6.1)
|
|
||||||
|
|
||||||
eslint-import-context@0.1.9(unrs-resolver@1.11.1):
|
eslint-import-context@0.1.9(unrs-resolver@1.11.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
get-tsconfig: 4.13.6
|
get-tsconfig: 4.13.6
|
||||||
@@ -6165,15 +6139,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-plugin-prettier@5.5.5(eslint-config-prettier@10.1.8(eslint@10.0.3(jiti@2.6.1)))(eslint@10.0.3(jiti@2.6.1))(prettier@3.8.1):
|
|
||||||
dependencies:
|
|
||||||
eslint: 10.0.3(jiti@2.6.1)
|
|
||||||
prettier: 3.8.1
|
|
||||||
prettier-linter-helpers: 1.0.1
|
|
||||||
synckit: 0.11.12
|
|
||||||
optionalDependencies:
|
|
||||||
eslint-config-prettier: 10.1.8(eslint@10.0.3(jiti@2.6.1))
|
|
||||||
|
|
||||||
eslint-plugin-react-compiler@19.1.0-rc.2(eslint@10.0.3(jiti@2.6.1)):
|
eslint-plugin-react-compiler@19.1.0-rc.2(eslint@10.0.3(jiti@2.6.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.29.0
|
||||||
@@ -6406,8 +6371,6 @@ snapshots:
|
|||||||
|
|
||||||
fast-deep-equal@3.1.3: {}
|
fast-deep-equal@3.1.3: {}
|
||||||
|
|
||||||
fast-diff@1.3.0: {}
|
|
||||||
|
|
||||||
fast-json-stable-stringify@2.1.0: {}
|
fast-json-stable-stringify@2.1.0: {}
|
||||||
|
|
||||||
fast-levenshtein@2.0.6: {}
|
fast-levenshtein@2.0.6: {}
|
||||||
@@ -7248,9 +7211,10 @@ snapshots:
|
|||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
prettier-linter-helpers@1.0.1:
|
prettier-plugin-toml@2.0.6(prettier@3.8.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-diff: 1.3.0
|
'@taplo/lib': 0.5.0
|
||||||
|
prettier: 3.8.1
|
||||||
|
|
||||||
prettier@3.8.1: {}
|
prettier@3.8.1: {}
|
||||||
|
|
||||||
@@ -7562,10 +7526,6 @@ snapshots:
|
|||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
use-sync-external-store: 1.6.0(react@19.2.4)
|
use-sync-external-store: 1.6.0(react@19.2.4)
|
||||||
|
|
||||||
synckit@0.11.12:
|
|
||||||
dependencies:
|
|
||||||
'@pkgr/core': 0.2.9
|
|
||||||
|
|
||||||
systemjs@6.15.1: {}
|
systemjs@6.15.1: {}
|
||||||
|
|
||||||
tar@7.5.10:
|
tar@7.5.10:
|
||||||
|
|||||||
+610
-619
File diff suppressed because it is too large
Load Diff
@@ -1,26 +1,26 @@
|
|||||||
import { exec } from "child_process";
|
import { exec } from 'child_process'
|
||||||
import fs from "fs/promises";
|
import fs from 'fs/promises'
|
||||||
import path from "path";
|
import path from 'path'
|
||||||
import { promisify } from "util";
|
import { promisify } from 'util'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 为Alpha版本重命名版本号
|
* 为Alpha版本重命名版本号
|
||||||
*/
|
*/
|
||||||
const execPromise = promisify(exec);
|
const execPromise = promisify(exec)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 标准输出HEAD hash
|
* 标准输出HEAD hash
|
||||||
*/
|
*/
|
||||||
async function getLatestCommitHash() {
|
async function getLatestCommitHash() {
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execPromise("git rev-parse HEAD");
|
const { stdout } = await execPromise('git rev-parse HEAD')
|
||||||
const commitHash = stdout.trim();
|
const commitHash = stdout.trim()
|
||||||
// 格式化,只截取前7位字符
|
// 格式化,只截取前7位字符
|
||||||
const formathash = commitHash.substring(0, 7);
|
const formathash = commitHash.substring(0, 7)
|
||||||
console.log(`Found the latest commit hash code: ${commitHash}`);
|
console.log(`Found the latest commit hash code: ${commitHash}`)
|
||||||
return formathash;
|
return formathash
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("pnpm run fix-alpha-version ERROR", error);
|
console.error('pnpm run fix-alpha-version ERROR', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,38 +30,35 @@ async function getLatestCommitHash() {
|
|||||||
*/
|
*/
|
||||||
async function updatePackageVersion(newVersion) {
|
async function updatePackageVersion(newVersion) {
|
||||||
// 获取内容根目录
|
// 获取内容根目录
|
||||||
const _dirname = process.cwd();
|
const _dirname = process.cwd()
|
||||||
const packageJsonPath = path.join(_dirname, "package.json");
|
const packageJsonPath = path.join(_dirname, 'package.json')
|
||||||
try {
|
try {
|
||||||
// 读取文件
|
// 读取文件
|
||||||
const data = await fs.readFile(packageJsonPath, "utf8");
|
const data = await fs.readFile(packageJsonPath, 'utf8')
|
||||||
const packageJson = JSON.parse(data);
|
const packageJson = JSON.parse(data)
|
||||||
// 获取键值替换
|
// 获取键值替换
|
||||||
let result = packageJson.version.replace("alpha", newVersion);
|
let result = packageJson.version.replace('alpha', newVersion)
|
||||||
// 检查当前版本号是否已经包含了 alpha- 后缀
|
// 检查当前版本号是否已经包含了 alpha- 后缀
|
||||||
if (!packageJson.version.includes(`alpha-`)) {
|
if (!packageJson.version.includes(`alpha-`)) {
|
||||||
// 如果只有 alpha 而没有 alpha-,则替换为 alpha-newVersion
|
// 如果只有 alpha 而没有 alpha-,则替换为 alpha-newVersion
|
||||||
result = packageJson.version.replace("alpha", `alpha-${newVersion}`);
|
result = packageJson.version.replace('alpha', `alpha-${newVersion}`)
|
||||||
} else {
|
} else {
|
||||||
// 如果已经是 alpha-xxx 格式,则更新 xxx 部分
|
// 如果已经是 alpha-xxx 格式,则更新 xxx 部分
|
||||||
result = packageJson.version.replace(
|
result = packageJson.version.replace(/alpha-[^-]*/, `alpha-${newVersion}`)
|
||||||
/alpha-[^-]*/,
|
|
||||||
`alpha-${newVersion}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
console.log("[INFO]: Current version is: ", result);
|
console.log('[INFO]: Current version is: ', result)
|
||||||
packageJson.version = result;
|
packageJson.version = result
|
||||||
// 写入版本号
|
// 写入版本号
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
packageJsonPath,
|
packageJsonPath,
|
||||||
JSON.stringify(packageJson, null, 2),
|
JSON.stringify(packageJson, null, 2),
|
||||||
"utf8",
|
'utf8',
|
||||||
);
|
)
|
||||||
console.log(`[INFO]: Alpha version update to: ${newVersion}`);
|
console.log(`[INFO]: Alpha version update to: ${newVersion}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("pnpm run fix-alpha-version ERROR", error);
|
console.error('pnpm run fix-alpha-version ERROR', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newVersion = await getLatestCommitHash();
|
const newVersion = await getLatestCommitHash()
|
||||||
updatePackageVersion(newVersion).catch(console.error);
|
updatePackageVersion(newVersion).catch(console.error)
|
||||||
|
|||||||
@@ -1,98 +1,98 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import { promises as fs } from "node:fs";
|
import { promises as fs } from 'node:fs'
|
||||||
import path from "node:path";
|
import path from 'node:path'
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename)
|
||||||
const ROOT_DIR = path.resolve(__dirname, "..");
|
const ROOT_DIR = path.resolve(__dirname, '..')
|
||||||
const LOCALE_DIR = path.resolve(ROOT_DIR, "src/locales/en");
|
const LOCALE_DIR = path.resolve(ROOT_DIR, 'src/locales/en')
|
||||||
const KEY_OUTPUT = path.resolve(ROOT_DIR, "src/types/generated/i18n-keys.ts");
|
const KEY_OUTPUT = path.resolve(ROOT_DIR, 'src/types/generated/i18n-keys.ts')
|
||||||
const RESOURCE_OUTPUT = path.resolve(
|
const RESOURCE_OUTPUT = path.resolve(
|
||||||
ROOT_DIR,
|
ROOT_DIR,
|
||||||
"src/types/generated/i18n-resources.ts",
|
'src/types/generated/i18n-resources.ts',
|
||||||
);
|
)
|
||||||
|
|
||||||
const isPlainObject = (value) =>
|
const isPlainObject = (value) =>
|
||||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||||
|
|
||||||
const flattenKeys = (data, prefix = "") => {
|
const flattenKeys = (data, prefix = '') => {
|
||||||
const keys = [];
|
const keys = []
|
||||||
for (const [key, value] of Object.entries(data)) {
|
for (const [key, value] of Object.entries(data)) {
|
||||||
const nextPrefix = prefix ? `${prefix}.${key}` : key;
|
const nextPrefix = prefix ? `${prefix}.${key}` : key
|
||||||
if (isPlainObject(value)) {
|
if (isPlainObject(value)) {
|
||||||
keys.push(...flattenKeys(value, nextPrefix));
|
keys.push(...flattenKeys(value, nextPrefix))
|
||||||
} else {
|
} else {
|
||||||
keys.push(nextPrefix);
|
keys.push(nextPrefix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return keys;
|
return keys
|
||||||
};
|
}
|
||||||
|
|
||||||
const buildType = (data, indent = 0) => {
|
const buildType = (data, indent = 0) => {
|
||||||
if (!isPlainObject(data)) {
|
if (!isPlainObject(data)) {
|
||||||
return "string";
|
return 'string'
|
||||||
}
|
}
|
||||||
|
|
||||||
const entries = Object.entries(data).sort(([a], [b]) => a.localeCompare(b));
|
const entries = Object.entries(data).sort(([a], [b]) => a.localeCompare(b))
|
||||||
const pad = " ".repeat(indent);
|
const pad = ' '.repeat(indent)
|
||||||
const inner = entries
|
const inner = entries
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
const typeStr = buildType(value, indent + 2);
|
const typeStr = buildType(value, indent + 2)
|
||||||
return `${" ".repeat(indent + 2)}${JSON.stringify(key)}: ${typeStr};`;
|
return `${' '.repeat(indent + 2)}${JSON.stringify(key)}: ${typeStr};`
|
||||||
})
|
})
|
||||||
.join("\n");
|
.join('\n')
|
||||||
|
|
||||||
return entries.length
|
return entries.length
|
||||||
? `{
|
? `{
|
||||||
${inner}
|
${inner}
|
||||||
${pad}}`
|
${pad}}`
|
||||||
: "{}";
|
: '{}'
|
||||||
};
|
}
|
||||||
|
|
||||||
const loadNamespaceJson = async () => {
|
const loadNamespaceJson = async () => {
|
||||||
const dirents = await fs.readdir(LOCALE_DIR, { withFileTypes: true });
|
const dirents = await fs.readdir(LOCALE_DIR, { withFileTypes: true })
|
||||||
const namespaces = [];
|
const namespaces = []
|
||||||
for (const dirent of dirents) {
|
for (const dirent of dirents) {
|
||||||
if (!dirent.isFile() || !dirent.name.endsWith(".json")) continue;
|
if (!dirent.isFile() || !dirent.name.endsWith('.json')) continue
|
||||||
const name = dirent.name.replace(/\.json$/, "");
|
const name = dirent.name.replace(/\.json$/, '')
|
||||||
const filePath = path.join(LOCALE_DIR, dirent.name);
|
const filePath = path.join(LOCALE_DIR, dirent.name)
|
||||||
const raw = await fs.readFile(filePath, "utf8");
|
const raw = await fs.readFile(filePath, 'utf8')
|
||||||
const json = JSON.parse(raw);
|
const json = JSON.parse(raw)
|
||||||
namespaces.push({ name, json });
|
namespaces.push({ name, json })
|
||||||
}
|
}
|
||||||
namespaces.sort((a, b) => a.name.localeCompare(b.name));
|
namespaces.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
return namespaces;
|
return namespaces
|
||||||
};
|
}
|
||||||
|
|
||||||
const buildKeysFile = (keys) => {
|
const buildKeysFile = (keys) => {
|
||||||
const arrayLiteral = keys.map((key) => ` "${key}"`).join(",\n");
|
const arrayLiteral = keys.map((key) => ` "${key}"`).join(',\n')
|
||||||
return `// This file is auto-generated by scripts/generate-i18n-keys.mjs\n// Do not edit this file manually.\n\nexport const translationKeys = [\n${arrayLiteral}\n] as const;\n\nexport type TranslationKey = typeof translationKeys[number];\n`;
|
return `// This file is auto-generated by scripts/generate-i18n-keys.mjs\n// Do not edit this file manually.\n\nexport const translationKeys = [\n${arrayLiteral}\n] as const;\n\nexport type TranslationKey = typeof translationKeys[number];\n`
|
||||||
};
|
}
|
||||||
|
|
||||||
const buildResourcesFile = (namespaces) => {
|
const buildResourcesFile = (namespaces) => {
|
||||||
const namespaceEntries = namespaces
|
const namespaceEntries = namespaces
|
||||||
.map(({ name, json }) => {
|
.map(({ name, json }) => {
|
||||||
const typeStr = buildType(json, 4);
|
const typeStr = buildType(json, 4)
|
||||||
return ` ${JSON.stringify(name)}: ${typeStr};`;
|
return ` ${JSON.stringify(name)}: ${typeStr};`
|
||||||
})
|
})
|
||||||
.join("\n");
|
.join('\n')
|
||||||
|
|
||||||
return `// This file is auto-generated by scripts/generate-i18n-keys.mjs\n// Do not edit this file manually.\n\nexport interface TranslationResources {\n translation: {\n${namespaceEntries}\n };\n}\n`;
|
return `// This file is auto-generated by scripts/generate-i18n-keys.mjs\n// Do not edit this file manually.\n\nexport interface TranslationResources {\n translation: {\n${namespaceEntries}\n };\n}\n`
|
||||||
};
|
}
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
const namespaces = await loadNamespaceJson();
|
const namespaces = await loadNamespaceJson()
|
||||||
const keys = namespaces.flatMap(({ name, json }) => flattenKeys(json, name));
|
const keys = namespaces.flatMap(({ name, json }) => flattenKeys(json, name))
|
||||||
const keysContent = buildKeysFile(keys);
|
const keysContent = buildKeysFile(keys)
|
||||||
const resourcesContent = buildResourcesFile(namespaces);
|
const resourcesContent = buildResourcesFile(namespaces)
|
||||||
await fs.mkdir(path.dirname(KEY_OUTPUT), { recursive: true });
|
await fs.mkdir(path.dirname(KEY_OUTPUT), { recursive: true })
|
||||||
await fs.writeFile(KEY_OUTPUT, keysContent, "utf8");
|
await fs.writeFile(KEY_OUTPUT, keysContent, 'utf8')
|
||||||
await fs.writeFile(RESOURCE_OUTPUT, resourcesContent, "utf8");
|
await fs.writeFile(RESOURCE_OUTPUT, resourcesContent, 'utf8')
|
||||||
console.log(`Generated ${keys.length} translation keys.`);
|
console.log(`Generated ${keys.length} translation keys.`)
|
||||||
};
|
}
|
||||||
|
|
||||||
main().catch((error) => {
|
main().catch((error) => {
|
||||||
console.error("Failed to generate i18n metadata:", error);
|
console.error('Failed to generate i18n metadata:', error)
|
||||||
process.exitCode = 1;
|
process.exitCode = 1
|
||||||
});
|
})
|
||||||
|
|||||||
@@ -1,104 +1,104 @@
|
|||||||
import fs from "fs";
|
import fs from 'fs'
|
||||||
import fsp from "fs/promises";
|
import fsp from 'fs/promises'
|
||||||
import { createRequire } from "module";
|
import { createRequire } from 'module'
|
||||||
import path from "path";
|
import path from 'path'
|
||||||
|
|
||||||
import { context, getOctokit } from "@actions/github";
|
import { context, getOctokit } from '@actions/github'
|
||||||
import AdmZip from "adm-zip";
|
import AdmZip from 'adm-zip'
|
||||||
|
|
||||||
const target = process.argv.slice(2)[0];
|
const target = process.argv.slice(2)[0]
|
||||||
const alpha = process.argv.slice(2)[1];
|
const alpha = process.argv.slice(2)[1]
|
||||||
|
|
||||||
const ARCH_MAP = {
|
const ARCH_MAP = {
|
||||||
"x86_64-pc-windows-msvc": "x64",
|
'x86_64-pc-windows-msvc': 'x64',
|
||||||
"i686-pc-windows-msvc": "x86",
|
'i686-pc-windows-msvc': 'x86',
|
||||||
"aarch64-pc-windows-msvc": "arm64",
|
'aarch64-pc-windows-msvc': 'arm64',
|
||||||
};
|
}
|
||||||
|
|
||||||
const PROCESS_MAP = {
|
const PROCESS_MAP = {
|
||||||
x64: "x64",
|
x64: 'x64',
|
||||||
ia32: "x86",
|
ia32: 'x86',
|
||||||
arm64: "arm64",
|
arm64: 'arm64',
|
||||||
};
|
}
|
||||||
const arch = target ? ARCH_MAP[target] : PROCESS_MAP[process.arch];
|
const arch = target ? ARCH_MAP[target] : PROCESS_MAP[process.arch]
|
||||||
/// Script for ci
|
/// Script for ci
|
||||||
/// 打包绿色版/便携版 (only Windows)
|
/// 打包绿色版/便携版 (only Windows)
|
||||||
async function resolvePortable() {
|
async function resolvePortable() {
|
||||||
if (process.platform !== "win32") return;
|
if (process.platform !== 'win32') return
|
||||||
|
|
||||||
const releaseDir = target
|
const releaseDir = target
|
||||||
? `./src-tauri/target/${target}/release`
|
? `./src-tauri/target/${target}/release`
|
||||||
: `./src-tauri/target/release`;
|
: `./src-tauri/target/release`
|
||||||
|
|
||||||
const configDir = path.join(releaseDir, ".config");
|
const configDir = path.join(releaseDir, '.config')
|
||||||
|
|
||||||
if (!fs.existsSync(releaseDir)) {
|
if (!fs.existsSync(releaseDir)) {
|
||||||
throw new Error("could not found the release dir");
|
throw new Error('could not found the release dir')
|
||||||
}
|
}
|
||||||
|
|
||||||
await fsp.mkdir(configDir, { recursive: true });
|
await fsp.mkdir(configDir, { recursive: true })
|
||||||
if (!fs.existsSync(path.join(configDir, "PORTABLE"))) {
|
if (!fs.existsSync(path.join(configDir, 'PORTABLE'))) {
|
||||||
await fsp.writeFile(path.join(configDir, "PORTABLE"), "");
|
await fsp.writeFile(path.join(configDir, 'PORTABLE'), '')
|
||||||
}
|
}
|
||||||
|
|
||||||
const zip = new AdmZip();
|
const zip = new AdmZip()
|
||||||
|
|
||||||
zip.addLocalFile(path.join(releaseDir, "Clash Verge.exe"));
|
zip.addLocalFile(path.join(releaseDir, 'Clash Verge.exe'))
|
||||||
zip.addLocalFile(path.join(releaseDir, "verge-mihomo.exe"));
|
zip.addLocalFile(path.join(releaseDir, 'verge-mihomo.exe'))
|
||||||
zip.addLocalFile(path.join(releaseDir, "verge-mihomo-alpha.exe"));
|
zip.addLocalFile(path.join(releaseDir, 'verge-mihomo-alpha.exe'))
|
||||||
zip.addLocalFolder(path.join(releaseDir, "resources"), "resources");
|
zip.addLocalFolder(path.join(releaseDir, 'resources'), 'resources')
|
||||||
zip.addLocalFolder(
|
zip.addLocalFolder(
|
||||||
path.join(
|
path.join(
|
||||||
releaseDir,
|
releaseDir,
|
||||||
`Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${arch}`,
|
`Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${arch}`,
|
||||||
),
|
),
|
||||||
`Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${arch}`,
|
`Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${arch}`,
|
||||||
);
|
)
|
||||||
zip.addLocalFolder(configDir, ".config");
|
zip.addLocalFolder(configDir, '.config')
|
||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url)
|
||||||
const packageJson = require("../package.json");
|
const packageJson = require('../package.json')
|
||||||
const { version } = packageJson;
|
const { version } = packageJson
|
||||||
|
|
||||||
const zipFile = `Clash.Verge_${version}_${arch}_fixed_webview2_portable.zip`;
|
const zipFile = `Clash.Verge_${version}_${arch}_fixed_webview2_portable.zip`
|
||||||
zip.writeZip(zipFile);
|
zip.writeZip(zipFile)
|
||||||
|
|
||||||
console.log("[INFO]: create portable zip successfully");
|
console.log('[INFO]: create portable zip successfully')
|
||||||
|
|
||||||
// push release assets
|
// push release assets
|
||||||
if (process.env.GITHUB_TOKEN === undefined) {
|
if (process.env.GITHUB_TOKEN === undefined) {
|
||||||
throw new Error("GITHUB_TOKEN is required");
|
throw new Error('GITHUB_TOKEN is required')
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = { owner: context.repo.owner, repo: context.repo.repo };
|
const options = { owner: context.repo.owner, repo: context.repo.repo }
|
||||||
const github = getOctokit(process.env.GITHUB_TOKEN);
|
const github = getOctokit(process.env.GITHUB_TOKEN)
|
||||||
const tag = alpha ? "alpha" : process.env.TAG_NAME || `v${version}`;
|
const tag = alpha ? 'alpha' : process.env.TAG_NAME || `v${version}`
|
||||||
console.log("[INFO]: upload to ", tag);
|
console.log('[INFO]: upload to ', tag)
|
||||||
|
|
||||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||||
...options,
|
...options,
|
||||||
tag,
|
tag,
|
||||||
});
|
})
|
||||||
|
|
||||||
const assets = release.assets.filter((x) => {
|
const assets = release.assets.filter((x) => {
|
||||||
return x.name === zipFile;
|
return x.name === zipFile
|
||||||
});
|
})
|
||||||
if (assets.length > 0) {
|
if (assets.length > 0) {
|
||||||
const id = assets[0].id;
|
const id = assets[0].id
|
||||||
await github.rest.repos.deleteReleaseAsset({
|
await github.rest.repos.deleteReleaseAsset({
|
||||||
...options,
|
...options,
|
||||||
asset_id: id,
|
asset_id: id,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(release.name);
|
console.log(release.name)
|
||||||
|
|
||||||
await github.rest.repos.uploadReleaseAsset({
|
await github.rest.repos.uploadReleaseAsset({
|
||||||
...options,
|
...options,
|
||||||
release_id: release.id,
|
release_id: release.id,
|
||||||
name: zipFile,
|
name: zipFile,
|
||||||
data: zip.toBuffer(),
|
data: zip.toBuffer(),
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
resolvePortable().catch(console.error);
|
resolvePortable().catch(console.error)
|
||||||
|
|||||||
+33
-33
@@ -1,53 +1,53 @@
|
|||||||
import fs from "fs";
|
import fs from 'fs'
|
||||||
import fsp from "fs/promises";
|
import fsp from 'fs/promises'
|
||||||
import { createRequire } from "module";
|
import { createRequire } from 'module'
|
||||||
import path from "path";
|
import path from 'path'
|
||||||
|
|
||||||
import AdmZip from "adm-zip";
|
import AdmZip from 'adm-zip'
|
||||||
|
|
||||||
const target = process.argv.slice(2)[0];
|
const target = process.argv.slice(2)[0]
|
||||||
const ARCH_MAP = {
|
const ARCH_MAP = {
|
||||||
"x86_64-pc-windows-msvc": "x64",
|
'x86_64-pc-windows-msvc': 'x64',
|
||||||
"aarch64-pc-windows-msvc": "arm64",
|
'aarch64-pc-windows-msvc': 'arm64',
|
||||||
};
|
}
|
||||||
|
|
||||||
const PROCESS_MAP = {
|
const PROCESS_MAP = {
|
||||||
x64: "x64",
|
x64: 'x64',
|
||||||
arm64: "arm64",
|
arm64: 'arm64',
|
||||||
};
|
}
|
||||||
const arch = target ? ARCH_MAP[target] : PROCESS_MAP[process.arch];
|
const arch = target ? ARCH_MAP[target] : PROCESS_MAP[process.arch]
|
||||||
/// Script for ci
|
/// Script for ci
|
||||||
/// 打包绿色版/便携版 (only Windows)
|
/// 打包绿色版/便携版 (only Windows)
|
||||||
async function resolvePortable() {
|
async function resolvePortable() {
|
||||||
if (process.platform !== "win32") return;
|
if (process.platform !== 'win32') return
|
||||||
|
|
||||||
const releaseDir = target
|
const releaseDir = target
|
||||||
? `./src-tauri/target/${target}/release`
|
? `./src-tauri/target/${target}/release`
|
||||||
: `./src-tauri/target/release`;
|
: `./src-tauri/target/release`
|
||||||
const configDir = path.join(releaseDir, ".config");
|
const configDir = path.join(releaseDir, '.config')
|
||||||
|
|
||||||
if (!fs.existsSync(releaseDir)) {
|
if (!fs.existsSync(releaseDir)) {
|
||||||
throw new Error("could not found the release dir");
|
throw new Error('could not found the release dir')
|
||||||
}
|
}
|
||||||
|
|
||||||
await fsp.mkdir(configDir, { recursive: true });
|
await fsp.mkdir(configDir, { recursive: true })
|
||||||
if (!fs.existsSync(path.join(configDir, "PORTABLE"))) {
|
if (!fs.existsSync(path.join(configDir, 'PORTABLE'))) {
|
||||||
await fsp.writeFile(path.join(configDir, "PORTABLE"), "");
|
await fsp.writeFile(path.join(configDir, 'PORTABLE'), '')
|
||||||
}
|
}
|
||||||
const zip = new AdmZip();
|
const zip = new AdmZip()
|
||||||
|
|
||||||
zip.addLocalFile(path.join(releaseDir, "clash-verge.exe"));
|
zip.addLocalFile(path.join(releaseDir, 'clash-verge.exe'))
|
||||||
zip.addLocalFile(path.join(releaseDir, "verge-mihomo.exe"));
|
zip.addLocalFile(path.join(releaseDir, 'verge-mihomo.exe'))
|
||||||
zip.addLocalFile(path.join(releaseDir, "verge-mihomo-alpha.exe"));
|
zip.addLocalFile(path.join(releaseDir, 'verge-mihomo-alpha.exe'))
|
||||||
zip.addLocalFolder(path.join(releaseDir, "resources"), "resources");
|
zip.addLocalFolder(path.join(releaseDir, 'resources'), 'resources')
|
||||||
zip.addLocalFolder(configDir, ".config");
|
zip.addLocalFolder(configDir, '.config')
|
||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url)
|
||||||
const packageJson = require("../package.json");
|
const packageJson = require('../package.json')
|
||||||
const { version } = packageJson;
|
const { version } = packageJson
|
||||||
const zipFile = `Clash.Verge_${version}_${arch}_portable.zip`;
|
const zipFile = `Clash.Verge_${version}_${arch}_portable.zip`
|
||||||
zip.writeZip(zipFile);
|
zip.writeZip(zipFile)
|
||||||
console.log("[INFO]: create portable zip successfully");
|
console.log('[INFO]: create portable zip successfully')
|
||||||
}
|
}
|
||||||
|
|
||||||
resolvePortable().catch(console.error);
|
resolvePortable().catch(console.error)
|
||||||
|
|||||||
+353
-357
File diff suppressed because it is too large
Load Diff
+33
-33
@@ -1,66 +1,66 @@
|
|||||||
// scripts/publish-version.mjs
|
// scripts/publish-version.mjs
|
||||||
import { spawn } from "child_process";
|
import { spawn } from 'child_process'
|
||||||
import { existsSync } from "fs";
|
import { existsSync } from 'fs'
|
||||||
import path from "path";
|
import path from 'path'
|
||||||
|
|
||||||
const rootDir = process.cwd();
|
const rootDir = process.cwd()
|
||||||
const scriptPath = path.join(rootDir, "scripts", "release-version.mjs");
|
const scriptPath = path.join(rootDir, 'scripts', 'release-version.mjs')
|
||||||
|
|
||||||
if (!existsSync(scriptPath)) {
|
if (!existsSync(scriptPath)) {
|
||||||
console.error("release-version.mjs not found!");
|
console.error('release-version.mjs not found!')
|
||||||
process.exit(1);
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const versionArg = process.argv[2];
|
const versionArg = process.argv[2]
|
||||||
if (!versionArg) {
|
if (!versionArg) {
|
||||||
console.error("Usage: pnpm publish-version <version>");
|
console.error('Usage: pnpm publish-version <version>')
|
||||||
process.exit(1);
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. 调用 release-version.mjs
|
// 1. 调用 release-version.mjs
|
||||||
const runRelease = () =>
|
const runRelease = () =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
const child = spawn("node", [scriptPath, versionArg], { stdio: "inherit" });
|
const child = spawn('node', [scriptPath, versionArg], { stdio: 'inherit' })
|
||||||
child.on("exit", (code) => {
|
child.on('exit', (code) => {
|
||||||
if (code === 0) resolve();
|
if (code === 0) resolve()
|
||||||
else reject(new Error("release-version failed"));
|
else reject(new Error('release-version failed'))
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|
||||||
// 2. 判断是否需要打 tag
|
// 2. 判断是否需要打 tag
|
||||||
function isSemver(version) {
|
function isSemver(version) {
|
||||||
return /^v?\d+\.\d+\.\d+(-[0-9A-Za-z-.]+)?$/.test(version);
|
return /^v?\d+\.\d+\.\d+(-[0-9A-Za-z-.]+)?$/.test(version)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
await runRelease();
|
await runRelease()
|
||||||
|
|
||||||
let tag = null;
|
let tag = null
|
||||||
if (versionArg === "alpha") {
|
if (versionArg === 'alpha') {
|
||||||
// 读取 package.json 里的主版本
|
// 读取 package.json 里的主版本
|
||||||
const pkg = await import(path.join(rootDir, "package.json"), {
|
const pkg = await import(path.join(rootDir, 'package.json'), {
|
||||||
assert: { type: "json" },
|
assert: { type: 'json' },
|
||||||
});
|
})
|
||||||
tag = `v${pkg.default.version}-alpha`;
|
tag = `v${pkg.default.version}-alpha`
|
||||||
} else if (isSemver(versionArg)) {
|
} else if (isSemver(versionArg)) {
|
||||||
// 1.2.3 或 v1.2.3
|
// 1.2.3 或 v1.2.3
|
||||||
tag = versionArg.startsWith("v") ? versionArg : `v${versionArg}`;
|
tag = versionArg.startsWith('v') ? versionArg : `v${versionArg}`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag) {
|
if (tag) {
|
||||||
// 打 tag 并推送
|
// 打 tag 并推送
|
||||||
const { execSync } = await import("child_process");
|
const { execSync } = await import('child_process')
|
||||||
try {
|
try {
|
||||||
execSync(`git tag ${tag}`, { stdio: "inherit" });
|
execSync(`git tag ${tag}`, { stdio: 'inherit' })
|
||||||
execSync(`git push origin ${tag}`, { stdio: "inherit" });
|
execSync(`git push origin ${tag}`, { stdio: 'inherit' })
|
||||||
console.log(`[INFO]: Git tag ${tag} created and pushed.`);
|
console.log(`[INFO]: Git tag ${tag} created and pushed.`)
|
||||||
} catch {
|
} catch {
|
||||||
console.error(`[ERROR]: Failed to create or push git tag: ${tag}`);
|
console.error(`[ERROR]: Failed to create or push git tag: ${tag}`)
|
||||||
process.exit(1);
|
process.exit(1)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("[INFO]: No git tag created for this version.");
|
console.log('[INFO]: No git tag created for this version.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
run();
|
run()
|
||||||
|
|||||||
+115
-115
@@ -29,11 +29,11 @@
|
|||||||
* Errors are logged and the process exits with code 1 on failure.
|
* Errors are logged and the process exits with code 1 on failure.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { execSync } from "child_process";
|
import { execSync } from 'child_process'
|
||||||
import fs from "fs/promises";
|
import fs from 'fs/promises'
|
||||||
import path from "path";
|
import path from 'path'
|
||||||
|
|
||||||
import { program } from "commander";
|
import { program } from 'commander'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前 git 短 commit hash
|
* 获取当前 git 短 commit hash
|
||||||
@@ -41,10 +41,10 @@ import { program } from "commander";
|
|||||||
*/
|
*/
|
||||||
function getGitShortCommit() {
|
function getGitShortCommit() {
|
||||||
try {
|
try {
|
||||||
return execSync("git rev-parse --short HEAD").toString().trim();
|
return execSync('git rev-parse --short HEAD').toString().trim()
|
||||||
} catch {
|
} catch {
|
||||||
console.warn("[WARN]: Failed to get git short commit, fallback to 'nogit'");
|
console.warn("[WARN]: Failed to get git short commit, fallback to 'nogit'")
|
||||||
return "nogit";
|
return 'nogit'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,21 +55,21 @@ function getGitShortCommit() {
|
|||||||
function getLatestTauriCommit() {
|
function getLatestTauriCommit() {
|
||||||
try {
|
try {
|
||||||
const fullHash = execSync(
|
const fullHash = execSync(
|
||||||
"bash ./scripts-workflow/get_latest_tauri_commit.bash",
|
'bash ./scripts-workflow/get_latest_tauri_commit.bash',
|
||||||
)
|
)
|
||||||
.toString()
|
.toString()
|
||||||
.trim();
|
.trim()
|
||||||
const shortHash = execSync(`git rev-parse --short ${fullHash}`)
|
const shortHash = execSync(`git rev-parse --short ${fullHash}`)
|
||||||
.toString()
|
.toString()
|
||||||
.trim();
|
.trim()
|
||||||
console.log(`[INFO]: Latest Tauri-related commit: ${shortHash}`);
|
console.log(`[INFO]: Latest Tauri-related commit: ${shortHash}`)
|
||||||
return shortHash;
|
return shortHash
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"[WARN]: Failed to get latest Tauri commit, fallback to current git short commit",
|
'[WARN]: Failed to get latest Tauri commit, fallback to current git short commit',
|
||||||
);
|
)
|
||||||
console.warn(`[WARN]: Error details: ${error.message}`);
|
console.warn(`[WARN]: Error details: ${error.message}`)
|
||||||
return getGitShortCommit();
|
return getGitShortCommit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,25 +81,25 @@ function getLatestTauriCommit() {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function generateShortTimestamp(withCommit = false, useTauriCommit = false) {
|
function generateShortTimestamp(withCommit = false, useTauriCommit = false) {
|
||||||
const now = new Date();
|
const now = new Date()
|
||||||
|
|
||||||
const formatter = new Intl.DateTimeFormat("en-CA", {
|
const formatter = new Intl.DateTimeFormat('en-CA', {
|
||||||
timeZone: "Asia/Shanghai",
|
timeZone: 'Asia/Shanghai',
|
||||||
month: "2-digit",
|
month: '2-digit',
|
||||||
day: "2-digit",
|
day: '2-digit',
|
||||||
});
|
})
|
||||||
|
|
||||||
const parts = formatter.formatToParts(now);
|
const parts = formatter.formatToParts(now)
|
||||||
const month = parts.find((part) => part.type === "month").value;
|
const month = parts.find((part) => part.type === 'month').value
|
||||||
const day = parts.find((part) => part.type === "day").value;
|
const day = parts.find((part) => part.type === 'day').value
|
||||||
|
|
||||||
if (withCommit) {
|
if (withCommit) {
|
||||||
const gitShort = useTauriCommit
|
const gitShort = useTauriCommit
|
||||||
? getLatestTauriCommit()
|
? getLatestTauriCommit()
|
||||||
: getGitShortCommit();
|
: getGitShortCommit()
|
||||||
return `${month}${day}.${gitShort}`;
|
return `${month}${day}.${gitShort}`
|
||||||
}
|
}
|
||||||
return `${month}${day}`;
|
return `${month}${day}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -110,7 +110,7 @@ function generateShortTimestamp(withCommit = false, useTauriCommit = false) {
|
|||||||
function isValidVersion(version) {
|
function isValidVersion(version) {
|
||||||
return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?$/i.test(
|
return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?$/i.test(
|
||||||
version,
|
version,
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -119,7 +119,7 @@ function isValidVersion(version) {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function normalizeVersion(version) {
|
function normalizeVersion(version) {
|
||||||
return version.startsWith("v") ? version : `v${version}`;
|
return version.startsWith('v') ? version : `v${version}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -128,9 +128,9 @@ function normalizeVersion(version) {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function getBaseVersion(version) {
|
function getBaseVersion(version) {
|
||||||
let base = version.replace(/-(alpha|beta|rc)(\.\d+)?/i, "");
|
let base = version.replace(/-(alpha|beta|rc)(\.\d+)?/i, '')
|
||||||
base = base.replace(/\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*/g, "");
|
base = base.replace(/\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*/g, '')
|
||||||
return base;
|
return base
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -138,30 +138,30 @@ function getBaseVersion(version) {
|
|||||||
* @param {string} newVersion
|
* @param {string} newVersion
|
||||||
*/
|
*/
|
||||||
async function updatePackageVersion(newVersion) {
|
async function updatePackageVersion(newVersion) {
|
||||||
const _dirname = process.cwd();
|
const _dirname = process.cwd()
|
||||||
const packageJsonPath = path.join(_dirname, "package.json");
|
const packageJsonPath = path.join(_dirname, 'package.json')
|
||||||
try {
|
try {
|
||||||
const data = await fs.readFile(packageJsonPath, "utf8");
|
const data = await fs.readFile(packageJsonPath, 'utf8')
|
||||||
const packageJson = JSON.parse(data);
|
const packageJson = JSON.parse(data)
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
"[INFO]: Current package.json version is: ",
|
'[INFO]: Current package.json version is: ',
|
||||||
packageJson.version,
|
packageJson.version,
|
||||||
);
|
)
|
||||||
packageJson.version = newVersion.startsWith("v")
|
packageJson.version = newVersion.startsWith('v')
|
||||||
? newVersion.slice(1)
|
? newVersion.slice(1)
|
||||||
: newVersion;
|
: newVersion
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
packageJsonPath,
|
packageJsonPath,
|
||||||
JSON.stringify(packageJson, null, 2),
|
JSON.stringify(packageJson, null, 2),
|
||||||
"utf8",
|
'utf8',
|
||||||
);
|
)
|
||||||
console.log(
|
console.log(
|
||||||
`[INFO]: package.json version updated to: ${packageJson.version}`,
|
`[INFO]: package.json version updated to: ${packageJson.version}`,
|
||||||
);
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating package.json version:", error);
|
console.error('Error updating package.json version:', error)
|
||||||
throw error;
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,30 +170,30 @@ async function updatePackageVersion(newVersion) {
|
|||||||
* @param {string} newVersion
|
* @param {string} newVersion
|
||||||
*/
|
*/
|
||||||
async function updateCargoVersion(newVersion) {
|
async function updateCargoVersion(newVersion) {
|
||||||
const _dirname = process.cwd();
|
const _dirname = process.cwd()
|
||||||
const cargoTomlPath = path.join(_dirname, "src-tauri", "Cargo.toml");
|
const cargoTomlPath = path.join(_dirname, 'src-tauri', 'Cargo.toml')
|
||||||
try {
|
try {
|
||||||
const data = await fs.readFile(cargoTomlPath, "utf8");
|
const data = await fs.readFile(cargoTomlPath, 'utf8')
|
||||||
const lines = data.split("\n");
|
const lines = data.split('\n')
|
||||||
const versionWithoutV = newVersion.startsWith("v")
|
const versionWithoutV = newVersion.startsWith('v')
|
||||||
? newVersion.slice(1)
|
? newVersion.slice(1)
|
||||||
: newVersion;
|
: newVersion
|
||||||
|
|
||||||
const updatedLines = lines.map((line) => {
|
const updatedLines = lines.map((line) => {
|
||||||
if (line.trim().startsWith("version =")) {
|
if (line.trim().startsWith('version =')) {
|
||||||
return line.replace(
|
return line.replace(
|
||||||
/version\s*=\s*"[^"]+"/,
|
/version\s*=\s*"[^"]+"/,
|
||||||
`version = "${versionWithoutV}"`,
|
`version = "${versionWithoutV}"`,
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
return line;
|
return line
|
||||||
});
|
})
|
||||||
|
|
||||||
await fs.writeFile(cargoTomlPath, updatedLines.join("\n"), "utf8");
|
await fs.writeFile(cargoTomlPath, updatedLines.join('\n'), 'utf8')
|
||||||
console.log(`[INFO]: Cargo.toml version updated to: ${versionWithoutV}`);
|
console.log(`[INFO]: Cargo.toml version updated to: ${versionWithoutV}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating Cargo.toml version:", error);
|
console.error('Error updating Cargo.toml version:', error)
|
||||||
throw error;
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,34 +202,34 @@ async function updateCargoVersion(newVersion) {
|
|||||||
* @param {string} newVersion
|
* @param {string} newVersion
|
||||||
*/
|
*/
|
||||||
async function updateTauriConfigVersion(newVersion) {
|
async function updateTauriConfigVersion(newVersion) {
|
||||||
const _dirname = process.cwd();
|
const _dirname = process.cwd()
|
||||||
const tauriConfigPath = path.join(_dirname, "src-tauri", "tauri.conf.json");
|
const tauriConfigPath = path.join(_dirname, 'src-tauri', 'tauri.conf.json')
|
||||||
try {
|
try {
|
||||||
const data = await fs.readFile(tauriConfigPath, "utf8");
|
const data = await fs.readFile(tauriConfigPath, 'utf8')
|
||||||
const tauriConfig = JSON.parse(data);
|
const tauriConfig = JSON.parse(data)
|
||||||
const versionWithoutV = newVersion.startsWith("v")
|
const versionWithoutV = newVersion.startsWith('v')
|
||||||
? newVersion.slice(1)
|
? newVersion.slice(1)
|
||||||
: newVersion;
|
: newVersion
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
"[INFO]: Current tauri.conf.json version is: ",
|
'[INFO]: Current tauri.conf.json version is: ',
|
||||||
tauriConfig.version,
|
tauriConfig.version,
|
||||||
);
|
)
|
||||||
|
|
||||||
// 使用完整版本信息,包含build metadata
|
// 使用完整版本信息,包含build metadata
|
||||||
tauriConfig.version = versionWithoutV;
|
tauriConfig.version = versionWithoutV
|
||||||
|
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
tauriConfigPath,
|
tauriConfigPath,
|
||||||
JSON.stringify(tauriConfig, null, 2),
|
JSON.stringify(tauriConfig, null, 2),
|
||||||
"utf8",
|
'utf8',
|
||||||
);
|
)
|
||||||
console.log(
|
console.log(
|
||||||
`[INFO]: tauri.conf.json version updated to: ${versionWithoutV}`,
|
`[INFO]: tauri.conf.json version updated to: ${versionWithoutV}`,
|
||||||
);
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating tauri.conf.json version:", error);
|
console.error('Error updating tauri.conf.json version:', error)
|
||||||
throw error;
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,15 +237,15 @@ async function updateTauriConfigVersion(newVersion) {
|
|||||||
* 获取当前版本号
|
* 获取当前版本号
|
||||||
*/
|
*/
|
||||||
async function getCurrentVersion() {
|
async function getCurrentVersion() {
|
||||||
const _dirname = process.cwd();
|
const _dirname = process.cwd()
|
||||||
const packageJsonPath = path.join(_dirname, "package.json");
|
const packageJsonPath = path.join(_dirname, 'package.json')
|
||||||
try {
|
try {
|
||||||
const data = await fs.readFile(packageJsonPath, "utf8");
|
const data = await fs.readFile(packageJsonPath, 'utf8')
|
||||||
const packageJson = JSON.parse(data);
|
const packageJson = JSON.parse(data)
|
||||||
return packageJson.version;
|
return packageJson.version
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error getting current version:", error);
|
console.error('Error getting current version:', error)
|
||||||
throw error;
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,62 +254,62 @@ async function getCurrentVersion() {
|
|||||||
*/
|
*/
|
||||||
async function main(versionArg) {
|
async function main(versionArg) {
|
||||||
if (!versionArg) {
|
if (!versionArg) {
|
||||||
console.error("Error: Version argument is required");
|
console.error('Error: Version argument is required')
|
||||||
process.exit(1);
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let newVersion;
|
let newVersion
|
||||||
const validTags = [
|
const validTags = [
|
||||||
"alpha",
|
'alpha',
|
||||||
"beta",
|
'beta',
|
||||||
"rc",
|
'rc',
|
||||||
"autobuild",
|
'autobuild',
|
||||||
"autobuild-latest",
|
'autobuild-latest',
|
||||||
"deploytest",
|
'deploytest',
|
||||||
];
|
]
|
||||||
|
|
||||||
if (validTags.includes(versionArg.toLowerCase())) {
|
if (validTags.includes(versionArg.toLowerCase())) {
|
||||||
const currentVersion = await getCurrentVersion();
|
const currentVersion = await getCurrentVersion()
|
||||||
const baseVersion = getBaseVersion(currentVersion);
|
const baseVersion = getBaseVersion(currentVersion)
|
||||||
|
|
||||||
if (versionArg.toLowerCase() === "autobuild") {
|
if (versionArg.toLowerCase() === 'autobuild') {
|
||||||
// 格式: 2.3.0+autobuild.1004.cc39b27
|
// 格式: 2.3.0+autobuild.1004.cc39b27
|
||||||
// 使用 Tauri 相关的最新 commit hash
|
// 使用 Tauri 相关的最新 commit hash
|
||||||
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp(true, true)}`;
|
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp(true, true)}`
|
||||||
} else if (versionArg.toLowerCase() === "autobuild-latest") {
|
} else if (versionArg.toLowerCase() === 'autobuild-latest') {
|
||||||
// 格式: 2.3.0+autobuild.1004.a1b2c3d (使用最新 Tauri 提交)
|
// 格式: 2.3.0+autobuild.1004.a1b2c3d (使用最新 Tauri 提交)
|
||||||
const latestTauriCommit = getLatestTauriCommit();
|
const latestTauriCommit = getLatestTauriCommit()
|
||||||
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp()}.${latestTauriCommit}`;
|
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp()}.${latestTauriCommit}`
|
||||||
} else if (versionArg.toLowerCase() === "deploytest") {
|
} else if (versionArg.toLowerCase() === 'deploytest') {
|
||||||
// 格式: 2.3.0+deploytest.1004.cc39b27
|
// 格式: 2.3.0+deploytest.1004.cc39b27
|
||||||
// 使用 Tauri 相关的最新 commit hash
|
// 使用 Tauri 相关的最新 commit hash
|
||||||
newVersion = `${baseVersion}+deploytest.${generateShortTimestamp(true, true)}`;
|
newVersion = `${baseVersion}+deploytest.${generateShortTimestamp(true, true)}`
|
||||||
} else {
|
} else {
|
||||||
newVersion = `${baseVersion}-${versionArg.toLowerCase()}`;
|
newVersion = `${baseVersion}-${versionArg.toLowerCase()}`
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!isValidVersion(versionArg)) {
|
if (!isValidVersion(versionArg)) {
|
||||||
console.error("Error: Invalid version format");
|
console.error('Error: Invalid version format')
|
||||||
process.exit(1);
|
process.exit(1)
|
||||||
}
|
}
|
||||||
newVersion = normalizeVersion(versionArg);
|
newVersion = normalizeVersion(versionArg)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[INFO]: Updating versions to: ${newVersion}`);
|
console.log(`[INFO]: Updating versions to: ${newVersion}`)
|
||||||
await updatePackageVersion(newVersion);
|
await updatePackageVersion(newVersion)
|
||||||
await updateCargoVersion(newVersion);
|
await updateCargoVersion(newVersion)
|
||||||
await updateTauriConfigVersion(newVersion);
|
await updateTauriConfigVersion(newVersion)
|
||||||
console.log("[SUCCESS]: All version updates completed successfully!");
|
console.log('[SUCCESS]: All version updates completed successfully!')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[ERROR]: Failed to update versions:", error);
|
console.error('[ERROR]: Failed to update versions:', error)
|
||||||
process.exit(1);
|
process.exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
program
|
program
|
||||||
.name("pnpm release-version")
|
.name('pnpm release-version')
|
||||||
.description("Update project version numbers")
|
.description('Update project version numbers')
|
||||||
.argument("<version>", "version tag or full version")
|
.argument('<version>', 'version tag or full version')
|
||||||
.action(main)
|
.action(main)
|
||||||
.parse(process.argv);
|
.parse(process.argv)
|
||||||
|
|||||||
+54
-57
@@ -1,97 +1,94 @@
|
|||||||
import { readFileSync } from "fs";
|
import { readFileSync } from 'fs'
|
||||||
|
|
||||||
import axios from "axios";
|
import axios from 'axios'
|
||||||
|
|
||||||
import { log_error, log_info, log_success } from "./utils.mjs";
|
import { log_error, log_info, log_success } from './utils.mjs'
|
||||||
|
|
||||||
const CHAT_ID_RELEASE = "@clash_verge_re"; // 正式发布频道
|
const CHAT_ID_RELEASE = '@clash_verge_re' // 正式发布频道
|
||||||
const CHAT_ID_TEST = "@vergetest"; // 测试频道
|
const CHAT_ID_TEST = '@vergetest' // 测试频道
|
||||||
|
|
||||||
async function sendTelegramNotification() {
|
async function sendTelegramNotification() {
|
||||||
if (!process.env.TELEGRAM_BOT_TOKEN) {
|
if (!process.env.TELEGRAM_BOT_TOKEN) {
|
||||||
throw new Error("TELEGRAM_BOT_TOKEN is required");
|
throw new Error('TELEGRAM_BOT_TOKEN is required')
|
||||||
}
|
}
|
||||||
|
|
||||||
const version =
|
const version =
|
||||||
process.env.VERSION ||
|
process.env.VERSION ||
|
||||||
(() => {
|
(() => {
|
||||||
const pkg = readFileSync("package.json", "utf-8");
|
const pkg = readFileSync('package.json', 'utf-8')
|
||||||
return JSON.parse(pkg).version;
|
return JSON.parse(pkg).version
|
||||||
})();
|
})()
|
||||||
|
|
||||||
const downloadUrl =
|
const downloadUrl =
|
||||||
process.env.DOWNLOAD_URL ||
|
process.env.DOWNLOAD_URL ||
|
||||||
`https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v${version}`;
|
`https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v${version}`
|
||||||
|
|
||||||
const isAutobuild =
|
const isAutobuild =
|
||||||
process.env.BUILD_TYPE === "autobuild" || version.includes("autobuild");
|
process.env.BUILD_TYPE === 'autobuild' || version.includes('autobuild')
|
||||||
const chatId = isAutobuild ? CHAT_ID_TEST : CHAT_ID_RELEASE;
|
const chatId = isAutobuild ? CHAT_ID_TEST : CHAT_ID_RELEASE
|
||||||
const buildType = isAutobuild ? "滚动更新版" : "正式版";
|
const buildType = isAutobuild ? '滚动更新版' : '正式版'
|
||||||
|
|
||||||
log_info(`Preparing Telegram notification for ${buildType} ${version}`);
|
log_info(`Preparing Telegram notification for ${buildType} ${version}`)
|
||||||
log_info(`Target channel: ${chatId}`);
|
log_info(`Target channel: ${chatId}`)
|
||||||
log_info(`Download URL: ${downloadUrl}`);
|
log_info(`Download URL: ${downloadUrl}`)
|
||||||
|
|
||||||
// 读取发布说明和下载地址
|
// 读取发布说明和下载地址
|
||||||
let releaseContent = "";
|
let releaseContent = ''
|
||||||
try {
|
try {
|
||||||
releaseContent = readFileSync("release.txt", "utf-8");
|
releaseContent = readFileSync('release.txt', 'utf-8')
|
||||||
log_info("成功读取 release.txt 文件");
|
log_info('成功读取 release.txt 文件')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log_error("无法读取 release.txt,使用默认发布说明", error);
|
log_error('无法读取 release.txt,使用默认发布说明', error)
|
||||||
releaseContent = "更多新功能现已支持,详细更新日志请查看发布页面。";
|
releaseContent = '更多新功能现已支持,详细更新日志请查看发布页面。'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Markdown 转换为 HTML
|
// Markdown 转换为 HTML
|
||||||
function convertMarkdownToTelegramHTML(content) {
|
function convertMarkdownToTelegramHTML(content) {
|
||||||
return content
|
return content
|
||||||
.split("\n")
|
.split('\n')
|
||||||
.map((line) => {
|
.map((line) => {
|
||||||
if (line.trim().length === 0) {
|
if (line.trim().length === 0) {
|
||||||
return "";
|
return ''
|
||||||
} else if (line.startsWith("## ")) {
|
} else if (line.startsWith('## ')) {
|
||||||
return `<b>${line.replace("## ", "")}</b>`;
|
return `<b>${line.replace('## ', '')}</b>`
|
||||||
} else if (line.startsWith("### ")) {
|
} else if (line.startsWith('### ')) {
|
||||||
return `<b>${line.replace("### ", "")}</b>`;
|
return `<b>${line.replace('### ', '')}</b>`
|
||||||
} else if (line.startsWith("#### ")) {
|
} else if (line.startsWith('#### ')) {
|
||||||
return `<b>${line.replace("#### ", "")}</b>`;
|
return `<b>${line.replace('#### ', '')}</b>`
|
||||||
} else {
|
} else {
|
||||||
let processedLine = line.replace(
|
let processedLine = line.replace(
|
||||||
/\[([^\]]+)\]\(([^)]+)\)/g,
|
/\[([^\]]+)\]\(([^)]+)\)/g,
|
||||||
(match, text, url) => {
|
(match, text, url) => {
|
||||||
const encodedUrl = encodeURI(url);
|
const encodedUrl = encodeURI(url)
|
||||||
return `<a href="${encodedUrl}">${text}</a>`;
|
return `<a href="${encodedUrl}">${text}</a>`
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
processedLine = processedLine.replace(
|
processedLine = processedLine.replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>')
|
||||||
/\*\*([^*]+)\*\*/g,
|
return processedLine
|
||||||
"<b>$1</b>",
|
|
||||||
);
|
|
||||||
return processedLine;
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.join("\n");
|
.join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeDetailsTags(content) {
|
function normalizeDetailsTags(content) {
|
||||||
return content
|
return content
|
||||||
.replace(
|
.replace(
|
||||||
/<summary>\s*<strong>\s*(.*?)\s*<\/strong>\s*<\/summary>/g,
|
/<summary>\s*<strong>\s*(.*?)\s*<\/strong>\s*<\/summary>/g,
|
||||||
"\n<b>$1</b>\n",
|
'\n<b>$1</b>\n',
|
||||||
)
|
)
|
||||||
.replace(/<summary>\s*(.*?)\s*<\/summary>/g, "\n<b>$1</b>\n")
|
.replace(/<summary>\s*(.*?)\s*<\/summary>/g, '\n<b>$1</b>\n')
|
||||||
.replace(/<\/?details>/g, "")
|
.replace(/<\/?details>/g, '')
|
||||||
.replace(/<\/?strong>/g, (m) => (m === "</strong>" ? "</b>" : "<b>"))
|
.replace(/<\/?strong>/g, (m) => (m === '</strong>' ? '</b>' : '<b>'))
|
||||||
.replace(/<br\s*\/?>/g, "\n");
|
.replace(/<br\s*\/?>/g, '\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
releaseContent = normalizeDetailsTags(releaseContent);
|
releaseContent = normalizeDetailsTags(releaseContent)
|
||||||
const formattedContent = convertMarkdownToTelegramHTML(releaseContent);
|
const formattedContent = convertMarkdownToTelegramHTML(releaseContent)
|
||||||
|
|
||||||
const releaseTitle = isAutobuild ? "滚动更新版发布" : "正式发布";
|
const releaseTitle = isAutobuild ? '滚动更新版发布' : '正式发布'
|
||||||
const encodedVersion = encodeURIComponent(version);
|
const encodedVersion = encodeURIComponent(version)
|
||||||
const releaseTag = isAutobuild ? "autobuild" : `v${version}`;
|
const releaseTag = isAutobuild ? 'autobuild' : `v${version}`
|
||||||
const content = `<b>🎉 <a href="https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/${releaseTag}">Clash Verge Rev v${version}</a> ${releaseTitle}</b>\n\n${formattedContent}`;
|
const content = `<b>🎉 <a href="https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/${releaseTag}">Clash Verge Rev v${version}</a> ${releaseTitle}</b>\n\n${formattedContent}`
|
||||||
|
|
||||||
// 发送到 Telegram
|
// 发送到 Telegram
|
||||||
try {
|
try {
|
||||||
@@ -105,22 +102,22 @@ async function sendTelegramNotification() {
|
|||||||
url: `https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${encodedVersion}`,
|
url: `https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${encodedVersion}`,
|
||||||
prefer_large_media: true,
|
prefer_large_media: true,
|
||||||
},
|
},
|
||||||
parse_mode: "HTML",
|
parse_mode: 'HTML',
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
log_success(`✅ Telegram 通知发送成功到 ${chatId}`);
|
log_success(`✅ Telegram 通知发送成功到 ${chatId}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log_error(
|
log_error(
|
||||||
`❌ Telegram 通知发送失败到 ${chatId}:`,
|
`❌ Telegram 通知发送失败到 ${chatId}:`,
|
||||||
error.response?.data || error.message,
|
error.response?.data || error.message,
|
||||||
error,
|
error,
|
||||||
);
|
)
|
||||||
process.exit(1);
|
process.exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行函数
|
// 执行函数
|
||||||
sendTelegramNotification().catch((error) => {
|
sendTelegramNotification().catch((error) => {
|
||||||
log_error("脚本执行失败:", error);
|
log_error('脚本执行失败:', error)
|
||||||
process.exit(1);
|
process.exit(1)
|
||||||
});
|
})
|
||||||
|
|||||||
+38
-38
@@ -1,84 +1,84 @@
|
|||||||
import fs from "fs";
|
import fs from 'fs'
|
||||||
import fsp from "fs/promises";
|
import fsp from 'fs/promises'
|
||||||
import path from "path";
|
import path from 'path'
|
||||||
|
|
||||||
const UPDATE_LOG = "Changelog.md";
|
const UPDATE_LOG = 'Changelog.md'
|
||||||
|
|
||||||
// parse the Changelog.md
|
// parse the Changelog.md
|
||||||
export async function resolveUpdateLog(tag) {
|
export async function resolveUpdateLog(tag) {
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd()
|
||||||
|
|
||||||
const reTitle = /^## v[\d.]+/;
|
const reTitle = /^## v[\d.]+/
|
||||||
const reEnd = /^---/;
|
const reEnd = /^---/
|
||||||
|
|
||||||
const file = path.join(cwd, UPDATE_LOG);
|
const file = path.join(cwd, UPDATE_LOG)
|
||||||
|
|
||||||
if (!fs.existsSync(file)) {
|
if (!fs.existsSync(file)) {
|
||||||
throw new Error("could not found Changelog.md");
|
throw new Error('could not found Changelog.md')
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await fsp.readFile(file, "utf-8");
|
const data = await fsp.readFile(file, 'utf-8')
|
||||||
|
|
||||||
const map = {};
|
const map = {}
|
||||||
let p = "";
|
let p = ''
|
||||||
|
|
||||||
data.split("\n").forEach((line) => {
|
data.split('\n').forEach((line) => {
|
||||||
if (reTitle.test(line)) {
|
if (reTitle.test(line)) {
|
||||||
p = line.slice(3).trim();
|
p = line.slice(3).trim()
|
||||||
if (!map[p]) {
|
if (!map[p]) {
|
||||||
map[p] = [];
|
map[p] = []
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Tag ${p} dup`);
|
throw new Error(`Tag ${p} dup`)
|
||||||
}
|
}
|
||||||
} else if (reEnd.test(line)) {
|
} else if (reEnd.test(line)) {
|
||||||
p = "";
|
p = ''
|
||||||
} else if (p) {
|
} else if (p) {
|
||||||
map[p].push(line);
|
map[p].push(line)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
if (!map[tag]) {
|
if (!map[tag]) {
|
||||||
throw new Error(`could not found "${tag}" in Changelog.md`);
|
throw new Error(`could not found "${tag}" in Changelog.md`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return map[tag].join("\n").trim();
|
return map[tag].join('\n').trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveUpdateLogDefault() {
|
export async function resolveUpdateLogDefault() {
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd()
|
||||||
const file = path.join(cwd, UPDATE_LOG);
|
const file = path.join(cwd, UPDATE_LOG)
|
||||||
|
|
||||||
if (!fs.existsSync(file)) {
|
if (!fs.existsSync(file)) {
|
||||||
throw new Error("could not found Changelog.md");
|
throw new Error('could not found Changelog.md')
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await fsp.readFile(file, "utf-8");
|
const data = await fsp.readFile(file, 'utf-8')
|
||||||
|
|
||||||
const reTitle = /^## v[\d.]+/;
|
const reTitle = /^## v[\d.]+/
|
||||||
const reEnd = /^---/;
|
const reEnd = /^---/
|
||||||
|
|
||||||
let isCapturing = false;
|
let isCapturing = false
|
||||||
const content = [];
|
const content = []
|
||||||
let firstTag = "";
|
let firstTag = ''
|
||||||
|
|
||||||
for (const line of data.split("\n")) {
|
for (const line of data.split('\n')) {
|
||||||
if (reTitle.test(line) && !isCapturing) {
|
if (reTitle.test(line) && !isCapturing) {
|
||||||
isCapturing = true;
|
isCapturing = true
|
||||||
firstTag = line.slice(3).trim();
|
firstTag = line.slice(3).trim()
|
||||||
continue;
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCapturing) {
|
if (isCapturing) {
|
||||||
if (reEnd.test(line)) {
|
if (reEnd.test(line)) {
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
content.push(line);
|
content.push(line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!firstTag) {
|
if (!firstTag) {
|
||||||
throw new Error("could not found any version tag in Changelog.md");
|
throw new Error('could not found any version tag in Changelog.md')
|
||||||
}
|
}
|
||||||
|
|
||||||
return content.join("\n").trim();
|
return content.join('\n').trim()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,117 +1,117 @@
|
|||||||
import { context, getOctokit } from "@actions/github";
|
import { context, getOctokit } from '@actions/github'
|
||||||
import fetch from "node-fetch";
|
import fetch from 'node-fetch'
|
||||||
|
|
||||||
import { resolveUpdateLog } from "./updatelog.mjs";
|
import { resolveUpdateLog } from './updatelog.mjs'
|
||||||
|
|
||||||
const UPDATE_TAG_NAME = "updater";
|
const UPDATE_TAG_NAME = 'updater'
|
||||||
const UPDATE_JSON_FILE = "update-fixed-webview2.json";
|
const UPDATE_JSON_FILE = 'update-fixed-webview2.json'
|
||||||
const UPDATE_JSON_PROXY = "update-fixed-webview2-proxy.json";
|
const UPDATE_JSON_PROXY = 'update-fixed-webview2-proxy.json'
|
||||||
|
|
||||||
/// generate update.json
|
/// generate update.json
|
||||||
/// upload to update tag's release asset
|
/// upload to update tag's release asset
|
||||||
async function resolveUpdater() {
|
async function resolveUpdater() {
|
||||||
if (process.env.GITHUB_TOKEN === undefined) {
|
if (process.env.GITHUB_TOKEN === undefined) {
|
||||||
throw new Error("GITHUB_TOKEN is required");
|
throw new Error('GITHUB_TOKEN is required')
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = { owner: context.repo.owner, repo: context.repo.repo };
|
const options = { owner: context.repo.owner, repo: context.repo.repo }
|
||||||
const github = getOctokit(process.env.GITHUB_TOKEN);
|
const github = getOctokit(process.env.GITHUB_TOKEN)
|
||||||
|
|
||||||
const { data: tags } = await github.rest.repos.listTags({
|
const { data: tags } = await github.rest.repos.listTags({
|
||||||
...options,
|
...options,
|
||||||
per_page: 10,
|
per_page: 10,
|
||||||
page: 1,
|
page: 1,
|
||||||
});
|
})
|
||||||
|
|
||||||
// get the latest publish tag
|
// get the latest publish tag
|
||||||
const tag = tags.find((t) => t.name.startsWith("v"));
|
const tag = tags.find((t) => t.name.startsWith('v'))
|
||||||
|
|
||||||
console.log(tag);
|
console.log(tag)
|
||||||
console.log();
|
console.log()
|
||||||
|
|
||||||
const { data: latestRelease } = await github.rest.repos.getReleaseByTag({
|
const { data: latestRelease } = await github.rest.repos.getReleaseByTag({
|
||||||
...options,
|
...options,
|
||||||
tag: tag.name,
|
tag: tag.name,
|
||||||
});
|
})
|
||||||
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
name: tag.name,
|
name: tag.name,
|
||||||
notes: await resolveUpdateLog(tag.name), // use Changelog.md
|
notes: await resolveUpdateLog(tag.name), // use Changelog.md
|
||||||
pub_date: new Date().toISOString(),
|
pub_date: new Date().toISOString(),
|
||||||
platforms: {
|
platforms: {
|
||||||
"windows-x86_64": { signature: "", url: "" },
|
'windows-x86_64': { signature: '', url: '' },
|
||||||
"windows-aarch64": { signature: "", url: "" },
|
'windows-aarch64': { signature: '', url: '' },
|
||||||
"windows-x86": { signature: "", url: "" },
|
'windows-x86': { signature: '', url: '' },
|
||||||
"windows-i686": { signature: "", url: "" },
|
'windows-i686': { signature: '', url: '' },
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
const promises = latestRelease.assets.map(async (asset) => {
|
const promises = latestRelease.assets.map(async (asset) => {
|
||||||
const { name, browser_download_url } = asset;
|
const { name, browser_download_url } = asset
|
||||||
|
|
||||||
// win64 url
|
// win64 url
|
||||||
if (name.endsWith("x64_fixed_webview2-setup.exe")) {
|
if (name.endsWith('x64_fixed_webview2-setup.exe')) {
|
||||||
updateData.platforms["windows-x86_64"].url = browser_download_url;
|
updateData.platforms['windows-x86_64'].url = browser_download_url
|
||||||
}
|
}
|
||||||
// win64 signature
|
// win64 signature
|
||||||
if (name.endsWith("x64_fixed_webview2-setup.exe.sig")) {
|
if (name.endsWith('x64_fixed_webview2-setup.exe.sig')) {
|
||||||
const sig = await getSignature(browser_download_url);
|
const sig = await getSignature(browser_download_url)
|
||||||
updateData.platforms["windows-x86_64"].signature = sig;
|
updateData.platforms['windows-x86_64'].signature = sig
|
||||||
}
|
}
|
||||||
|
|
||||||
// win32 url
|
// win32 url
|
||||||
if (name.endsWith("x86_fixed_webview2-setup.exe")) {
|
if (name.endsWith('x86_fixed_webview2-setup.exe')) {
|
||||||
updateData.platforms["windows-x86"].url = browser_download_url;
|
updateData.platforms['windows-x86'].url = browser_download_url
|
||||||
updateData.platforms["windows-i686"].url = browser_download_url;
|
updateData.platforms['windows-i686'].url = browser_download_url
|
||||||
}
|
}
|
||||||
// win32 signature
|
// win32 signature
|
||||||
if (name.endsWith("x86_fixed_webview2-setup.exe.sig")) {
|
if (name.endsWith('x86_fixed_webview2-setup.exe.sig')) {
|
||||||
const sig = await getSignature(browser_download_url);
|
const sig = await getSignature(browser_download_url)
|
||||||
updateData.platforms["windows-x86"].signature = sig;
|
updateData.platforms['windows-x86'].signature = sig
|
||||||
updateData.platforms["windows-i686"].signature = sig;
|
updateData.platforms['windows-i686'].signature = sig
|
||||||
}
|
}
|
||||||
|
|
||||||
// win arm url
|
// win arm url
|
||||||
if (name.endsWith("arm64_fixed_webview2-setup.exe")) {
|
if (name.endsWith('arm64_fixed_webview2-setup.exe')) {
|
||||||
updateData.platforms["windows-aarch64"].url = browser_download_url;
|
updateData.platforms['windows-aarch64'].url = browser_download_url
|
||||||
}
|
}
|
||||||
// win arm signature
|
// win arm signature
|
||||||
if (name.endsWith("arm64_fixed_webview2-setup.exe.sig")) {
|
if (name.endsWith('arm64_fixed_webview2-setup.exe.sig')) {
|
||||||
const sig = await getSignature(browser_download_url);
|
const sig = await getSignature(browser_download_url)
|
||||||
updateData.platforms["windows-aarch64"].signature = sig;
|
updateData.platforms['windows-aarch64'].signature = sig
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
await Promise.allSettled(promises);
|
await Promise.allSettled(promises)
|
||||||
console.log(updateData);
|
console.log(updateData)
|
||||||
|
|
||||||
// maybe should test the signature as well
|
// maybe should test the signature as well
|
||||||
// delete the null field
|
// delete the null field
|
||||||
Object.entries(updateData.platforms).forEach(([key, value]) => {
|
Object.entries(updateData.platforms).forEach(([key, value]) => {
|
||||||
if (!value.url) {
|
if (!value.url) {
|
||||||
console.log(`[Error]: failed to parse release for "${key}"`);
|
console.log(`[Error]: failed to parse release for "${key}"`)
|
||||||
delete updateData.platforms[key];
|
delete updateData.platforms[key]
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
// 生成一个代理github的更新文件
|
// 生成一个代理github的更新文件
|
||||||
// 使用 https://hub.fastgit.xyz/ 做github资源的加速
|
// 使用 https://hub.fastgit.xyz/ 做github资源的加速
|
||||||
const updateDataNew = JSON.parse(JSON.stringify(updateData));
|
const updateDataNew = JSON.parse(JSON.stringify(updateData))
|
||||||
|
|
||||||
Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
|
Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
|
||||||
if (value.url) {
|
if (value.url) {
|
||||||
updateDataNew.platforms[key].url =
|
updateDataNew.platforms[key].url =
|
||||||
"https://download.clashverge.dev/" + value.url;
|
'https://download.clashverge.dev/' + value.url
|
||||||
} else {
|
} else {
|
||||||
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
|
console.log(`[Error]: updateDataNew.platforms.${key} is null`)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
// update the update.json
|
// update the update.json
|
||||||
const { data: updateRelease } = await github.rest.repos.getReleaseByTag({
|
const { data: updateRelease } = await github.rest.repos.getReleaseByTag({
|
||||||
...options,
|
...options,
|
||||||
tag: UPDATE_TAG_NAME,
|
tag: UPDATE_TAG_NAME,
|
||||||
});
|
})
|
||||||
|
|
||||||
// delete the old assets
|
// delete the old assets
|
||||||
for (const asset of updateRelease.assets) {
|
for (const asset of updateRelease.assets) {
|
||||||
@@ -119,13 +119,13 @@ async function resolveUpdater() {
|
|||||||
await github.rest.repos.deleteReleaseAsset({
|
await github.rest.repos.deleteReleaseAsset({
|
||||||
...options,
|
...options,
|
||||||
asset_id: asset.id,
|
asset_id: asset.id,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (asset.name === UPDATE_JSON_PROXY) {
|
if (asset.name === UPDATE_JSON_PROXY) {
|
||||||
await github.rest.repos
|
await github.rest.repos
|
||||||
.deleteReleaseAsset({ ...options, asset_id: asset.id })
|
.deleteReleaseAsset({ ...options, asset_id: asset.id })
|
||||||
.catch(console.error); // do not break the pipeline
|
.catch(console.error) // do not break the pipeline
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,24 +135,24 @@ async function resolveUpdater() {
|
|||||||
release_id: updateRelease.id,
|
release_id: updateRelease.id,
|
||||||
name: UPDATE_JSON_FILE,
|
name: UPDATE_JSON_FILE,
|
||||||
data: JSON.stringify(updateData, null, 2),
|
data: JSON.stringify(updateData, null, 2),
|
||||||
});
|
})
|
||||||
|
|
||||||
await github.rest.repos.uploadReleaseAsset({
|
await github.rest.repos.uploadReleaseAsset({
|
||||||
...options,
|
...options,
|
||||||
release_id: updateRelease.id,
|
release_id: updateRelease.id,
|
||||||
name: UPDATE_JSON_PROXY,
|
name: UPDATE_JSON_PROXY,
|
||||||
data: JSON.stringify(updateDataNew, null, 2),
|
data: JSON.stringify(updateDataNew, null, 2),
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the signature file content
|
// get the signature file content
|
||||||
async function getSignature(url) {
|
async function getSignature(url) {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: "GET",
|
method: 'GET',
|
||||||
headers: { "Content-Type": "application/octet-stream" },
|
headers: { 'Content-Type': 'application/octet-stream' },
|
||||||
});
|
})
|
||||||
|
|
||||||
return response.text();
|
return response.text()
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveUpdater().catch(console.error);
|
resolveUpdater().catch(console.error)
|
||||||
|
|||||||
+140
-143
@@ -1,263 +1,263 @@
|
|||||||
import { getOctokit, context } from "@actions/github";
|
import { getOctokit, context } from '@actions/github'
|
||||||
import fetch from "node-fetch";
|
import fetch from 'node-fetch'
|
||||||
|
|
||||||
import { resolveUpdateLog, resolveUpdateLogDefault } from "./updatelog.mjs";
|
import { resolveUpdateLog, resolveUpdateLogDefault } from './updatelog.mjs'
|
||||||
|
|
||||||
// Add stable update JSON filenames
|
// Add stable update JSON filenames
|
||||||
const UPDATE_TAG_NAME = "updater";
|
const UPDATE_TAG_NAME = 'updater'
|
||||||
const UPDATE_JSON_FILE = "update.json";
|
const UPDATE_JSON_FILE = 'update.json'
|
||||||
const UPDATE_JSON_PROXY = "update-proxy.json";
|
const UPDATE_JSON_PROXY = 'update-proxy.json'
|
||||||
// Add alpha update JSON filenames
|
// Add alpha update JSON filenames
|
||||||
const ALPHA_TAG_NAME = "updater-alpha";
|
const ALPHA_TAG_NAME = 'updater-alpha'
|
||||||
const ALPHA_UPDATE_JSON_FILE = "update.json";
|
const ALPHA_UPDATE_JSON_FILE = 'update.json'
|
||||||
const ALPHA_UPDATE_JSON_PROXY = "update-proxy.json";
|
const ALPHA_UPDATE_JSON_PROXY = 'update-proxy.json'
|
||||||
|
|
||||||
/// generate update.json
|
/// generate update.json
|
||||||
/// upload to update tag's release asset
|
/// upload to update tag's release asset
|
||||||
async function resolveUpdater() {
|
async function resolveUpdater() {
|
||||||
if (process.env.GITHUB_TOKEN === undefined) {
|
if (process.env.GITHUB_TOKEN === undefined) {
|
||||||
throw new Error("GITHUB_TOKEN is required");
|
throw new Error('GITHUB_TOKEN is required')
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = { owner: context.repo.owner, repo: context.repo.repo };
|
const options = { owner: context.repo.owner, repo: context.repo.repo }
|
||||||
const github = getOctokit(process.env.GITHUB_TOKEN);
|
const github = getOctokit(process.env.GITHUB_TOKEN)
|
||||||
|
|
||||||
// Fetch all tags using pagination
|
// Fetch all tags using pagination
|
||||||
let allTags = [];
|
let allTags = []
|
||||||
let page = 1;
|
let page = 1
|
||||||
const perPage = 100;
|
const perPage = 100
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const { data: pageTags } = await github.rest.repos.listTags({
|
const { data: pageTags } = await github.rest.repos.listTags({
|
||||||
...options,
|
...options,
|
||||||
per_page: perPage,
|
per_page: perPage,
|
||||||
page: page,
|
page: page,
|
||||||
});
|
})
|
||||||
|
|
||||||
allTags = allTags.concat(pageTags);
|
allTags = allTags.concat(pageTags)
|
||||||
|
|
||||||
// Break if we received fewer tags than requested (last page)
|
// Break if we received fewer tags than requested (last page)
|
||||||
if (pageTags.length < perPage) {
|
if (pageTags.length < perPage) {
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
page++;
|
page++
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = allTags;
|
const tags = allTags
|
||||||
console.log(`Retrieved ${tags.length} tags in total`);
|
console.log(`Retrieved ${tags.length} tags in total`)
|
||||||
|
|
||||||
// More flexible tag detection with regex patterns
|
// More flexible tag detection with regex patterns
|
||||||
const stableTagRegex = /^v\d+\.\d+\.\d+$/; // Matches vX.Y.Z format
|
const stableTagRegex = /^v\d+\.\d+\.\d+$/ // Matches vX.Y.Z format
|
||||||
// const preReleaseRegex = /^v\d+\.\d+\.\d+-(alpha|beta|rc|pre)/i; // Matches vX.Y.Z-alpha/beta/rc format
|
// const preReleaseRegex = /^v\d+\.\d+\.\d+-(alpha|beta|rc|pre)/i; // Matches vX.Y.Z-alpha/beta/rc format
|
||||||
const preReleaseRegex = /^(alpha|beta|rc|pre)$/i; // Matches exact alpha/beta/rc/pre tags
|
const preReleaseRegex = /^(alpha|beta|rc|pre)$/i // Matches exact alpha/beta/rc/pre tags
|
||||||
|
|
||||||
// Get the latest stable tag and pre-release tag
|
// Get the latest stable tag and pre-release tag
|
||||||
const stableTag = tags.find((t) => stableTagRegex.test(t.name));
|
const stableTag = tags.find((t) => stableTagRegex.test(t.name))
|
||||||
const preReleaseTag = tags.find((t) => preReleaseRegex.test(t.name));
|
const preReleaseTag = tags.find((t) => preReleaseRegex.test(t.name))
|
||||||
|
|
||||||
console.log("All tags:", tags.map((t) => t.name).join(", "));
|
console.log('All tags:', tags.map((t) => t.name).join(', '))
|
||||||
console.log("Stable tag:", stableTag ? stableTag.name : "None found");
|
console.log('Stable tag:', stableTag ? stableTag.name : 'None found')
|
||||||
console.log(
|
console.log(
|
||||||
"Pre-release tag:",
|
'Pre-release tag:',
|
||||||
preReleaseTag ? preReleaseTag.name : "None found",
|
preReleaseTag ? preReleaseTag.name : 'None found',
|
||||||
);
|
)
|
||||||
console.log();
|
console.log()
|
||||||
|
|
||||||
// Process stable release
|
// Process stable release
|
||||||
if (stableTag) {
|
if (stableTag) {
|
||||||
await processRelease(github, options, stableTag, false);
|
await processRelease(github, options, stableTag, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process pre-release if found
|
// Process pre-release if found
|
||||||
if (preReleaseTag) {
|
if (preReleaseTag) {
|
||||||
await processRelease(github, options, preReleaseTag, true);
|
await processRelease(github, options, preReleaseTag, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process a release (stable or alpha) and generate update files
|
// Process a release (stable or alpha) and generate update files
|
||||||
async function processRelease(github, options, tag, isAlpha) {
|
async function processRelease(github, options, tag, isAlpha) {
|
||||||
if (!tag) return;
|
if (!tag) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||||
...options,
|
...options,
|
||||||
tag: tag.name,
|
tag: tag.name,
|
||||||
});
|
})
|
||||||
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
name: tag.name,
|
name: tag.name,
|
||||||
notes: await resolveUpdateLog(tag.name).catch(() =>
|
notes: await resolveUpdateLog(tag.name).catch(() =>
|
||||||
resolveUpdateLogDefault().catch(() => "No changelog available"),
|
resolveUpdateLogDefault().catch(() => 'No changelog available'),
|
||||||
),
|
),
|
||||||
pub_date: new Date().toISOString(),
|
pub_date: new Date().toISOString(),
|
||||||
platforms: {
|
platforms: {
|
||||||
win64: { signature: "", url: "" }, // compatible with older formats
|
win64: { signature: '', url: '' }, // compatible with older formats
|
||||||
linux: { signature: "", url: "" }, // compatible with older formats
|
linux: { signature: '', url: '' }, // compatible with older formats
|
||||||
darwin: { signature: "", url: "" }, // compatible with older formats
|
darwin: { signature: '', url: '' }, // compatible with older formats
|
||||||
"darwin-aarch64": { signature: "", url: "" },
|
'darwin-aarch64': { signature: '', url: '' },
|
||||||
"darwin-intel": { signature: "", url: "" },
|
'darwin-intel': { signature: '', url: '' },
|
||||||
"darwin-x86_64": { signature: "", url: "" },
|
'darwin-x86_64': { signature: '', url: '' },
|
||||||
"linux-x86_64": { signature: "", url: "" },
|
'linux-x86_64': { signature: '', url: '' },
|
||||||
"linux-x86": { signature: "", url: "" },
|
'linux-x86': { signature: '', url: '' },
|
||||||
"linux-i686": { signature: "", url: "" },
|
'linux-i686': { signature: '', url: '' },
|
||||||
"linux-aarch64": { signature: "", url: "" },
|
'linux-aarch64': { signature: '', url: '' },
|
||||||
"linux-armv7": { signature: "", url: "" },
|
'linux-armv7': { signature: '', url: '' },
|
||||||
"windows-x86_64": { signature: "", url: "" },
|
'windows-x86_64': { signature: '', url: '' },
|
||||||
"windows-aarch64": { signature: "", url: "" },
|
'windows-aarch64': { signature: '', url: '' },
|
||||||
"windows-x86": { signature: "", url: "" },
|
'windows-x86': { signature: '', url: '' },
|
||||||
"windows-i686": { signature: "", url: "" },
|
'windows-i686': { signature: '', url: '' },
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
const promises = release.assets.map(async (asset) => {
|
const promises = release.assets.map(async (asset) => {
|
||||||
const { name, browser_download_url } = asset;
|
const { name, browser_download_url } = asset
|
||||||
|
|
||||||
// Process all the platform URL and signature data
|
// Process all the platform URL and signature data
|
||||||
// win64 url
|
// win64 url
|
||||||
if (name.endsWith("x64-setup.exe")) {
|
if (name.endsWith('x64-setup.exe')) {
|
||||||
updateData.platforms.win64.url = browser_download_url;
|
updateData.platforms.win64.url = browser_download_url
|
||||||
updateData.platforms["windows-x86_64"].url = browser_download_url;
|
updateData.platforms['windows-x86_64'].url = browser_download_url
|
||||||
}
|
}
|
||||||
// win64 signature
|
// win64 signature
|
||||||
if (name.endsWith("x64-setup.exe.sig")) {
|
if (name.endsWith('x64-setup.exe.sig')) {
|
||||||
const sig = await getSignature(browser_download_url);
|
const sig = await getSignature(browser_download_url)
|
||||||
updateData.platforms.win64.signature = sig;
|
updateData.platforms.win64.signature = sig
|
||||||
updateData.platforms["windows-x86_64"].signature = sig;
|
updateData.platforms['windows-x86_64'].signature = sig
|
||||||
}
|
}
|
||||||
|
|
||||||
// win32 url
|
// win32 url
|
||||||
if (name.endsWith("x86-setup.exe")) {
|
if (name.endsWith('x86-setup.exe')) {
|
||||||
updateData.platforms["windows-x86"].url = browser_download_url;
|
updateData.platforms['windows-x86'].url = browser_download_url
|
||||||
updateData.platforms["windows-i686"].url = browser_download_url;
|
updateData.platforms['windows-i686'].url = browser_download_url
|
||||||
}
|
}
|
||||||
// win32 signature
|
// win32 signature
|
||||||
if (name.endsWith("x86-setup.exe.sig")) {
|
if (name.endsWith('x86-setup.exe.sig')) {
|
||||||
const sig = await getSignature(browser_download_url);
|
const sig = await getSignature(browser_download_url)
|
||||||
updateData.platforms["windows-x86"].signature = sig;
|
updateData.platforms['windows-x86'].signature = sig
|
||||||
updateData.platforms["windows-i686"].signature = sig;
|
updateData.platforms['windows-i686'].signature = sig
|
||||||
}
|
}
|
||||||
|
|
||||||
// win arm url
|
// win arm url
|
||||||
if (name.endsWith("arm64-setup.exe")) {
|
if (name.endsWith('arm64-setup.exe')) {
|
||||||
updateData.platforms["windows-aarch64"].url = browser_download_url;
|
updateData.platforms['windows-aarch64'].url = browser_download_url
|
||||||
}
|
}
|
||||||
// win arm signature
|
// win arm signature
|
||||||
if (name.endsWith("arm64-setup.exe.sig")) {
|
if (name.endsWith('arm64-setup.exe.sig')) {
|
||||||
const sig = await getSignature(browser_download_url);
|
const sig = await getSignature(browser_download_url)
|
||||||
updateData.platforms["windows-aarch64"].signature = sig;
|
updateData.platforms['windows-aarch64'].signature = sig
|
||||||
}
|
}
|
||||||
|
|
||||||
// darwin url (intel)
|
// darwin url (intel)
|
||||||
if (name.endsWith(".app.tar.gz") && !name.includes("aarch")) {
|
if (name.endsWith('.app.tar.gz') && !name.includes('aarch')) {
|
||||||
updateData.platforms.darwin.url = browser_download_url;
|
updateData.platforms.darwin.url = browser_download_url
|
||||||
updateData.platforms["darwin-intel"].url = browser_download_url;
|
updateData.platforms['darwin-intel'].url = browser_download_url
|
||||||
updateData.platforms["darwin-x86_64"].url = browser_download_url;
|
updateData.platforms['darwin-x86_64'].url = browser_download_url
|
||||||
}
|
}
|
||||||
// darwin signature (intel)
|
// darwin signature (intel)
|
||||||
if (name.endsWith(".app.tar.gz.sig") && !name.includes("aarch")) {
|
if (name.endsWith('.app.tar.gz.sig') && !name.includes('aarch')) {
|
||||||
const sig = await getSignature(browser_download_url);
|
const sig = await getSignature(browser_download_url)
|
||||||
updateData.platforms.darwin.signature = sig;
|
updateData.platforms.darwin.signature = sig
|
||||||
updateData.platforms["darwin-intel"].signature = sig;
|
updateData.platforms['darwin-intel'].signature = sig
|
||||||
updateData.platforms["darwin-x86_64"].signature = sig;
|
updateData.platforms['darwin-x86_64'].signature = sig
|
||||||
}
|
}
|
||||||
|
|
||||||
// darwin url (aarch)
|
// darwin url (aarch)
|
||||||
if (name.endsWith("aarch64.app.tar.gz")) {
|
if (name.endsWith('aarch64.app.tar.gz')) {
|
||||||
updateData.platforms["darwin-aarch64"].url = browser_download_url;
|
updateData.platforms['darwin-aarch64'].url = browser_download_url
|
||||||
// 使linux可以检查更新
|
// 使linux可以检查更新
|
||||||
updateData.platforms.linux.url = browser_download_url;
|
updateData.platforms.linux.url = browser_download_url
|
||||||
updateData.platforms["linux-x86_64"].url = browser_download_url;
|
updateData.platforms['linux-x86_64'].url = browser_download_url
|
||||||
updateData.platforms["linux-x86"].url = browser_download_url;
|
updateData.platforms['linux-x86'].url = browser_download_url
|
||||||
updateData.platforms["linux-i686"].url = browser_download_url;
|
updateData.platforms['linux-i686'].url = browser_download_url
|
||||||
updateData.platforms["linux-aarch64"].url = browser_download_url;
|
updateData.platforms['linux-aarch64'].url = browser_download_url
|
||||||
updateData.platforms["linux-armv7"].url = browser_download_url;
|
updateData.platforms['linux-armv7'].url = browser_download_url
|
||||||
}
|
}
|
||||||
// darwin signature (aarch)
|
// darwin signature (aarch)
|
||||||
if (name.endsWith("aarch64.app.tar.gz.sig")) {
|
if (name.endsWith('aarch64.app.tar.gz.sig')) {
|
||||||
const sig = await getSignature(browser_download_url);
|
const sig = await getSignature(browser_download_url)
|
||||||
updateData.platforms["darwin-aarch64"].signature = sig;
|
updateData.platforms['darwin-aarch64'].signature = sig
|
||||||
updateData.platforms.linux.signature = sig;
|
updateData.platforms.linux.signature = sig
|
||||||
updateData.platforms["linux-x86_64"].signature = sig;
|
updateData.platforms['linux-x86_64'].signature = sig
|
||||||
updateData.platforms["linux-x86"].url = browser_download_url;
|
updateData.platforms['linux-x86'].url = browser_download_url
|
||||||
updateData.platforms["linux-i686"].url = browser_download_url;
|
updateData.platforms['linux-i686'].url = browser_download_url
|
||||||
updateData.platforms["linux-aarch64"].signature = sig;
|
updateData.platforms['linux-aarch64'].signature = sig
|
||||||
updateData.platforms["linux-armv7"].signature = sig;
|
updateData.platforms['linux-armv7'].signature = sig
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
await Promise.allSettled(promises);
|
await Promise.allSettled(promises)
|
||||||
console.log(updateData);
|
console.log(updateData)
|
||||||
|
|
||||||
// maybe should test the signature as well
|
// maybe should test the signature as well
|
||||||
// delete the null field
|
// delete the null field
|
||||||
Object.entries(updateData.platforms).forEach(([key, value]) => {
|
Object.entries(updateData.platforms).forEach(([key, value]) => {
|
||||||
if (!value.url) {
|
if (!value.url) {
|
||||||
console.log(`[Error]: failed to parse release for "${key}"`);
|
console.log(`[Error]: failed to parse release for "${key}"`)
|
||||||
delete updateData.platforms[key];
|
delete updateData.platforms[key]
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
// Generate a proxy update file for accelerated GitHub resources
|
// Generate a proxy update file for accelerated GitHub resources
|
||||||
const updateDataNew = JSON.parse(JSON.stringify(updateData));
|
const updateDataNew = JSON.parse(JSON.stringify(updateData))
|
||||||
|
|
||||||
Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
|
Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
|
||||||
if (value.url) {
|
if (value.url) {
|
||||||
updateDataNew.platforms[key].url =
|
updateDataNew.platforms[key].url =
|
||||||
"https://download.clashverge.dev/" + value.url;
|
'https://download.clashverge.dev/' + value.url
|
||||||
} else {
|
} else {
|
||||||
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
|
console.log(`[Error]: updateDataNew.platforms.${key} is null`)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
// Get the appropriate updater release based on isAlpha flag
|
// Get the appropriate updater release based on isAlpha flag
|
||||||
const releaseTag = isAlpha ? ALPHA_TAG_NAME : UPDATE_TAG_NAME;
|
const releaseTag = isAlpha ? ALPHA_TAG_NAME : UPDATE_TAG_NAME
|
||||||
console.log(
|
console.log(
|
||||||
`Processing ${isAlpha ? "alpha" : "stable"} release:`,
|
`Processing ${isAlpha ? 'alpha' : 'stable'} release:`,
|
||||||
releaseTag,
|
releaseTag,
|
||||||
);
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let updateRelease;
|
let updateRelease
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Try to get the existing release
|
// Try to get the existing release
|
||||||
const response = await github.rest.repos.getReleaseByTag({
|
const response = await github.rest.repos.getReleaseByTag({
|
||||||
...options,
|
...options,
|
||||||
tag: releaseTag,
|
tag: releaseTag,
|
||||||
});
|
})
|
||||||
updateRelease = response.data;
|
updateRelease = response.data
|
||||||
console.log(
|
console.log(
|
||||||
`Found existing ${releaseTag} release with ID: ${updateRelease.id}`,
|
`Found existing ${releaseTag} release with ID: ${updateRelease.id}`,
|
||||||
);
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If release doesn't exist, create it
|
// If release doesn't exist, create it
|
||||||
if (error.status === 404) {
|
if (error.status === 404) {
|
||||||
console.log(
|
console.log(
|
||||||
`Release with tag ${releaseTag} not found, creating new release...`,
|
`Release with tag ${releaseTag} not found, creating new release...`,
|
||||||
);
|
)
|
||||||
const createResponse = await github.rest.repos.createRelease({
|
const createResponse = await github.rest.repos.createRelease({
|
||||||
...options,
|
...options,
|
||||||
tag_name: releaseTag,
|
tag_name: releaseTag,
|
||||||
name: isAlpha
|
name: isAlpha
|
||||||
? "Auto-update Alpha Channel"
|
? 'Auto-update Alpha Channel'
|
||||||
: "Auto-update Stable Channel",
|
: 'Auto-update Stable Channel',
|
||||||
body: `This release contains the update information for ${isAlpha ? "alpha" : "stable"} channel.`,
|
body: `This release contains the update information for ${isAlpha ? 'alpha' : 'stable'} channel.`,
|
||||||
prerelease: isAlpha,
|
prerelease: isAlpha,
|
||||||
});
|
})
|
||||||
updateRelease = createResponse.data;
|
updateRelease = createResponse.data
|
||||||
console.log(
|
console.log(
|
||||||
`Created new ${releaseTag} release with ID: ${updateRelease.id}`,
|
`Created new ${releaseTag} release with ID: ${updateRelease.id}`,
|
||||||
);
|
)
|
||||||
} else {
|
} else {
|
||||||
// If it's another error, throw it
|
// If it's another error, throw it
|
||||||
throw error;
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// File names based on release type
|
// File names based on release type
|
||||||
const jsonFile = isAlpha ? ALPHA_UPDATE_JSON_FILE : UPDATE_JSON_FILE;
|
const jsonFile = isAlpha ? ALPHA_UPDATE_JSON_FILE : UPDATE_JSON_FILE
|
||||||
const proxyFile = isAlpha ? ALPHA_UPDATE_JSON_PROXY : UPDATE_JSON_PROXY;
|
const proxyFile = isAlpha ? ALPHA_UPDATE_JSON_PROXY : UPDATE_JSON_PROXY
|
||||||
|
|
||||||
// Delete existing assets with these names
|
// Delete existing assets with these names
|
||||||
for (const asset of updateRelease.assets) {
|
for (const asset of updateRelease.assets) {
|
||||||
@@ -265,13 +265,13 @@ async function processRelease(github, options, tag, isAlpha) {
|
|||||||
await github.rest.repos.deleteReleaseAsset({
|
await github.rest.repos.deleteReleaseAsset({
|
||||||
...options,
|
...options,
|
||||||
asset_id: asset.id,
|
asset_id: asset.id,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (asset.name === proxyFile) {
|
if (asset.name === proxyFile) {
|
||||||
await github.rest.repos
|
await github.rest.repos
|
||||||
.deleteReleaseAsset({ ...options, asset_id: asset.id })
|
.deleteReleaseAsset({ ...options, asset_id: asset.id })
|
||||||
.catch(console.error); // do not break the pipeline
|
.catch(console.error) // do not break the pipeline
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,32 +281,29 @@ async function processRelease(github, options, tag, isAlpha) {
|
|||||||
release_id: updateRelease.id,
|
release_id: updateRelease.id,
|
||||||
name: jsonFile,
|
name: jsonFile,
|
||||||
data: JSON.stringify(updateData, null, 2),
|
data: JSON.stringify(updateData, null, 2),
|
||||||
});
|
})
|
||||||
|
|
||||||
await github.rest.repos.uploadReleaseAsset({
|
await github.rest.repos.uploadReleaseAsset({
|
||||||
...options,
|
...options,
|
||||||
release_id: updateRelease.id,
|
release_id: updateRelease.id,
|
||||||
name: proxyFile,
|
name: proxyFile,
|
||||||
data: JSON.stringify(updateDataNew, null, 2),
|
data: JSON.stringify(updateDataNew, null, 2),
|
||||||
});
|
})
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Successfully uploaded ${isAlpha ? "alpha" : "stable"} update files to ${releaseTag}`,
|
`Successfully uploaded ${isAlpha ? 'alpha' : 'stable'} update files to ${releaseTag}`,
|
||||||
);
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
`Failed to process ${isAlpha ? "alpha" : "stable"} release:`,
|
`Failed to process ${isAlpha ? 'alpha' : 'stable'} release:`,
|
||||||
error.message,
|
error.message,
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.status === 404) {
|
if (error.status === 404) {
|
||||||
console.log(`Release not found for tag: ${tag.name}, skipping...`);
|
console.log(`Release not found for tag: ${tag.name}, skipping...`)
|
||||||
} else {
|
} else {
|
||||||
console.error(
|
console.error(`Failed to get release for tag: ${tag.name}`, error.message)
|
||||||
`Failed to get release for tag: ${tag.name}`,
|
|
||||||
error.message,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -314,11 +311,11 @@ async function processRelease(github, options, tag, isAlpha) {
|
|||||||
// get the signature file content
|
// get the signature file content
|
||||||
async function getSignature(url) {
|
async function getSignature(url) {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: "GET",
|
method: 'GET',
|
||||||
headers: { "Content-Type": "application/octet-stream" },
|
headers: { 'Content-Type': 'application/octet-stream' },
|
||||||
});
|
})
|
||||||
|
|
||||||
return response.text();
|
return response.text()
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveUpdater().catch(console.error);
|
resolveUpdater().catch(console.error)
|
||||||
|
|||||||
+6
-6
@@ -1,11 +1,11 @@
|
|||||||
import clc from "cli-color";
|
import clc from 'cli-color'
|
||||||
|
|
||||||
export const log_success = (msg, ...optionalParams) =>
|
export const log_success = (msg, ...optionalParams) =>
|
||||||
console.log(clc.green(msg), ...optionalParams);
|
console.log(clc.green(msg), ...optionalParams)
|
||||||
export const log_error = (msg, ...optionalParams) =>
|
export const log_error = (msg, ...optionalParams) =>
|
||||||
console.log(clc.red(msg), ...optionalParams);
|
console.log(clc.red(msg), ...optionalParams)
|
||||||
export const log_info = (msg, ...optionalParams) =>
|
export const log_info = (msg, ...optionalParams) =>
|
||||||
console.log(clc.bgBlue(msg), ...optionalParams);
|
console.log(clc.bgBlue(msg), ...optionalParams)
|
||||||
var debugMsg = clc.xterm(245);
|
var debugMsg = clc.xterm(245)
|
||||||
export const log_debug = (msg, ...optionalParams) =>
|
export const log_debug = (msg, ...optionalParams) =>
|
||||||
console.log(debugMsg(msg), ...optionalParams);
|
console.log(debugMsg(msg), ...optionalParams)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// This function is exported for use by the Clash core
|
// This function is exported for use by the Clash core
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||||
function main(config, _name) {
|
function main(config, _name) {
|
||||||
if (config.mode === "script") {
|
if (config.mode === 'script') {
|
||||||
config.mode = "rule";
|
config.mode = 'rule'
|
||||||
}
|
}
|
||||||
return config;
|
return config
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
// This function is exported for use by the Clash core
|
// This function is exported for use by the Clash core
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||||
function main(config, _name) {
|
function main(config, _name) {
|
||||||
if (Array.isArray(config.proxies)) {
|
if (Array.isArray(config.proxies)) {
|
||||||
config.proxies.forEach((p, i) => {
|
config.proxies.forEach((p, i) => {
|
||||||
if (p.type === "hysteria" && typeof p.alpn === "string") {
|
if (p.type === 'hysteria' && typeof p.alpn === 'string') {
|
||||||
config.proxies[i].alpn = [p.alpn];
|
config.proxies[i].alpn = [p.alpn]
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
return config;
|
return config
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: "twemoji mozilla";
|
font-family: 'twemoji mozilla';
|
||||||
src: url("../fonts/Twemoji.Mozilla.ttf");
|
src: url('../fonts/Twemoji.Mozilla.ttf');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
@use "./layout.scss";
|
@use './layout.scss';
|
||||||
@use "./page.scss";
|
@use './page.scss';
|
||||||
@use "./font.scss";
|
@use './font.scss';
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family:
|
font-family:
|
||||||
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
|
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
|
||||||
"Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LoadingButton } from "@mui/lab";
|
import { LoadingButton } from '@mui/lab'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -7,29 +7,29 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
type SxProps,
|
type SxProps,
|
||||||
type Theme,
|
type Theme,
|
||||||
} from "@mui/material";
|
} from '@mui/material'
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: ReactNode;
|
title: ReactNode
|
||||||
open: boolean;
|
open: boolean
|
||||||
okBtn?: ReactNode;
|
okBtn?: ReactNode
|
||||||
cancelBtn?: ReactNode;
|
cancelBtn?: ReactNode
|
||||||
disableEnforceFocus?: boolean;
|
disableEnforceFocus?: boolean
|
||||||
disableOk?: boolean;
|
disableOk?: boolean
|
||||||
disableCancel?: boolean;
|
disableCancel?: boolean
|
||||||
disableFooter?: boolean;
|
disableFooter?: boolean
|
||||||
contentSx?: SxProps<Theme>;
|
contentSx?: SxProps<Theme>
|
||||||
children?: ReactNode;
|
children?: ReactNode
|
||||||
loading?: boolean;
|
loading?: boolean
|
||||||
onOk?: () => void;
|
onOk?: () => void
|
||||||
onCancel?: () => void;
|
onCancel?: () => void
|
||||||
onClose?: () => void;
|
onClose?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DialogRef {
|
export interface DialogRef {
|
||||||
open: () => void;
|
open: () => void
|
||||||
close: () => void;
|
close: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BaseDialog: React.FC<Props> = ({
|
export const BaseDialog: React.FC<Props> = ({
|
||||||
@@ -73,5 +73,5 @@ export const BaseDialog: React.FC<Props> = ({
|
|||||||
</DialogActions>
|
</DialogActions>
|
||||||
)}
|
)}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,40 +1,40 @@
|
|||||||
import { InboxRounded } from "@mui/icons-material";
|
import { InboxRounded } from '@mui/icons-material'
|
||||||
import { alpha, Box, Typography } from "@mui/material";
|
import { alpha, Box, Typography } from '@mui/material'
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from 'react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import type { TranslationKey } from "@/types/generated/i18n-keys";
|
import type { TranslationKey } from '@/types/generated/i18n-keys'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
text?: ReactNode;
|
text?: ReactNode
|
||||||
textKey?: TranslationKey;
|
textKey?: TranslationKey
|
||||||
extra?: ReactNode;
|
extra?: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BaseEmpty = ({
|
export const BaseEmpty = ({
|
||||||
text,
|
text,
|
||||||
textKey = "shared.statuses.empty",
|
textKey = 'shared.statuses.empty',
|
||||||
extra,
|
extra,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const resolvedText: ReactNode = text !== undefined ? text : t(textKey);
|
const resolvedText: ReactNode = text !== undefined ? text : t(textKey)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={({ palette }) => ({
|
sx={({ palette }) => ({
|
||||||
width: "100%",
|
width: '100%',
|
||||||
height: "100%",
|
height: '100%',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
flexDirection: "column",
|
flexDirection: 'column',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
color: alpha(palette.text.secondary, 0.75),
|
color: alpha(palette.text.secondary, 0.75),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<InboxRounded sx={{ fontSize: "4em" }} />
|
<InboxRounded sx={{ fontSize: '4em' }} />
|
||||||
<Typography sx={{ fontSize: "1.25em" }}>{resolvedText}</Typography>
|
<Typography sx={{ fontSize: '1.25em' }}>{resolvedText}</Typography>
|
||||||
{extra}
|
{extra}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { ReactNode } from "react";
|
import { ReactNode } from 'react'
|
||||||
import { ErrorBoundary, FallbackProps } from "react-error-boundary";
|
import { ErrorBoundary, FallbackProps } from 'react-error-boundary'
|
||||||
|
|
||||||
function ErrorFallback({ error }: FallbackProps) {
|
function ErrorFallback({ error }: FallbackProps) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
const errorStack = error instanceof Error ? error.stack : undefined;
|
const errorStack = error instanceof Error ? error.stack : undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div role="alert" style={{ padding: 16 }}>
|
<div role="alert" style={{ padding: 16 }}>
|
||||||
@@ -16,15 +16,15 @@ function ErrorFallback({ error }: FallbackProps) {
|
|||||||
<pre>{errorStack}</pre>
|
<pre>{errorStack}</pre>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children?: ReactNode;
|
children?: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BaseErrorBoundary = ({ children }: Props) => {
|
export const BaseErrorBoundary = ({ children }: Props) => {
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary FallbackComponent={ErrorFallback}>{children}</ErrorBoundary>
|
<ErrorBoundary FallbackComponent={ErrorFallback}>{children}</ErrorBoundary>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Box, styled } from "@mui/material";
|
import { Box, styled } from '@mui/material'
|
||||||
import React from "react";
|
import React from 'react'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label: string;
|
label: string
|
||||||
fontSize?: string;
|
fontSize?: string
|
||||||
width?: string;
|
width?: string
|
||||||
padding?: string;
|
padding?: string
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode
|
||||||
};
|
}
|
||||||
|
|
||||||
export const BaseFieldset: React.FC<Props> = ({
|
export const BaseFieldset: React.FC<Props> = ({
|
||||||
label,
|
label,
|
||||||
@@ -17,28 +17,28 @@ export const BaseFieldset: React.FC<Props> = ({
|
|||||||
children,
|
children,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const Fieldset = styled(Box)<{ component?: string }>(() => ({
|
const Fieldset = styled(Box)<{ component?: string }>(() => ({
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
border: "1px solid #bbb",
|
border: '1px solid #bbb',
|
||||||
borderRadius: "5px",
|
borderRadius: '5px',
|
||||||
width: width ?? "auto",
|
width: width ?? 'auto',
|
||||||
padding: padding ?? "15px",
|
padding: padding ?? '15px',
|
||||||
}));
|
}))
|
||||||
|
|
||||||
const Label = styled("legend")(({ theme }) => ({
|
const Label = styled('legend')(({ theme }) => ({
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
top: "-10px",
|
top: '-10px',
|
||||||
left: padding ?? "15px",
|
left: padding ?? '15px',
|
||||||
backgroundColor: theme.palette.background.paper,
|
backgroundColor: theme.palette.background.paper,
|
||||||
backgroundImage:
|
backgroundImage:
|
||||||
"linear-gradient(rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.16))",
|
'linear-gradient(rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.16))',
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
fontSize: fontSize ?? "1em",
|
fontSize: fontSize ?? '1em',
|
||||||
}));
|
}))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fieldset component="fieldset">
|
<Fieldset component="fieldset">
|
||||||
<Label>{label}</Label>
|
<Label>{label}</Label>
|
||||||
{children}
|
{children}
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,35 +1,35 @@
|
|||||||
import { Box, CircularProgress } from "@mui/material";
|
import { Box, CircularProgress } from '@mui/material'
|
||||||
import React from "react";
|
import React from 'react'
|
||||||
|
|
||||||
interface BaseLoadingOverlayProps {
|
interface BaseLoadingOverlayProps {
|
||||||
isLoading: boolean;
|
isLoading: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BaseLoadingOverlay: React.FC<BaseLoadingOverlayProps> = ({
|
export const BaseLoadingOverlay: React.FC<BaseLoadingOverlayProps> = ({
|
||||||
isLoading,
|
isLoading,
|
||||||
}) => {
|
}) => {
|
||||||
if (!isLoading) return null;
|
if (!isLoading) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
// Respect current theme; avoid bright flash in dark mode
|
// Respect current theme; avoid bright flash in dark mode
|
||||||
backgroundColor: (theme) =>
|
backgroundColor: (theme) =>
|
||||||
theme.palette.mode === "dark"
|
theme.palette.mode === 'dark'
|
||||||
? "rgba(0, 0, 0, 0.5)"
|
? 'rgba(0, 0, 0, 0.5)'
|
||||||
: "rgba(255, 255, 255, 0.7)",
|
: 'rgba(255, 255, 255, 0.7)',
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { styled } from "@mui/material";
|
import { styled } from '@mui/material'
|
||||||
|
|
||||||
const Loading = styled("div")`
|
const Loading = styled('div')`
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -31,11 +31,11 @@ const Loading = styled("div")`
|
|||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
const LoadingItem = styled("div")(({ theme }) => ({
|
const LoadingItem = styled('div')(({ theme }) => ({
|
||||||
background: theme.palette.text.secondary,
|
background: theme.palette.text.secondary,
|
||||||
}));
|
}))
|
||||||
|
|
||||||
export const BaseLoading = () => {
|
export const BaseLoading = () => {
|
||||||
return (
|
return (
|
||||||
@@ -44,5 +44,5 @@ export const BaseLoading = () => {
|
|||||||
<LoadingItem />
|
<LoadingItem />
|
||||||
<LoadingItem />
|
<LoadingItem />
|
||||||
</Loading>
|
</Loading>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
import { Typography } from "@mui/material";
|
import { Typography } from '@mui/material'
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from '@mui/material/styles'
|
||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from 'react'
|
||||||
|
|
||||||
import { BaseErrorBoundary } from "./base-error-boundary";
|
import { BaseErrorBoundary } from './base-error-boundary'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: React.ReactNode; // the page title
|
title?: React.ReactNode // the page title
|
||||||
header?: React.ReactNode; // something behind title
|
header?: React.ReactNode // something behind title
|
||||||
contentStyle?: React.CSSProperties;
|
contentStyle?: React.CSSProperties
|
||||||
children?: ReactNode;
|
children?: ReactNode
|
||||||
full?: boolean;
|
full?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BasePage: React.FC<Props> = (props) => {
|
export const BasePage: React.FC<Props> = (props) => {
|
||||||
const { title, header, contentStyle, full, children } = props;
|
const { title, header, contentStyle, full, children } = props
|
||||||
const theme = useTheme();
|
const theme = useTheme()
|
||||||
|
|
||||||
const isDark = theme.palette.mode === "dark";
|
const isDark = theme.palette.mode === 'dark'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseErrorBoundary>
|
<BaseErrorBoundary>
|
||||||
<div className="base-page">
|
<div className="base-page">
|
||||||
<header data-tauri-drag-region="true" style={{ userSelect: "none" }}>
|
<header data-tauri-drag-region="true" style={{ userSelect: 'none' }}>
|
||||||
<Typography
|
<Typography
|
||||||
sx={{ fontSize: "20px", fontWeight: "700 " }}
|
sx={{ fontSize: '20px', fontWeight: '700 ' }}
|
||||||
data-tauri-drag-region="true"
|
data-tauri-drag-region="true"
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
@@ -33,12 +33,12 @@ export const BasePage: React.FC<Props> = (props) => {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={full ? "base-container no-padding" : "base-container"}
|
className={full ? 'base-container no-padding' : 'base-container'}
|
||||||
style={{ backgroundColor: isDark ? "#1e1f27" : "#ffffff" }}
|
style={{ backgroundColor: isDark ? '#1e1f27' : '#ffffff' }}
|
||||||
>
|
>
|
||||||
<section
|
<section
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: isDark ? "#1e1f27" : "var(--background-color)",
|
backgroundColor: isDark ? '#1e1f27' : 'var(--background-color)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="base-content" style={contentStyle}>
|
<div className="base-content" style={contentStyle}>
|
||||||
@@ -48,5 +48,5 @@ export const BasePage: React.FC<Props> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BaseErrorBoundary>
|
</BaseErrorBoundary>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ClearRounded } from "@mui/icons-material";
|
import { ClearRounded } from '@mui/icons-material'
|
||||||
import { Box, SvgIcon, TextField, styled, IconButton } from "@mui/material";
|
import { Box, SvgIcon, TextField, styled, IconButton } from '@mui/material'
|
||||||
import Tooltip from "@mui/material/Tooltip";
|
import Tooltip from '@mui/material/Tooltip'
|
||||||
import {
|
import {
|
||||||
ChangeEvent,
|
ChangeEvent,
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -8,67 +8,67 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from 'react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import matchCaseIcon from "@/assets/image/component/match_case.svg?react";
|
import matchCaseIcon from '@/assets/image/component/match_case.svg?react'
|
||||||
import matchWholeWordIcon from "@/assets/image/component/match_whole_word.svg?react";
|
import matchWholeWordIcon from '@/assets/image/component/match_whole_word.svg?react'
|
||||||
import UseRegularExpressionIcon from "@/assets/image/component/use_regular_expression.svg?react";
|
import UseRegularExpressionIcon from '@/assets/image/component/use_regular_expression.svg?react'
|
||||||
import { buildRegex, compileStringMatcher } from "@/utils/search-matcher";
|
import { buildRegex, compileStringMatcher } from '@/utils/search-matcher'
|
||||||
|
|
||||||
export type SearchState = {
|
export type SearchState = {
|
||||||
text: string;
|
text: string
|
||||||
matchCase: boolean;
|
matchCase: boolean
|
||||||
matchWholeWord: boolean;
|
matchWholeWord: boolean
|
||||||
useRegularExpression: boolean;
|
useRegularExpression: boolean
|
||||||
};
|
}
|
||||||
|
|
||||||
type SearchOptionState = Omit<SearchState, "text">;
|
type SearchOptionState = Omit<SearchState, 'text'>
|
||||||
|
|
||||||
type SearchProps = {
|
type SearchProps = {
|
||||||
value?: string;
|
value?: string
|
||||||
defaultValue?: string;
|
defaultValue?: string
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean
|
||||||
placeholder?: string;
|
placeholder?: string
|
||||||
matchCase?: boolean;
|
matchCase?: boolean
|
||||||
matchWholeWord?: boolean;
|
matchWholeWord?: boolean
|
||||||
useRegularExpression?: boolean;
|
useRegularExpression?: boolean
|
||||||
searchState?: Partial<SearchOptionState>;
|
searchState?: Partial<SearchOptionState>
|
||||||
onSearch: (match: (content: string) => boolean, state: SearchState) => void;
|
onSearch: (match: (content: string) => boolean, state: SearchState) => void
|
||||||
};
|
}
|
||||||
|
|
||||||
const StyledTextField = styled(TextField)(({ theme }) => ({
|
const StyledTextField = styled(TextField)(({ theme }) => ({
|
||||||
"& .MuiInputBase-root": {
|
'& .MuiInputBase-root': {
|
||||||
background: theme.palette.mode === "light" ? "#fff" : undefined,
|
background: theme.palette.mode === 'light' ? '#fff' : undefined,
|
||||||
paddingRight: "4px",
|
paddingRight: '4px',
|
||||||
},
|
},
|
||||||
"& .MuiInputBase-root svg[aria-label='active'] path": {
|
"& .MuiInputBase-root svg[aria-label='active'] path": {
|
||||||
fill: theme.palette.primary.light,
|
fill: theme.palette.primary.light,
|
||||||
},
|
},
|
||||||
"& .MuiInputBase-root svg[aria-label='inactive'] path": {
|
"& .MuiInputBase-root svg[aria-label='inactive'] path": {
|
||||||
fill: "#A7A7A7",
|
fill: '#A7A7A7',
|
||||||
},
|
},
|
||||||
}));
|
}))
|
||||||
|
|
||||||
const useControllableState = <T,>(options: {
|
const useControllableState = <T,>(options: {
|
||||||
controlled: T | undefined;
|
controlled: T | undefined
|
||||||
defaultValue: T;
|
defaultValue: T
|
||||||
}) => {
|
}) => {
|
||||||
const { controlled, defaultValue } = options;
|
const { controlled, defaultValue } = options
|
||||||
const [uncontrolled, setUncontrolled] = useState(defaultValue);
|
const [uncontrolled, setUncontrolled] = useState(defaultValue)
|
||||||
const isControlled = controlled !== undefined;
|
const isControlled = controlled !== undefined
|
||||||
|
|
||||||
const value = isControlled ? controlled : uncontrolled;
|
const value = isControlled ? controlled : uncontrolled
|
||||||
|
|
||||||
const setValue = useCallback(
|
const setValue = useCallback(
|
||||||
(next: T) => {
|
(next: T) => {
|
||||||
if (!isControlled) setUncontrolled(next);
|
if (!isControlled) setUncontrolled(next)
|
||||||
},
|
},
|
||||||
[isControlled],
|
[isControlled],
|
||||||
);
|
)
|
||||||
|
|
||||||
return [value, setValue] as const;
|
return [value, setValue] as const
|
||||||
};
|
}
|
||||||
|
|
||||||
export const BaseSearchBox = ({
|
export const BaseSearchBox = ({
|
||||||
value,
|
value,
|
||||||
@@ -81,113 +81,113 @@ export const BaseSearchBox = ({
|
|||||||
useRegularExpression: defaultUseRegularExpression = false,
|
useRegularExpression: defaultUseRegularExpression = false,
|
||||||
onSearch,
|
onSearch,
|
||||||
}: SearchProps) => {
|
}: SearchProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const onSearchRef = useRef(onSearch);
|
const onSearchRef = useRef(onSearch)
|
||||||
const lastSearchStateRef = useRef<SearchState | null>(null);
|
const lastSearchStateRef = useRef<SearchState | null>(null)
|
||||||
|
|
||||||
const [text, setText] = useControllableState<string>({
|
const [text, setText] = useControllableState<string>({
|
||||||
controlled: value,
|
controlled: value,
|
||||||
defaultValue: defaultValue ?? "",
|
defaultValue: defaultValue ?? '',
|
||||||
});
|
})
|
||||||
|
|
||||||
const [matchCase, setMatchCase] = useControllableState<boolean>({
|
const [matchCase, setMatchCase] = useControllableState<boolean>({
|
||||||
controlled: searchState?.matchCase,
|
controlled: searchState?.matchCase,
|
||||||
defaultValue: defaultMatchCase,
|
defaultValue: defaultMatchCase,
|
||||||
});
|
})
|
||||||
|
|
||||||
const [matchWholeWord, setMatchWholeWord] = useControllableState<boolean>({
|
const [matchWholeWord, setMatchWholeWord] = useControllableState<boolean>({
|
||||||
controlled: searchState?.matchWholeWord,
|
controlled: searchState?.matchWholeWord,
|
||||||
defaultValue: defaultMatchWholeWord,
|
defaultValue: defaultMatchWholeWord,
|
||||||
});
|
})
|
||||||
|
|
||||||
const [useRegularExpression, setUseRegularExpression] =
|
const [useRegularExpression, setUseRegularExpression] =
|
||||||
useControllableState<boolean>({
|
useControllableState<boolean>({
|
||||||
controlled: searchState?.useRegularExpression,
|
controlled: searchState?.useRegularExpression,
|
||||||
defaultValue: defaultUseRegularExpression,
|
defaultValue: defaultUseRegularExpression,
|
||||||
});
|
})
|
||||||
|
|
||||||
const iconStyle = {
|
const iconStyle = {
|
||||||
style: {
|
style: {
|
||||||
height: "24px",
|
height: '24px',
|
||||||
width: "24px",
|
width: '24px',
|
||||||
cursor: "pointer",
|
cursor: 'pointer',
|
||||||
} as React.CSSProperties,
|
} as React.CSSProperties,
|
||||||
inheritViewBox: true,
|
inheritViewBox: true,
|
||||||
};
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onSearchRef.current = onSearch;
|
onSearchRef.current = onSearch
|
||||||
}, [onSearch]);
|
}, [onSearch])
|
||||||
|
|
||||||
const emitSearch = useCallback((nextState: SearchState) => {
|
const emitSearch = useCallback((nextState: SearchState) => {
|
||||||
const prevState = lastSearchStateRef.current;
|
const prevState = lastSearchStateRef.current
|
||||||
const isSameState =
|
const isSameState =
|
||||||
!!prevState &&
|
!!prevState &&
|
||||||
prevState.text === nextState.text &&
|
prevState.text === nextState.text &&
|
||||||
prevState.matchCase === nextState.matchCase &&
|
prevState.matchCase === nextState.matchCase &&
|
||||||
prevState.matchWholeWord === nextState.matchWholeWord &&
|
prevState.matchWholeWord === nextState.matchWholeWord &&
|
||||||
prevState.useRegularExpression === nextState.useRegularExpression;
|
prevState.useRegularExpression === nextState.useRegularExpression
|
||||||
if (isSameState) return;
|
if (isSameState) return
|
||||||
|
|
||||||
const compiled = compileStringMatcher(nextState.text, nextState);
|
const compiled = compileStringMatcher(nextState.text, nextState)
|
||||||
onSearchRef.current(compiled.matcher, nextState);
|
onSearchRef.current(compiled.matcher, nextState)
|
||||||
|
|
||||||
lastSearchStateRef.current = nextState;
|
lastSearchStateRef.current = nextState
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
emitSearch({ text, matchCase, matchWholeWord, useRegularExpression });
|
emitSearch({ text, matchCase, matchWholeWord, useRegularExpression })
|
||||||
}, [emitSearch, matchCase, matchWholeWord, text, useRegularExpression]);
|
}, [emitSearch, matchCase, matchWholeWord, text, useRegularExpression])
|
||||||
|
|
||||||
const effectiveErrorMessage = useMemo(() => {
|
const effectiveErrorMessage = useMemo(() => {
|
||||||
if (!useRegularExpression || !text) return "";
|
if (!useRegularExpression || !text) return ''
|
||||||
const flags = matchCase ? "" : "i";
|
const flags = matchCase ? '' : 'i'
|
||||||
return buildRegex(text, flags) ? "" : t("shared.validation.invalidRegex");
|
return buildRegex(text, flags) ? '' : t('shared.validation.invalidRegex')
|
||||||
}, [matchCase, t, text, useRegularExpression]);
|
}, [matchCase, t, text, useRegularExpression])
|
||||||
|
|
||||||
const handleChangeText = (
|
const handleChangeText = (
|
||||||
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
) => {
|
) => {
|
||||||
const nextText = e.target?.value ?? "";
|
const nextText = e.target?.value ?? ''
|
||||||
setText(nextText);
|
setText(nextText)
|
||||||
emitSearch({
|
emitSearch({
|
||||||
text: nextText,
|
text: nextText,
|
||||||
matchCase,
|
matchCase,
|
||||||
matchWholeWord,
|
matchWholeWord,
|
||||||
useRegularExpression,
|
useRegularExpression,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleToggleUseRegularExpression = () => {
|
const handleToggleUseRegularExpression = () => {
|
||||||
const next = !useRegularExpression;
|
const next = !useRegularExpression
|
||||||
setUseRegularExpression(next);
|
setUseRegularExpression(next)
|
||||||
emitSearch({
|
emitSearch({
|
||||||
text,
|
text,
|
||||||
matchCase,
|
matchCase,
|
||||||
matchWholeWord,
|
matchWholeWord,
|
||||||
useRegularExpression: next,
|
useRegularExpression: next,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleClearInput = () => {
|
const handleClearInput = () => {
|
||||||
setText("");
|
setText('')
|
||||||
emitSearch({ text: "", matchCase, matchWholeWord, useRegularExpression });
|
emitSearch({ text: '', matchCase, matchWholeWord, useRegularExpression })
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleToggleMatchCase = () => {
|
const handleToggleMatchCase = () => {
|
||||||
const next = !matchCase;
|
const next = !matchCase
|
||||||
setMatchCase(next);
|
setMatchCase(next)
|
||||||
emitSearch({ text, matchCase: next, matchWholeWord, useRegularExpression });
|
emitSearch({ text, matchCase: next, matchWholeWord, useRegularExpression })
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleToggleMatchWholeWord = () => {
|
const handleToggleMatchWholeWord = () => {
|
||||||
const next = !matchWholeWord;
|
const next = !matchWholeWord
|
||||||
setMatchWholeWord(next);
|
setMatchWholeWord(next)
|
||||||
emitSearch({ text, matchCase, matchWholeWord: next, useRegularExpression });
|
emitSearch({ text, matchCase, matchWholeWord: next, useRegularExpression })
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={effectiveErrorMessage || ""} placement="bottom-start">
|
<Tooltip title={effectiveErrorMessage || ''} placement="bottom-start">
|
||||||
<StyledTextField
|
<StyledTextField
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
hiddenLabel
|
hiddenLabel
|
||||||
@@ -196,7 +196,7 @@ export const BaseSearchBox = ({
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
placeholder={placeholder ?? t("shared.placeholders.filter")}
|
placeholder={placeholder ?? t('shared.placeholders.filter')}
|
||||||
sx={{ input: { py: 0.65, px: 1.25 } }}
|
sx={{ input: { py: 0.65, px: 1.25 } }}
|
||||||
value={text}
|
value={text}
|
||||||
onChange={handleChangeText}
|
onChange={handleChangeText}
|
||||||
@@ -207,7 +207,7 @@ export const BaseSearchBox = ({
|
|||||||
endAdornment: (
|
endAdornment: (
|
||||||
<Box display="flex">
|
<Box display="flex">
|
||||||
{!!text && (
|
{!!text && (
|
||||||
<Tooltip title={t("shared.placeholders.resetInput")}>
|
<Tooltip title={t('shared.placeholders.resetInput')}>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
{...iconStyle}
|
{...iconStyle}
|
||||||
@@ -217,26 +217,26 @@ export const BaseSearchBox = ({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<Tooltip title={t("shared.placeholders.matchCase")}>
|
<Tooltip title={t('shared.placeholders.matchCase')}>
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
component={matchCaseIcon}
|
component={matchCaseIcon}
|
||||||
{...iconStyle}
|
{...iconStyle}
|
||||||
aria-label={matchCase ? "active" : "inactive"}
|
aria-label={matchCase ? 'active' : 'inactive'}
|
||||||
onClick={handleToggleMatchCase}
|
onClick={handleToggleMatchCase}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t("shared.placeholders.matchWholeWord")}>
|
<Tooltip title={t('shared.placeholders.matchWholeWord')}>
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
component={matchWholeWordIcon}
|
component={matchWholeWordIcon}
|
||||||
{...iconStyle}
|
{...iconStyle}
|
||||||
aria-label={matchWholeWord ? "active" : "inactive"}
|
aria-label={matchWholeWord ? 'active' : 'inactive'}
|
||||||
onClick={handleToggleMatchWholeWord}
|
onClick={handleToggleMatchWholeWord}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t("shared.placeholders.useRegex")}>
|
<Tooltip title={t('shared.placeholders.useRegex')}>
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
component={UseRegularExpressionIcon}
|
component={UseRegularExpressionIcon}
|
||||||
aria-label={useRegularExpression ? "active" : "inactive"}
|
aria-label={useRegularExpression ? 'active' : 'inactive'}
|
||||||
{...iconStyle}
|
{...iconStyle}
|
||||||
onClick={handleToggleUseRegularExpression}
|
onClick={handleToggleUseRegularExpression}
|
||||||
/>
|
/>
|
||||||
@@ -247,5 +247,5 @@ export const BaseSearchBox = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CodeRounded, ViewModuleRounded } from "@mui/icons-material";
|
import { CodeRounded, ViewModuleRounded } from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -8,51 +8,51 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from '@mui/material'
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from 'react'
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from 'react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export type BaseSplitChipEditorMode = "visual" | "advanced";
|
export type BaseSplitChipEditorMode = 'visual' | 'advanced'
|
||||||
|
|
||||||
interface BaseSplitChipEditorProps {
|
interface BaseSplitChipEditorProps {
|
||||||
value?: string;
|
value?: string
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void
|
||||||
disabled?: boolean;
|
disabled?: boolean
|
||||||
error?: boolean;
|
error?: boolean
|
||||||
helperText?: ReactNode;
|
helperText?: ReactNode
|
||||||
placeholder?: string;
|
placeholder?: string
|
||||||
rows?: number;
|
rows?: number
|
||||||
separator?: string;
|
separator?: string
|
||||||
splitPattern?: RegExp;
|
splitPattern?: RegExp
|
||||||
defaultMode?: BaseSplitChipEditorMode;
|
defaultMode?: BaseSplitChipEditorMode
|
||||||
showModeToggle?: boolean;
|
showModeToggle?: boolean
|
||||||
ariaLabel?: string;
|
ariaLabel?: string
|
||||||
addLabel?: ReactNode;
|
addLabel?: ReactNode
|
||||||
emptyLabel?: ReactNode;
|
emptyLabel?: ReactNode
|
||||||
modeLabels?: Partial<Record<BaseSplitChipEditorMode, ReactNode>>;
|
modeLabels?: Partial<Record<BaseSplitChipEditorMode, ReactNode>>
|
||||||
renderHeader?: (modeToggle: ReactNode) => ReactNode;
|
renderHeader?: (modeToggle: ReactNode) => ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_SPLIT_PATTERN = /[,\n;\r]+/;
|
const DEFAULT_SPLIT_PATTERN = /[,\n;\r]+/
|
||||||
|
|
||||||
const splitValue = (value: string, splitPattern: RegExp) =>
|
const splitValue = (value: string, splitPattern: RegExp) =>
|
||||||
value
|
value
|
||||||
.split(splitPattern)
|
.split(splitPattern)
|
||||||
.map((item) => item.trim())
|
.map((item) => item.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean)
|
||||||
|
|
||||||
export const BaseSplitChipEditor = ({
|
export const BaseSplitChipEditor = ({
|
||||||
value = "",
|
value = '',
|
||||||
onChange,
|
onChange,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
error = false,
|
error = false,
|
||||||
helperText,
|
helperText,
|
||||||
placeholder,
|
placeholder,
|
||||||
rows = 4,
|
rows = 4,
|
||||||
separator = ",",
|
separator = ',',
|
||||||
splitPattern = DEFAULT_SPLIT_PATTERN,
|
splitPattern = DEFAULT_SPLIT_PATTERN,
|
||||||
defaultMode = "visual",
|
defaultMode = 'visual',
|
||||||
showModeToggle = true,
|
showModeToggle = true,
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
addLabel,
|
addLabel,
|
||||||
@@ -60,58 +60,58 @@ export const BaseSplitChipEditor = ({
|
|||||||
modeLabels,
|
modeLabels,
|
||||||
renderHeader,
|
renderHeader,
|
||||||
}: BaseSplitChipEditorProps) => {
|
}: BaseSplitChipEditorProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const [mode, setMode] = useState<BaseSplitChipEditorMode>(defaultMode);
|
const [mode, setMode] = useState<BaseSplitChipEditorMode>(defaultMode)
|
||||||
const [draft, setDraft] = useState("");
|
const [draft, setDraft] = useState('')
|
||||||
|
|
||||||
const resolvedLabels = useMemo(
|
const resolvedLabels = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
visual: modeLabels?.visual ?? t("shared.editorModes.visualization"),
|
visual: modeLabels?.visual ?? t('shared.editorModes.visualization'),
|
||||||
advanced: modeLabels?.advanced ?? t("shared.editorModes.advanced"),
|
advanced: modeLabels?.advanced ?? t('shared.editorModes.advanced'),
|
||||||
add: addLabel ?? t("shared.actions.new"),
|
add: addLabel ?? t('shared.actions.new'),
|
||||||
empty: emptyLabel ?? t("shared.statuses.empty"),
|
empty: emptyLabel ?? t('shared.statuses.empty'),
|
||||||
}),
|
}),
|
||||||
[t, modeLabels, addLabel, emptyLabel],
|
[t, modeLabels, addLabel, emptyLabel],
|
||||||
);
|
)
|
||||||
|
|
||||||
const values = useMemo(
|
const values = useMemo(
|
||||||
() => splitValue(value, splitPattern),
|
() => splitValue(value, splitPattern),
|
||||||
[value, splitPattern],
|
[value, splitPattern],
|
||||||
);
|
)
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
const counts = new Map<string, number>();
|
const counts = new Map<string, number>()
|
||||||
return values.map((item) => {
|
return values.map((item) => {
|
||||||
const nextCount = (counts.get(item) ?? 0) + 1;
|
const nextCount = (counts.get(item) ?? 0) + 1
|
||||||
counts.set(item, nextCount);
|
counts.set(item, nextCount)
|
||||||
return {
|
return {
|
||||||
key: `${item}-${nextCount}`,
|
key: `${item}-${nextCount}`,
|
||||||
value: item,
|
value: item,
|
||||||
};
|
}
|
||||||
});
|
})
|
||||||
}, [values]);
|
}, [values])
|
||||||
|
|
||||||
const handleAddDraft = () => {
|
const handleAddDraft = () => {
|
||||||
const nextValues = splitValue(draft, splitPattern);
|
const nextValues = splitValue(draft, splitPattern)
|
||||||
if (!nextValues.length) {
|
if (!nextValues.length) {
|
||||||
return;
|
return
|
||||||
|
}
|
||||||
|
const nextValue = [...values, ...nextValues].join(separator)
|
||||||
|
onChange(nextValue)
|
||||||
|
setDraft('')
|
||||||
}
|
}
|
||||||
const nextValue = [...values, ...nextValues].join(separator);
|
|
||||||
onChange(nextValue);
|
|
||||||
setDraft("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveItem = (index: number) => {
|
const handleRemoveItem = (index: number) => {
|
||||||
const nextValue = values.filter((_, itemIndex) => itemIndex !== index);
|
const nextValue = values.filter((_, itemIndex) => itemIndex !== index)
|
||||||
onChange(nextValue.join(separator));
|
onChange(nextValue.join(separator))
|
||||||
};
|
}
|
||||||
|
|
||||||
const nextMode = mode === "visual" ? "advanced" : "visual";
|
const nextMode = mode === 'visual' ? 'advanced' : 'visual'
|
||||||
const toggleLabel =
|
const toggleLabel =
|
||||||
nextMode === "visual" ? resolvedLabels.visual : resolvedLabels.advanced;
|
nextMode === 'visual' ? resolvedLabels.visual : resolvedLabels.advanced
|
||||||
const ToggleIcon = nextMode === "visual" ? ViewModuleRounded : CodeRounded;
|
const ToggleIcon = nextMode === 'visual' ? ViewModuleRounded : CodeRounded
|
||||||
const resolvedAriaLabel =
|
const resolvedAriaLabel =
|
||||||
ariaLabel ?? (typeof toggleLabel === "string" ? toggleLabel : undefined);
|
ariaLabel ?? (typeof toggleLabel === 'string' ? toggleLabel : undefined)
|
||||||
|
|
||||||
const modeToggle = showModeToggle ? (
|
const modeToggle = showModeToggle ? (
|
||||||
<Tooltip title={toggleLabel}>
|
<Tooltip title={toggleLabel}>
|
||||||
@@ -119,26 +119,26 @@ export const BaseSplitChipEditor = ({
|
|||||||
size="small"
|
size="small"
|
||||||
aria-label={resolvedAriaLabel}
|
aria-label={resolvedAriaLabel}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMode(nextMode);
|
setMode(nextMode)
|
||||||
if (nextMode === "visual") {
|
if (nextMode === 'visual') {
|
||||||
setDraft("");
|
setDraft('')
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ToggleIcon fontSize="small" />
|
<ToggleIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : null;
|
) : null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderHeader ? renderHeader(modeToggle) : modeToggle}
|
{renderHeader ? renderHeader(modeToggle) : modeToggle}
|
||||||
{mode === "visual" ? (
|
{mode === 'visual' ? (
|
||||||
<Box sx={{ padding: "0 2px 5px" }}>
|
<Box sx={{ padding: '0 2px 5px' }}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
flexWrap: "wrap",
|
flexWrap: 'wrap',
|
||||||
gap: 1,
|
gap: 1,
|
||||||
minHeight: 32,
|
minHeight: 32,
|
||||||
}}
|
}}
|
||||||
@@ -161,7 +161,7 @@ export const BaseSplitChipEditor = ({
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
sx={{ display: "flex", gap: 1, marginTop: 1, alignItems: "center" }}
|
sx={{ display: 'flex', gap: 1, marginTop: 1, alignItems: 'center' }}
|
||||||
>
|
>
|
||||||
<TextField
|
<TextField
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -171,14 +171,14 @@ export const BaseSplitChipEditor = ({
|
|||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
error={error}
|
error={error}
|
||||||
sx={{
|
sx={{
|
||||||
"& .MuiInputBase-root": { minHeight: 32 },
|
'& .MuiInputBase-root': { minHeight: 32 },
|
||||||
"& .MuiInputBase-input": { padding: "4px 8px" },
|
'& .MuiInputBase-input': { padding: '4px 8px' },
|
||||||
}}
|
}}
|
||||||
onChange={(event) => setDraft(event.target.value)}
|
onChange={(event) => setDraft(event.target.value)}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === 'Enter') {
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
handleAddDraft();
|
handleAddDraft()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -187,7 +187,7 @@ export const BaseSplitChipEditor = ({
|
|||||||
size="small"
|
size="small"
|
||||||
onClick={handleAddDraft}
|
onClick={handleAddDraft}
|
||||||
disabled={disabled || !draft.trim()}
|
disabled={disabled || !draft.trim()}
|
||||||
sx={{ minHeight: 32, padding: "2px 8px" }}
|
sx={{ minHeight: 32, padding: '2px 8px' }}
|
||||||
>
|
>
|
||||||
{resolvedLabels.add}
|
{resolvedLabels.add}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -203,14 +203,14 @@ export const BaseSplitChipEditor = ({
|
|||||||
size="small"
|
size="small"
|
||||||
multiline
|
multiline
|
||||||
rows={rows}
|
rows={rows}
|
||||||
sx={{ width: "100%" }}
|
sx={{ width: '100%' }}
|
||||||
value={value}
|
value={value}
|
||||||
helperText={helperText}
|
helperText={helperText}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
onChange(event.target.value);
|
onChange(event.target.value)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Select, SelectProps, styled } from "@mui/material";
|
import { Select, SelectProps, styled } from '@mui/material'
|
||||||
|
|
||||||
export const BaseStyledSelect = styled((props: SelectProps<string>) => {
|
export const BaseStyledSelect = styled((props: SelectProps<string>) => {
|
||||||
return (
|
return (
|
||||||
@@ -13,7 +13,7 @@ export const BaseStyledSelect = styled((props: SelectProps<string>) => {
|
|||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
})(({ theme }) => ({
|
})(({ theme }) => ({
|
||||||
background: theme.palette.mode === "light" ? "#fff" : undefined,
|
background: theme.palette.mode === 'light' ? '#fff' : undefined,
|
||||||
}));
|
}))
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { TextField, type TextFieldProps, styled } from "@mui/material";
|
import { TextField, type TextFieldProps, styled } from '@mui/material'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export const BaseStyledTextField = styled((props: TextFieldProps) => {
|
export const BaseStyledTextField = styled((props: TextFieldProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextField
|
<TextField
|
||||||
@@ -12,13 +12,13 @@ export const BaseStyledTextField = styled((props: TextFieldProps) => {
|
|||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
placeholder={t("shared.placeholders.filter")}
|
placeholder={t('shared.placeholders.filter')}
|
||||||
sx={{ input: { py: 0.65, px: 1.25 } }}
|
sx={{ input: { py: 0.65, px: 1.25 } }}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
})(({ theme }) => ({
|
})(({ theme }) => ({
|
||||||
"& .MuiInputBase-root": {
|
'& .MuiInputBase-root': {
|
||||||
background: theme.palette.mode === "light" ? "#fff" : undefined,
|
background: theme.palette.mode === 'light' ? '#fff' : undefined,
|
||||||
},
|
},
|
||||||
}));
|
}))
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { styled } from "@mui/material/styles";
|
import { styled } from '@mui/material/styles'
|
||||||
import { default as MuiSwitch, SwitchProps } from "@mui/material/Switch";
|
import { default as MuiSwitch, SwitchProps } from '@mui/material/Switch'
|
||||||
|
|
||||||
export const Switch = styled((props: SwitchProps) => (
|
export const Switch = styled((props: SwitchProps) => (
|
||||||
<MuiSwitch
|
<MuiSwitch
|
||||||
@@ -12,47 +12,47 @@ export const Switch = styled((props: SwitchProps) => (
|
|||||||
height: 26,
|
height: 26,
|
||||||
padding: 0,
|
padding: 0,
|
||||||
marginRight: 1,
|
marginRight: 1,
|
||||||
"& .MuiSwitch-switchBase": {
|
'& .MuiSwitch-switchBase': {
|
||||||
padding: 0,
|
padding: 0,
|
||||||
margin: 2,
|
margin: 2,
|
||||||
transitionDuration: "300ms",
|
transitionDuration: '300ms',
|
||||||
"&.Mui-checked": {
|
'&.Mui-checked': {
|
||||||
transform: "translateX(16px)",
|
transform: 'translateX(16px)',
|
||||||
color: "#fff",
|
color: '#fff',
|
||||||
"& + .MuiSwitch-track": {
|
'& + .MuiSwitch-track': {
|
||||||
backgroundColor: theme.palette.primary.main,
|
backgroundColor: theme.palette.primary.main,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
border: 0,
|
border: 0,
|
||||||
},
|
},
|
||||||
"&.Mui-disabled + .MuiSwitch-track": {
|
'&.Mui-disabled + .MuiSwitch-track': {
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"&.Mui-focusVisible .MuiSwitch-thumb": {
|
'&.Mui-focusVisible .MuiSwitch-thumb': {
|
||||||
color: "#33cf4d",
|
color: '#33cf4d',
|
||||||
border: "6px solid #fff",
|
border: '6px solid #fff',
|
||||||
},
|
},
|
||||||
"&.Mui-disabled .MuiSwitch-thumb": {
|
'&.Mui-disabled .MuiSwitch-thumb': {
|
||||||
color:
|
color:
|
||||||
theme.palette.mode === "light"
|
theme.palette.mode === 'light'
|
||||||
? theme.palette.grey[100]
|
? theme.palette.grey[100]
|
||||||
: theme.palette.grey[600],
|
: theme.palette.grey[600],
|
||||||
},
|
},
|
||||||
"&.Mui-disabled + .MuiSwitch-track": {
|
'&.Mui-disabled + .MuiSwitch-track': {
|
||||||
opacity: theme.palette.mode === "light" ? 0.7 : 0.3,
|
opacity: theme.palette.mode === 'light' ? 0.7 : 0.3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"& .MuiSwitch-thumb": {
|
'& .MuiSwitch-thumb': {
|
||||||
boxSizing: "border-box",
|
boxSizing: 'border-box',
|
||||||
width: 22,
|
width: 22,
|
||||||
height: 22,
|
height: 22,
|
||||||
},
|
},
|
||||||
"& .MuiSwitch-track": {
|
'& .MuiSwitch-track': {
|
||||||
borderRadius: 26 / 2,
|
borderRadius: 26 / 2,
|
||||||
backgroundColor: theme.palette.mode === "light" ? "#BBBBBB" : "#39393D",
|
backgroundColor: theme.palette.mode === 'light' ? '#BBBBBB' : '#39393D',
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
transition: theme.transitions.create(["background-color"], {
|
transition: theme.transitions.create(['background-color'], {
|
||||||
duration: 500,
|
duration: 500,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}));
|
}))
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
import { InfoRounded } from "@mui/icons-material";
|
import { InfoRounded } from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
IconButton,
|
IconButton,
|
||||||
IconButtonProps,
|
IconButtonProps,
|
||||||
SvgIconProps,
|
SvgIconProps,
|
||||||
} from "@mui/material";
|
} from '@mui/material'
|
||||||
|
|
||||||
interface Props extends IconButtonProps {
|
interface Props extends IconButtonProps {
|
||||||
title?: string;
|
title?: string
|
||||||
icon?: React.ElementType<SvgIconProps>;
|
icon?: React.ElementType<SvgIconProps>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TooltipIcon: React.FC<Props> = (props: Props) => {
|
export const TooltipIcon: React.FC<Props> = (props: Props) => {
|
||||||
const { title = "", icon: Icon = InfoRounded, ...restProps } = props;
|
const { title = '', icon: Icon = InfoRounded, ...restProps } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={title} placement="top">
|
<Tooltip title={title} placement="top">
|
||||||
<IconButton color="inherit" size="small" {...restProps}>
|
<IconButton color="inherit" size="small" {...restProps}>
|
||||||
<Icon fontSize="inherit" style={{ cursor: "pointer", opacity: 0.75 }} />
|
<Icon fontSize="inherit" style={{ cursor: 'pointer', opacity: 0.75 }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
export { BaseDialog, type DialogRef } from "./base-dialog";
|
export { BaseDialog, type DialogRef } from './base-dialog'
|
||||||
export { BaseEmpty } from "./base-empty";
|
export { BaseEmpty } from './base-empty'
|
||||||
export { BaseErrorBoundary } from "./base-error-boundary";
|
export { BaseErrorBoundary } from './base-error-boundary'
|
||||||
export { BaseFieldset } from "./base-fieldset";
|
export { BaseFieldset } from './base-fieldset'
|
||||||
export { BaseLoading } from "./base-loading";
|
export { BaseLoading } from './base-loading'
|
||||||
export { BaseLoadingOverlay } from "./base-loading-overlay";
|
export { BaseLoadingOverlay } from './base-loading-overlay'
|
||||||
export { BasePage } from "./base-page";
|
export { BasePage } from './base-page'
|
||||||
export { BaseSearchBox, type SearchState } from "./base-search-box";
|
export { BaseSearchBox, type SearchState } from './base-search-box'
|
||||||
export {
|
export {
|
||||||
BaseSplitChipEditor,
|
BaseSplitChipEditor,
|
||||||
type BaseSplitChipEditorMode,
|
type BaseSplitChipEditorMode,
|
||||||
} from "./base-split-chip-editor";
|
} from './base-split-chip-editor'
|
||||||
export { BaseStyledSelect } from "./base-styled-select";
|
export { BaseStyledSelect } from './base-styled-select'
|
||||||
export { BaseStyledTextField } from "./base-styled-text-field";
|
export { BaseStyledTextField } from './base-styled-text-field'
|
||||||
export { Switch } from "./base-switch";
|
export { Switch } from './base-switch'
|
||||||
export { TooltipIcon } from "./base-tooltip-icon";
|
export { TooltipIcon } from './base-tooltip-icon'
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import {
|
|||||||
useSensor,
|
useSensor,
|
||||||
useSensors,
|
useSensors,
|
||||||
type DragEndEvent,
|
type DragEndEvent,
|
||||||
} from "@dnd-kit/core";
|
} from '@dnd-kit/core'
|
||||||
import { arrayMove, SortableContext, useSortable } from "@dnd-kit/sortable";
|
import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable'
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
import { DragIndicatorRounded } from "@mui/icons-material";
|
import { DragIndicatorRounded } from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
@@ -20,17 +20,17 @@ import {
|
|||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
} from "@mui/material";
|
} from '@mui/material'
|
||||||
import type { Column } from "@tanstack/react-table";
|
import type { Column } from '@tanstack/react-table'
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from 'react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean
|
||||||
columns: Column<IConnectionsItem, unknown>[];
|
columns: Column<IConnectionsItem, unknown>[]
|
||||||
onClose: () => void;
|
onClose: () => void
|
||||||
onOrderChange: (order: string[]) => void;
|
onOrderChange: (order: string[]) => void
|
||||||
onReset: () => void;
|
onReset: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConnectionColumnManager = ({
|
export const ConnectionColumnManager = ({
|
||||||
@@ -44,34 +44,34 @@ export const ConnectionColumnManager = ({
|
|||||||
useSensor(PointerSensor, {
|
useSensor(PointerSensor, {
|
||||||
activationConstraint: { distance: 6 },
|
activationConstraint: { distance: 6 },
|
||||||
}),
|
}),
|
||||||
);
|
)
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const items = useMemo(() => columns.map((column) => column.id), [columns]);
|
const items = useMemo(() => columns.map((column) => column.id), [columns])
|
||||||
const visibleCount = useMemo(
|
const visibleCount = useMemo(
|
||||||
() => columns.filter((column) => column.getIsVisible()).length,
|
() => columns.filter((column) => column.getIsVisible()).length,
|
||||||
[columns],
|
[columns],
|
||||||
);
|
)
|
||||||
|
|
||||||
const handleDragEnd = useCallback(
|
const handleDragEnd = useCallback(
|
||||||
(event: DragEndEvent) => {
|
(event: DragEndEvent) => {
|
||||||
const { active, over } = event;
|
const { active, over } = event
|
||||||
if (!over || active.id === over.id) return;
|
if (!over || active.id === over.id) return
|
||||||
|
|
||||||
const order = columns.map((column) => column.id);
|
const order = columns.map((column) => column.id)
|
||||||
const oldIndex = order.indexOf(active.id as string);
|
const oldIndex = order.indexOf(active.id as string)
|
||||||
const newIndex = order.indexOf(over.id as string);
|
const newIndex = order.indexOf(over.id as string)
|
||||||
if (oldIndex === -1 || newIndex === -1) return;
|
if (oldIndex === -1 || newIndex === -1) return
|
||||||
|
|
||||||
onOrderChange(arrayMove(order, oldIndex, newIndex));
|
onOrderChange(arrayMove(order, oldIndex, newIndex))
|
||||||
},
|
},
|
||||||
[columns, onOrderChange],
|
[columns, onOrderChange],
|
||||||
);
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onClose} maxWidth="xs" fullWidth>
|
<Dialog open={open} onClose={onClose} maxWidth="xs" fullWidth>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{t("connections.components.columnManager.title")}
|
{t('connections.components.columnManager.title')}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent sx={{ pt: 1 }}>
|
<DialogContent sx={{ pt: 1 }}>
|
||||||
<DndContext
|
<DndContext
|
||||||
@@ -83,7 +83,7 @@ export const ConnectionColumnManager = ({
|
|||||||
<List
|
<List
|
||||||
dense
|
dense
|
||||||
disablePadding
|
disablePadding
|
||||||
sx={{ display: "flex", flexDirection: "column", gap: 1 }}
|
sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}
|
||||||
>
|
>
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<SortableColumnItem
|
<SortableColumnItem
|
||||||
@@ -91,7 +91,7 @@ export const ConnectionColumnManager = ({
|
|||||||
column={column}
|
column={column}
|
||||||
label={getColumnLabel(column)}
|
label={getColumnLabel(column)}
|
||||||
dragHandleLabel={t(
|
dragHandleLabel={t(
|
||||||
"connections.components.columnManager.dragHandle",
|
'connections.components.columnManager.dragHandle',
|
||||||
)}
|
)}
|
||||||
disableToggle={
|
disableToggle={
|
||||||
!column.getCanHide() ||
|
!column.getCanHide() ||
|
||||||
@@ -105,21 +105,21 @@ export const ConnectionColumnManager = ({
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||||
<Button variant="text" onClick={onReset}>
|
<Button variant="text" onClick={onReset}>
|
||||||
{t("shared.actions.resetToDefault")}
|
{t('shared.actions.resetToDefault')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="contained" onClick={onClose}>
|
<Button variant="contained" onClick={onClose}>
|
||||||
{t("shared.actions.close")}
|
{t('shared.actions.close')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
interface SortableColumnItemProps {
|
interface SortableColumnItemProps {
|
||||||
column: Column<IConnectionsItem, unknown>;
|
column: Column<IConnectionsItem, unknown>
|
||||||
label: string;
|
label: string
|
||||||
dragHandleLabel: string;
|
dragHandleLabel: string
|
||||||
disableToggle?: boolean;
|
disableToggle?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const SortableColumnItem = ({
|
const SortableColumnItem = ({
|
||||||
@@ -135,7 +135,7 @@ const SortableColumnItem = ({
|
|||||||
transform,
|
transform,
|
||||||
transition,
|
transition,
|
||||||
isDragging,
|
isDragging,
|
||||||
} = useSortable({ id: column.id });
|
} = useSortable({ id: column.id })
|
||||||
|
|
||||||
const style = useMemo(
|
const style = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -143,7 +143,7 @@ const SortableColumnItem = ({
|
|||||||
transition,
|
transition,
|
||||||
}),
|
}),
|
||||||
[transform, transition],
|
[transform, transition],
|
||||||
);
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
@@ -154,9 +154,9 @@ const SortableColumnItem = ({
|
|||||||
py: 0.5,
|
py: 0.5,
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
border: (theme) => `1px solid ${theme.palette.divider}`,
|
border: (theme) => `1px solid ${theme.palette.divider}`,
|
||||||
backgroundColor: isDragging ? "action.hover" : "transparent",
|
backgroundColor: isDragging ? 'action.hover' : 'transparent',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
gap: 1,
|
gap: 1,
|
||||||
}}
|
}}
|
||||||
style={style}
|
style={style}
|
||||||
@@ -169,13 +169,13 @@ const SortableColumnItem = ({
|
|||||||
/>
|
/>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={label}
|
primary={label}
|
||||||
slotProps={{ primary: { variant: "body2" } }}
|
slotProps={{ primary: { variant: 'body2' } }}
|
||||||
sx={{ mr: 1 }}
|
sx={{ mr: 1 }}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
edge="end"
|
edge="end"
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ cursor: isDragging ? "grabbing" : "grab" }}
|
sx={{ cursor: isDragging ? 'grabbing' : 'grab' }}
|
||||||
aria-label={dragHandleLabel}
|
aria-label={dragHandleLabel}
|
||||||
{...attributes}
|
{...attributes}
|
||||||
{...listeners}
|
{...listeners}
|
||||||
@@ -183,13 +183,13 @@ const SortableColumnItem = ({
|
|||||||
<DragIndicatorRounded fontSize="small" />
|
<DragIndicatorRounded fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const getColumnLabel = (column: Column<IConnectionsItem, unknown>) => {
|
const getColumnLabel = (column: Column<IConnectionsItem, unknown>) => {
|
||||||
const meta = column.columnDef.meta as { label?: string } | undefined;
|
const meta = column.columnDef.meta as { label?: string } | undefined
|
||||||
if (meta?.label) return meta.label;
|
if (meta?.label) return meta.label
|
||||||
|
|
||||||
const header = column.columnDef.header;
|
const header = column.columnDef.header
|
||||||
return typeof header === "string" ? header : column.id;
|
return typeof header === 'string' ? header : column.id
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,43 +1,43 @@
|
|||||||
import { Box, Button, Snackbar, useTheme } from "@mui/material";
|
import { Box, Button, Snackbar, useTheme } from '@mui/material'
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from 'ahooks'
|
||||||
import dayjs from "dayjs";
|
import dayjs from 'dayjs'
|
||||||
import { useImperativeHandle, useState, type Ref } from "react";
|
import { useImperativeHandle, useState, type Ref } from 'react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
import { closeConnection } from "tauri-plugin-mihomo-api";
|
import { closeConnection } from 'tauri-plugin-mihomo-api'
|
||||||
|
|
||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from '@/utils/parse-traffic'
|
||||||
|
|
||||||
export interface ConnectionDetailRef {
|
export interface ConnectionDetailRef {
|
||||||
open: (detail: IConnectionsItem, closed: boolean) => void;
|
open: (detail: IConnectionsItem, closed: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConnectionDetail({ ref }: { ref?: Ref<ConnectionDetailRef> }) {
|
export function ConnectionDetail({ ref }: { ref?: Ref<ConnectionDetailRef> }) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false)
|
||||||
const [detail, setDetail] = useState<IConnectionsItem>(null!);
|
const [detail, setDetail] = useState<IConnectionsItem>(null!)
|
||||||
const [closed, setClosed] = useState(false);
|
const [closed, setClosed] = useState(false)
|
||||||
const theme = useTheme();
|
const theme = useTheme()
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
open: (detail: IConnectionsItem, closed: boolean) => {
|
open: (detail: IConnectionsItem, closed: boolean) => {
|
||||||
if (open) return;
|
if (open) return
|
||||||
setOpen(true);
|
setOpen(true)
|
||||||
setDetail(detail);
|
setDetail(detail)
|
||||||
setClosed(closed);
|
setClosed(closed)
|
||||||
},
|
},
|
||||||
}));
|
}))
|
||||||
|
|
||||||
const onClose = () => setOpen(false);
|
const onClose = () => setOpen(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Snackbar
|
<Snackbar
|
||||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||||
open={open}
|
open={open}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
sx={{
|
sx={{
|
||||||
".MuiSnackbarContent-root": {
|
'.MuiSnackbarContent-root': {
|
||||||
maxWidth: "520px",
|
maxWidth: '520px',
|
||||||
maxHeight: "480px",
|
maxHeight: '480px',
|
||||||
overflowY: "auto",
|
overflowY: 'auto',
|
||||||
backgroundColor: theme.palette.background.paper,
|
backgroundColor: theme.palette.background.paper,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
},
|
},
|
||||||
@@ -52,87 +52,87 @@ export function ConnectionDetail({ ref }: { ref?: Ref<ConnectionDetailRef> }) {
|
|||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InnerProps {
|
interface InnerProps {
|
||||||
data: IConnectionsItem;
|
data: IConnectionsItem
|
||||||
closed: boolean;
|
closed: boolean
|
||||||
onClose?: () => void;
|
onClose?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const InnerConnectionDetail = ({ data, closed, onClose }: InnerProps) => {
|
const InnerConnectionDetail = ({ data, closed, onClose }: InnerProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const { metadata, rulePayload } = data;
|
const { metadata, rulePayload } = data
|
||||||
const theme = useTheme();
|
const theme = useTheme()
|
||||||
const chains = [...data.chains].reverse().join(" / ");
|
const chains = [...data.chains].reverse().join(' / ')
|
||||||
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule;
|
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule
|
||||||
const host = metadata.host
|
const host = metadata.host
|
||||||
? `${metadata.host}:${metadata.destinationPort}`
|
? `${metadata.host}:${metadata.destinationPort}`
|
||||||
: `${metadata.remoteDestination}:${metadata.destinationPort}`;
|
: `${metadata.remoteDestination}:${metadata.destinationPort}`
|
||||||
const Destination = metadata.destinationIP
|
const Destination = metadata.destinationIP
|
||||||
? metadata.destinationIP
|
? metadata.destinationIP
|
||||||
: metadata.remoteDestination;
|
: metadata.remoteDestination
|
||||||
|
|
||||||
const information = [
|
const information = [
|
||||||
{ label: t("connections.components.fields.host"), value: host },
|
{ label: t('connections.components.fields.host'), value: host },
|
||||||
{
|
{
|
||||||
label: t("shared.labels.downloaded"),
|
label: t('shared.labels.downloaded'),
|
||||||
value: parseTraffic(data.download).join(" "),
|
value: parseTraffic(data.download).join(' '),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("shared.labels.uploaded"),
|
label: t('shared.labels.uploaded'),
|
||||||
value: parseTraffic(data.upload).join(" "),
|
value: parseTraffic(data.upload).join(' '),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("connections.components.fields.dlSpeed"),
|
label: t('connections.components.fields.dlSpeed'),
|
||||||
value: parseTraffic(data.curDownload ?? -1).join(" ") + "/s",
|
value: parseTraffic(data.curDownload ?? -1).join(' ') + '/s',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("connections.components.fields.ulSpeed"),
|
label: t('connections.components.fields.ulSpeed'),
|
||||||
value: parseTraffic(data.curUpload ?? -1).join(" ") + "/s",
|
value: parseTraffic(data.curUpload ?? -1).join(' ') + '/s',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("connections.components.fields.chains"),
|
label: t('connections.components.fields.chains'),
|
||||||
value: chains,
|
value: chains,
|
||||||
},
|
},
|
||||||
{ label: t("connections.components.fields.rule"), value: rule },
|
{ label: t('connections.components.fields.rule'), value: rule },
|
||||||
{
|
{
|
||||||
label: t("connections.components.fields.process"),
|
label: t('connections.components.fields.process'),
|
||||||
value: `${metadata.process}${metadata.processPath ? `(${metadata.processPath})` : ""}`,
|
value: `${metadata.process}${metadata.processPath ? `(${metadata.processPath})` : ''}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("connections.components.fields.time"),
|
label: t('connections.components.fields.time'),
|
||||||
value: dayjs(data.start).fromNow(),
|
value: dayjs(data.start).fromNow(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("connections.components.fields.source"),
|
label: t('connections.components.fields.source'),
|
||||||
value: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
value: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("connections.components.fields.destination"),
|
label: t('connections.components.fields.destination'),
|
||||||
value: Destination,
|
value: Destination,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("connections.components.fields.destinationPort"),
|
label: t('connections.components.fields.destinationPort'),
|
||||||
value: `${metadata.destinationPort}`,
|
value: `${metadata.destinationPort}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("connections.components.fields.type"),
|
label: t('connections.components.fields.type'),
|
||||||
value: `${metadata.type}(${metadata.network})`,
|
value: `${metadata.type}(${metadata.network})`,
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
|
|
||||||
const onDelete = useLockFn(async () => closeConnection(data.id));
|
const onDelete = useLockFn(async () => closeConnection(data.id))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ userSelect: "text", color: theme.palette.text.secondary }}>
|
<Box sx={{ userSelect: 'text', color: theme.palette.text.secondary }}>
|
||||||
{information.map((each) => (
|
{information.map((each) => (
|
||||||
<div key={each.label}>
|
<div key={each.label}>
|
||||||
<b>{each.label}</b>
|
<b>{each.label}</b>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
wordBreak: "break-all",
|
wordBreak: 'break-all',
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -142,19 +142,19 @@ const InnerConnectionDetail = ({ data, closed, onClose }: InnerProps) => {
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
{!closed && (
|
{!closed && (
|
||||||
<Box sx={{ textAlign: "right" }}>
|
<Box sx={{ textAlign: 'right' }}>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
title={t("connections.components.actions.closeConnection")}
|
title={t('connections.components.actions.closeConnection')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onDelete();
|
onDelete()
|
||||||
onClose?.();
|
onClose?.()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("connections.components.actions.closeConnection")}
|
{t('connections.components.actions.closeConnection')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CloseRounded } from "@mui/icons-material";
|
import { CloseRounded } from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
styled,
|
styled,
|
||||||
ListItem,
|
ListItem,
|
||||||
@@ -6,52 +6,52 @@ import {
|
|||||||
ListItemText,
|
ListItemText,
|
||||||
Box,
|
Box,
|
||||||
alpha,
|
alpha,
|
||||||
} from "@mui/material";
|
} from '@mui/material'
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from 'ahooks'
|
||||||
import dayjs from "dayjs";
|
import dayjs from 'dayjs'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
import { closeConnection } from "tauri-plugin-mihomo-api";
|
import { closeConnection } from 'tauri-plugin-mihomo-api'
|
||||||
|
|
||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from '@/utils/parse-traffic'
|
||||||
|
|
||||||
const Tag = styled("span")(({ theme }) => ({
|
const Tag = styled('span')(({ theme }) => ({
|
||||||
fontSize: "10px",
|
fontSize: '10px',
|
||||||
padding: "0 4px",
|
padding: '0 4px',
|
||||||
lineHeight: 1.375,
|
lineHeight: 1.375,
|
||||||
border: "1px solid",
|
border: '1px solid',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
borderColor: alpha(theme.palette.text.secondary, 0.35),
|
borderColor: alpha(theme.palette.text.secondary, 0.35),
|
||||||
marginTop: "4px",
|
marginTop: '4px',
|
||||||
marginRight: "4px",
|
marginRight: '4px',
|
||||||
}));
|
}))
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: IConnectionsItem;
|
value: IConnectionsItem
|
||||||
closed: boolean;
|
closed: boolean
|
||||||
onShowDetail?: () => void;
|
onShowDetail?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConnectionItem = (props: Props) => {
|
export const ConnectionItem = (props: Props) => {
|
||||||
const { value, closed, onShowDetail } = props;
|
const { value, closed, onShowDetail } = props
|
||||||
|
|
||||||
const { id, metadata, chains, start, curUpload, curDownload } = value;
|
const { id, metadata, chains, start, curUpload, curDownload } = value
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const onDelete = useLockFn(async () => closeConnection(id));
|
const onDelete = useLockFn(async () => closeConnection(id))
|
||||||
const showTraffic = curUpload! >= 100 || curDownload! >= 100;
|
const showTraffic = curUpload! >= 100 || curDownload! >= 100
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
dense
|
dense
|
||||||
sx={{ borderBottom: "1px solid var(--divider-color)" }}
|
sx={{ borderBottom: '1px solid var(--divider-color)' }}
|
||||||
secondaryAction={
|
secondaryAction={
|
||||||
!closed && (
|
!closed && (
|
||||||
<IconButton
|
<IconButton
|
||||||
edge="end"
|
edge="end"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
title={t("connections.components.actions.closeConnection")}
|
title={t('connections.components.actions.closeConnection')}
|
||||||
aria-label={t("connections.components.actions.closeConnection")}
|
aria-label={t('connections.components.actions.closeConnection')}
|
||||||
>
|
>
|
||||||
<CloseRounded />
|
<CloseRounded />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -59,12 +59,12 @@ export const ConnectionItem = (props: Props) => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
sx={{ userSelect: "text", cursor: "pointer" }}
|
sx={{ userSelect: 'text', cursor: 'pointer' }}
|
||||||
primary={metadata.host || metadata.destinationIP}
|
primary={metadata.host || metadata.destinationIP}
|
||||||
onClick={onShowDetail}
|
onClick={onShowDetail}
|
||||||
secondary={
|
secondary={
|
||||||
<Box sx={{ display: "flex", flexWrap: "wrap" }}>
|
<Box sx={{ display: 'flex', flexWrap: 'wrap' }}>
|
||||||
<Tag sx={{ textTransform: "uppercase", color: "success" }}>
|
<Tag sx={{ textTransform: 'uppercase', color: 'success' }}>
|
||||||
{metadata.network}
|
{metadata.network}
|
||||||
</Tag>
|
</Tag>
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ export const ConnectionItem = (props: Props) => {
|
|||||||
{!!metadata.process && <Tag>{metadata.process}</Tag>}
|
{!!metadata.process && <Tag>{metadata.process}</Tag>}
|
||||||
|
|
||||||
{chains?.length > 0 && (
|
{chains?.length > 0 && (
|
||||||
<Tag>{[...chains].reverse().join(" / ")}</Tag>
|
<Tag>{[...chains].reverse().join(' / ')}</Tag>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Tag>{dayjs(start).fromNow()}</Tag>
|
<Tag>{dayjs(start).fromNow()}</Tag>
|
||||||
@@ -87,5 +87,5 @@ export const ConnectionItem = (props: Props) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ViewColumnRounded } from "@mui/icons-material";
|
import { ViewColumnRounded } from '@mui/icons-material'
|
||||||
import { Box, IconButton, Tooltip } from "@mui/material";
|
import { Box, IconButton, Tooltip } from '@mui/material'
|
||||||
import {
|
import {
|
||||||
ColumnDef,
|
ColumnDef,
|
||||||
ColumnOrderState,
|
ColumnOrderState,
|
||||||
@@ -11,10 +11,10 @@ import {
|
|||||||
Updater,
|
Updater,
|
||||||
useReactTable,
|
useReactTable,
|
||||||
VisibilityState,
|
VisibilityState,
|
||||||
} from "@tanstack/react-table";
|
} from '@tanstack/react-table'
|
||||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||||
import dayjs from "dayjs";
|
import dayjs from 'dayjs'
|
||||||
import { useLocalStorage } from "foxact/use-local-storage";
|
import { useLocalStorage } from 'foxact/use-local-storage'
|
||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
@@ -22,15 +22,15 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
} from "react";
|
} from 'react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from '@/utils/parse-traffic'
|
||||||
import { truncateStr } from "@/utils/truncate-str";
|
import { truncateStr } from '@/utils/truncate-str'
|
||||||
|
|
||||||
import { ConnectionColumnManager } from "./connection-column-manager";
|
import { ConnectionColumnManager } from './connection-column-manager'
|
||||||
|
|
||||||
const ROW_HEIGHT = 40;
|
const ROW_HEIGHT = 40
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reconcile stored column order with base columns to handle added/removed fields
|
* Reconcile stored column order with base columns to handle added/removed fields
|
||||||
@@ -39,68 +39,68 @@ const reconcileColumnOrder = (
|
|||||||
storedOrder: string[],
|
storedOrder: string[],
|
||||||
baseFields: string[],
|
baseFields: string[],
|
||||||
): string[] => {
|
): string[] => {
|
||||||
const filtered = storedOrder.filter((field) => baseFields.includes(field));
|
const filtered = storedOrder.filter((field) => baseFields.includes(field))
|
||||||
const missing = baseFields.filter((field) => !filtered.includes(field));
|
const missing = baseFields.filter((field) => !filtered.includes(field))
|
||||||
return [...filtered, ...missing];
|
return [...filtered, ...missing]
|
||||||
};
|
}
|
||||||
|
|
||||||
type ColumnField =
|
type ColumnField =
|
||||||
| "host"
|
| 'host'
|
||||||
| "download"
|
| 'download'
|
||||||
| "upload"
|
| 'upload'
|
||||||
| "dlSpeed"
|
| 'dlSpeed'
|
||||||
| "ulSpeed"
|
| 'ulSpeed'
|
||||||
| "chains"
|
| 'chains'
|
||||||
| "rule"
|
| 'rule'
|
||||||
| "process"
|
| 'process'
|
||||||
| "time"
|
| 'time'
|
||||||
| "source"
|
| 'source'
|
||||||
| "remoteDestination"
|
| 'remoteDestination'
|
||||||
| "type";
|
| 'type'
|
||||||
|
|
||||||
const getConnectionCellValue = (field: ColumnField, each: IConnectionsItem) => {
|
const getConnectionCellValue = (field: ColumnField, each: IConnectionsItem) => {
|
||||||
const { metadata, rulePayload } = each;
|
const { metadata, rulePayload } = each
|
||||||
|
|
||||||
switch (field) {
|
switch (field) {
|
||||||
case "host":
|
case 'host':
|
||||||
return metadata.host
|
return metadata.host
|
||||||
? `${metadata.host}:${metadata.destinationPort}`
|
? `${metadata.host}:${metadata.destinationPort}`
|
||||||
: `${metadata.remoteDestination}:${metadata.destinationPort}`;
|
: `${metadata.remoteDestination}:${metadata.destinationPort}`
|
||||||
case "download":
|
case 'download':
|
||||||
return each.download;
|
return each.download
|
||||||
case "upload":
|
case 'upload':
|
||||||
return each.upload;
|
return each.upload
|
||||||
case "dlSpeed":
|
case 'dlSpeed':
|
||||||
return each.curDownload;
|
return each.curDownload
|
||||||
case "ulSpeed":
|
case 'ulSpeed':
|
||||||
return each.curUpload;
|
return each.curUpload
|
||||||
case "chains":
|
case 'chains':
|
||||||
return [...each.chains].reverse().join(" / ");
|
return [...each.chains].reverse().join(' / ')
|
||||||
case "rule":
|
case 'rule':
|
||||||
return rulePayload ? `${each.rule}(${rulePayload})` : each.rule;
|
return rulePayload ? `${each.rule}(${rulePayload})` : each.rule
|
||||||
case "process":
|
case 'process':
|
||||||
return truncateStr(metadata.process || metadata.processPath);
|
return truncateStr(metadata.process || metadata.processPath)
|
||||||
case "time":
|
case 'time':
|
||||||
return each.start;
|
return each.start
|
||||||
case "source":
|
case 'source':
|
||||||
return `${metadata.sourceIP}:${metadata.sourcePort}`;
|
return `${metadata.sourceIP}:${metadata.sourcePort}`
|
||||||
case "remoteDestination":
|
case 'remoteDestination':
|
||||||
return metadata.destinationIP
|
return metadata.destinationIP
|
||||||
? `${metadata.destinationIP}:${metadata.destinationPort}`
|
? `${metadata.destinationIP}:${metadata.destinationPort}`
|
||||||
: `${metadata.remoteDestination}:${metadata.destinationPort}`;
|
: `${metadata.remoteDestination}:${metadata.destinationPort}`
|
||||||
case "type":
|
case 'type':
|
||||||
return `${metadata.type}(${metadata.network})`;
|
return `${metadata.type}(${metadata.network})`
|
||||||
default:
|
default:
|
||||||
return "";
|
return ''
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
connections: IConnectionsItem[];
|
connections: IConnectionsItem[]
|
||||||
onShowDetail: (data: IConnectionsItem) => void;
|
onShowDetail: (data: IConnectionsItem) => void
|
||||||
columnManagerOpen: boolean;
|
columnManagerOpen: boolean
|
||||||
onOpenColumnManager: () => void;
|
onOpenColumnManager: () => void
|
||||||
onCloseColumnManager: () => void;
|
onCloseColumnManager: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConnectionTable = (props: Props) => {
|
export const ConnectionTable = (props: Props) => {
|
||||||
@@ -110,215 +110,215 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
columnManagerOpen,
|
columnManagerOpen,
|
||||||
onOpenColumnManager,
|
onOpenColumnManager,
|
||||||
onCloseColumnManager,
|
onCloseColumnManager,
|
||||||
} = props;
|
} = props
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const [columnWidths, setColumnWidths] = useLocalStorage<ColumnSizingState>(
|
const [columnWidths, setColumnWidths] = useLocalStorage<ColumnSizingState>(
|
||||||
"connection-table-widths",
|
'connection-table-widths',
|
||||||
// server-side value, this is the default value used by server-side rendering (if any)
|
// server-side value, this is the default value used by server-side rendering (if any)
|
||||||
// Do not omit (otherwise a Suspense boundary will be triggered)
|
// Do not omit (otherwise a Suspense boundary will be triggered)
|
||||||
{},
|
{},
|
||||||
);
|
)
|
||||||
|
|
||||||
const [columnVisibilityModel, setColumnVisibilityModel] =
|
const [columnVisibilityModel, setColumnVisibilityModel] =
|
||||||
useLocalStorage<VisibilityState>(
|
useLocalStorage<VisibilityState>(
|
||||||
"connection-table-visibility",
|
'connection-table-visibility',
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
serializer: JSON.stringify,
|
serializer: JSON.stringify,
|
||||||
deserializer: (value) => {
|
deserializer: (value) => {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(value);
|
const parsed = JSON.parse(value)
|
||||||
if (parsed && typeof parsed === "object") return parsed;
|
if (parsed && typeof parsed === 'object') return parsed
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("Failed to parse connection-table-visibility", err);
|
console.warn('Failed to parse connection-table-visibility', err)
|
||||||
}
|
}
|
||||||
return {};
|
return {}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
const [columnOrder, setColumnOrder] = useLocalStorage<string[]>(
|
const [columnOrder, setColumnOrder] = useLocalStorage<string[]>(
|
||||||
"connection-table-order",
|
'connection-table-order',
|
||||||
[],
|
[],
|
||||||
{
|
{
|
||||||
serializer: JSON.stringify,
|
serializer: JSON.stringify,
|
||||||
deserializer: (value) => {
|
deserializer: (value) => {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(value);
|
const parsed = JSON.parse(value)
|
||||||
if (Array.isArray(parsed)) return parsed;
|
if (Array.isArray(parsed)) return parsed
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("Failed to parse connection-table-order", err);
|
console.warn('Failed to parse connection-table-order', err)
|
||||||
}
|
}
|
||||||
return [];
|
return []
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
interface BaseColumn {
|
interface BaseColumn {
|
||||||
field: ColumnField;
|
field: ColumnField
|
||||||
headerName: string;
|
headerName: string
|
||||||
width?: number;
|
width?: number
|
||||||
minWidth?: number;
|
minWidth?: number
|
||||||
align?: "left" | "right";
|
align?: 'left' | 'right'
|
||||||
cell?: (row: IConnectionsItem) => ReactNode;
|
cell?: (row: IConnectionsItem) => ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseColumns = useMemo<BaseColumn[]>(() => {
|
const baseColumns = useMemo<BaseColumn[]>(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
field: "host",
|
field: 'host',
|
||||||
headerName: t("connections.components.fields.host"),
|
headerName: t('connections.components.fields.host'),
|
||||||
width: 180,
|
width: 180,
|
||||||
minWidth: 140,
|
minWidth: 140,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "download",
|
field: 'download',
|
||||||
headerName: t("shared.labels.downloaded"),
|
headerName: t('shared.labels.downloaded'),
|
||||||
width: 76,
|
width: 76,
|
||||||
minWidth: 60,
|
minWidth: 60,
|
||||||
align: "right",
|
align: 'right',
|
||||||
cell: (row) => parseTraffic(row.download).join(" "),
|
cell: (row) => parseTraffic(row.download).join(' '),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "upload",
|
field: 'upload',
|
||||||
headerName: t("shared.labels.uploaded"),
|
headerName: t('shared.labels.uploaded'),
|
||||||
width: 76,
|
width: 76,
|
||||||
minWidth: 60,
|
minWidth: 60,
|
||||||
align: "right",
|
align: 'right',
|
||||||
cell: (row) => parseTraffic(row.upload).join(" "),
|
cell: (row) => parseTraffic(row.upload).join(' '),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "dlSpeed",
|
field: 'dlSpeed',
|
||||||
headerName: t("connections.components.fields.dlSpeed"),
|
headerName: t('connections.components.fields.dlSpeed'),
|
||||||
width: 76,
|
width: 76,
|
||||||
minWidth: 60,
|
minWidth: 60,
|
||||||
align: "right",
|
align: 'right',
|
||||||
cell: (row) => `${parseTraffic(row.curDownload).join(" ")}/s`,
|
cell: (row) => `${parseTraffic(row.curDownload).join(' ')}/s`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "ulSpeed",
|
field: 'ulSpeed',
|
||||||
headerName: t("connections.components.fields.ulSpeed"),
|
headerName: t('connections.components.fields.ulSpeed'),
|
||||||
width: 76,
|
width: 76,
|
||||||
minWidth: 60,
|
minWidth: 60,
|
||||||
align: "right",
|
align: 'right',
|
||||||
cell: (row) => `${parseTraffic(row.curUpload).join(" ")}/s`,
|
cell: (row) => `${parseTraffic(row.curUpload).join(' ')}/s`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "chains",
|
field: 'chains',
|
||||||
headerName: t("connections.components.fields.chains"),
|
headerName: t('connections.components.fields.chains'),
|
||||||
width: 280,
|
width: 280,
|
||||||
minWidth: 160,
|
minWidth: 160,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "rule",
|
field: 'rule',
|
||||||
headerName: t("connections.components.fields.rule"),
|
headerName: t('connections.components.fields.rule'),
|
||||||
width: 220,
|
width: 220,
|
||||||
minWidth: 160,
|
minWidth: 160,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "process",
|
field: 'process',
|
||||||
headerName: t("connections.components.fields.process"),
|
headerName: t('connections.components.fields.process'),
|
||||||
width: 180,
|
width: 180,
|
||||||
minWidth: 140,
|
minWidth: 140,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "time",
|
field: 'time',
|
||||||
headerName: t("connections.components.fields.time"),
|
headerName: t('connections.components.fields.time'),
|
||||||
width: 100,
|
width: 100,
|
||||||
minWidth: 80,
|
minWidth: 80,
|
||||||
align: "right",
|
align: 'right',
|
||||||
// cell filled later with shared relativeNow ticker
|
// cell filled later with shared relativeNow ticker
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "source",
|
field: 'source',
|
||||||
headerName: t("connections.components.fields.source"),
|
headerName: t('connections.components.fields.source'),
|
||||||
width: 160,
|
width: 160,
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "remoteDestination",
|
field: 'remoteDestination',
|
||||||
headerName: t("connections.components.fields.destination"),
|
headerName: t('connections.components.fields.destination'),
|
||||||
width: 160,
|
width: 160,
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "type",
|
field: 'type',
|
||||||
headerName: t("connections.components.fields.type"),
|
headerName: t('connections.components.fields.type'),
|
||||||
width: 120,
|
width: 120,
|
||||||
minWidth: 80,
|
minWidth: 80,
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
}, [t]);
|
}, [t])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setColumnOrder((prevValue) => {
|
setColumnOrder((prevValue) => {
|
||||||
const baseFields = baseColumns.map((col) => col.field);
|
const baseFields = baseColumns.map((col) => col.field)
|
||||||
const prev = Array.isArray(prevValue) ? prevValue : [];
|
const prev = Array.isArray(prevValue) ? prevValue : []
|
||||||
const reconciled = reconcileColumnOrder(prev, baseFields);
|
const reconciled = reconcileColumnOrder(prev, baseFields)
|
||||||
if (
|
if (
|
||||||
reconciled.length === prev.length &&
|
reconciled.length === prev.length &&
|
||||||
reconciled.every((field, i) => field === prev[i])
|
reconciled.every((field, i) => field === prev[i])
|
||||||
) {
|
) {
|
||||||
return prevValue;
|
return prevValue
|
||||||
}
|
}
|
||||||
return reconciled;
|
return reconciled
|
||||||
});
|
})
|
||||||
}, [baseColumns, setColumnOrder]);
|
}, [baseColumns, setColumnOrder])
|
||||||
|
|
||||||
const handleColumnVisibilityChange = useCallback(
|
const handleColumnVisibilityChange = useCallback(
|
||||||
(update: Updater<VisibilityState>) => {
|
(update: Updater<VisibilityState>) => {
|
||||||
setColumnVisibilityModel((prev) => {
|
setColumnVisibilityModel((prev) => {
|
||||||
const current = prev ?? {};
|
const current = prev ?? {}
|
||||||
const nextState =
|
const nextState =
|
||||||
typeof update === "function" ? update(current) : update;
|
typeof update === 'function' ? update(current) : update
|
||||||
|
|
||||||
const visibleCount = baseColumns.reduce((count, column) => {
|
const visibleCount = baseColumns.reduce((count, column) => {
|
||||||
const isVisible = (nextState[column.field] ?? true) !== false;
|
const isVisible = (nextState[column.field] ?? true) !== false
|
||||||
return count + (isVisible ? 1 : 0);
|
return count + (isVisible ? 1 : 0)
|
||||||
}, 0);
|
}, 0)
|
||||||
|
|
||||||
if (visibleCount === 0) {
|
if (visibleCount === 0) {
|
||||||
return current;
|
return current
|
||||||
}
|
}
|
||||||
|
|
||||||
const sanitized: VisibilityState = {};
|
const sanitized: VisibilityState = {}
|
||||||
baseColumns.forEach((column) => {
|
baseColumns.forEach((column) => {
|
||||||
if (nextState[column.field] === false) {
|
if (nextState[column.field] === false) {
|
||||||
sanitized[column.field] = false;
|
sanitized[column.field] = false
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
return sanitized;
|
return sanitized
|
||||||
});
|
})
|
||||||
},
|
},
|
||||||
[baseColumns, setColumnVisibilityModel],
|
[baseColumns, setColumnVisibilityModel],
|
||||||
);
|
)
|
||||||
|
|
||||||
const handleColumnOrderChange = useCallback(
|
const handleColumnOrderChange = useCallback(
|
||||||
(update: Updater<ColumnOrderState>) => {
|
(update: Updater<ColumnOrderState>) => {
|
||||||
setColumnOrder((prev) => {
|
setColumnOrder((prev) => {
|
||||||
const current = Array.isArray(prev) ? prev : [];
|
const current = Array.isArray(prev) ? prev : []
|
||||||
const nextState =
|
const nextState =
|
||||||
typeof update === "function" ? update(current) : update;
|
typeof update === 'function' ? update(current) : update
|
||||||
const baseFields = baseColumns.map((col) => col.field);
|
const baseFields = baseColumns.map((col) => col.field)
|
||||||
return reconcileColumnOrder(nextState, baseFields);
|
return reconcileColumnOrder(nextState, baseFields)
|
||||||
});
|
})
|
||||||
},
|
},
|
||||||
[baseColumns, setColumnOrder],
|
[baseColumns, setColumnOrder],
|
||||||
);
|
)
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([])
|
||||||
const [relativeNow, setRelativeNow] = useState(() => Date.now());
|
const [relativeNow, setRelativeNow] = useState(() => Date.now())
|
||||||
|
|
||||||
const columnDefs = useMemo<ColumnDef<IConnectionsItem>[]>(() => {
|
const columnDefs = useMemo<ColumnDef<IConnectionsItem>[]>(() => {
|
||||||
return baseColumns.map((column) => {
|
return baseColumns.map((column) => {
|
||||||
const baseCell: ColumnDef<IConnectionsItem>["cell"] = column.cell
|
const baseCell: ColumnDef<IConnectionsItem>['cell'] = column.cell
|
||||||
? (ctx) => column.cell?.(ctx.row.original)
|
? (ctx) => column.cell?.(ctx.row.original)
|
||||||
: (ctx) => ctx.getValue() as ReactNode;
|
: (ctx) => ctx.getValue() as ReactNode
|
||||||
|
|
||||||
const cell: ColumnDef<IConnectionsItem>["cell"] =
|
const cell: ColumnDef<IConnectionsItem>['cell'] =
|
||||||
column.field === "time"
|
column.field === 'time'
|
||||||
? (ctx) => dayjs(ctx.getValue() as string).from(relativeNow)
|
? (ctx) => dayjs(ctx.getValue() as string).from(relativeNow)
|
||||||
: baseCell;
|
: baseCell
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: column.field,
|
id: column.field,
|
||||||
@@ -327,42 +327,42 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
size: column.width,
|
size: column.width,
|
||||||
minSize: column.minWidth,
|
minSize: column.minWidth,
|
||||||
meta: {
|
meta: {
|
||||||
align: column.align ?? "left",
|
align: column.align ?? 'left',
|
||||||
field: column.field,
|
field: column.field,
|
||||||
label: column.headerName,
|
label: column.headerName,
|
||||||
},
|
},
|
||||||
cell,
|
cell,
|
||||||
} satisfies ColumnDef<IConnectionsItem>;
|
} satisfies ColumnDef<IConnectionsItem>
|
||||||
});
|
})
|
||||||
}, [baseColumns, relativeNow]);
|
}, [baseColumns, relativeNow])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === "undefined") return undefined;
|
if (typeof window === 'undefined') return undefined
|
||||||
|
|
||||||
const timer = window.setInterval(() => {
|
const timer = window.setInterval(() => {
|
||||||
setRelativeNow(Date.now());
|
setRelativeNow(Date.now())
|
||||||
}, 5000);
|
}, 5000)
|
||||||
|
|
||||||
return () => window.clearInterval(timer);
|
return () => window.clearInterval(timer)
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
const handleColumnSizingChange = useCallback(
|
const handleColumnSizingChange = useCallback(
|
||||||
(updater: Updater<ColumnSizingState>) => {
|
(updater: Updater<ColumnSizingState>) => {
|
||||||
setColumnWidths((prev) => {
|
setColumnWidths((prev) => {
|
||||||
const prevState = prev ?? {};
|
const prevState = prev ?? {}
|
||||||
const nextState =
|
const nextState =
|
||||||
typeof updater === "function" ? updater(prevState) : updater;
|
typeof updater === 'function' ? updater(prevState) : updater
|
||||||
const sanitized: ColumnSizingState = {};
|
const sanitized: ColumnSizingState = {}
|
||||||
Object.entries(nextState).forEach(([key, size]) => {
|
Object.entries(nextState).forEach(([key, size]) => {
|
||||||
if (typeof size === "number" && Number.isFinite(size)) {
|
if (typeof size === 'number' && Number.isFinite(size)) {
|
||||||
sanitized[key] = size;
|
sanitized[key] = size
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
return sanitized;
|
return sanitized
|
||||||
});
|
})
|
||||||
},
|
},
|
||||||
[setColumnWidths],
|
[setColumnWidths],
|
||||||
);
|
)
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: connections,
|
data: connections,
|
||||||
@@ -379,7 +379,7 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
minSize: 80,
|
minSize: 80,
|
||||||
enableResizing: true,
|
enableResizing: true,
|
||||||
},
|
},
|
||||||
columnResizeMode: "onChange",
|
columnResizeMode: 'onChange',
|
||||||
enableSortingRemoval: true,
|
enableSortingRemoval: true,
|
||||||
getRowId: (row) => row.id,
|
getRowId: (row) => row.id,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
@@ -389,61 +389,61 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
onColumnVisibilityChange: handleColumnVisibilityChange,
|
onColumnVisibilityChange: handleColumnVisibilityChange,
|
||||||
onColumnOrderChange: handleColumnOrderChange,
|
onColumnOrderChange: handleColumnOrderChange,
|
||||||
columns: columnDefs,
|
columns: columnDefs,
|
||||||
});
|
})
|
||||||
|
|
||||||
const handleManagerOrderChange = useCallback(
|
const handleManagerOrderChange = useCallback(
|
||||||
(order: string[]) => {
|
(order: string[]) => {
|
||||||
const baseFields = baseColumns.map((col) => col.field);
|
const baseFields = baseColumns.map((col) => col.field)
|
||||||
table.setColumnOrder(reconcileColumnOrder(order, baseFields));
|
table.setColumnOrder(reconcileColumnOrder(order, baseFields))
|
||||||
},
|
},
|
||||||
[baseColumns, table],
|
[baseColumns, table],
|
||||||
);
|
)
|
||||||
|
|
||||||
const handleResetColumns = useCallback(() => {
|
const handleResetColumns = useCallback(() => {
|
||||||
table.resetColumnVisibility();
|
table.resetColumnVisibility()
|
||||||
table.resetColumnOrder();
|
table.resetColumnOrder()
|
||||||
}, [table]);
|
}, [table])
|
||||||
|
|
||||||
const rows = table.getRowModel().rows;
|
const rows = table.getRowModel().rows
|
||||||
const tableContainerRef = useRef<HTMLDivElement | null>(null);
|
const tableContainerRef = useRef<HTMLDivElement | null>(null)
|
||||||
const rowVirtualizer = useVirtualizer({
|
const rowVirtualizer = useVirtualizer({
|
||||||
count: rows.length,
|
count: rows.length,
|
||||||
getScrollElement: () => tableContainerRef.current,
|
getScrollElement: () => tableContainerRef.current,
|
||||||
estimateSize: () => ROW_HEIGHT,
|
estimateSize: () => ROW_HEIGHT,
|
||||||
overscan: 4,
|
overscan: 4,
|
||||||
});
|
})
|
||||||
|
|
||||||
const virtualRows = rowVirtualizer.getVirtualItems();
|
const virtualRows = rowVirtualizer.getVirtualItems()
|
||||||
const totalSize = rowVirtualizer.getTotalSize();
|
const totalSize = rowVirtualizer.getTotalSize()
|
||||||
const tableWidth = table.getTotalSize();
|
const tableWidth = table.getTotalSize()
|
||||||
const managerColumns = table.getAllLeafColumns();
|
const managerColumns = table.getAllLeafColumns()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
flexDirection: "column",
|
flexDirection: 'column',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
fontFamily: (theme) => theme.typography.fontFamily,
|
fontFamily: (theme) => theme.typography.fontFamily,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tooltip title={t("connections.components.columnManager.title")}>
|
<Tooltip title={t('connections.components.columnManager.title')}>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={onOpenColumnManager}
|
onClick={onOpenColumnManager}
|
||||||
sx={{
|
sx={{
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
top: 4,
|
top: 4,
|
||||||
right: 4,
|
right: 4,
|
||||||
zIndex: 3,
|
zIndex: 3,
|
||||||
backgroundColor: (theme) =>
|
backgroundColor: (theme) =>
|
||||||
theme.palette.mode === "dark"
|
theme.palette.mode === 'dark'
|
||||||
? theme.palette.background.default
|
? theme.palette.background.default
|
||||||
: theme.palette.background.paper,
|
: theme.palette.background.paper,
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
backgroundColor: (theme) => theme.palette.action.hover,
|
backgroundColor: (theme) => theme.palette.action.hover,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@@ -456,25 +456,25 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
sx={{
|
sx={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
overflow: "auto",
|
overflow: 'auto',
|
||||||
WebkitOverflowScrolling: "touch",
|
WebkitOverflowScrolling: 'touch',
|
||||||
overscrollBehavior: "contain",
|
overscrollBehavior: 'contain',
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
border: "none",
|
border: 'none',
|
||||||
"&::-webkit-scrollbar": {
|
'&::-webkit-scrollbar': {
|
||||||
height: 8,
|
height: 8,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
minWidth: "100%",
|
minWidth: '100%',
|
||||||
width: tableWidth,
|
width: tableWidth,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: "sticky",
|
position: 'sticky',
|
||||||
top: 0,
|
top: 0,
|
||||||
zIndex: 2,
|
zIndex: 2,
|
||||||
}}
|
}}
|
||||||
@@ -483,7 +483,7 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
<Box
|
<Box
|
||||||
key={headerGroup.id}
|
key={headerGroup.id}
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
borderBottom: (theme) =>
|
borderBottom: (theme) =>
|
||||||
`1px solid ${theme.palette.divider}`,
|
`1px solid ${theme.palette.divider}`,
|
||||||
backgroundColor: (theme) => theme.palette.background.paper,
|
backgroundColor: (theme) => theme.palette.background.paper,
|
||||||
@@ -491,12 +491,12 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
>
|
>
|
||||||
{headerGroup.headers.map((header) => {
|
{headerGroup.headers.map((header) => {
|
||||||
if (header.isPlaceholder) {
|
if (header.isPlaceholder) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
const meta = header.column.columnDef.meta as {
|
const meta = header.column.columnDef.meta as {
|
||||||
align?: "left" | "right";
|
align?: 'left' | 'right'
|
||||||
field: string;
|
field: string
|
||||||
};
|
}
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
key={header.id}
|
key={header.id}
|
||||||
@@ -504,15 +504,15 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
flex: `0 0 ${header.getSize()}px`,
|
flex: `0 0 ${header.getSize()}px`,
|
||||||
minWidth: header.column.columnDef.minSize || 80,
|
minWidth: header.column.columnDef.minSize || 80,
|
||||||
maxWidth: header.column.columnDef.maxSize,
|
maxWidth: header.column.columnDef.maxSize,
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
boxSizing: "border-box",
|
boxSizing: 'border-box',
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: "text.secondary",
|
color: 'text.secondary',
|
||||||
userSelect: "none",
|
userSelect: 'none',
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
backgroundColor: (theme) =>
|
backgroundColor: (theme) =>
|
||||||
theme.palette.action.hover,
|
theme.palette.action.hover,
|
||||||
},
|
},
|
||||||
@@ -527,18 +527,18 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
}
|
}
|
||||||
sx={{
|
sx={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent:
|
justifyContent:
|
||||||
meta?.align === "right"
|
meta?.align === 'right'
|
||||||
? "flex-end"
|
? 'flex-end'
|
||||||
: "flex-start",
|
: 'flex-start',
|
||||||
gap: 0.5,
|
gap: 0.5,
|
||||||
px: 1,
|
px: 1,
|
||||||
py: 1,
|
py: 1,
|
||||||
cursor: header.column.getCanSort()
|
cursor: header.column.getCanSort()
|
||||||
? "pointer"
|
? 'pointer'
|
||||||
: "default",
|
: 'default',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{flexRender(
|
{flexRender(
|
||||||
@@ -546,30 +546,30 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
header.getContext(),
|
header.getContext(),
|
||||||
)}
|
)}
|
||||||
{{
|
{{
|
||||||
asc: "▲",
|
asc: '▲',
|
||||||
desc: "▼",
|
desc: '▼',
|
||||||
}[header.column.getIsSorted() as string] ?? null}
|
}[header.column.getIsSorted() as string] ?? null}
|
||||||
</Box>
|
</Box>
|
||||||
{header.column.getCanResize() && (
|
{header.column.getCanResize() && (
|
||||||
<Box
|
<Box
|
||||||
onClick={(event) => event.stopPropagation()}
|
onClick={(event) => event.stopPropagation()}
|
||||||
onMouseDown={(event) => {
|
onMouseDown={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation()
|
||||||
header.getResizeHandler()(event);
|
header.getResizeHandler()(event)
|
||||||
}}
|
}}
|
||||||
onTouchStart={(event) => {
|
onTouchStart={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation()
|
||||||
header.getResizeHandler()(event);
|
header.getResizeHandler()(event)
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
cursor: "col-resize",
|
cursor: 'col-resize',
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
right: 0,
|
right: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
width: 4,
|
width: 4,
|
||||||
height: "100%",
|
height: '100%',
|
||||||
transform: "translateX(50%)",
|
transform: 'translateX(50%)',
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
backgroundColor: (theme) =>
|
backgroundColor: (theme) =>
|
||||||
theme.palette.action.active,
|
theme.palette.action.active,
|
||||||
},
|
},
|
||||||
@@ -577,44 +577,44 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
height: totalSize,
|
height: totalSize,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{virtualRows.map((virtualRow) => {
|
{virtualRows.map((virtualRow) => {
|
||||||
const row = rows[virtualRow.index];
|
const row = rows[virtualRow.index]
|
||||||
if (!row) return null;
|
if (!row) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
key={row.id}
|
key={row.id}
|
||||||
onClick={() => onShowDetail(row.original)}
|
onClick={() => onShowDetail(row.original)}
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
height: virtualRow.size,
|
height: virtualRow.size,
|
||||||
transform: `translateY(${virtualRow.start}px)`,
|
transform: `translateY(${virtualRow.start}px)`,
|
||||||
borderBottom: (theme) =>
|
borderBottom: (theme) =>
|
||||||
`1px solid ${theme.palette.divider}`,
|
`1px solid ${theme.palette.divider}`,
|
||||||
cursor: "pointer",
|
cursor: 'pointer',
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
backgroundColor: (theme) => theme.palette.action.hover,
|
backgroundColor: (theme) => theme.palette.action.hover,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{row.getVisibleCells().map((cell) => {
|
{row.getVisibleCells().map((cell) => {
|
||||||
const meta = cell.column.columnDef.meta as {
|
const meta = cell.column.columnDef.meta as {
|
||||||
align?: "left" | "right";
|
align?: 'left' | 'right'
|
||||||
};
|
}
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
key={cell.id}
|
key={cell.id}
|
||||||
@@ -622,18 +622,18 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
flex: `0 0 ${cell.column.getSize()}px`,
|
flex: `0 0 ${cell.column.getSize()}px`,
|
||||||
minWidth: cell.column.columnDef.minSize || 80,
|
minWidth: cell.column.columnDef.minSize || 80,
|
||||||
maxWidth: cell.column.columnDef.maxSize,
|
maxWidth: cell.column.columnDef.maxSize,
|
||||||
boxSizing: "border-box",
|
boxSizing: 'border-box',
|
||||||
px: 1,
|
px: 1,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent:
|
justifyContent:
|
||||||
meta?.align === "right"
|
meta?.align === 'right'
|
||||||
? "flex-end"
|
? 'flex-end'
|
||||||
: "flex-start",
|
: 'flex-start',
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: 'nowrap',
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
textOverflow: "ellipsis",
|
textOverflow: 'ellipsis',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{flexRender(
|
{flexRender(
|
||||||
@@ -641,10 +641,10 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
cell.getContext(),
|
cell.getContext(),
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -658,5 +658,5 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
onReset={handleResetColumns}
|
onReset={handleResetColumns}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,47 +1,47 @@
|
|||||||
import { DeveloperBoardOutlined } from "@mui/icons-material";
|
import { DeveloperBoardOutlined } from '@mui/icons-material'
|
||||||
import { Divider, Stack, Typography } from "@mui/material";
|
import { Divider, Stack, Typography } from '@mui/material'
|
||||||
import { useMemo } from "react";
|
import { useMemo } from 'react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { useClash } from "@/hooks/use-clash";
|
import { useClash } from '@/hooks/use-clash'
|
||||||
import { useAppData } from "@/providers/app-data-context";
|
import { useAppData } from '@/providers/app-data-context'
|
||||||
|
|
||||||
import { EnhancedCard } from "./enhanced-card";
|
import { EnhancedCard } from './enhanced-card'
|
||||||
|
|
||||||
// 将毫秒转换为时:分:秒格式的函数
|
// 将毫秒转换为时:分:秒格式的函数
|
||||||
const formatUptime = (uptimeMs: number) => {
|
const formatUptime = (uptimeMs: number) => {
|
||||||
const hours = Math.floor(uptimeMs / 3600000);
|
const hours = Math.floor(uptimeMs / 3600000)
|
||||||
const minutes = Math.floor((uptimeMs % 3600000) / 60000);
|
const minutes = Math.floor((uptimeMs % 3600000) / 60000)
|
||||||
const seconds = Math.floor((uptimeMs % 60000) / 1000);
|
const seconds = Math.floor((uptimeMs % 60000) / 1000)
|
||||||
return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
|
||||||
};
|
}
|
||||||
|
|
||||||
export const ClashInfoCard = () => {
|
export const ClashInfoCard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const { version: clashVersion } = useClash();
|
const { version: clashVersion } = useClash()
|
||||||
const { clashConfig, rules, uptime, systemProxyAddress } = useAppData();
|
const { clashConfig, rules, uptime, systemProxyAddress } = useAppData()
|
||||||
|
|
||||||
// 使用useMemo缓存格式化后的uptime,避免频繁计算
|
// 使用useMemo缓存格式化后的uptime,避免频繁计算
|
||||||
const formattedUptime = useMemo(() => formatUptime(uptime), [uptime]);
|
const formattedUptime = useMemo(() => formatUptime(uptime), [uptime])
|
||||||
|
|
||||||
// 使用备忘录组件内容,减少重新渲染
|
// 使用备忘录组件内容,减少重新渲染
|
||||||
const cardContent = useMemo(() => {
|
const cardContent = useMemo(() => {
|
||||||
if (!clashConfig) return null;
|
if (!clashConfig) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={1.5}>
|
<Stack spacing={1.5}>
|
||||||
<Stack direction="row" justifyContent="space-between">
|
<Stack direction="row" justifyContent="space-between">
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("home.components.clashInfo.fields.coreVersion")}
|
{t('home.components.clashInfo.fields.coreVersion')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" fontWeight="medium">
|
<Typography variant="body2" fontWeight="medium">
|
||||||
{clashVersion || "-"}
|
{clashVersion || '-'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Stack direction="row" justifyContent="space-between">
|
<Stack direction="row" justifyContent="space-between">
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("home.components.clashInfo.fields.systemProxyAddress")}
|
{t('home.components.clashInfo.fields.systemProxyAddress')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" fontWeight="medium">
|
<Typography variant="body2" fontWeight="medium">
|
||||||
{systemProxyAddress}
|
{systemProxyAddress}
|
||||||
@@ -50,16 +50,16 @@ export const ClashInfoCard = () => {
|
|||||||
<Divider />
|
<Divider />
|
||||||
<Stack direction="row" justifyContent="space-between">
|
<Stack direction="row" justifyContent="space-between">
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("home.components.clashInfo.fields.mixedPort")}
|
{t('home.components.clashInfo.fields.mixedPort')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" fontWeight="medium">
|
<Typography variant="body2" fontWeight="medium">
|
||||||
{clashConfig.mixedPort || "-"}
|
{clashConfig.mixedPort || '-'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Stack direction="row" justifyContent="space-between">
|
<Stack direction="row" justifyContent="space-between">
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("home.components.clashInfo.fields.uptime")}
|
{t('home.components.clashInfo.fields.uptime')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" fontWeight="medium">
|
<Typography variant="body2" fontWeight="medium">
|
||||||
{formattedUptime}
|
{formattedUptime}
|
||||||
@@ -68,14 +68,14 @@ export const ClashInfoCard = () => {
|
|||||||
<Divider />
|
<Divider />
|
||||||
<Stack direction="row" justifyContent="space-between">
|
<Stack direction="row" justifyContent="space-between">
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("home.components.clashInfo.fields.rulesCount")}
|
{t('home.components.clashInfo.fields.rulesCount')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" fontWeight="medium">
|
<Typography variant="body2" fontWeight="medium">
|
||||||
{rules.length}
|
{rules.length}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
)
|
||||||
}, [
|
}, [
|
||||||
clashConfig,
|
clashConfig,
|
||||||
clashVersion,
|
clashVersion,
|
||||||
@@ -83,16 +83,16 @@ export const ClashInfoCard = () => {
|
|||||||
formattedUptime,
|
formattedUptime,
|
||||||
rules.length,
|
rules.length,
|
||||||
systemProxyAddress,
|
systemProxyAddress,
|
||||||
]);
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedCard
|
<EnhancedCard
|
||||||
title={t("home.components.clashInfo.title")}
|
title={t('home.components.clashInfo.title')}
|
||||||
icon={<DeveloperBoardOutlined />}
|
icon={<DeveloperBoardOutlined />}
|
||||||
iconColor="warning"
|
iconColor="warning"
|
||||||
action={null}
|
action={null}
|
||||||
>
|
>
|
||||||
{cardContent}
|
{cardContent}
|
||||||
</EnhancedCard>
|
</EnhancedCard>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -2,63 +2,63 @@ import {
|
|||||||
DirectionsRounded,
|
DirectionsRounded,
|
||||||
LanguageRounded,
|
LanguageRounded,
|
||||||
MultipleStopRounded,
|
MultipleStopRounded,
|
||||||
} from "@mui/icons-material";
|
} from '@mui/icons-material'
|
||||||
import { Box, Paper, Stack, Typography } from "@mui/material";
|
import { Box, Paper, Stack, Typography } from '@mui/material'
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from 'ahooks'
|
||||||
import { useMemo } from "react";
|
import { useMemo } from 'react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
import { closeAllConnections } from "tauri-plugin-mihomo-api";
|
import { closeAllConnections } from 'tauri-plugin-mihomo-api'
|
||||||
|
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
import { useAppData } from "@/providers/app-data-context";
|
import { useAppData } from '@/providers/app-data-context'
|
||||||
import { patchClashMode } from "@/services/cmds";
|
import { patchClashMode } from '@/services/cmds'
|
||||||
import type { TranslationKey } from "@/types/generated/i18n-keys";
|
import type { TranslationKey } from '@/types/generated/i18n-keys'
|
||||||
|
|
||||||
const CLASH_MODES = ["rule", "global", "direct"] as const;
|
const CLASH_MODES = ['rule', 'global', 'direct'] as const
|
||||||
type ClashMode = (typeof CLASH_MODES)[number];
|
type ClashMode = (typeof CLASH_MODES)[number]
|
||||||
|
|
||||||
const isClashMode = (mode: string): mode is ClashMode =>
|
const isClashMode = (mode: string): mode is ClashMode =>
|
||||||
(CLASH_MODES as readonly string[]).includes(mode);
|
(CLASH_MODES as readonly string[]).includes(mode)
|
||||||
|
|
||||||
const MODE_META: Record<
|
const MODE_META: Record<
|
||||||
ClashMode,
|
ClashMode,
|
||||||
{ label: TranslationKey; description: TranslationKey }
|
{ label: TranslationKey; description: TranslationKey }
|
||||||
> = {
|
> = {
|
||||||
rule: {
|
rule: {
|
||||||
label: "home.components.clashMode.labels.rule",
|
label: 'home.components.clashMode.labels.rule',
|
||||||
description: "home.components.clashMode.descriptions.rule",
|
description: 'home.components.clashMode.descriptions.rule',
|
||||||
},
|
},
|
||||||
global: {
|
global: {
|
||||||
label: "home.components.clashMode.labels.global",
|
label: 'home.components.clashMode.labels.global',
|
||||||
description: "home.components.clashMode.descriptions.global",
|
description: 'home.components.clashMode.descriptions.global',
|
||||||
},
|
},
|
||||||
direct: {
|
direct: {
|
||||||
label: "home.components.clashMode.labels.direct",
|
label: 'home.components.clashMode.labels.direct',
|
||||||
description: "home.components.clashMode.descriptions.direct",
|
description: 'home.components.clashMode.descriptions.direct',
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
export const ClashModeCard = () => {
|
export const ClashModeCard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge()
|
||||||
const { clashConfig, refreshClashConfig } = useAppData();
|
const { clashConfig, refreshClashConfig } = useAppData()
|
||||||
|
|
||||||
// 支持的模式列表
|
// 支持的模式列表
|
||||||
const modeList = CLASH_MODES;
|
const modeList = CLASH_MODES
|
||||||
|
|
||||||
// 直接使用API返回的模式,不维护本地状态
|
// 直接使用API返回的模式,不维护本地状态
|
||||||
const currentMode = clashConfig?.mode?.toLowerCase();
|
const currentMode = clashConfig?.mode?.toLowerCase()
|
||||||
const currentModeKey =
|
const currentModeKey =
|
||||||
typeof currentMode === "string" && isClashMode(currentMode)
|
typeof currentMode === 'string' && isClashMode(currentMode)
|
||||||
? currentMode
|
? currentMode
|
||||||
: undefined;
|
: undefined
|
||||||
|
|
||||||
const modeDescription = useMemo(() => {
|
const modeDescription = useMemo(() => {
|
||||||
if (currentModeKey) {
|
if (currentModeKey) {
|
||||||
return t(MODE_META[currentModeKey].description);
|
return t(MODE_META[currentModeKey].description)
|
||||||
}
|
}
|
||||||
return t("home.components.clashMode.errors.communication");
|
return t('home.components.clashMode.errors.communication')
|
||||||
}, [currentModeKey, t]);
|
}, [currentModeKey, t])
|
||||||
|
|
||||||
// 模式图标映射
|
// 模式图标映射
|
||||||
const modeIcons = useMemo(
|
const modeIcons = useMemo(
|
||||||
@@ -68,87 +68,87 @@ export const ClashModeCard = () => {
|
|||||||
direct: <DirectionsRounded fontSize="small" />,
|
direct: <DirectionsRounded fontSize="small" />,
|
||||||
}),
|
}),
|
||||||
[],
|
[],
|
||||||
);
|
)
|
||||||
|
|
||||||
// 切换模式的处理函数
|
// 切换模式的处理函数
|
||||||
const onChangeMode = useLockFn(async (mode: ClashMode) => {
|
const onChangeMode = useLockFn(async (mode: ClashMode) => {
|
||||||
if (mode === currentModeKey) return;
|
if (mode === currentModeKey) return
|
||||||
if (verge?.auto_close_connection) {
|
if (verge?.auto_close_connection) {
|
||||||
closeAllConnections();
|
closeAllConnections()
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await patchClashMode(mode);
|
await patchClashMode(mode)
|
||||||
// 使用共享的刷新方法
|
// 使用共享的刷新方法
|
||||||
refreshClashConfig();
|
refreshClashConfig()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to change mode:", error);
|
console.error('Failed to change mode:', error)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
// 按钮样式
|
// 按钮样式
|
||||||
const buttonStyles = (mode: ClashMode) => ({
|
const buttonStyles = (mode: ClashMode) => ({
|
||||||
cursor: "pointer",
|
cursor: 'pointer',
|
||||||
px: 2,
|
px: 2,
|
||||||
py: 1.2,
|
py: 1.2,
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
gap: 1,
|
gap: 1,
|
||||||
bgcolor: mode === currentModeKey ? "primary.main" : "background.paper",
|
bgcolor: mode === currentModeKey ? 'primary.main' : 'background.paper',
|
||||||
color: mode === currentModeKey ? "primary.contrastText" : "text.primary",
|
color: mode === currentModeKey ? 'primary.contrastText' : 'text.primary',
|
||||||
borderRadius: 1.5,
|
borderRadius: 1.5,
|
||||||
transition: "all 0.2s ease-in-out",
|
transition: 'all 0.2s ease-in-out',
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
overflow: "visible",
|
overflow: 'visible',
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
transform: "translateY(-1px)",
|
transform: 'translateY(-1px)',
|
||||||
boxShadow: 1,
|
boxShadow: 1,
|
||||||
},
|
},
|
||||||
"&:active": {
|
'&:active': {
|
||||||
transform: "translateY(1px)",
|
transform: 'translateY(1px)',
|
||||||
},
|
},
|
||||||
"&::after":
|
'&::after':
|
||||||
mode === currentModeKey
|
mode === currentModeKey
|
||||||
? {
|
? {
|
||||||
content: '""',
|
content: '""',
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
bottom: -16,
|
bottom: -16,
|
||||||
left: "50%",
|
left: '50%',
|
||||||
width: 2,
|
width: 2,
|
||||||
height: 16,
|
height: 16,
|
||||||
bgcolor: "primary.main",
|
bgcolor: 'primary.main',
|
||||||
transform: "translateX(-50%)",
|
transform: 'translateX(-50%)',
|
||||||
}
|
}
|
||||||
: {},
|
: {},
|
||||||
});
|
})
|
||||||
|
|
||||||
// 描述样式
|
// 描述样式
|
||||||
const descriptionStyles = {
|
const descriptionStyles = {
|
||||||
width: "95%",
|
width: '95%',
|
||||||
textAlign: "center",
|
textAlign: 'center',
|
||||||
color: "text.secondary",
|
color: 'text.secondary',
|
||||||
p: 0.8,
|
p: 0.8,
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
borderColor: "primary.main",
|
borderColor: 'primary.main',
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderStyle: "solid",
|
borderStyle: 'solid',
|
||||||
backgroundColor: "background.paper",
|
backgroundColor: 'background.paper',
|
||||||
wordBreak: "break-word",
|
wordBreak: 'break-word',
|
||||||
hyphens: "auto",
|
hyphens: 'auto',
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', width: '100%' }}>
|
||||||
{/* 模式选择按钮组 */}
|
{/* 模式选择按钮组 */}
|
||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
spacing={1}
|
spacing={1}
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
py: 1,
|
py: 1,
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
zIndex: 2,
|
zIndex: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -163,7 +163,7 @@ export const ClashModeCard = () => {
|
|||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
sx={{
|
sx={{
|
||||||
textTransform: "capitalize",
|
textTransform: 'capitalize',
|
||||||
fontWeight: mode === currentModeKey ? 600 : 400,
|
fontWeight: mode === currentModeKey ? 600 : 400,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -176,12 +176,12 @@ export const ClashModeCard = () => {
|
|||||||
{/* 说明文本区域 */}
|
{/* 说明文本区域 */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
width: "100%",
|
width: '100%',
|
||||||
my: 1,
|
my: 1,
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
overflow: "visible",
|
overflow: 'visible',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="caption" component="div" sx={descriptionStyles}>
|
<Typography variant="caption" component="div" sx={descriptionStyles}>
|
||||||
@@ -189,5 +189,5 @@ export const ClashModeCard = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,21 +1,15 @@
|
|||||||
import { Box, Typography, alpha, useTheme } from "@mui/material";
|
import { Box, Typography, alpha, useTheme } from '@mui/material'
|
||||||
import React, { forwardRef, ReactNode } from "react";
|
import React, { forwardRef, ReactNode } from 'react'
|
||||||
|
|
||||||
// 自定义卡片组件接口
|
// 自定义卡片组件接口
|
||||||
interface EnhancedCardProps {
|
interface EnhancedCardProps {
|
||||||
title: ReactNode;
|
title: ReactNode
|
||||||
icon: ReactNode;
|
icon: ReactNode
|
||||||
action?: ReactNode;
|
action?: ReactNode
|
||||||
children: ReactNode;
|
children: ReactNode
|
||||||
iconColor?:
|
iconColor?: 'primary' | 'secondary' | 'error' | 'warning' | 'info' | 'success'
|
||||||
| "primary"
|
minHeight?: number | string
|
||||||
| "secondary"
|
noContentPadding?: boolean
|
||||||
| "error"
|
|
||||||
| "warning"
|
|
||||||
| "info"
|
|
||||||
| "success";
|
|
||||||
minHeight?: number | string;
|
|
||||||
noContentPadding?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自定义卡片组件
|
// 自定义卡片组件
|
||||||
@@ -26,33 +20,33 @@ export const EnhancedCard = forwardRef<HTMLElement, EnhancedCardProps>(
|
|||||||
icon,
|
icon,
|
||||||
action,
|
action,
|
||||||
children,
|
children,
|
||||||
iconColor = "primary",
|
iconColor = 'primary',
|
||||||
minHeight,
|
minHeight,
|
||||||
noContentPadding = false,
|
noContentPadding = false,
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme()
|
||||||
const isDark = theme.palette.mode === "dark";
|
const isDark = theme.palette.mode === 'dark'
|
||||||
|
|
||||||
// 统一的标题截断样式
|
// 统一的标题截断样式
|
||||||
const titleTruncateStyle = {
|
const titleTruncateStyle = {
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
maxWidth: "100%",
|
maxWidth: '100%',
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
textOverflow: "ellipsis",
|
textOverflow: 'ellipsis',
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: 'nowrap',
|
||||||
display: "block",
|
display: 'block',
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
height: "100%",
|
height: '100%',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
flexDirection: "column",
|
flexDirection: 'column',
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
backgroundColor: isDark ? "#282a36" : "#ffffff",
|
backgroundColor: isDark ? '#282a36' : '#ffffff',
|
||||||
}}
|
}}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
@@ -60,27 +54,27 @@ export const EnhancedCard = forwardRef<HTMLElement, EnhancedCardProps>(
|
|||||||
sx={{
|
sx={{
|
||||||
px: 2,
|
px: 2,
|
||||||
py: 1,
|
py: 1,
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "space-between",
|
justifyContent: 'space-between',
|
||||||
borderBottom: 1,
|
borderBottom: 1,
|
||||||
borderColor: "divider",
|
borderColor: 'divider',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
borderRadius: 1.5,
|
borderRadius: 1.5,
|
||||||
width: 38,
|
width: 38,
|
||||||
height: 38,
|
height: 38,
|
||||||
@@ -93,7 +87,7 @@ export const EnhancedCard = forwardRef<HTMLElement, EnhancedCardProps>(
|
|||||||
{icon}
|
{icon}
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ minWidth: 0, flex: 1 }}>
|
<Box sx={{ minWidth: 0, flex: 1 }}>
|
||||||
{typeof title === "string" ? (
|
{typeof title === 'string' ? (
|
||||||
<Typography
|
<Typography
|
||||||
variant="h6"
|
variant="h6"
|
||||||
fontWeight="medium"
|
fontWeight="medium"
|
||||||
@@ -113,8 +107,8 @@ export const EnhancedCard = forwardRef<HTMLElement, EnhancedCardProps>(
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
flexDirection: "column",
|
flexDirection: 'column',
|
||||||
p: noContentPadding ? 0 : 2,
|
p: noContentPadding ? 0 : 2,
|
||||||
...(minHeight && { minHeight }),
|
...(minHeight && { minHeight }),
|
||||||
}}
|
}}
|
||||||
@@ -122,6 +116,6 @@ export const EnhancedCard = forwardRef<HTMLElement, EnhancedCardProps>(
|
|||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
CloudUploadRounded,
|
CloudUploadRounded,
|
||||||
LinkRounded,
|
LinkRounded,
|
||||||
MemoryRounded,
|
MemoryRounded,
|
||||||
} from "@mui/icons-material";
|
} from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Grid,
|
Grid,
|
||||||
PaletteColor,
|
PaletteColor,
|
||||||
@@ -13,74 +13,74 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
alpha,
|
alpha,
|
||||||
useTheme,
|
useTheme,
|
||||||
} from "@mui/material";
|
} from '@mui/material'
|
||||||
import { ReactNode, memo, useMemo, useRef } from "react";
|
import { ReactNode, memo, useMemo, useRef } from 'react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { TrafficErrorBoundary } from "@/components/shared/traffic-error-boundary";
|
import { TrafficErrorBoundary } from '@/components/shared/traffic-error-boundary'
|
||||||
import { useConnectionData } from "@/hooks/use-connection-data";
|
import { useConnectionData } from '@/hooks/use-connection-data'
|
||||||
import { useMemoryData } from "@/hooks/use-memory-data";
|
import { useMemoryData } from '@/hooks/use-memory-data'
|
||||||
import { useTrafficData } from "@/hooks/use-traffic-data";
|
import { useTrafficData } from '@/hooks/use-traffic-data'
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
import { useVisibility } from "@/hooks/use-visibility";
|
import { useVisibility } from '@/hooks/use-visibility'
|
||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from '@/utils/parse-traffic'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EnhancedCanvasTrafficGraph,
|
EnhancedCanvasTrafficGraph,
|
||||||
type EnhancedCanvasTrafficGraphRef,
|
type EnhancedCanvasTrafficGraphRef,
|
||||||
} from "./enhanced-canvas-traffic-graph";
|
} from './enhanced-canvas-traffic-graph'
|
||||||
|
|
||||||
interface StatCardProps {
|
interface StatCardProps {
|
||||||
icon: ReactNode;
|
icon: ReactNode
|
||||||
title: string;
|
title: string
|
||||||
value: string | number;
|
value: string | number
|
||||||
unit: string;
|
unit: string
|
||||||
color: "primary" | "secondary" | "error" | "warning" | "info" | "success";
|
color: 'primary' | 'secondary' | 'error' | 'warning' | 'info' | 'success'
|
||||||
onClick?: () => void;
|
onClick?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全局变量类型定义
|
// 全局变量类型定义
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
animationFrameId?: number;
|
animationFrameId?: number
|
||||||
lastTrafficData?: {
|
lastTrafficData?: {
|
||||||
up: number;
|
up: number
|
||||||
down: number;
|
down: number
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统计卡片组件 - 使用memo优化
|
// 统计卡片组件 - 使用memo优化
|
||||||
const CompactStatCard = memo(
|
const CompactStatCard = memo(
|
||||||
({ icon, title, value, unit, color, onClick }: StatCardProps) => {
|
({ icon, title, value, unit, color, onClick }: StatCardProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme()
|
||||||
|
|
||||||
// 获取调色板颜色 - 使用useMemo避免重复计算
|
// 获取调色板颜色 - 使用useMemo避免重复计算
|
||||||
const colorValue = useMemo(() => {
|
const colorValue = useMemo(() => {
|
||||||
const palette = theme.palette;
|
const palette = theme.palette
|
||||||
if (
|
if (
|
||||||
color in palette &&
|
color in palette &&
|
||||||
palette[color as keyof typeof palette] &&
|
palette[color as keyof typeof palette] &&
|
||||||
"main" in (palette[color as keyof typeof palette] as PaletteColor)
|
'main' in (palette[color as keyof typeof palette] as PaletteColor)
|
||||||
) {
|
) {
|
||||||
return (palette[color as keyof typeof palette] as PaletteColor).main;
|
return (palette[color as keyof typeof palette] as PaletteColor).main
|
||||||
}
|
}
|
||||||
return palette.primary.main;
|
return palette.primary.main
|
||||||
}, [theme.palette, color]);
|
}, [theme.palette, color])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
elevation={0}
|
elevation={0}
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
bgcolor: alpha(colorValue, 0.05),
|
bgcolor: alpha(colorValue, 0.05),
|
||||||
border: `1px solid ${alpha(colorValue, 0.15)}`,
|
border: `1px solid ${alpha(colorValue, 0.15)}`,
|
||||||
padding: "8px",
|
padding: '8px',
|
||||||
transition: "all 0.2s ease-in-out",
|
transition: 'all 0.2s ease-in-out',
|
||||||
cursor: onClick ? "pointer" : "default",
|
cursor: onClick ? 'pointer' : 'default',
|
||||||
"&:hover": onClick
|
'&:hover': onClick
|
||||||
? {
|
? {
|
||||||
bgcolor: alpha(colorValue, 0.1),
|
bgcolor: alpha(colorValue, 0.1),
|
||||||
border: `1px solid ${alpha(colorValue, 0.3)}`,
|
border: `1px solid ${alpha(colorValue, 0.3)}`,
|
||||||
@@ -95,13 +95,13 @@ const CompactStatCard = memo(
|
|||||||
component="div"
|
component="div"
|
||||||
sx={{
|
sx={{
|
||||||
mr: 1,
|
mr: 1,
|
||||||
ml: "2px",
|
ml: '2px',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
borderRadius: "50%",
|
borderRadius: '50%',
|
||||||
bgcolor: alpha(colorValue, 0.1),
|
bgcolor: alpha(colorValue, 0.1),
|
||||||
color: colorValue,
|
color: colorValue,
|
||||||
}}
|
}}
|
||||||
@@ -116,7 +116,7 @@ const CompactStatCard = memo(
|
|||||||
</Typography>
|
</Typography>
|
||||||
<Grid
|
<Grid
|
||||||
component="div"
|
component="div"
|
||||||
sx={{ display: "flex", alignItems: "baseline" }}
|
sx={{ display: 'flex', alignItems: 'baseline' }}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body1"
|
variant="body1"
|
||||||
@@ -132,48 +132,48 @@ const CompactStatCard = memo(
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
)
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
// 添加显示名称
|
// 添加显示名称
|
||||||
CompactStatCard.displayName = "CompactStatCard";
|
CompactStatCard.displayName = 'CompactStatCard'
|
||||||
|
|
||||||
export const EnhancedTrafficStats = () => {
|
export const EnhancedTrafficStats = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const theme = useTheme();
|
const theme = useTheme()
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge()
|
||||||
const trafficRef = useRef<EnhancedCanvasTrafficGraphRef>(null);
|
const trafficRef = useRef<EnhancedCanvasTrafficGraphRef>(null)
|
||||||
const pageVisible = useVisibility();
|
const pageVisible = useVisibility()
|
||||||
|
|
||||||
// 是否显示流量图表
|
// 是否显示流量图表
|
||||||
const trafficGraph = verge?.traffic_graph ?? true;
|
const trafficGraph = verge?.traffic_graph ?? true
|
||||||
|
|
||||||
const {
|
const {
|
||||||
response: { data: traffic },
|
response: { data: traffic },
|
||||||
} = useTrafficData({ enabled: trafficGraph && pageVisible });
|
} = useTrafficData({ enabled: trafficGraph && pageVisible })
|
||||||
|
|
||||||
const {
|
const {
|
||||||
response: { data: memory },
|
response: { data: memory },
|
||||||
} = useMemoryData();
|
} = useMemoryData()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
response: { data: connections },
|
response: { data: connections },
|
||||||
} = useConnectionData();
|
} = useConnectionData()
|
||||||
|
|
||||||
// Canvas组件现在直接从全局Hook获取数据,无需手动添加数据点
|
// Canvas组件现在直接从全局Hook获取数据,无需手动添加数据点
|
||||||
|
|
||||||
// 使用useMemo计算解析后的流量数据
|
// 使用useMemo计算解析后的流量数据
|
||||||
const parsedData = useMemo(() => {
|
const parsedData = useMemo(() => {
|
||||||
const [up, upUnit] = parseTraffic(traffic?.up || 0);
|
const [up, upUnit] = parseTraffic(traffic?.up || 0)
|
||||||
const [down, downUnit] = parseTraffic(traffic?.down || 0);
|
const [down, downUnit] = parseTraffic(traffic?.down || 0)
|
||||||
const [inuse, inuseUnit] = parseTraffic(memory?.inuse || 0);
|
const [inuse, inuseUnit] = parseTraffic(memory?.inuse || 0)
|
||||||
const [uploadTotal, uploadTotalUnit] = parseTraffic(
|
const [uploadTotal, uploadTotalUnit] = parseTraffic(
|
||||||
connections?.uploadTotal,
|
connections?.uploadTotal,
|
||||||
);
|
)
|
||||||
const [downloadTotal, downloadTotalUnit] = parseTraffic(
|
const [downloadTotal, downloadTotalUnit] = parseTraffic(
|
||||||
connections?.downloadTotal,
|
connections?.downloadTotal,
|
||||||
);
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
up,
|
up,
|
||||||
@@ -187,86 +187,86 @@ export const EnhancedTrafficStats = () => {
|
|||||||
downloadTotal,
|
downloadTotal,
|
||||||
downloadTotalUnit,
|
downloadTotalUnit,
|
||||||
connectionsCount: connections?.activeConnections.length,
|
connectionsCount: connections?.activeConnections.length,
|
||||||
};
|
}
|
||||||
}, [traffic, memory, connections]);
|
}, [traffic, memory, connections])
|
||||||
|
|
||||||
// 渲染流量图表 - 使用useMemo缓存渲染结果
|
// 渲染流量图表 - 使用useMemo缓存渲染结果
|
||||||
const trafficGraphComponent = useMemo(() => {
|
const trafficGraphComponent = useMemo(() => {
|
||||||
if (!trafficGraph || !pageVisible) return null;
|
if (!trafficGraph || !pageVisible) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
elevation={0}
|
elevation={0}
|
||||||
sx={{
|
sx={{
|
||||||
height: 130,
|
height: 130,
|
||||||
cursor: "pointer",
|
cursor: 'pointer',
|
||||||
border: `1px solid ${alpha(theme.palette.divider, 0.2)}`,
|
border: `1px solid ${alpha(theme.palette.divider, 0.2)}`,
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
onClick={() => trafficRef.current?.toggleStyle()}
|
onClick={() => trafficRef.current?.toggleStyle()}
|
||||||
>
|
>
|
||||||
<div style={{ height: "100%", position: "relative" }}>
|
<div style={{ height: '100%', position: 'relative' }}>
|
||||||
<EnhancedCanvasTrafficGraph ref={trafficRef} />
|
<EnhancedCanvasTrafficGraph ref={trafficRef} />
|
||||||
</div>
|
</div>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
)
|
||||||
}, [trafficGraph, pageVisible, theme.palette.divider]);
|
}, [trafficGraph, pageVisible, theme.palette.divider])
|
||||||
|
|
||||||
// 使用useMemo计算统计卡片配置
|
// 使用useMemo计算统计卡片配置
|
||||||
const statCards = useMemo(
|
const statCards = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
icon: <ArrowUpwardRounded fontSize="small" />,
|
icon: <ArrowUpwardRounded fontSize="small" />,
|
||||||
title: t("home.components.traffic.metrics.uploadSpeed"),
|
title: t('home.components.traffic.metrics.uploadSpeed'),
|
||||||
value: parsedData.up,
|
value: parsedData.up,
|
||||||
unit: `${parsedData.upUnit}/s`,
|
unit: `${parsedData.upUnit}/s`,
|
||||||
color: "secondary" as const,
|
color: 'secondary' as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <ArrowDownwardRounded fontSize="small" />,
|
icon: <ArrowDownwardRounded fontSize="small" />,
|
||||||
title: t("home.components.traffic.metrics.downloadSpeed"),
|
title: t('home.components.traffic.metrics.downloadSpeed'),
|
||||||
value: parsedData.down,
|
value: parsedData.down,
|
||||||
unit: `${parsedData.downUnit}/s`,
|
unit: `${parsedData.downUnit}/s`,
|
||||||
color: "primary" as const,
|
color: 'primary' as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <LinkRounded fontSize="small" />,
|
icon: <LinkRounded fontSize="small" />,
|
||||||
title: t("home.components.traffic.metrics.activeConnections"),
|
title: t('home.components.traffic.metrics.activeConnections'),
|
||||||
value: parsedData.connectionsCount,
|
value: parsedData.connectionsCount,
|
||||||
unit: "",
|
unit: '',
|
||||||
color: "success" as const,
|
color: 'success' as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <CloudUploadRounded fontSize="small" />,
|
icon: <CloudUploadRounded fontSize="small" />,
|
||||||
title: t("shared.labels.uploaded"),
|
title: t('shared.labels.uploaded'),
|
||||||
value: parsedData.uploadTotal,
|
value: parsedData.uploadTotal,
|
||||||
unit: parsedData.uploadTotalUnit,
|
unit: parsedData.uploadTotalUnit,
|
||||||
color: "secondary" as const,
|
color: 'secondary' as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <CloudDownloadRounded fontSize="small" />,
|
icon: <CloudDownloadRounded fontSize="small" />,
|
||||||
title: t("shared.labels.downloaded"),
|
title: t('shared.labels.downloaded'),
|
||||||
value: parsedData.downloadTotal,
|
value: parsedData.downloadTotal,
|
||||||
unit: parsedData.downloadTotalUnit,
|
unit: parsedData.downloadTotalUnit,
|
||||||
color: "primary" as const,
|
color: 'primary' as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <MemoryRounded fontSize="small" />,
|
icon: <MemoryRounded fontSize="small" />,
|
||||||
title: t("home.components.traffic.metrics.memoryUsage"),
|
title: t('home.components.traffic.metrics.memoryUsage'),
|
||||||
value: parsedData.inuse,
|
value: parsedData.inuse,
|
||||||
unit: parsedData.inuseUnit,
|
unit: parsedData.inuseUnit,
|
||||||
color: "error" as const,
|
color: 'error' as const,
|
||||||
onClick: undefined,
|
onClick: undefined,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[t, parsedData],
|
[t, parsedData],
|
||||||
);
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TrafficErrorBoundary
|
<TrafficErrorBoundary
|
||||||
onError={(error, errorInfo) => {
|
onError={(error, errorInfo) => {
|
||||||
console.error("[EnhancedTrafficStats] 组件错误:", error, errorInfo);
|
console.error('[EnhancedTrafficStats] 组件错误:', error, errorInfo)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}>
|
<Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}>
|
||||||
@@ -284,5 +284,5 @@ export const EnhancedTrafficStats = () => {
|
|||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
</TrafficErrorBoundary>
|
</TrafficErrorBoundary>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
SpeedOutlined,
|
SpeedOutlined,
|
||||||
StorageOutlined,
|
StorageOutlined,
|
||||||
UpdateOutlined,
|
UpdateOutlined,
|
||||||
} from "@mui/icons-material";
|
} from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -17,62 +17,62 @@ import {
|
|||||||
alpha,
|
alpha,
|
||||||
keyframes,
|
keyframes,
|
||||||
useTheme,
|
useTheme,
|
||||||
} from "@mui/material";
|
} from '@mui/material'
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from 'ahooks'
|
||||||
import dayjs from "dayjs";
|
import dayjs from 'dayjs'
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from 'react-router'
|
||||||
|
|
||||||
import { useAppData } from "@/providers/app-data-context";
|
import { useAppData } from '@/providers/app-data-context'
|
||||||
import { openWebUrl, updateProfile } from "@/services/cmds";
|
import { openWebUrl, updateProfile } from '@/services/cmds'
|
||||||
import { showNotice } from "@/services/notice-service";
|
import { showNotice } from '@/services/notice-service'
|
||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from '@/utils/parse-traffic'
|
||||||
|
|
||||||
import { EnhancedCard } from "./enhanced-card";
|
import { EnhancedCard } from './enhanced-card'
|
||||||
|
|
||||||
// 定义旋转动画
|
// 定义旋转动画
|
||||||
const round = keyframes`
|
const round = keyframes`
|
||||||
from { transform: rotate(0deg); }
|
from { transform: rotate(0deg); }
|
||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
`;
|
`
|
||||||
|
|
||||||
// 辅助函数解析URL和过期时间
|
// 辅助函数解析URL和过期时间
|
||||||
const parseUrl = (url?: string) => {
|
const parseUrl = (url?: string) => {
|
||||||
if (!url) return "-";
|
if (!url) return '-'
|
||||||
if (url.startsWith("http")) return new URL(url).host;
|
if (url.startsWith('http')) return new URL(url).host
|
||||||
return "local";
|
return 'local'
|
||||||
};
|
}
|
||||||
|
|
||||||
const parseExpire = (expire?: number) => {
|
const parseExpire = (expire?: number) => {
|
||||||
if (!expire) return "-";
|
if (!expire) return '-'
|
||||||
return dayjs(expire * 1000).format("YYYY-MM-DD");
|
return dayjs(expire * 1000).format('YYYY-MM-DD')
|
||||||
};
|
}
|
||||||
|
|
||||||
// 使用类型定义,而不是导入
|
// 使用类型定义,而不是导入
|
||||||
interface ProfileExtra {
|
interface ProfileExtra {
|
||||||
upload: number;
|
upload: number
|
||||||
download: number;
|
download: number
|
||||||
total: number;
|
total: number
|
||||||
expire: number;
|
expire: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProfileItem {
|
interface ProfileItem {
|
||||||
uid: string;
|
uid: string
|
||||||
type?: "local" | "remote" | "merge" | "script";
|
type?: 'local' | 'remote' | 'merge' | 'script'
|
||||||
name?: string;
|
name?: string
|
||||||
desc?: string;
|
desc?: string
|
||||||
file?: string;
|
file?: string
|
||||||
url?: string;
|
url?: string
|
||||||
updated?: number;
|
updated?: number
|
||||||
extra?: ProfileExtra;
|
extra?: ProfileExtra
|
||||||
home?: string;
|
home?: string
|
||||||
option?: any;
|
option?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HomeProfileCardProps {
|
interface HomeProfileCardProps {
|
||||||
current: ProfileItem | null | undefined;
|
current: ProfileItem | null | undefined
|
||||||
onProfileUpdated?: () => void;
|
onProfileUpdated?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取独立组件减少主组件复杂度
|
// 提取独立组件减少主组件复杂度
|
||||||
@@ -81,23 +81,23 @@ const ProfileDetails = ({
|
|||||||
onUpdateProfile,
|
onUpdateProfile,
|
||||||
updating,
|
updating,
|
||||||
}: {
|
}: {
|
||||||
current: ProfileItem;
|
current: ProfileItem
|
||||||
onUpdateProfile: () => void;
|
onUpdateProfile: () => void
|
||||||
updating: boolean;
|
updating: boolean
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const theme = useTheme();
|
const theme = useTheme()
|
||||||
|
|
||||||
const usedTraffic = useMemo(() => {
|
const usedTraffic = useMemo(() => {
|
||||||
if (!current.extra) return 0;
|
if (!current.extra) return 0
|
||||||
return current.extra.upload + current.extra.download;
|
return current.extra.upload + current.extra.download
|
||||||
}, [current.extra]);
|
}, [current.extra])
|
||||||
|
|
||||||
const trafficPercentage = useMemo(() => {
|
const trafficPercentage = useMemo(() => {
|
||||||
if (!current.extra || !current.extra.total || current.extra.total <= 0)
|
if (!current.extra || !current.extra.total || current.extra.total <= 0)
|
||||||
return 0;
|
return 0
|
||||||
return Math.min(Math.round((usedTraffic / current.extra.total) * 100), 100);
|
return Math.min(Math.round((usedTraffic / current.extra.total) * 100), 100)
|
||||||
}, [current.extra, usedTraffic]);
|
}, [current.extra, usedTraffic])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
@@ -109,19 +109,19 @@ const ProfileDetails = ({
|
|||||||
variant="body2"
|
variant="body2"
|
||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
noWrap
|
noWrap
|
||||||
sx={{ display: "flex", alignItems: "center" }}
|
sx={{ display: 'flex', alignItems: 'center' }}
|
||||||
>
|
>
|
||||||
<span style={{ flexShrink: 0 }}>{t("shared.labels.from")}: </span>
|
<span style={{ flexShrink: 0 }}>{t('shared.labels.from')}: </span>
|
||||||
{current.home ? (
|
{current.home ? (
|
||||||
<Link
|
<Link
|
||||||
component="button"
|
component="button"
|
||||||
fontWeight="medium"
|
fontWeight="medium"
|
||||||
onClick={() => current.home && openWebUrl(current.home)}
|
onClick={() => current.home && openWebUrl(current.home)}
|
||||||
sx={{
|
sx={{
|
||||||
display: "inline-flex",
|
display: 'inline-flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
maxWidth: "calc(100% - 40px)",
|
maxWidth: 'calc(100% - 40px)',
|
||||||
ml: 0.5,
|
ml: 0.5,
|
||||||
}}
|
}}
|
||||||
title={parseUrl(current.url)}
|
title={parseUrl(current.url)}
|
||||||
@@ -129,9 +129,9 @@ const ProfileDetails = ({
|
|||||||
<Typography
|
<Typography
|
||||||
component="span"
|
component="span"
|
||||||
sx={{
|
sx={{
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
textOverflow: "ellipsis",
|
textOverflow: 'ellipsis',
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: 'nowrap',
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
@@ -142,7 +142,7 @@ const ProfileDetails = ({
|
|||||||
fontSize="inherit"
|
fontSize="inherit"
|
||||||
sx={{
|
sx={{
|
||||||
ml: 0.5,
|
ml: 0.5,
|
||||||
fontSize: "0.8rem",
|
fontSize: '0.8rem',
|
||||||
opacity: 0.7,
|
opacity: 0.7,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}}
|
}}
|
||||||
@@ -153,9 +153,9 @@ const ProfileDetails = ({
|
|||||||
component="span"
|
component="span"
|
||||||
fontWeight="medium"
|
fontWeight="medium"
|
||||||
sx={{
|
sx={{
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
textOverflow: "ellipsis",
|
textOverflow: 'ellipsis',
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: 'nowrap',
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
ml: 0.5,
|
ml: 0.5,
|
||||||
@@ -175,20 +175,20 @@ const ProfileDetails = ({
|
|||||||
fontSize="small"
|
fontSize="small"
|
||||||
color="action"
|
color="action"
|
||||||
sx={{
|
sx={{
|
||||||
cursor: "pointer",
|
cursor: 'pointer',
|
||||||
animation: updating ? `${round} 1.5s linear infinite` : "none",
|
animation: updating ? `${round} 1.5s linear infinite` : 'none',
|
||||||
}}
|
}}
|
||||||
onClick={onUpdateProfile}
|
onClick={onUpdateProfile}
|
||||||
/>
|
/>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
sx={{ cursor: "pointer" }}
|
sx={{ cursor: 'pointer' }}
|
||||||
onClick={onUpdateProfile}
|
onClick={onUpdateProfile}
|
||||||
>
|
>
|
||||||
{t("shared.labels.updateTime")}:{" "}
|
{t('shared.labels.updateTime')}:{' '}
|
||||||
<Box component="span" fontWeight="medium">
|
<Box component="span" fontWeight="medium">
|
||||||
{dayjs(current.updated * 1000).format("YYYY-MM-DD HH:mm")}
|
{dayjs(current.updated * 1000).format('YYYY-MM-DD HH:mm')}
|
||||||
</Box>
|
</Box>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -199,9 +199,9 @@ const ProfileDetails = ({
|
|||||||
<Stack direction="row" alignItems="center" spacing={1}>
|
<Stack direction="row" alignItems="center" spacing={1}>
|
||||||
<SpeedOutlined fontSize="small" color="action" />
|
<SpeedOutlined fontSize="small" color="action" />
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("shared.labels.usedTotal")}:{" "}
|
{t('shared.labels.usedTotal')}:{' '}
|
||||||
<Box component="span" fontWeight="medium">
|
<Box component="span" fontWeight="medium">
|
||||||
{parseTraffic(usedTraffic)} /{" "}
|
{parseTraffic(usedTraffic)} /{' '}
|
||||||
{parseTraffic(current.extra.total)}
|
{parseTraffic(current.extra.total)}
|
||||||
</Box>
|
</Box>
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -211,7 +211,7 @@ const ProfileDetails = ({
|
|||||||
<Stack direction="row" alignItems="center" spacing={1}>
|
<Stack direction="row" alignItems="center" spacing={1}>
|
||||||
<EventOutlined fontSize="small" color="action" />
|
<EventOutlined fontSize="small" color="action" />
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("shared.labels.expireTime")}:{" "}
|
{t('shared.labels.expireTime')}:{' '}
|
||||||
<Box component="span" fontWeight="medium">
|
<Box component="span" fontWeight="medium">
|
||||||
{parseExpire(current.extra.expire)}
|
{parseExpire(current.extra.expire)}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -223,7 +223,7 @@ const ProfileDetails = ({
|
|||||||
<Typography
|
<Typography
|
||||||
variant="caption"
|
variant="caption"
|
||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
sx={{ mb: 0.5, display: "block" }}
|
sx={{ mb: 0.5, display: 'block' }}
|
||||||
>
|
>
|
||||||
{trafficPercentage}%
|
{trafficPercentage}%
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -241,78 +241,78 @@ const ProfileDetails = ({
|
|||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
// 提取空配置组件
|
// 提取空配置组件
|
||||||
const EmptyProfile = ({ onClick }: { onClick: () => void }) => {
|
const EmptyProfile = ({ onClick }: { onClick: () => void }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
flexDirection: "column",
|
flexDirection: 'column',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
py: 2.4,
|
py: 2.4,
|
||||||
cursor: "pointer",
|
cursor: 'pointer',
|
||||||
"&:hover": { bgcolor: "action.hover" },
|
'&:hover': { bgcolor: 'action.hover' },
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
}}
|
}}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<CloudUploadOutlined
|
<CloudUploadOutlined
|
||||||
sx={{ fontSize: 60, color: "primary.main", mb: 2 }}
|
sx={{ fontSize: 60, color: 'primary.main', mb: 2 }}
|
||||||
/>
|
/>
|
||||||
<Typography variant="h6" gutterBottom>
|
<Typography variant="h6" gutterBottom>
|
||||||
{t("profiles.page.actions.import")} {t("profiles.page.title")}
|
{t('profiles.page.actions.import')} {t('profiles.page.title')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("profiles.components.card.labels.clickToImport")}
|
{t('profiles.components.card.labels.clickToImport')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export const HomeProfileCard = ({
|
export const HomeProfileCard = ({
|
||||||
current,
|
current,
|
||||||
onProfileUpdated,
|
onProfileUpdated,
|
||||||
}: HomeProfileCardProps) => {
|
}: HomeProfileCardProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate()
|
||||||
const { refreshAll } = useAppData();
|
const { refreshAll } = useAppData()
|
||||||
|
|
||||||
// 更新当前订阅
|
// 更新当前订阅
|
||||||
const [updating, setUpdating] = useState(false);
|
const [updating, setUpdating] = useState(false)
|
||||||
|
|
||||||
const onUpdateProfile = useLockFn(async () => {
|
const onUpdateProfile = useLockFn(async () => {
|
||||||
if (!current?.uid) return;
|
if (!current?.uid) return
|
||||||
|
|
||||||
setUpdating(true);
|
setUpdating(true)
|
||||||
try {
|
try {
|
||||||
await updateProfile(current.uid, current.option);
|
await updateProfile(current.uid, current.option)
|
||||||
onProfileUpdated?.();
|
onProfileUpdated?.()
|
||||||
|
|
||||||
// 刷新首页数据
|
// 刷新首页数据
|
||||||
refreshAll();
|
refreshAll()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showNotice.error(err, 3000);
|
showNotice.error(err, 3000)
|
||||||
} finally {
|
} finally {
|
||||||
setUpdating(false);
|
setUpdating(false)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
// 导航到订阅页面
|
// 导航到订阅页面
|
||||||
const goToProfiles = useCallback(() => {
|
const goToProfiles = useCallback(() => {
|
||||||
navigate("/profile");
|
navigate('/profile')
|
||||||
}, [navigate]);
|
}, [navigate])
|
||||||
|
|
||||||
// 卡片标题
|
// 卡片标题
|
||||||
const cardTitle = useMemo(() => {
|
const cardTitle = useMemo(() => {
|
||||||
if (!current) return t("profiles.page.title");
|
if (!current) return t('profiles.page.title')
|
||||||
|
|
||||||
if (!current.home) return current.name;
|
if (!current.home) return current.name
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
@@ -322,16 +322,16 @@ export const HomeProfileCard = ({
|
|||||||
fontSize={18}
|
fontSize={18}
|
||||||
onClick={() => current.home && openWebUrl(current.home)}
|
onClick={() => current.home && openWebUrl(current.home)}
|
||||||
sx={{
|
sx={{
|
||||||
color: "inherit",
|
color: 'inherit',
|
||||||
textDecoration: "none",
|
textDecoration: 'none',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
maxWidth: "100%",
|
maxWidth: '100%',
|
||||||
"& > span": {
|
'& > span': {
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
textOverflow: "ellipsis",
|
textOverflow: 'ellipsis',
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: 'nowrap',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@@ -342,18 +342,18 @@ export const HomeProfileCard = ({
|
|||||||
fontSize="inherit"
|
fontSize="inherit"
|
||||||
sx={{
|
sx={{
|
||||||
ml: 0.5,
|
ml: 0.5,
|
||||||
fontSize: "0.8rem",
|
fontSize: '0.8rem',
|
||||||
opacity: 0.7,
|
opacity: 0.7,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
)
|
||||||
}, [current, t]);
|
}, [current, t])
|
||||||
|
|
||||||
// 卡片操作按钮
|
// 卡片操作按钮
|
||||||
const cardAction = useMemo(() => {
|
const cardAction = useMemo(() => {
|
||||||
if (!current) return null;
|
if (!current) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@@ -363,10 +363,10 @@ export const HomeProfileCard = ({
|
|||||||
endIcon={<StorageOutlined fontSize="small" />}
|
endIcon={<StorageOutlined fontSize="small" />}
|
||||||
sx={{ borderRadius: 1.5 }}
|
sx={{ borderRadius: 1.5 }}
|
||||||
>
|
>
|
||||||
{t("layout.components.navigation.tabs.profiles")}
|
{t('layout.components.navigation.tabs.profiles')}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
)
|
||||||
}, [current, goToProfiles, t]);
|
}, [current, goToProfiles, t])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedCard
|
<EnhancedCard
|
||||||
@@ -385,5 +385,5 @@ export const HomeProfileCard = ({
|
|||||||
<EmptyProfile onClick={goToProfiles} />
|
<EmptyProfile onClick={goToProfiles} />
|
||||||
)}
|
)}
|
||||||
</EnhancedCard>
|
</EnhancedCard>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import {
|
|||||||
RefreshOutlined,
|
RefreshOutlined,
|
||||||
VisibilityOffOutlined,
|
VisibilityOffOutlined,
|
||||||
VisibilityOutlined,
|
VisibilityOutlined,
|
||||||
} from "@mui/icons-material";
|
} from '@mui/icons-material'
|
||||||
import { Box, Button, IconButton, Skeleton, Typography } from "@mui/material";
|
import { Box, Button, IconButton, Skeleton, 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 type { XOR } from "foxts/ts-xor";
|
import type { XOR } from 'foxts/ts-xor'
|
||||||
import {
|
import {
|
||||||
memo,
|
memo,
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -16,26 +16,26 @@ import {
|
|||||||
useEffectEvent,
|
useEffectEvent,
|
||||||
useMemo,
|
useMemo,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
} from "react";
|
} from 'react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
import useSWRImmutable from "swr/immutable";
|
import useSWRImmutable from 'swr/immutable'
|
||||||
|
|
||||||
import { getIpInfo } from "@/services/api";
|
import { getIpInfo } from '@/services/api'
|
||||||
import { SWR_EXTERNAL_API } from "@/services/config";
|
import { SWR_EXTERNAL_API } from '@/services/config'
|
||||||
|
|
||||||
import { EnhancedCard } from "./enhanced-card";
|
import { EnhancedCard } from './enhanced-card'
|
||||||
|
|
||||||
// 定义刷新时间(秒)
|
// 定义刷新时间(秒)
|
||||||
const IP_REFRESH_SECONDS = 300;
|
const IP_REFRESH_SECONDS = 300
|
||||||
const COUNTDOWN_TICK_INTERVAL = 5_000;
|
const COUNTDOWN_TICK_INTERVAL = 5_000
|
||||||
const IP_INFO_CACHE_KEY = "cv_ip_info_cache";
|
const IP_INFO_CACHE_KEY = 'cv_ip_info_cache'
|
||||||
|
|
||||||
const InfoItem = memo(({ label, value }: { label: string; value?: string }) => (
|
const InfoItem = memo(({ label, value }: { label: string; value?: string }) => (
|
||||||
<Box sx={{ mb: 0.7, display: "flex", alignItems: "flex-start" }}>
|
<Box sx={{ mb: 0.7, display: 'flex', alignItems: 'flex-start' }}>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
sx={{ minwidth: 60, mr: 0.5, flexShrink: 0, textAlign: "right" }}
|
sx={{ minwidth: 60, mr: 0.5, flexShrink: 0, textAlign: 'right' }}
|
||||||
>
|
>
|
||||||
{label}:
|
{label}:
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -43,46 +43,46 @@ const InfoItem = memo(({ label, value }: { label: string; value?: string }) => (
|
|||||||
variant="body2"
|
variant="body2"
|
||||||
sx={{
|
sx={{
|
||||||
ml: 0.5,
|
ml: 0.5,
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
textOverflow: "ellipsis",
|
textOverflow: 'ellipsis',
|
||||||
wordBreak: "break-word",
|
wordBreak: 'break-word',
|
||||||
whiteSpace: "normal",
|
whiteSpace: 'normal',
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{value || "Unknown"}
|
{value || 'Unknown'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
));
|
))
|
||||||
|
|
||||||
// 获取国旗表情
|
// 获取国旗表情
|
||||||
const getCountryFlag = (countryCode: string | undefined) => {
|
const getCountryFlag = (countryCode: string | undefined) => {
|
||||||
if (!countryCode) return "";
|
if (!countryCode) return ''
|
||||||
const codePoints = countryCode
|
const codePoints = countryCode
|
||||||
.toUpperCase()
|
.toUpperCase()
|
||||||
.split("")
|
.split('')
|
||||||
.map((char) => 127397 + char.charCodeAt(0));
|
.map((char) => 127397 + char.charCodeAt(0))
|
||||||
return String.fromCodePoint(...codePoints);
|
return String.fromCodePoint(...codePoints)
|
||||||
};
|
}
|
||||||
|
|
||||||
type CountDownState = XOR<
|
type CountDownState = XOR<
|
||||||
{
|
{
|
||||||
type: "countdown";
|
type: 'countdown'
|
||||||
remainingSeconds: number;
|
remainingSeconds: number
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "revalidating";
|
type: 'revalidating'
|
||||||
}
|
}
|
||||||
>;
|
>
|
||||||
|
|
||||||
const IPInfoCardContainer = forwardRef<HTMLElement, React.PropsWithChildren>(
|
const IPInfoCardContainer = forwardRef<HTMLElement, React.PropsWithChildren>(
|
||||||
({ children }, ref) => {
|
({ children }, ref) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const { mutate } = useIPInfo();
|
const { mutate } = useIPInfo()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedCard
|
<EnhancedCard
|
||||||
title={t("home.components.ipInfo.title")}
|
title={t('home.components.ipInfo.title')}
|
||||||
icon={<LocationOnOutlined />}
|
icon={<LocationOnOutlined />}
|
||||||
iconColor="info"
|
iconColor="info"
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -94,41 +94,41 @@ const IPInfoCardContainer = forwardRef<HTMLElement, React.PropsWithChildren>(
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</EnhancedCard>
|
</EnhancedCard>
|
||||||
);
|
)
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
// IP信息卡片组件
|
// IP信息卡片组件
|
||||||
export const IpInfoCard = () => {
|
export const IpInfoCard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const [showIp, setShowIp] = useState(false);
|
const [showIp, setShowIp] = useState(false)
|
||||||
const appWindow = useMemo(() => getCurrentWebviewWindow(), []);
|
const appWindow = useMemo(() => getCurrentWebviewWindow(), [])
|
||||||
|
|
||||||
// track ip info card has been in viewport or not
|
// track ip info card has been in viewport or not
|
||||||
// hasIntersected default to false, and will be true once the card is in viewport
|
// hasIntersected default to false, and will be true once the card is in viewport
|
||||||
// and will never be false again afterwards (unless resetIntersected is called or
|
// and will never be false again afterwards (unless resetIntersected is called or
|
||||||
// the component is unmounted)
|
// the component is unmounted)
|
||||||
const [containerRef, hasIntersected, _resetIntersected] = useIntersection({
|
const [containerRef, hasIntersected, _resetIntersected] = useIntersection({
|
||||||
rootMargin: "0px",
|
rootMargin: '0px',
|
||||||
});
|
})
|
||||||
|
|
||||||
const [countdown, setCountdown] = useState<CountDownState>({
|
const [countdown, setCountdown] = useState<CountDownState>({
|
||||||
type: "countdown",
|
type: 'countdown',
|
||||||
remainingSeconds: IP_REFRESH_SECONDS,
|
remainingSeconds: IP_REFRESH_SECONDS,
|
||||||
});
|
})
|
||||||
|
|
||||||
const { data: ipInfo, error, isLoading, mutate } = useIPInfo();
|
const { data: ipInfo, error, isLoading, mutate } = useIPInfo()
|
||||||
|
|
||||||
// function useEffectEvent
|
// function useEffectEvent
|
||||||
const onCountdownTick = useEffectEvent(async () => {
|
const onCountdownTick = useEffectEvent(async () => {
|
||||||
const now = Date.now();
|
const now = Date.now()
|
||||||
const ts = ipInfo?.lastFetchTs;
|
const ts = ipInfo?.lastFetchTs
|
||||||
if (!ts) {
|
if (!ts) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const elapsed = Math.floor((now - ts) / 1000);
|
const elapsed = Math.floor((now - ts) / 1000)
|
||||||
const remaining = IP_REFRESH_SECONDS - elapsed;
|
const remaining = IP_REFRESH_SECONDS - elapsed
|
||||||
|
|
||||||
if (remaining <= 0) {
|
if (remaining <= 0) {
|
||||||
if (
|
if (
|
||||||
@@ -139,21 +139,21 @@ export const IpInfoCard = () => {
|
|||||||
// is online
|
// is online
|
||||||
navigator.onLine &&
|
navigator.onLine &&
|
||||||
// there is no ongoing revalidation already scheduled
|
// there is no ongoing revalidation already scheduled
|
||||||
countdown.type !== "revalidating" &&
|
countdown.type !== 'revalidating' &&
|
||||||
// window is visible
|
// window is visible
|
||||||
(await appWindow.isVisible())
|
(await appWindow.isVisible())
|
||||||
) {
|
) {
|
||||||
setCountdown({ type: "revalidating" });
|
setCountdown({ type: 'revalidating' })
|
||||||
// we do not care about the result of mutate here. after mutate is done,
|
// we do not care about the result of mutate here. after mutate is done,
|
||||||
// simply wait for next interval tick with `setCountdown({ type: "countdown", ... })`
|
// simply wait for next interval tick with `setCountdown({ type: "countdown", ... })`
|
||||||
try {
|
try {
|
||||||
await mutate();
|
await mutate()
|
||||||
} finally {
|
} finally {
|
||||||
// in case mutate throws error, we still need to reset the countdown state
|
// in case mutate throws error, we still need to reset the countdown state
|
||||||
setCountdown({
|
setCountdown({
|
||||||
type: "countdown",
|
type: 'countdown',
|
||||||
remainingSeconds: IP_REFRESH_SECONDS,
|
remainingSeconds: IP_REFRESH_SECONDS,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// do nothing. we even skip "setCountdown" to reduce re-renders
|
// do nothing. we even skip "setCountdown" to reduce re-renders
|
||||||
@@ -164,15 +164,15 @@ export const IpInfoCard = () => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setCountdown({
|
setCountdown({
|
||||||
type: "countdown",
|
type: 'countdown',
|
||||||
remainingSeconds: remaining,
|
remainingSeconds: remaining,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
// Countdown / refresh scheduler — updates UI every 1s and triggers immediate revalidation when expired
|
// Countdown / refresh scheduler — updates UI every 1s and triggers immediate revalidation when expired
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timer: number | null = null;
|
let timer: number | null = null
|
||||||
|
|
||||||
// Do not add document.hidden check here as it is not reliable in Tauri.
|
// Do not add document.hidden check here as it is not reliable in Tauri.
|
||||||
//
|
//
|
||||||
@@ -180,17 +180,17 @@ export const IpInfoCard = () => {
|
|||||||
// instead of Tauri, which is reliable enough.
|
// instead of Tauri, which is reliable enough.
|
||||||
if (hasIntersected) {
|
if (hasIntersected) {
|
||||||
console.debug(
|
console.debug(
|
||||||
"IP info card has entered the viewport, starting the countdown interval.",
|
'IP info card has entered the viewport, starting the countdown interval.',
|
||||||
);
|
)
|
||||||
timer = window.setInterval(onCountdownTick, COUNTDOWN_TICK_INTERVAL);
|
timer = window.setInterval(onCountdownTick, COUNTDOWN_TICK_INTERVAL)
|
||||||
} else {
|
} else {
|
||||||
console.debug(
|
console.debug(
|
||||||
"IP info card has not yet entered the viewport, no counting down.",
|
'IP info card has not yet entered the viewport, no counting down.',
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// This will fire when the window is minimized or restored
|
// This will fire when the window is minimized or restored
|
||||||
document.addEventListener("visibilitychange", onVisibilityChange);
|
document.addEventListener('visibilitychange', onVisibilityChange)
|
||||||
// Tauri's visibility change detection is actually broken on some platforms:
|
// Tauri's visibility change detection is actually broken on some platforms:
|
||||||
// https://github.com/tauri-apps/tauri/issues/10592
|
// https://github.com/tauri-apps/tauri/issues/10592
|
||||||
//
|
//
|
||||||
@@ -199,100 +199,100 @@ export const IpInfoCard = () => {
|
|||||||
// reduce power consumption.
|
// reduce power consumption.
|
||||||
function onVisibilityChange() {
|
function onVisibilityChange() {
|
||||||
if (document.hidden) {
|
if (document.hidden) {
|
||||||
console.debug("Document hidden, pause the interval");
|
console.debug('Document hidden, pause the interval')
|
||||||
// Pause the timer
|
// Pause the timer
|
||||||
if (timer != null) {
|
if (timer != null) {
|
||||||
clearInterval(timer);
|
clearInterval(timer)
|
||||||
timer = null;
|
timer = null
|
||||||
}
|
}
|
||||||
} else if (hasIntersected) {
|
} else if (hasIntersected) {
|
||||||
console.debug("Document visible, resume the interval");
|
console.debug('Document visible, resume the interval')
|
||||||
// Resume the timer only when previous one is cleared
|
// Resume the timer only when previous one is cleared
|
||||||
if (timer == null) {
|
if (timer == null) {
|
||||||
timer = window.setInterval(onCountdownTick, COUNTDOWN_TICK_INTERVAL);
|
timer = window.setInterval(onCountdownTick, COUNTDOWN_TICK_INTERVAL)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.debug(
|
console.debug(
|
||||||
"Document visible, but IP info card has never entered the viewport, not even once, not starting the interval.",
|
'Document visible, but IP info card has never entered the viewport, not even once, not starting the interval.',
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (timer != null) clearInterval(timer);
|
if (timer != null) clearInterval(timer)
|
||||||
document.removeEventListener("visibilitychange", onVisibilityChange);
|
document.removeEventListener('visibilitychange', onVisibilityChange)
|
||||||
};
|
}
|
||||||
}, [hasIntersected]);
|
}, [hasIntersected])
|
||||||
|
|
||||||
const toggleShowIp = useCallback(() => {
|
const toggleShowIp = useCallback(() => {
|
||||||
setShowIp((prev) => !prev);
|
setShowIp((prev) => !prev)
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
let mainElement: React.ReactElement;
|
let mainElement: React.ReactElement
|
||||||
|
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case isLoading:
|
case isLoading:
|
||||||
mainElement = (
|
mainElement = (
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||||
<Skeleton variant="text" width="60%" height={30} />
|
<Skeleton variant="text" width="60%" height={30} />
|
||||||
<Skeleton variant="text" width="80%" height={24} />
|
<Skeleton variant="text" width="80%" height={24} />
|
||||||
<Skeleton variant="text" width="70%" height={24} />
|
<Skeleton variant="text" width="70%" height={24} />
|
||||||
<Skeleton variant="text" width="50%" height={24} />
|
<Skeleton variant="text" width="50%" height={24} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
break;
|
break
|
||||||
case !!error:
|
case !!error:
|
||||||
mainElement = (
|
mainElement = (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
flexDirection: "column",
|
flexDirection: 'column',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
height: "100%",
|
height: '100%',
|
||||||
color: "error.main",
|
color: 'error.main',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="body1" color="error">
|
<Typography variant="body1" color="error">
|
||||||
{error instanceof Error
|
{error instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
: t("home.components.ipInfo.errors.load")}
|
: t('home.components.ipInfo.errors.load')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button onClick={() => mutate()} sx={{ mt: 2 }}>
|
<Button onClick={() => mutate()} sx={{ mt: 2 }}>
|
||||||
{t("shared.actions.retry")}
|
{t('shared.actions.retry')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
break;
|
break
|
||||||
default: // Normal render
|
default: // Normal render
|
||||||
mainElement = (
|
mainElement = (
|
||||||
<Box sx={{ height: "100%", display: "flex", flexDirection: "column" }}>
|
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
flexDirection: "row",
|
flexDirection: 'row',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 左侧:国家和IP地址 */}
|
{/* 左侧:国家和IP地址 */}
|
||||||
<Box sx={{ width: "40%", overflow: "hidden" }}>
|
<Box sx={{ width: '40%', overflow: 'hidden' }}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
mb: 1,
|
mb: 1,
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
component="span"
|
component="span"
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: "1.5rem",
|
fontSize: '1.5rem',
|
||||||
mr: 1,
|
mr: 1,
|
||||||
display: "inline-block",
|
display: 'inline-block',
|
||||||
width: 28,
|
width: 28,
|
||||||
textAlign: "center",
|
textAlign: 'center',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
fontFamily: '"twemoji mozilla", sans-serif',
|
fontFamily: '"twemoji mozilla", sans-serif',
|
||||||
}}
|
}}
|
||||||
@@ -302,46 +302,46 @@ export const IpInfoCard = () => {
|
|||||||
<Typography
|
<Typography
|
||||||
variant="subtitle1"
|
variant="subtitle1"
|
||||||
sx={{
|
sx={{
|
||||||
fontWeight: "medium",
|
fontWeight: 'medium',
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
textOverflow: "ellipsis",
|
textOverflow: 'ellipsis',
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: 'nowrap',
|
||||||
maxWidth: "100%",
|
maxWidth: '100%',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{ipInfo?.country ||
|
{ipInfo?.country ||
|
||||||
t("home.components.ipInfo.labels.unknown")}
|
t('home.components.ipInfo.labels.unknown')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ display: "flex", alignItems: "center", mb: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
sx={{ flexShrink: 0 }}
|
sx={{ flexShrink: 0 }}
|
||||||
>
|
>
|
||||||
{t("home.components.ipInfo.labels.ip")}:
|
{t('home.components.ipInfo.labels.ip')}:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
ml: 1,
|
ml: 1,
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
maxWidth: "calc(100% - 30px)",
|
maxWidth: 'calc(100% - 30px)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
sx={{
|
sx={{
|
||||||
fontFamily: "monospace",
|
fontFamily: 'monospace',
|
||||||
fontSize: "0.75rem",
|
fontSize: '0.75rem',
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
textOverflow: "ellipsis",
|
textOverflow: 'ellipsis',
|
||||||
wordBreak: "break-all",
|
wordBreak: 'break-all',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{showIp ? ipInfo?.ip : "••••••••••"}
|
{showIp ? ipInfo?.ip : '••••••••••'}
|
||||||
</Typography>
|
</Typography>
|
||||||
<IconButton size="small" onClick={toggleShowIp}>
|
<IconButton size="small" onClick={toggleShowIp}>
|
||||||
{showIp ? (
|
{showIp ? (
|
||||||
@@ -354,29 +354,29 @@ export const IpInfoCard = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<InfoItem
|
<InfoItem
|
||||||
label={t("home.components.ipInfo.labels.asn")}
|
label={t('home.components.ipInfo.labels.asn')}
|
||||||
value={ipInfo?.asn ? `AS${ipInfo.asn}` : "N/A"}
|
value={ipInfo?.asn ? `AS${ipInfo.asn}` : 'N/A'}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 右侧:组织、ISP和位置信息 */}
|
{/* 右侧:组织、ISP和位置信息 */}
|
||||||
<Box sx={{ width: "60%", overflow: "auto" }}>
|
<Box sx={{ width: '60%', overflow: 'auto' }}>
|
||||||
<InfoItem
|
<InfoItem
|
||||||
label={t("home.components.ipInfo.labels.isp")}
|
label={t('home.components.ipInfo.labels.isp')}
|
||||||
value={ipInfo?.organization}
|
value={ipInfo?.organization}
|
||||||
/>
|
/>
|
||||||
<InfoItem
|
<InfoItem
|
||||||
label={t("home.components.ipInfo.labels.org")}
|
label={t('home.components.ipInfo.labels.org')}
|
||||||
value={ipInfo?.asn_organization}
|
value={ipInfo?.asn_organization}
|
||||||
/>
|
/>
|
||||||
<InfoItem
|
<InfoItem
|
||||||
label={t("home.components.ipInfo.labels.location")}
|
label={t('home.components.ipInfo.labels.location')}
|
||||||
value={[ipInfo?.city, ipInfo?.region]
|
value={[ipInfo?.city, ipInfo?.region]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(", ")}
|
.join(', ')}
|
||||||
/>
|
/>
|
||||||
<InfoItem
|
<InfoItem
|
||||||
label={t("home.components.ipInfo.labels.timezone")}
|
label={t('home.components.ipInfo.labels.timezone')}
|
||||||
value={ipInfo?.timezone}
|
value={ipInfo?.timezone}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -384,43 +384,43 @@ export const IpInfoCard = () => {
|
|||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
mt: "auto",
|
mt: 'auto',
|
||||||
pt: 0.5,
|
pt: 0.5,
|
||||||
borderTop: 1,
|
borderTop: 1,
|
||||||
borderColor: "divider",
|
borderColor: 'divider',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
justifyContent: "space-between",
|
justifyContent: 'space-between',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
opacity: 0.7,
|
opacity: 0.7,
|
||||||
fontSize: "0.7rem",
|
fontSize: '0.7rem',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="caption">
|
<Typography variant="caption">
|
||||||
{t("home.components.ipInfo.labels.autoRefresh")}
|
{t('home.components.ipInfo.labels.autoRefresh')}
|
||||||
{countdown.type === "countdown"
|
{countdown.type === 'countdown'
|
||||||
? `: ${countdown.remainingSeconds}s`
|
? `: ${countdown.remainingSeconds}s`
|
||||||
: "..."}
|
: '...'}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
variant="caption"
|
variant="caption"
|
||||||
sx={{
|
sx={{
|
||||||
textOverflow: "ellipsis",
|
textOverflow: 'ellipsis',
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{`${ipInfo?.country_code ?? "N/A"}, ${ipInfo?.longitude?.toFixed(2) ?? "N/A"}, ${ipInfo?.latitude?.toFixed(2) ?? "N/A"}`}
|
{`${ipInfo?.country_code ?? 'N/A'}, ${ipInfo?.longitude?.toFixed(2) ?? 'N/A'}, ${ipInfo?.latitude?.toFixed(2) ?? 'N/A'}`}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IPInfoCardContainer ref={containerRef}>{mainElement}</IPInfoCardContainer>
|
<IPInfoCardContainer ref={containerRef}>{mainElement}</IPInfoCardContainer>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
function useIPInfo() {
|
function useIPInfo() {
|
||||||
return useSWRImmutable(IP_INFO_CACHE_KEY, getIpInfo, SWR_EXTERNAL_API);
|
return useSWRImmutable(IP_INFO_CACHE_KEY, getIpInfo, SWR_EXTERNAL_API)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
TroubleshootRounded,
|
TroubleshootRounded,
|
||||||
HelpOutlineRounded,
|
HelpOutlineRounded,
|
||||||
SvgIconComponent,
|
SvgIconComponent,
|
||||||
} from "@mui/icons-material";
|
} from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Typography,
|
Typography,
|
||||||
@@ -13,24 +13,24 @@ import {
|
|||||||
alpha,
|
alpha,
|
||||||
useTheme,
|
useTheme,
|
||||||
Fade,
|
Fade,
|
||||||
} from "@mui/material";
|
} from '@mui/material'
|
||||||
import { useState, useMemo, memo, FC } from "react";
|
import { useState, useMemo, memo, FC } from 'react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import ProxyControlSwitches from "@/components/shared/proxy-control-switches";
|
import ProxyControlSwitches from '@/components/shared/proxy-control-switches'
|
||||||
import { useSystemProxyState } from "@/hooks/use-system-proxy-state";
|
import { useSystemProxyState } from '@/hooks/use-system-proxy-state'
|
||||||
import { useSystemState } from "@/hooks/use-system-state";
|
import { useSystemState } from '@/hooks/use-system-state'
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
import { showNotice } from "@/services/notice-service";
|
import { showNotice } from '@/services/notice-service'
|
||||||
|
|
||||||
const LOCAL_STORAGE_TAB_KEY = "clash-verge-proxy-active-tab";
|
const LOCAL_STORAGE_TAB_KEY = 'clash-verge-proxy-active-tab'
|
||||||
|
|
||||||
interface TabButtonProps {
|
interface TabButtonProps {
|
||||||
isActive: boolean;
|
isActive: boolean
|
||||||
onClick: () => void;
|
onClick: () => void
|
||||||
icon: SvgIconComponent;
|
icon: SvgIconComponent
|
||||||
label: string;
|
label: string
|
||||||
hasIndicator?: boolean;
|
hasIndicator?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tab组件
|
// Tab组件
|
||||||
@@ -40,34 +40,34 @@ const TabButton: FC<TabButtonProps> = memo(
|
|||||||
elevation={isActive ? 2 : 0}
|
elevation={isActive ? 2 : 0}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
sx={{
|
sx={{
|
||||||
cursor: "pointer",
|
cursor: 'pointer',
|
||||||
px: 2,
|
px: 2,
|
||||||
py: 1,
|
py: 1,
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
gap: 1,
|
gap: 1,
|
||||||
bgcolor: isActive ? "primary.main" : "background.paper",
|
bgcolor: isActive ? 'primary.main' : 'background.paper',
|
||||||
color: isActive ? "primary.contrastText" : "text.primary",
|
color: isActive ? 'primary.contrastText' : 'text.primary',
|
||||||
borderRadius: 1.5,
|
borderRadius: 1.5,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
maxWidth: 160,
|
maxWidth: 160,
|
||||||
transition: "all 0.2s ease-in-out",
|
transition: 'all 0.2s ease-in-out',
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
transform: "translateY(-1px)",
|
transform: 'translateY(-1px)',
|
||||||
boxShadow: 1,
|
boxShadow: 1,
|
||||||
},
|
},
|
||||||
"&:after": isActive
|
'&:after': isActive
|
||||||
? {
|
? {
|
||||||
content: '""',
|
content: '""',
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
bottom: -9,
|
bottom: -9,
|
||||||
left: "50%",
|
left: '50%',
|
||||||
width: 2,
|
width: 2,
|
||||||
height: 9,
|
height: 9,
|
||||||
bgcolor: "primary.main",
|
bgcolor: 'primary.main',
|
||||||
transform: "translateX(-50%)",
|
transform: 'translateX(-50%)',
|
||||||
}
|
}
|
||||||
: {},
|
: {},
|
||||||
}}
|
}}
|
||||||
@@ -81,9 +81,9 @@ const TabButton: FC<TabButtonProps> = memo(
|
|||||||
sx={{
|
sx={{
|
||||||
width: 8,
|
width: 8,
|
||||||
height: 8,
|
height: 8,
|
||||||
borderRadius: "50%",
|
borderRadius: '50%',
|
||||||
bgcolor: isActive ? "#fff" : "success.main",
|
bgcolor: isActive ? '#fff' : 'success.main',
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
top: 8,
|
top: 8,
|
||||||
right: 8,
|
right: 8,
|
||||||
}}
|
}}
|
||||||
@@ -91,11 +91,11 @@ const TabButton: FC<TabButtonProps> = memo(
|
|||||||
)}
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
|
|
||||||
interface TabDescriptionProps {
|
interface TabDescriptionProps {
|
||||||
description: string;
|
description: string
|
||||||
tooltipTitle: string;
|
tooltipTitle: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 描述文本组件
|
// 描述文本组件
|
||||||
@@ -106,21 +106,21 @@ const TabDescription: FC<TabDescriptionProps> = memo(
|
|||||||
variant="caption"
|
variant="caption"
|
||||||
component="div"
|
component="div"
|
||||||
sx={{
|
sx={{
|
||||||
width: "95%",
|
width: '95%',
|
||||||
textAlign: "center",
|
textAlign: 'center',
|
||||||
color: "text.secondary",
|
color: 'text.secondary',
|
||||||
p: 0.8,
|
p: 0.8,
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
borderColor: "primary.main",
|
borderColor: 'primary.main',
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderStyle: "solid",
|
borderStyle: 'solid',
|
||||||
backgroundColor: "background.paper",
|
backgroundColor: 'background.paper',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
gap: 0.5,
|
gap: 0.5,
|
||||||
wordBreak: "break-word",
|
wordBreak: 'break-word',
|
||||||
hyphens: "auto",
|
hyphens: 'auto',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{description}
|
{description}
|
||||||
@@ -132,47 +132,47 @@ const TabDescription: FC<TabDescriptionProps> = memo(
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Fade>
|
</Fade>
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
|
|
||||||
export const ProxyTunCard: FC = () => {
|
export const ProxyTunCard: FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const theme = useTheme();
|
const theme = useTheme()
|
||||||
const [activeTab, setActiveTab] = useState<string>(
|
const [activeTab, setActiveTab] = useState<string>(
|
||||||
() => localStorage.getItem(LOCAL_STORAGE_TAB_KEY) || "system",
|
() => localStorage.getItem(LOCAL_STORAGE_TAB_KEY) || 'system',
|
||||||
);
|
)
|
||||||
|
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge()
|
||||||
const { isTunModeAvailable } = useSystemState();
|
const { isTunModeAvailable } = useSystemState()
|
||||||
const { configState: systemProxyConfigState } = useSystemProxyState();
|
const { configState: systemProxyConfigState } = useSystemProxyState()
|
||||||
|
|
||||||
const { enable_tun_mode } = verge ?? {};
|
const { enable_tun_mode } = verge ?? {}
|
||||||
|
|
||||||
const handleError = (err: unknown) => {
|
const handleError = (err: unknown) => {
|
||||||
showNotice.error(err);
|
showNotice.error(err)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleTabChange = (tab: string) => {
|
const handleTabChange = (tab: string) => {
|
||||||
setActiveTab(tab);
|
setActiveTab(tab)
|
||||||
localStorage.setItem(LOCAL_STORAGE_TAB_KEY, tab);
|
localStorage.setItem(LOCAL_STORAGE_TAB_KEY, tab)
|
||||||
};
|
}
|
||||||
|
|
||||||
const tabDescription = useMemo(() => {
|
const tabDescription = useMemo(() => {
|
||||||
if (activeTab === "system") {
|
if (activeTab === 'system') {
|
||||||
return {
|
return {
|
||||||
text: systemProxyConfigState
|
text: systemProxyConfigState
|
||||||
? t("home.components.proxyTun.status.systemProxyEnabled")
|
? t('home.components.proxyTun.status.systemProxyEnabled')
|
||||||
: t("home.components.proxyTun.status.systemProxyDisabled"),
|
: t('home.components.proxyTun.status.systemProxyDisabled'),
|
||||||
tooltip: t("home.components.proxyTun.tooltips.systemProxy"),
|
tooltip: t('home.components.proxyTun.tooltips.systemProxy'),
|
||||||
};
|
}
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
text: !isTunModeAvailable
|
text: !isTunModeAvailable
|
||||||
? t("home.components.proxyTun.status.tunModeServiceRequired")
|
? t('home.components.proxyTun.status.tunModeServiceRequired')
|
||||||
: enable_tun_mode
|
: enable_tun_mode
|
||||||
? t("home.components.proxyTun.status.tunModeEnabled")
|
? t('home.components.proxyTun.status.tunModeEnabled')
|
||||||
: t("home.components.proxyTun.status.tunModeDisabled"),
|
: t('home.components.proxyTun.status.tunModeDisabled'),
|
||||||
tooltip: t("home.components.proxyTun.tooltips.tunMode"),
|
tooltip: t('home.components.proxyTun.tooltips.tunMode'),
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
activeTab,
|
activeTab,
|
||||||
@@ -180,44 +180,44 @@ export const ProxyTunCard: FC = () => {
|
|||||||
enable_tun_mode,
|
enable_tun_mode,
|
||||||
isTunModeAvailable,
|
isTunModeAvailable,
|
||||||
t,
|
t,
|
||||||
]);
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', width: '100%' }}>
|
||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
spacing={1}
|
spacing={1}
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
zIndex: 2,
|
zIndex: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TabButton
|
<TabButton
|
||||||
isActive={activeTab === "system"}
|
isActive={activeTab === 'system'}
|
||||||
onClick={() => handleTabChange("system")}
|
onClick={() => handleTabChange('system')}
|
||||||
icon={ComputerRounded}
|
icon={ComputerRounded}
|
||||||
label={t("settings.sections.system.toggles.systemProxy")}
|
label={t('settings.sections.system.toggles.systemProxy')}
|
||||||
hasIndicator={systemProxyConfigState}
|
hasIndicator={systemProxyConfigState}
|
||||||
/>
|
/>
|
||||||
<TabButton
|
<TabButton
|
||||||
isActive={activeTab === "tun"}
|
isActive={activeTab === 'tun'}
|
||||||
onClick={() => handleTabChange("tun")}
|
onClick={() => handleTabChange('tun')}
|
||||||
icon={TroubleshootRounded}
|
icon={TroubleshootRounded}
|
||||||
label={t("settings.sections.system.toggles.tunMode")}
|
label={t('settings.sections.system.toggles.tunMode')}
|
||||||
hasIndicator={enable_tun_mode && isTunModeAvailable}
|
hasIndicator={enable_tun_mode && isTunModeAvailable}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
width: "100%",
|
width: '100%',
|
||||||
my: 1,
|
my: 1,
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
overflow: "visible",
|
overflow: 'visible',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TabDescription
|
<TabDescription
|
||||||
@@ -237,13 +237,13 @@ export const ProxyTunCard: FC = () => {
|
|||||||
<ProxyControlSwitches
|
<ProxyControlSwitches
|
||||||
onError={handleError}
|
onError={handleError}
|
||||||
label={
|
label={
|
||||||
activeTab === "system"
|
activeTab === 'system'
|
||||||
? t("settings.sections.system.toggles.systemProxy")
|
? t('settings.sections.system.toggles.systemProxy')
|
||||||
: t("settings.sections.system.toggles.tunMode")
|
: t('settings.sections.system.toggles.tunMode')
|
||||||
}
|
}
|
||||||
noRightPadding={true}
|
noRightPadding={true}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -4,195 +4,195 @@ import {
|
|||||||
AdminPanelSettingsOutlined,
|
AdminPanelSettingsOutlined,
|
||||||
DnsOutlined,
|
DnsOutlined,
|
||||||
ExtensionOutlined,
|
ExtensionOutlined,
|
||||||
} from "@mui/icons-material";
|
} from '@mui/icons-material'
|
||||||
import { Typography, Stack, Divider, Chip, IconButton } from "@mui/material";
|
import { Typography, Stack, Divider, Chip, IconButton } from '@mui/material'
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from 'ahooks'
|
||||||
import { useCallback, useEffect, useMemo, useReducer } from "react";
|
import { useCallback, useEffect, useMemo, useReducer } from 'react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from 'react-router'
|
||||||
|
|
||||||
import { useServiceInstaller } from "@/hooks/use-service-installer";
|
import { useServiceInstaller } from '@/hooks/use-service-installer'
|
||||||
import { useSystemState } from "@/hooks/use-system-state";
|
import { useSystemState } from '@/hooks/use-system-state'
|
||||||
import { useUpdate } from "@/hooks/use-update";
|
import { useUpdate } from '@/hooks/use-update'
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
import { getSystemInfo } from "@/services/cmds";
|
import { getSystemInfo } from '@/services/cmds'
|
||||||
import { showNotice } from "@/services/notice-service";
|
import { showNotice } from '@/services/notice-service'
|
||||||
import { version as appVersion } from "@root/package.json";
|
import { version as appVersion } from '@root/package.json'
|
||||||
|
|
||||||
import { EnhancedCard } from "./enhanced-card";
|
import { EnhancedCard } from './enhanced-card'
|
||||||
|
|
||||||
interface SystemState {
|
interface SystemState {
|
||||||
osInfo: string;
|
osInfo: string
|
||||||
lastCheckUpdate: string;
|
lastCheckUpdate: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type SystemStateAction =
|
type SystemStateAction =
|
||||||
| { type: "set-os-info"; payload: string }
|
| { type: 'set-os-info'; payload: string }
|
||||||
| { type: "set-last-check-update"; payload: string };
|
| { type: 'set-last-check-update'; payload: string }
|
||||||
|
|
||||||
const systemStateReducer = (
|
const systemStateReducer = (
|
||||||
state: SystemState,
|
state: SystemState,
|
||||||
action: SystemStateAction,
|
action: SystemStateAction,
|
||||||
): SystemState => {
|
): SystemState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "set-os-info":
|
case 'set-os-info':
|
||||||
return { ...state, osInfo: action.payload };
|
return { ...state, osInfo: action.payload }
|
||||||
case "set-last-check-update":
|
case 'set-last-check-update':
|
||||||
return { ...state, lastCheckUpdate: action.payload };
|
return { ...state, lastCheckUpdate: action.payload }
|
||||||
default:
|
default:
|
||||||
return state;
|
return state
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export const SystemInfoCard = () => {
|
export const SystemInfoCard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const { verge, patchVerge } = useVerge();
|
const { verge, patchVerge } = useVerge()
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate()
|
||||||
const { isAdminMode, isSidecarMode } = useSystemState();
|
const { isAdminMode, isSidecarMode } = useSystemState()
|
||||||
const { installServiceAndRestartCore } = useServiceInstaller();
|
const { installServiceAndRestartCore } = useServiceInstaller()
|
||||||
|
|
||||||
// 自动检查更新逻辑
|
// 自动检查更新逻辑
|
||||||
const { checkUpdate: triggerCheckUpdate } = useUpdate(true, {
|
const { checkUpdate: triggerCheckUpdate } = useUpdate(true, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
const now = Date.now();
|
const now = Date.now()
|
||||||
localStorage.setItem("last_check_update", now.toString());
|
localStorage.setItem('last_check_update', now.toString())
|
||||||
dispatchSystemState({
|
dispatchSystemState({
|
||||||
type: "set-last-check-update",
|
type: 'set-last-check-update',
|
||||||
payload: new Date(now).toLocaleString(),
|
payload: new Date(now).toLocaleString(),
|
||||||
});
|
})
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
// 系统信息状态
|
// 系统信息状态
|
||||||
const [systemState, dispatchSystemState] = useReducer(systemStateReducer, {
|
const [systemState, dispatchSystemState] = useReducer(systemStateReducer, {
|
||||||
osInfo: "",
|
osInfo: '',
|
||||||
lastCheckUpdate: "-",
|
lastCheckUpdate: '-',
|
||||||
});
|
})
|
||||||
|
|
||||||
// 初始化系统信息
|
// 初始化系统信息
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timeoutId: number | undefined;
|
let timeoutId: number | undefined
|
||||||
|
|
||||||
getSystemInfo()
|
getSystemInfo()
|
||||||
.then((info) => {
|
.then((info) => {
|
||||||
const lines = info.split("\n");
|
const lines = info.split('\n')
|
||||||
if (lines.length > 0) {
|
if (lines.length > 0) {
|
||||||
const sysName = lines[0].split(": ")[1] || "";
|
const sysName = lines[0].split(': ')[1] || ''
|
||||||
let sysVersion = lines[1].split(": ")[1] || "";
|
let sysVersion = lines[1].split(': ')[1] || ''
|
||||||
|
|
||||||
if (
|
if (
|
||||||
sysName &&
|
sysName &&
|
||||||
sysVersion.toLowerCase().startsWith(sysName.toLowerCase())
|
sysVersion.toLowerCase().startsWith(sysName.toLowerCase())
|
||||||
) {
|
) {
|
||||||
sysVersion = sysVersion.substring(sysName.length).trim();
|
sysVersion = sysVersion.substring(sysName.length).trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatchSystemState({
|
dispatchSystemState({
|
||||||
type: "set-os-info",
|
type: 'set-os-info',
|
||||||
payload: `${sysName} ${sysVersion}`,
|
payload: `${sysName} ${sysVersion}`,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(console.error);
|
.catch(console.error)
|
||||||
|
|
||||||
// 获取最后检查更新时间
|
// 获取最后检查更新时间
|
||||||
const lastCheck = localStorage.getItem("last_check_update");
|
const lastCheck = localStorage.getItem('last_check_update')
|
||||||
if (lastCheck) {
|
if (lastCheck) {
|
||||||
try {
|
try {
|
||||||
const timestamp = parseInt(lastCheck, 10);
|
const timestamp = parseInt(lastCheck, 10)
|
||||||
if (!isNaN(timestamp)) {
|
if (!isNaN(timestamp)) {
|
||||||
dispatchSystemState({
|
dispatchSystemState({
|
||||||
type: "set-last-check-update",
|
type: 'set-last-check-update',
|
||||||
payload: new Date(timestamp).toLocaleString(),
|
payload: new Date(timestamp).toLocaleString(),
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error parsing last check update time", e);
|
console.error('Error parsing last check update time', e)
|
||||||
}
|
}
|
||||||
} else if (verge?.auto_check_update) {
|
} else if (verge?.auto_check_update) {
|
||||||
// 如果启用了自动检查更新但没有记录,设置当前时间并延迟检查
|
// 如果启用了自动检查更新但没有记录,设置当前时间并延迟检查
|
||||||
const now = Date.now();
|
const now = Date.now()
|
||||||
localStorage.setItem("last_check_update", now.toString());
|
localStorage.setItem('last_check_update', now.toString())
|
||||||
dispatchSystemState({
|
dispatchSystemState({
|
||||||
type: "set-last-check-update",
|
type: 'set-last-check-update',
|
||||||
payload: new Date(now).toLocaleString(),
|
payload: new Date(now).toLocaleString(),
|
||||||
});
|
})
|
||||||
|
|
||||||
timeoutId = window.setTimeout(() => {
|
timeoutId = window.setTimeout(() => {
|
||||||
if (verge?.auto_check_update) {
|
if (verge?.auto_check_update) {
|
||||||
triggerCheckUpdate().catch(console.error);
|
triggerCheckUpdate().catch(console.error)
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000)
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
if (timeoutId !== undefined) {
|
if (timeoutId !== undefined) {
|
||||||
window.clearTimeout(timeoutId);
|
window.clearTimeout(timeoutId)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}, [verge?.auto_check_update, dispatchSystemState, triggerCheckUpdate]);
|
}, [verge?.auto_check_update, dispatchSystemState, triggerCheckUpdate])
|
||||||
|
|
||||||
// 导航到设置页面
|
// 导航到设置页面
|
||||||
const goToSettings = useCallback(() => {
|
const goToSettings = useCallback(() => {
|
||||||
navigate("/settings");
|
navigate('/settings')
|
||||||
}, [navigate]);
|
}, [navigate])
|
||||||
|
|
||||||
// 切换自启动状态
|
// 切换自启动状态
|
||||||
const toggleAutoLaunch = useCallback(async () => {
|
const toggleAutoLaunch = useCallback(async () => {
|
||||||
if (!verge) return;
|
if (!verge) return
|
||||||
try {
|
try {
|
||||||
await patchVerge({ enable_auto_launch: !verge.enable_auto_launch });
|
await patchVerge({ enable_auto_launch: !verge.enable_auto_launch })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("切换开机自启动状态失败:", err);
|
console.error('切换开机自启动状态失败:', err)
|
||||||
}
|
}
|
||||||
}, [verge, patchVerge]);
|
}, [verge, patchVerge])
|
||||||
|
|
||||||
// 点击运行模式处理,Sidecar或纯管理员模式允许安装服务
|
// 点击运行模式处理,Sidecar或纯管理员模式允许安装服务
|
||||||
const handleRunningModeClick = useCallback(() => {
|
const handleRunningModeClick = useCallback(() => {
|
||||||
if (isSidecarMode || (isAdminMode && isSidecarMode)) {
|
if (isSidecarMode || (isAdminMode && isSidecarMode)) {
|
||||||
installServiceAndRestartCore();
|
installServiceAndRestartCore()
|
||||||
}
|
}
|
||||||
}, [isSidecarMode, isAdminMode, installServiceAndRestartCore]);
|
}, [isSidecarMode, isAdminMode, installServiceAndRestartCore])
|
||||||
|
|
||||||
// 检查更新
|
// 检查更新
|
||||||
const onCheckUpdate = useLockFn(async () => {
|
const onCheckUpdate = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
const info = await triggerCheckUpdate();
|
const info = await triggerCheckUpdate()
|
||||||
if (!info?.available) {
|
if (!info?.available) {
|
||||||
showNotice.success(
|
showNotice.success(
|
||||||
"settings.components.verge.advanced.notifications.latestVersion",
|
'settings.components.verge.advanced.notifications.latestVersion',
|
||||||
);
|
)
|
||||||
} else {
|
} else {
|
||||||
showNotice.info("shared.feedback.notifications.updateAvailable", 2000);
|
showNotice.info('shared.feedback.notifications.updateAvailable', 2000)
|
||||||
goToSettings();
|
goToSettings()
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showNotice.error(err);
|
showNotice.error(err)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
// 是否启用自启动
|
// 是否启用自启动
|
||||||
const autoLaunchEnabled = useMemo(
|
const autoLaunchEnabled = useMemo(
|
||||||
() => verge?.enable_auto_launch || false,
|
() => verge?.enable_auto_launch || false,
|
||||||
[verge],
|
[verge],
|
||||||
);
|
)
|
||||||
|
|
||||||
// 运行模式样式
|
// 运行模式样式
|
||||||
const runningModeStyle = useMemo(
|
const runningModeStyle = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
// Sidecar或纯管理员模式允许安装服务
|
// Sidecar或纯管理员模式允许安装服务
|
||||||
cursor:
|
cursor:
|
||||||
isSidecarMode || (isAdminMode && isSidecarMode) ? "pointer" : "default",
|
isSidecarMode || (isAdminMode && isSidecarMode) ? 'pointer' : 'default',
|
||||||
textDecoration:
|
textDecoration:
|
||||||
isSidecarMode || (isAdminMode && isSidecarMode) ? "underline" : "none",
|
isSidecarMode || (isAdminMode && isSidecarMode) ? 'underline' : 'none',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
gap: 0.5,
|
gap: 0.5,
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
opacity: isSidecarMode || (isAdminMode && isSidecarMode) ? 0.7 : 1,
|
opacity: isSidecarMode || (isAdminMode && isSidecarMode) ? 0.7 : 1,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[isSidecarMode, isAdminMode],
|
[isSidecarMode, isAdminMode],
|
||||||
);
|
)
|
||||||
|
|
||||||
// 获取模式图标和文本
|
// 获取模式图标和文本
|
||||||
const getModeIcon = () => {
|
const getModeIcon = () => {
|
||||||
@@ -202,67 +202,67 @@ export const SystemInfoCard = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AdminPanelSettingsOutlined
|
<AdminPanelSettingsOutlined
|
||||||
sx={{ color: "primary.main", fontSize: 16 }}
|
sx={{ color: 'primary.main', fontSize: 16 }}
|
||||||
titleAccess={t("home.components.systemInfo.badges.adminMode")}
|
titleAccess={t('home.components.systemInfo.badges.adminMode')}
|
||||||
/>
|
/>
|
||||||
<DnsOutlined
|
<DnsOutlined
|
||||||
sx={{ color: "success.main", fontSize: 16, ml: 0.5 }}
|
sx={{ color: 'success.main', fontSize: 16, ml: 0.5 }}
|
||||||
titleAccess={t("home.components.systemInfo.badges.serviceMode")}
|
titleAccess={t('home.components.systemInfo.badges.serviceMode')}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<AdminPanelSettingsOutlined
|
<AdminPanelSettingsOutlined
|
||||||
sx={{ color: "primary.main", fontSize: 16 }}
|
sx={{ color: 'primary.main', fontSize: 16 }}
|
||||||
titleAccess={t("home.components.systemInfo.badges.adminMode")}
|
titleAccess={t('home.components.systemInfo.badges.adminMode')}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
} else if (isSidecarMode) {
|
} else if (isSidecarMode) {
|
||||||
return (
|
return (
|
||||||
<ExtensionOutlined
|
<ExtensionOutlined
|
||||||
sx={{ color: "info.main", fontSize: 16 }}
|
sx={{ color: 'info.main', fontSize: 16 }}
|
||||||
titleAccess={t("home.components.systemInfo.badges.sidecarMode")}
|
titleAccess={t('home.components.systemInfo.badges.sidecarMode')}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<DnsOutlined
|
<DnsOutlined
|
||||||
sx={{ color: "success.main", fontSize: 16 }}
|
sx={{ color: 'success.main', fontSize: 16 }}
|
||||||
titleAccess={t("home.components.systemInfo.badges.serviceMode")}
|
titleAccess={t('home.components.systemInfo.badges.serviceMode')}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// 获取模式文本
|
// 获取模式文本
|
||||||
const getModeText = () => {
|
const getModeText = () => {
|
||||||
if (isAdminMode) {
|
if (isAdminMode) {
|
||||||
// 判断是否同时处于服务模式
|
// 判断是否同时处于服务模式
|
||||||
if (!isSidecarMode) {
|
if (!isSidecarMode) {
|
||||||
return t("home.components.systemInfo.badges.adminServiceMode");
|
return t('home.components.systemInfo.badges.adminServiceMode')
|
||||||
}
|
}
|
||||||
return t("home.components.systemInfo.badges.adminMode");
|
return t('home.components.systemInfo.badges.adminMode')
|
||||||
} else if (isSidecarMode) {
|
} else if (isSidecarMode) {
|
||||||
return t("home.components.systemInfo.badges.sidecarMode");
|
return t('home.components.systemInfo.badges.sidecarMode')
|
||||||
} else {
|
} else {
|
||||||
return t("home.components.systemInfo.badges.serviceMode");
|
return t('home.components.systemInfo.badges.serviceMode')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// 只有当verge存在时才渲染内容
|
// 只有当verge存在时才渲染内容
|
||||||
if (!verge) return null;
|
if (!verge) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedCard
|
<EnhancedCard
|
||||||
title={t("home.components.systemInfo.title")}
|
title={t('home.components.systemInfo.title')}
|
||||||
icon={<InfoOutlined />}
|
icon={<InfoOutlined />}
|
||||||
iconColor="error"
|
iconColor="error"
|
||||||
action={
|
action={
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={goToSettings}
|
onClick={goToSettings}
|
||||||
title={t("home.components.systemInfo.actions.settings")}
|
title={t('home.components.systemInfo.actions.settings')}
|
||||||
>
|
>
|
||||||
<SettingsOutlined fontSize="small" />
|
<SettingsOutlined fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -271,7 +271,7 @@ export const SystemInfoCard = () => {
|
|||||||
<Stack spacing={1.5}>
|
<Stack spacing={1.5}>
|
||||||
<Stack direction="row" justifyContent="space-between">
|
<Stack direction="row" justifyContent="space-between">
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("home.components.systemInfo.fields.osInfo")}
|
{t('home.components.systemInfo.fields.osInfo')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" fontWeight="medium">
|
<Typography variant="body2" fontWeight="medium">
|
||||||
{systemState.osInfo}
|
{systemState.osInfo}
|
||||||
@@ -284,20 +284,20 @@ export const SystemInfoCard = () => {
|
|||||||
alignItems="center"
|
alignItems="center"
|
||||||
>
|
>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("home.components.systemInfo.fields.autoLaunch")}
|
{t('home.components.systemInfo.fields.autoLaunch')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Stack direction="row" spacing={1} alignItems="center">
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
<Chip
|
<Chip
|
||||||
size="small"
|
size="small"
|
||||||
label={
|
label={
|
||||||
autoLaunchEnabled
|
autoLaunchEnabled
|
||||||
? t("shared.statuses.enabled")
|
? t('shared.statuses.enabled')
|
||||||
: t("shared.statuses.disabled")
|
: t('shared.statuses.disabled')
|
||||||
}
|
}
|
||||||
color={autoLaunchEnabled ? "success" : "default"}
|
color={autoLaunchEnabled ? 'success' : 'default'}
|
||||||
variant={autoLaunchEnabled ? "filled" : "outlined"}
|
variant={autoLaunchEnabled ? 'filled' : 'outlined'}
|
||||||
onClick={toggleAutoLaunch}
|
onClick={toggleAutoLaunch}
|
||||||
sx={{ cursor: "pointer" }}
|
sx={{ cursor: 'pointer' }}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -308,7 +308,7 @@ export const SystemInfoCard = () => {
|
|||||||
alignItems="center"
|
alignItems="center"
|
||||||
>
|
>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("home.components.systemInfo.fields.runningMode")}
|
{t('home.components.systemInfo.fields.runningMode')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
@@ -323,16 +323,16 @@ export const SystemInfoCard = () => {
|
|||||||
<Divider />
|
<Divider />
|
||||||
<Stack direction="row" justifyContent="space-between">
|
<Stack direction="row" justifyContent="space-between">
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("home.components.systemInfo.fields.lastCheckUpdate")}
|
{t('home.components.systemInfo.fields.lastCheckUpdate')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
fontWeight="medium"
|
fontWeight="medium"
|
||||||
onClick={onCheckUpdate}
|
onClick={onCheckUpdate}
|
||||||
sx={{
|
sx={{
|
||||||
cursor: "pointer",
|
cursor: 'pointer',
|
||||||
textDecoration: "underline",
|
textDecoration: 'underline',
|
||||||
"&:hover": { opacity: 0.7 },
|
'&:hover': { opacity: 0.7 },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{systemState.lastCheckUpdate}
|
{systemState.lastCheckUpdate}
|
||||||
@@ -341,7 +341,7 @@ export const SystemInfoCard = () => {
|
|||||||
<Divider />
|
<Divider />
|
||||||
<Stack direction="row" justifyContent="space-between">
|
<Stack direction="row" justifyContent="space-between">
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("home.components.systemInfo.fields.vergeVersion")}
|
{t('home.components.systemInfo.fields.vergeVersion')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" fontWeight="medium">
|
<Typography variant="body2" fontWeight="medium">
|
||||||
v{appVersion}
|
v{appVersion}
|
||||||
@@ -349,5 +349,5 @@ export const SystemInfoCard = () => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</EnhancedCard>
|
</EnhancedCard>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -6,145 +6,145 @@ import {
|
|||||||
useSensors,
|
useSensors,
|
||||||
DragEndEvent,
|
DragEndEvent,
|
||||||
DragOverlay,
|
DragOverlay,
|
||||||
} from "@dnd-kit/core";
|
} from '@dnd-kit/core'
|
||||||
import { SortableContext } from "@dnd-kit/sortable";
|
import { SortableContext } from '@dnd-kit/sortable'
|
||||||
import { Add, NetworkCheck } from "@mui/icons-material";
|
import { Add, NetworkCheck } from '@mui/icons-material'
|
||||||
import { Box, IconButton, Tooltip, alpha, styled, Grid } from "@mui/material";
|
import { Box, IconButton, Tooltip, alpha, styled, Grid } from '@mui/material'
|
||||||
import { emit } from "@tauri-apps/api/event";
|
import { emit } from '@tauri-apps/api/event'
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from 'nanoid'
|
||||||
import { useEffect, useRef, useMemo, useCallback } from "react";
|
import { useEffect, useRef, useMemo, useCallback } from 'react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
// test icons
|
// test icons
|
||||||
import apple from "@/assets/image/test/apple.svg?raw";
|
import apple from '@/assets/image/test/apple.svg?raw'
|
||||||
import github from "@/assets/image/test/github.svg?raw";
|
import github from '@/assets/image/test/github.svg?raw'
|
||||||
import google from "@/assets/image/test/google.svg?raw";
|
import google from '@/assets/image/test/google.svg?raw'
|
||||||
import youtube from "@/assets/image/test/youtube.svg?raw";
|
import youtube from '@/assets/image/test/youtube.svg?raw'
|
||||||
import { TestItem } from "@/components/test/test-item";
|
import { TestItem } from '@/components/test/test-item'
|
||||||
import { TestViewer, TestViewerRef } from "@/components/test/test-viewer";
|
import { TestViewer, TestViewerRef } from '@/components/test/test-viewer'
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
|
|
||||||
import { EnhancedCard } from "./enhanced-card";
|
import { EnhancedCard } from './enhanced-card'
|
||||||
|
|
||||||
// 自定义滚动条样式
|
// 自定义滚动条样式
|
||||||
const ScrollBox = styled(Box)(({ theme }) => ({
|
const ScrollBox = styled(Box)(({ theme }) => ({
|
||||||
maxHeight: "180px",
|
maxHeight: '180px',
|
||||||
overflowY: "auto",
|
overflowY: 'auto',
|
||||||
overflowX: "hidden",
|
overflowX: 'hidden',
|
||||||
"&::-webkit-scrollbar": {
|
'&::-webkit-scrollbar': {
|
||||||
width: "6px",
|
width: '6px',
|
||||||
},
|
},
|
||||||
"&::-webkit-scrollbar-thumb": {
|
'&::-webkit-scrollbar-thumb': {
|
||||||
backgroundColor: alpha(theme.palette.text.primary, 0.2),
|
backgroundColor: alpha(theme.palette.text.primary, 0.2),
|
||||||
borderRadius: "3px",
|
borderRadius: '3px',
|
||||||
},
|
},
|
||||||
}));
|
}))
|
||||||
|
|
||||||
// 默认测试列表,移到组件外部避免重复创建
|
// 默认测试列表,移到组件外部避免重复创建
|
||||||
const DEFAULT_TEST_LIST = [
|
const DEFAULT_TEST_LIST = [
|
||||||
{
|
{
|
||||||
uid: nanoid(),
|
uid: nanoid(),
|
||||||
name: "Apple",
|
name: 'Apple',
|
||||||
url: "https://www.apple.com",
|
url: 'https://www.apple.com',
|
||||||
icon: apple,
|
icon: apple,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
uid: nanoid(),
|
uid: nanoid(),
|
||||||
name: "GitHub",
|
name: 'GitHub',
|
||||||
url: "https://www.github.com",
|
url: 'https://www.github.com',
|
||||||
icon: github,
|
icon: github,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
uid: nanoid(),
|
uid: nanoid(),
|
||||||
name: "Google",
|
name: 'Google',
|
||||||
url: "https://www.google.com",
|
url: 'https://www.google.com',
|
||||||
icon: google,
|
icon: google,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
uid: nanoid(),
|
uid: nanoid(),
|
||||||
name: "YouTube",
|
name: 'YouTube',
|
||||||
url: "https://www.youtube.com",
|
url: 'https://www.youtube.com',
|
||||||
icon: youtube,
|
icon: youtube,
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
|
|
||||||
export const TestCard = () => {
|
export const TestCard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(PointerSensor, {
|
||||||
activationConstraint: { distance: 8 },
|
activationConstraint: { distance: 8 },
|
||||||
}),
|
}),
|
||||||
);
|
)
|
||||||
const { verge, mutateVerge, patchVerge } = useVerge();
|
const { verge, mutateVerge, patchVerge } = useVerge()
|
||||||
const viewerRef = useRef<TestViewerRef>(null);
|
const viewerRef = useRef<TestViewerRef>(null)
|
||||||
|
|
||||||
// 使用useMemo优化测试列表,避免每次渲染重新计算
|
// 使用useMemo优化测试列表,避免每次渲染重新计算
|
||||||
const testList = useMemo(() => {
|
const testList = useMemo(() => {
|
||||||
return verge?.test_list ?? DEFAULT_TEST_LIST;
|
return verge?.test_list ?? DEFAULT_TEST_LIST
|
||||||
}, [verge?.test_list]);
|
}, [verge?.test_list])
|
||||||
|
|
||||||
// 使用useCallback优化函数引用,避免不必要的重新渲染
|
// 使用useCallback优化函数引用,避免不必要的重新渲染
|
||||||
const onTestListItemChange = useCallback(
|
const onTestListItemChange = useCallback(
|
||||||
(uid: string, patch?: Partial<IVergeTestItem>) => {
|
(uid: string, patch?: Partial<IVergeTestItem>) => {
|
||||||
if (!patch) {
|
if (!patch) {
|
||||||
mutateVerge();
|
mutateVerge()
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const newList = testList.map((x) =>
|
const newList = testList.map((x) =>
|
||||||
x.uid === uid ? { ...x, ...patch } : x,
|
x.uid === uid ? { ...x, ...patch } : x,
|
||||||
);
|
)
|
||||||
|
|
||||||
mutateVerge({ ...verge, test_list: newList }, false);
|
mutateVerge({ ...verge, test_list: newList }, false)
|
||||||
},
|
},
|
||||||
[testList, verge, mutateVerge],
|
[testList, verge, mutateVerge],
|
||||||
);
|
)
|
||||||
|
|
||||||
const onDeleteTestListItem = useCallback(
|
const onDeleteTestListItem = useCallback(
|
||||||
(uid: string) => {
|
(uid: string) => {
|
||||||
const newList = testList.filter((x) => x.uid !== uid);
|
const newList = testList.filter((x) => x.uid !== uid)
|
||||||
patchVerge({ test_list: newList });
|
patchVerge({ test_list: newList })
|
||||||
mutateVerge({ ...verge, test_list: newList }, false);
|
mutateVerge({ ...verge, test_list: newList }, false)
|
||||||
},
|
},
|
||||||
[testList, verge, patchVerge, mutateVerge],
|
[testList, verge, patchVerge, mutateVerge],
|
||||||
);
|
)
|
||||||
|
|
||||||
const onDragEnd = useCallback(
|
const onDragEnd = useCallback(
|
||||||
async (event: DragEndEvent) => {
|
async (event: DragEndEvent) => {
|
||||||
const { active, over } = event;
|
const { active, over } = event
|
||||||
if (!over || active.id === over.id) return;
|
if (!over || active.id === over.id) return
|
||||||
|
|
||||||
const old_index = testList.findIndex((x) => x.uid === active.id);
|
const old_index = testList.findIndex((x) => x.uid === active.id)
|
||||||
const new_index = testList.findIndex((x) => x.uid === over.id);
|
const new_index = testList.findIndex((x) => x.uid === over.id)
|
||||||
|
|
||||||
if (old_index >= 0 && new_index >= 0) {
|
if (old_index >= 0 && new_index >= 0) {
|
||||||
const newList = [...testList];
|
const newList = [...testList]
|
||||||
const [removed] = newList.splice(old_index, 1);
|
const [removed] = newList.splice(old_index, 1)
|
||||||
newList.splice(new_index, 0, removed);
|
newList.splice(new_index, 0, removed)
|
||||||
|
|
||||||
// 优化:先本地更新,再异步 patch,避免UI卡死
|
// 优化:先本地更新,再异步 patch,避免UI卡死
|
||||||
mutateVerge({ ...verge, test_list: newList }, false);
|
mutateVerge({ ...verge, test_list: newList }, false)
|
||||||
const patchFn = () => {
|
const patchFn = () => {
|
||||||
try {
|
try {
|
||||||
patchVerge({ test_list: newList });
|
patchVerge({ test_list: newList })
|
||||||
} catch {}
|
} catch {}
|
||||||
};
|
}
|
||||||
if (window.requestIdleCallback) {
|
if (window.requestIdleCallback) {
|
||||||
window.requestIdleCallback(patchFn);
|
window.requestIdleCallback(patchFn)
|
||||||
} else {
|
} else {
|
||||||
setTimeout(patchFn, 0);
|
setTimeout(patchFn, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[testList, verge, mutateVerge, patchVerge],
|
[testList, verge, mutateVerge, patchVerge],
|
||||||
);
|
)
|
||||||
|
|
||||||
// 仅在verge首次加载时初始化测试列表
|
// 仅在verge首次加载时初始化测试列表
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (verge && !verge.test_list) {
|
if (verge && !verge.test_list) {
|
||||||
patchVerge({ test_list: DEFAULT_TEST_LIST });
|
patchVerge({ test_list: DEFAULT_TEST_LIST })
|
||||||
}
|
}
|
||||||
}, [verge, patchVerge]);
|
}, [verge, patchVerge])
|
||||||
|
|
||||||
// 使用useMemo优化UI内容,减少渲染计算
|
// 使用useMemo优化UI内容,减少渲染计算
|
||||||
const renderTestItems = useMemo(
|
const renderTestItems = useMemo(
|
||||||
@@ -165,28 +165,28 @@ export const TestCard = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
),
|
),
|
||||||
[testList, onDeleteTestListItem],
|
[testList, onDeleteTestListItem],
|
||||||
);
|
)
|
||||||
|
|
||||||
const handleTestAll = useCallback(() => {
|
const handleTestAll = useCallback(() => {
|
||||||
emit("verge://test-all");
|
emit('verge://test-all')
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
const handleCreateTest = useCallback(() => {
|
const handleCreateTest = useCallback(() => {
|
||||||
viewerRef.current?.create();
|
viewerRef.current?.create()
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedCard
|
<EnhancedCard
|
||||||
title={t("home.components.tests.title")}
|
title={t('home.components.tests.title')}
|
||||||
icon={<NetworkCheck />}
|
icon={<NetworkCheck />}
|
||||||
action={
|
action={
|
||||||
<Box sx={{ display: "flex", gap: 1 }}>
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
<Tooltip title={t("tests.page.actions.testAll")} arrow>
|
<Tooltip title={t('tests.page.actions.testAll')} arrow>
|
||||||
<IconButton size="small" onClick={handleTestAll}>
|
<IconButton size="small" onClick={handleTestAll}>
|
||||||
<NetworkCheck fontSize="small" />
|
<NetworkCheck fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t("tests.modals.test.title.create")} arrow>
|
<Tooltip title={t('tests.modals.test.title.create')} arrow>
|
||||||
<IconButton size="small" onClick={handleCreateTest}>
|
<IconButton size="small" onClick={handleCreateTest}>
|
||||||
<Add fontSize="small" />
|
<Add fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -207,5 +207,5 @@ export const TestCard = () => {
|
|||||||
|
|
||||||
<TestViewer ref={viewerRef} onChange={onTestListItemChange} />
|
<TestViewer ref={viewerRef} onChange={onTestListItemChange} />
|
||||||
</EnhancedCard>
|
</EnhancedCard>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,60 +1,60 @@
|
|||||||
import type {
|
import type {
|
||||||
DraggableAttributes,
|
DraggableAttributes,
|
||||||
DraggableSyntheticListeners,
|
DraggableSyntheticListeners,
|
||||||
} from "@dnd-kit/core";
|
} from '@dnd-kit/core'
|
||||||
import {
|
import {
|
||||||
alpha,
|
alpha,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemButton,
|
ListItemButton,
|
||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
} from "@mui/material";
|
} from '@mui/material'
|
||||||
import type { CSSProperties, ReactNode } from "react";
|
import type { CSSProperties, ReactNode } from 'react'
|
||||||
import { useMatch, useNavigate, useResolvedPath } from "react-router";
|
import { useMatch, useNavigate, useResolvedPath } from 'react-router'
|
||||||
|
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
|
|
||||||
interface SortableProps {
|
interface SortableProps {
|
||||||
setNodeRef?: (element: HTMLElement | null) => void;
|
setNodeRef?: (element: HTMLElement | null) => void
|
||||||
attributes?: DraggableAttributes;
|
attributes?: DraggableAttributes
|
||||||
listeners?: DraggableSyntheticListeners;
|
listeners?: DraggableSyntheticListeners
|
||||||
style?: CSSProperties;
|
style?: CSSProperties
|
||||||
isDragging?: boolean;
|
isDragging?: boolean
|
||||||
disabled?: boolean;
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
to: string;
|
to: string
|
||||||
children: string;
|
children: string
|
||||||
icon: ReactNode[];
|
icon: ReactNode[]
|
||||||
sortable?: SortableProps;
|
sortable?: SortableProps
|
||||||
}
|
}
|
||||||
export const LayoutItem = (props: Props) => {
|
export const LayoutItem = (props: Props) => {
|
||||||
const { to, children, icon, sortable } = props;
|
const { to, children, icon, sortable } = props
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge()
|
||||||
const { menu_icon } = verge ?? {};
|
const { menu_icon } = verge ?? {}
|
||||||
const navCollapsed = verge?.collapse_navbar ?? false;
|
const navCollapsed = verge?.collapse_navbar ?? false
|
||||||
const resolved = useResolvedPath(to);
|
const resolved = useResolvedPath(to)
|
||||||
const match = useMatch({ path: resolved.pathname, end: true });
|
const match = useMatch({ path: resolved.pathname, end: true })
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const effectiveMenuIcon =
|
const effectiveMenuIcon =
|
||||||
navCollapsed && menu_icon === "disable" ? "monochrome" : menu_icon;
|
navCollapsed && menu_icon === 'disable' ? 'monochrome' : menu_icon
|
||||||
|
|
||||||
const { setNodeRef, attributes, listeners, style, isDragging, disabled } =
|
const { setNodeRef, attributes, listeners, style, isDragging, disabled } =
|
||||||
sortable ?? {};
|
sortable ?? {}
|
||||||
|
|
||||||
const draggable = Boolean(sortable) && !disabled;
|
const draggable = Boolean(sortable) && !disabled
|
||||||
const dragHandleProps = draggable
|
const dragHandleProps = draggable
|
||||||
? { ...(attributes ?? {}), ...(listeners ?? {}) }
|
? { ...(attributes ?? {}), ...(listeners ?? {}) }
|
||||||
: undefined;
|
: undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
style={style}
|
style={style}
|
||||||
sx={[
|
sx={[
|
||||||
{ py: 0.5, maxWidth: 250, mx: "auto", padding: "4px 0px" },
|
{ py: 0.5, maxWidth: 250, mx: 'auto', padding: '4px 0px' },
|
||||||
isDragging ? { opacity: 0.78 } : {},
|
isDragging ? { opacity: 0.78 } : {},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -68,54 +68,54 @@ export const LayoutItem = (props: Props) => {
|
|||||||
paddingLeft: 1,
|
paddingLeft: 1,
|
||||||
paddingRight: 1,
|
paddingRight: 1,
|
||||||
marginRight: 1.25,
|
marginRight: 1.25,
|
||||||
cursor: draggable ? "grab" : "pointer",
|
cursor: draggable ? 'grab' : 'pointer',
|
||||||
"&:active": draggable ? { cursor: "grabbing" } : {},
|
'&:active': draggable ? { cursor: 'grabbing' } : {},
|
||||||
"& .MuiListItemText-primary": {
|
'& .MuiListItemText-primary': {
|
||||||
color: "text.primary",
|
color: 'text.primary',
|
||||||
fontWeight: "700",
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
({ palette: { mode, primary } }) => {
|
({ palette: { mode, primary } }) => {
|
||||||
const bgcolor =
|
const bgcolor =
|
||||||
mode === "light"
|
mode === 'light'
|
||||||
? alpha(primary.main, 0.15)
|
? alpha(primary.main, 0.15)
|
||||||
: alpha(primary.main, 0.35);
|
: alpha(primary.main, 0.35)
|
||||||
const color = mode === "light" ? "#1f1f1f" : "#ffffff";
|
const color = mode === 'light' ? '#1f1f1f' : '#ffffff'
|
||||||
return {
|
return {
|
||||||
"&.Mui-selected": { bgcolor },
|
'&.Mui-selected': { bgcolor },
|
||||||
"&.Mui-selected:hover": { bgcolor },
|
'&.Mui-selected:hover': { bgcolor },
|
||||||
"&.Mui-selected .MuiListItemText-primary": { color },
|
'&.Mui-selected .MuiListItemText-primary': { color },
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
title={navCollapsed ? children : undefined}
|
title={navCollapsed ? children : undefined}
|
||||||
aria-label={navCollapsed ? children : undefined}
|
aria-label={navCollapsed ? children : undefined}
|
||||||
onClick={() => navigate(to)}
|
onClick={() => navigate(to)}
|
||||||
>
|
>
|
||||||
{(effectiveMenuIcon === "monochrome" || !effectiveMenuIcon) && (
|
{(effectiveMenuIcon === 'monochrome' || !effectiveMenuIcon) && (
|
||||||
<ListItemIcon
|
<ListItemIcon
|
||||||
sx={{
|
sx={{
|
||||||
color: "text.primary",
|
color: 'text.primary',
|
||||||
marginLeft: "6px",
|
marginLeft: '6px',
|
||||||
cursor: draggable ? "grab" : "inherit",
|
cursor: draggable ? 'grab' : 'inherit',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{icon[0]}
|
{icon[0]}
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
)}
|
)}
|
||||||
{effectiveMenuIcon === "colorful" && (
|
{effectiveMenuIcon === 'colorful' && (
|
||||||
<ListItemIcon sx={{ cursor: draggable ? "grab" : "inherit" }}>
|
<ListItemIcon sx={{ cursor: draggable ? 'grab' : 'inherit' }}>
|
||||||
{icon[1]}
|
{icon[1]}
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
)}
|
)}
|
||||||
<ListItemText
|
<ListItemText
|
||||||
sx={{
|
sx={{
|
||||||
textAlign: "center",
|
textAlign: 'center',
|
||||||
marginLeft: effectiveMenuIcon === "disable" ? "" : "-35px",
|
marginLeft: effectiveMenuIcon === 'disable' ? '' : '-35px',
|
||||||
}}
|
}}
|
||||||
primary={children}
|
primary={children}
|
||||||
/>
|
/>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -2,37 +2,37 @@ import {
|
|||||||
ArrowDownwardRounded,
|
ArrowDownwardRounded,
|
||||||
ArrowUpwardRounded,
|
ArrowUpwardRounded,
|
||||||
MemoryRounded,
|
MemoryRounded,
|
||||||
} from "@mui/icons-material";
|
} from '@mui/icons-material'
|
||||||
import { Box, Typography } from "@mui/material";
|
import { Box, Typography } from '@mui/material'
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from 'react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { LightweightTrafficErrorBoundary } from "@/components/shared/traffic-error-boundary";
|
import { LightweightTrafficErrorBoundary } from '@/components/shared/traffic-error-boundary'
|
||||||
import { useMemoryData } from "@/hooks/use-memory-data";
|
import { useMemoryData } from '@/hooks/use-memory-data'
|
||||||
import { useTrafficData } from "@/hooks/use-traffic-data";
|
import { useTrafficData } from '@/hooks/use-traffic-data'
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
import { useVisibility } from "@/hooks/use-visibility";
|
import { useVisibility } from '@/hooks/use-visibility'
|
||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from '@/utils/parse-traffic'
|
||||||
|
|
||||||
import { TrafficGraph, type TrafficRef } from "./traffic-graph";
|
import { TrafficGraph, type TrafficRef } from './traffic-graph'
|
||||||
|
|
||||||
// setup the traffic
|
// setup the traffic
|
||||||
export const LayoutTraffic = () => {
|
export const LayoutTraffic = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge()
|
||||||
|
|
||||||
// whether hide traffic graph
|
// whether hide traffic graph
|
||||||
const trafficGraph = verge?.traffic_graph ?? true;
|
const trafficGraph = verge?.traffic_graph ?? true
|
||||||
|
|
||||||
const trafficRef = useRef<TrafficRef>(null);
|
const trafficRef = useRef<TrafficRef>(null)
|
||||||
const pageVisible = useVisibility();
|
const pageVisible = useVisibility()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
response: { data: traffic },
|
response: { data: traffic },
|
||||||
} = useTrafficData({ enabled: trafficGraph && pageVisible });
|
} = useTrafficData({ enabled: trafficGraph && pageVisible })
|
||||||
const {
|
const {
|
||||||
response: { data: memory },
|
response: { data: memory },
|
||||||
} = useMemoryData();
|
} = useMemoryData()
|
||||||
|
|
||||||
// 监听数据变化,为图表添加数据点
|
// 监听数据变化,为图表添加数据点
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -40,45 +40,45 @@ export const LayoutTraffic = () => {
|
|||||||
trafficRef.current.appendData({
|
trafficRef.current.appendData({
|
||||||
up: traffic?.up || 0,
|
up: traffic?.up || 0,
|
||||||
down: traffic?.down || 0,
|
down: traffic?.down || 0,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}, [traffic]);
|
}, [traffic])
|
||||||
|
|
||||||
// 显示内存使用情况的设置
|
// 显示内存使用情况的设置
|
||||||
const displayMemory = verge?.enable_memory_usage ?? true;
|
const displayMemory = verge?.enable_memory_usage ?? true
|
||||||
|
|
||||||
// 使用parseTraffic统一处理转换,保持与首页一致的显示格式
|
// 使用parseTraffic统一处理转换,保持与首页一致的显示格式
|
||||||
const [up, upUnit] = parseTraffic(traffic?.up || 0);
|
const [up, upUnit] = parseTraffic(traffic?.up || 0)
|
||||||
const [down, downUnit] = parseTraffic(traffic?.down || 0);
|
const [down, downUnit] = parseTraffic(traffic?.down || 0)
|
||||||
const [inuse, inuseUnit] = parseTraffic(memory?.inuse || 0);
|
const [inuse, inuseUnit] = parseTraffic(memory?.inuse || 0)
|
||||||
|
|
||||||
const boxStyle: any = {
|
const boxStyle: any = {
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: 'nowrap',
|
||||||
};
|
}
|
||||||
const iconStyle: any = {
|
const iconStyle: any = {
|
||||||
sx: { mr: "8px", fontSize: 16 },
|
sx: { mr: '8px', fontSize: 16 },
|
||||||
};
|
}
|
||||||
const valStyle: any = {
|
const valStyle: any = {
|
||||||
component: "span",
|
component: 'span',
|
||||||
textAlign: "center",
|
textAlign: 'center',
|
||||||
sx: { flex: "1 1 56px", userSelect: "none" },
|
sx: { flex: '1 1 56px', userSelect: 'none' },
|
||||||
};
|
}
|
||||||
const unitStyle: any = {
|
const unitStyle: any = {
|
||||||
component: "span",
|
component: 'span',
|
||||||
color: "grey.500",
|
color: 'grey.500',
|
||||||
fontSize: "12px",
|
fontSize: '12px',
|
||||||
textAlign: "right",
|
textAlign: 'right',
|
||||||
sx: { flex: "0 1 27px", userSelect: "none" },
|
sx: { flex: '0 1 27px', userSelect: 'none' },
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LightweightTrafficErrorBoundary>
|
<LightweightTrafficErrorBoundary>
|
||||||
<Box position="relative">
|
<Box position="relative">
|
||||||
{trafficGraph && pageVisible && (
|
{trafficGraph && pageVisible && (
|
||||||
<div
|
<div
|
||||||
style={{ width: "100%", height: 60, marginBottom: 6 }}
|
style={{ width: '100%', height: 60, marginBottom: 6 }}
|
||||||
onClick={trafficRef.current?.toggleStyle}
|
onClick={trafficRef.current?.toggleStyle}
|
||||||
>
|
>
|
||||||
<TrafficGraph ref={trafficRef} />
|
<TrafficGraph ref={trafficRef} />
|
||||||
@@ -87,7 +87,7 @@ export const LayoutTraffic = () => {
|
|||||||
|
|
||||||
<Box display="flex" flexDirection="column" gap={0.75}>
|
<Box display="flex" flexDirection="column" gap={0.75}>
|
||||||
<Box
|
<Box
|
||||||
title={`${t("home.components.traffic.metrics.uploadSpeed")}`}
|
title={`${t('home.components.traffic.metrics.uploadSpeed')}`}
|
||||||
{...boxStyle}
|
{...boxStyle}
|
||||||
sx={{
|
sx={{
|
||||||
...boxStyle.sx,
|
...boxStyle.sx,
|
||||||
@@ -96,7 +96,7 @@ export const LayoutTraffic = () => {
|
|||||||
>
|
>
|
||||||
<ArrowUpwardRounded
|
<ArrowUpwardRounded
|
||||||
{...iconStyle}
|
{...iconStyle}
|
||||||
color={(traffic?.up || 0) > 0 ? "secondary" : "disabled"}
|
color={(traffic?.up || 0) > 0 ? 'secondary' : 'disabled'}
|
||||||
/>
|
/>
|
||||||
<Typography {...valStyle} color="secondary">
|
<Typography {...valStyle} color="secondary">
|
||||||
{up}
|
{up}
|
||||||
@@ -105,7 +105,7 @@ export const LayoutTraffic = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
title={`${t("home.components.traffic.metrics.downloadSpeed")}`}
|
title={`${t('home.components.traffic.metrics.downloadSpeed')}`}
|
||||||
{...boxStyle}
|
{...boxStyle}
|
||||||
sx={{
|
sx={{
|
||||||
...boxStyle.sx,
|
...boxStyle.sx,
|
||||||
@@ -114,7 +114,7 @@ export const LayoutTraffic = () => {
|
|||||||
>
|
>
|
||||||
<ArrowDownwardRounded
|
<ArrowDownwardRounded
|
||||||
{...iconStyle}
|
{...iconStyle}
|
||||||
color={(traffic?.down || 0) > 0 ? "primary" : "disabled"}
|
color={(traffic?.down || 0) > 0 ? 'primary' : 'disabled'}
|
||||||
/>
|
/>
|
||||||
<Typography {...valStyle} color="primary">
|
<Typography {...valStyle} color="primary">
|
||||||
{down}
|
{down}
|
||||||
@@ -124,13 +124,13 @@ export const LayoutTraffic = () => {
|
|||||||
|
|
||||||
{displayMemory && (
|
{displayMemory && (
|
||||||
<Box
|
<Box
|
||||||
title={`${t("home.components.traffic.metrics.memoryUsage")} `}
|
title={`${t('home.components.traffic.metrics.memoryUsage')} `}
|
||||||
{...boxStyle}
|
{...boxStyle}
|
||||||
sx={{
|
sx={{
|
||||||
cursor: "auto",
|
cursor: 'auto',
|
||||||
// opacity: memory?.is_fresh ? 1 : 0.6,
|
// opacity: memory?.is_fresh ? 1 : 0.6,
|
||||||
}}
|
}}
|
||||||
color={"disabled"}
|
color={'disabled'}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
// isDebug && (await gc());
|
// isDebug && (await gc());
|
||||||
}}
|
}}
|
||||||
@@ -143,5 +143,5 @@ export const LayoutTraffic = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</LightweightTrafficErrorBoundary>
|
</LightweightTrafficErrorBoundary>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,180 +1,180 @@
|
|||||||
import { CloseRounded } from "@mui/icons-material";
|
import { CloseRounded } from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Snackbar,
|
Snackbar,
|
||||||
Alert,
|
Alert,
|
||||||
IconButton,
|
IconButton,
|
||||||
Box,
|
Box,
|
||||||
type SnackbarOrigin,
|
type SnackbarOrigin,
|
||||||
} from "@mui/material";
|
} from '@mui/material'
|
||||||
import React, { useCallback, useMemo, useSyncExternalStore } from "react";
|
import React, { useCallback, useMemo, useSyncExternalStore } from 'react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
subscribeNotices,
|
subscribeNotices,
|
||||||
hideNotice,
|
hideNotice,
|
||||||
getSnapshotNotices,
|
getSnapshotNotices,
|
||||||
showNotice,
|
showNotice,
|
||||||
} from "@/services/notice-service";
|
} from '@/services/notice-service'
|
||||||
import type { TranslationKey } from "@/types/generated/i18n-keys";
|
import type { TranslationKey } from '@/types/generated/i18n-keys'
|
||||||
|
|
||||||
type NoticePosition = NonNullable<IVergeConfig["notice_position"]>;
|
type NoticePosition = NonNullable<IVergeConfig['notice_position']>
|
||||||
type NoticeItem = ReturnType<typeof getSnapshotNotices>[number];
|
type NoticeItem = ReturnType<typeof getSnapshotNotices>[number]
|
||||||
type TranslationFn = ReturnType<typeof useTranslation>["t"];
|
type TranslationFn = ReturnType<typeof useTranslation>['t']
|
||||||
|
|
||||||
const VALID_POSITIONS: NoticePosition[] = [
|
const VALID_POSITIONS: NoticePosition[] = [
|
||||||
"top-left",
|
'top-left',
|
||||||
"top-right",
|
'top-right',
|
||||||
"bottom-left",
|
'bottom-left',
|
||||||
"bottom-right",
|
'bottom-right',
|
||||||
];
|
]
|
||||||
|
|
||||||
const resolvePosition = (position?: NoticePosition | null): NoticePosition => {
|
const resolvePosition = (position?: NoticePosition | null): NoticePosition => {
|
||||||
if (position && VALID_POSITIONS.includes(position)) {
|
if (position && VALID_POSITIONS.includes(position)) {
|
||||||
return position;
|
return position
|
||||||
}
|
}
|
||||||
return "top-right";
|
return 'top-right'
|
||||||
};
|
}
|
||||||
|
|
||||||
const getAnchorOrigin = (position: NoticePosition): SnackbarOrigin => {
|
const getAnchorOrigin = (position: NoticePosition): SnackbarOrigin => {
|
||||||
const [vertical, horizontal] = position.split("-") as [
|
const [vertical, horizontal] = position.split('-') as [
|
||||||
SnackbarOrigin["vertical"],
|
SnackbarOrigin['vertical'],
|
||||||
SnackbarOrigin["horizontal"],
|
SnackbarOrigin['horizontal'],
|
||||||
];
|
]
|
||||||
return { vertical, horizontal };
|
return { vertical, horizontal }
|
||||||
};
|
}
|
||||||
|
|
||||||
const resolveNoticeMessage = (
|
const resolveNoticeMessage = (
|
||||||
notice: NoticeItem,
|
notice: NoticeItem,
|
||||||
t: TranslationFn,
|
t: TranslationFn,
|
||||||
): React.ReactNode => {
|
): React.ReactNode => {
|
||||||
const i18n = notice.i18n;
|
const i18n = notice.i18n
|
||||||
if (!i18n) return notice.message;
|
if (!i18n) return notice.message
|
||||||
|
|
||||||
const params = (i18n.params ?? {}) as Record<string, unknown>;
|
const params = (i18n.params ?? {}) as Record<string, unknown>
|
||||||
const { prefixKey, prefixParams, prefix, message, ...restParams } = params;
|
const { prefixKey, prefixParams, prefix, message, ...restParams } = params
|
||||||
|
|
||||||
const prefixKeyParams =
|
const prefixKeyParams =
|
||||||
prefixParams && typeof prefixParams === "object"
|
prefixParams && typeof prefixParams === 'object'
|
||||||
? (prefixParams as Record<string, unknown>)
|
? (prefixParams as Record<string, unknown>)
|
||||||
: undefined;
|
: undefined
|
||||||
|
|
||||||
const resolvedPrefix =
|
const resolvedPrefix =
|
||||||
typeof prefixKey === "string"
|
typeof prefixKey === 'string'
|
||||||
? t(prefixKey as TranslationKey, {
|
? t(prefixKey as TranslationKey, {
|
||||||
defaultValue: prefixKey,
|
defaultValue: prefixKey,
|
||||||
...(prefixKeyParams ?? {}),
|
...(prefixKeyParams ?? {}),
|
||||||
...restParams,
|
...restParams,
|
||||||
})
|
})
|
||||||
: typeof prefix === "string"
|
: typeof prefix === 'string'
|
||||||
? prefix
|
? prefix
|
||||||
: undefined;
|
: undefined
|
||||||
|
|
||||||
const messageStr = typeof message === "string" ? message : undefined;
|
const messageStr = typeof message === 'string' ? message : undefined
|
||||||
|
|
||||||
const defaultValue =
|
const defaultValue =
|
||||||
messageStr === undefined
|
messageStr === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: resolvedPrefix
|
: resolvedPrefix
|
||||||
? `${resolvedPrefix} ${messageStr}`
|
? `${resolvedPrefix} ${messageStr}`
|
||||||
: messageStr;
|
: messageStr
|
||||||
|
|
||||||
return t(i18n.key as TranslationKey, {
|
return t(i18n.key as TranslationKey, {
|
||||||
defaultValue,
|
defaultValue,
|
||||||
...restParams,
|
...restParams,
|
||||||
...(resolvedPrefix !== undefined ? { prefix: resolvedPrefix } : {}),
|
...(resolvedPrefix !== undefined ? { prefix: resolvedPrefix } : {}),
|
||||||
...(messageStr !== undefined ? { message: messageStr } : {}),
|
...(messageStr !== undefined ? { message: messageStr } : {}),
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const extractNoticeCopyText = (input: unknown): string | undefined => {
|
const extractNoticeCopyText = (input: unknown): string | undefined => {
|
||||||
if (input === null || input === undefined) return undefined;
|
if (input === null || input === undefined) return undefined
|
||||||
if (typeof input === "string") return input;
|
if (typeof input === 'string') return input
|
||||||
if (typeof input === "number" || typeof input === "boolean") {
|
if (typeof input === 'number' || typeof input === 'boolean') {
|
||||||
return String(input);
|
return String(input)
|
||||||
}
|
}
|
||||||
if (input instanceof Error) {
|
if (input instanceof Error) {
|
||||||
return input.message || input.name;
|
return input.message || input.name
|
||||||
}
|
}
|
||||||
if (React.isValidElement(input)) return undefined;
|
if (React.isValidElement(input)) return undefined
|
||||||
if (typeof input === "object") {
|
if (typeof input === 'object') {
|
||||||
const maybeMessage = (input as { message?: unknown }).message;
|
const maybeMessage = (input as { message?: unknown }).message
|
||||||
if (typeof maybeMessage === "string") return maybeMessage;
|
if (typeof maybeMessage === 'string') return maybeMessage
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return JSON.stringify(input);
|
return JSON.stringify(input)
|
||||||
} catch {
|
} catch {
|
||||||
return String(input);
|
return String(input)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const resolveNoticeCopyText = (
|
const resolveNoticeCopyText = (
|
||||||
notice: NoticeItem,
|
notice: NoticeItem,
|
||||||
t: TranslationFn,
|
t: TranslationFn,
|
||||||
): string | undefined => {
|
): string | undefined => {
|
||||||
if (
|
if (
|
||||||
notice.i18n?.key === "shared.feedback.notices.prefixedRaw" ||
|
notice.i18n?.key === 'shared.feedback.notices.prefixedRaw' ||
|
||||||
notice.i18n?.key === "shared.feedback.notices.raw"
|
notice.i18n?.key === 'shared.feedback.notices.raw'
|
||||||
) {
|
) {
|
||||||
const rawText = extractNoticeCopyText(notice.i18n?.params?.message);
|
const rawText = extractNoticeCopyText(notice.i18n?.params?.message)
|
||||||
if (rawText) return rawText;
|
if (rawText) return rawText
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
extractNoticeCopyText(resolveNoticeMessage(notice, t)) ??
|
extractNoticeCopyText(resolveNoticeMessage(notice, t)) ??
|
||||||
extractNoticeCopyText(notice.message)
|
extractNoticeCopyText(notice.message)
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
interface NoticeManagerProps {
|
interface NoticeManagerProps {
|
||||||
position?: NoticePosition | null;
|
position?: NoticePosition | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NoticeManager: React.FC<NoticeManagerProps> = ({ position }) => {
|
export const NoticeManager: React.FC<NoticeManagerProps> = ({ position }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const resolvedPosition = useMemo(() => resolvePosition(position), [position]);
|
const resolvedPosition = useMemo(() => resolvePosition(position), [position])
|
||||||
const anchorOrigin = useMemo(
|
const anchorOrigin = useMemo(
|
||||||
() => getAnchorOrigin(resolvedPosition),
|
() => getAnchorOrigin(resolvedPosition),
|
||||||
[resolvedPosition],
|
[resolvedPosition],
|
||||||
);
|
)
|
||||||
const currentNotices = useSyncExternalStore(
|
const currentNotices = useSyncExternalStore(
|
||||||
subscribeNotices,
|
subscribeNotices,
|
||||||
getSnapshotNotices,
|
getSnapshotNotices,
|
||||||
);
|
)
|
||||||
|
|
||||||
const handleClose = (id: number) => {
|
const handleClose = (id: number) => {
|
||||||
hideNotice(id);
|
hideNotice(id)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleNoticeCopy = useCallback(
|
const handleNoticeCopy = useCallback(
|
||||||
async (notice: NoticeItem) => {
|
async (notice: NoticeItem) => {
|
||||||
const text = resolveNoticeCopyText(notice, t);
|
const text = resolveNoticeCopyText(notice, t)
|
||||||
if (!text) return;
|
if (!text) return
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text)
|
||||||
showNotice.success(
|
showNotice.success(
|
||||||
"shared.feedback.notifications.common.copySuccess",
|
'shared.feedback.notifications.common.copySuccess',
|
||||||
1000,
|
1000,
|
||||||
);
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[NoticeManager] copy to clipboard failed:", error);
|
console.warn('[NoticeManager] copy to clipboard failed:', error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[t],
|
[t],
|
||||||
);
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: "fixed",
|
position: 'fixed',
|
||||||
top: anchorOrigin.vertical === "top" ? "20px" : "auto",
|
top: anchorOrigin.vertical === 'top' ? '20px' : 'auto',
|
||||||
bottom: anchorOrigin.vertical === "bottom" ? "20px" : "auto",
|
bottom: anchorOrigin.vertical === 'bottom' ? '20px' : 'auto',
|
||||||
left: anchorOrigin.horizontal === "left" ? "20px" : "auto",
|
left: anchorOrigin.horizontal === 'left' ? '20px' : 'auto',
|
||||||
right: anchorOrigin.horizontal === "right" ? "20px" : "auto",
|
right: anchorOrigin.horizontal === 'right' ? '20px' : 'auto',
|
||||||
zIndex: 1500,
|
zIndex: 1500,
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
flexDirection: "column",
|
flexDirection: 'column',
|
||||||
gap: "10px",
|
gap: '10px',
|
||||||
maxWidth: "360px",
|
maxWidth: '360px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{currentNotices.map((notice) => (
|
{currentNotices.map((notice) => (
|
||||||
@@ -183,23 +183,23 @@ export const NoticeManager: React.FC<NoticeManagerProps> = ({ position }) => {
|
|||||||
open={true}
|
open={true}
|
||||||
anchorOrigin={anchorOrigin}
|
anchorOrigin={anchorOrigin}
|
||||||
sx={{
|
sx={{
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
transform: "none",
|
transform: 'none',
|
||||||
top: "auto",
|
top: 'auto',
|
||||||
right: "auto",
|
right: 'auto',
|
||||||
bottom: "auto",
|
bottom: 'auto',
|
||||||
left: "auto",
|
left: 'auto',
|
||||||
width: "100%",
|
width: '100%',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Alert
|
<Alert
|
||||||
severity={notice.type}
|
severity={notice.type}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
sx={{ width: "100%" }}
|
sx={{ width: '100%' }}
|
||||||
onContextMenu={(event) => {
|
onContextMenu={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
event.stopPropagation();
|
event.stopPropagation()
|
||||||
void handleNoticeCopy(notice);
|
void handleNoticeCopy(notice)
|
||||||
}}
|
}}
|
||||||
action={
|
action={
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -216,5 +216,5 @@ export const NoticeManager: React.FC<NoticeManagerProps> = ({ position }) => {
|
|||||||
</Snackbar>
|
</Snackbar>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'
|
||||||
import { IconButton, Fade, SxProps, Theme } from "@mui/material";
|
import { IconButton, Fade, SxProps, Theme } from '@mui/material'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClick: () => void;
|
onClick: () => void
|
||||||
show: boolean;
|
show: boolean
|
||||||
sx?: SxProps<Theme>;
|
sx?: SxProps<Theme>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ScrollTopButton = ({ onClick, show, sx }: Props) => {
|
export const ScrollTopButton = ({ onClick, show, sx }: Props) => {
|
||||||
@@ -13,25 +13,25 @@ export const ScrollTopButton = ({ onClick, show, sx }: Props) => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
sx={{
|
sx={{
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
bottom: "20px",
|
bottom: '20px',
|
||||||
right: "20px",
|
right: '20px',
|
||||||
backgroundColor: (theme) =>
|
backgroundColor: (theme) =>
|
||||||
theme.palette.mode === "dark"
|
theme.palette.mode === 'dark'
|
||||||
? "rgba(255,255,255,0.1)"
|
? 'rgba(255,255,255,0.1)'
|
||||||
: "rgba(0,0,0,0.1)",
|
: 'rgba(0,0,0,0.1)',
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
backgroundColor: (theme) =>
|
backgroundColor: (theme) =>
|
||||||
theme.palette.mode === "dark"
|
theme.palette.mode === 'dark'
|
||||||
? "rgba(255,255,255,0.2)"
|
? 'rgba(255,255,255,0.2)'
|
||||||
: "rgba(0,0,0,0.2)",
|
: 'rgba(0,0,0,0.2)',
|
||||||
},
|
},
|
||||||
visibility: show ? "visible" : "hidden",
|
visibility: show ? 'visible' : 'hidden',
|
||||||
...sx,
|
...sx,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<KeyboardArrowUpIcon />
|
<KeyboardArrowUpIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Fade>
|
</Fade>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,201 +1,201 @@
|
|||||||
import { useTheme } from "@mui/material";
|
import { useTheme } from '@mui/material'
|
||||||
import { useEffect, useImperativeHandle, useRef, type Ref } from "react";
|
import { useEffect, useImperativeHandle, useRef, type Ref } from 'react'
|
||||||
import { Traffic } from "tauri-plugin-mihomo-api";
|
import { Traffic } from 'tauri-plugin-mihomo-api'
|
||||||
|
|
||||||
const maxPoint = 30;
|
const maxPoint = 30
|
||||||
|
|
||||||
const refLineAlpha = 1;
|
const refLineAlpha = 1
|
||||||
const refLineWidth = 2;
|
const refLineWidth = 2
|
||||||
|
|
||||||
const upLineAlpha = 0.6;
|
const upLineAlpha = 0.6
|
||||||
const upLineWidth = 4;
|
const upLineWidth = 4
|
||||||
|
|
||||||
const downLineAlpha = 1;
|
const downLineAlpha = 1
|
||||||
const downLineWidth = 4;
|
const downLineWidth = 4
|
||||||
|
|
||||||
const defaultList = Array(maxPoint + 2).fill({ up: 0, down: 0 });
|
const defaultList = Array(maxPoint + 2).fill({ up: 0, down: 0 })
|
||||||
|
|
||||||
export interface TrafficRef {
|
export interface TrafficRef {
|
||||||
appendData: (data: Traffic) => void;
|
appendData: (data: Traffic) => void
|
||||||
toggleStyle: () => void;
|
toggleStyle: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* draw the traffic graph
|
* draw the traffic graph
|
||||||
*/
|
*/
|
||||||
export function TrafficGraph({ ref }: { ref?: Ref<TrafficRef> }) {
|
export function TrafficGraph({ ref }: { ref?: Ref<TrafficRef> }) {
|
||||||
const countRef = useRef(0);
|
const countRef = useRef(0)
|
||||||
const styleRef = useRef(true);
|
const styleRef = useRef(true)
|
||||||
const listRef = useRef<Traffic[]>(defaultList);
|
const listRef = useRef<Traffic[]>(defaultList)
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null!);
|
const canvasRef = useRef<HTMLCanvasElement>(null!)
|
||||||
|
|
||||||
const cacheRef = useRef<Traffic | null>(null);
|
const cacheRef = useRef<Traffic | null>(null)
|
||||||
|
|
||||||
const { palette } = useTheme();
|
const { palette } = useTheme()
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
appendData: (data: Traffic) => {
|
appendData: (data: Traffic) => {
|
||||||
cacheRef.current = data;
|
cacheRef.current = data
|
||||||
},
|
},
|
||||||
toggleStyle: () => {
|
toggleStyle: () => {
|
||||||
styleRef.current = !styleRef.current;
|
styleRef.current = !styleRef.current
|
||||||
},
|
},
|
||||||
}));
|
}))
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timer: any;
|
let timer: any
|
||||||
const zero = { up: 0, down: 0 };
|
const zero = { up: 0, down: 0 }
|
||||||
|
|
||||||
const handleData = () => {
|
const handleData = () => {
|
||||||
const data = cacheRef.current ? cacheRef.current : zero;
|
const data = cacheRef.current ? cacheRef.current : zero
|
||||||
cacheRef.current = null;
|
cacheRef.current = null
|
||||||
|
|
||||||
const list = listRef.current;
|
const list = listRef.current
|
||||||
if (list.length > maxPoint + 2) list.shift();
|
if (list.length > maxPoint + 2) list.shift()
|
||||||
list.push(data);
|
list.push(data)
|
||||||
countRef.current = 0;
|
countRef.current = 0
|
||||||
|
|
||||||
timer = setTimeout(handleData, 1000);
|
timer = setTimeout(handleData, 1000)
|
||||||
};
|
}
|
||||||
|
|
||||||
handleData();
|
handleData()
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (timer) clearTimeout(timer);
|
if (timer) clearTimeout(timer)
|
||||||
};
|
}
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let raf = 0;
|
let raf = 0
|
||||||
const canvas = canvasRef.current!;
|
const canvas = canvasRef.current!
|
||||||
|
|
||||||
if (!canvas) return;
|
if (!canvas) return
|
||||||
|
|
||||||
const context = canvas.getContext("2d")!;
|
const context = canvas.getContext('2d')!
|
||||||
|
|
||||||
if (!context) return;
|
if (!context) return
|
||||||
|
|
||||||
const { primary, secondary, divider } = palette;
|
const { primary, secondary, divider } = palette
|
||||||
const refLineColor = divider || "rgba(0, 0, 0, 0.12)";
|
const refLineColor = divider || 'rgba(0, 0, 0, 0.12)'
|
||||||
const upLineColor = secondary.main || "#9c27b0";
|
const upLineColor = secondary.main || '#9c27b0'
|
||||||
const downLineColor = primary.main || "#5b5c9d";
|
const downLineColor = primary.main || '#5b5c9d'
|
||||||
|
|
||||||
const width = canvas.width;
|
const width = canvas.width
|
||||||
const height = canvas.height;
|
const height = canvas.height
|
||||||
const dx = width / maxPoint;
|
const dx = width / maxPoint
|
||||||
const dy = height / 7;
|
const dy = height / 7
|
||||||
const l1 = dy;
|
const l1 = dy
|
||||||
const l2 = dy * 4;
|
const l2 = dy * 4
|
||||||
|
|
||||||
const countY = (v: number) => {
|
const countY = (v: number) => {
|
||||||
const h = height;
|
const h = height
|
||||||
|
|
||||||
if (v == 0) return h - 1;
|
if (v == 0) return h - 1
|
||||||
if (v <= 10) return h - (v / 10) * dy;
|
if (v <= 10) return h - (v / 10) * dy
|
||||||
if (v <= 100) return h - (v / 100 + 1) * dy;
|
if (v <= 100) return h - (v / 100 + 1) * dy
|
||||||
if (v <= 1024) return h - (v / 1024 + 2) * dy;
|
if (v <= 1024) return h - (v / 1024 + 2) * dy
|
||||||
if (v <= 10240) return h - (v / 10240 + 3) * dy;
|
if (v <= 10240) return h - (v / 10240 + 3) * dy
|
||||||
if (v <= 102400) return h - (v / 102400 + 4) * dy;
|
if (v <= 102400) return h - (v / 102400 + 4) * dy
|
||||||
if (v <= 1048576) return h - (v / 1048576 + 5) * dy;
|
if (v <= 1048576) return h - (v / 1048576 + 5) * dy
|
||||||
if (v <= 10485760) return h - (v / 10485760 + 6) * dy;
|
if (v <= 10485760) return h - (v / 10485760 + 6) * dy
|
||||||
return 1;
|
return 1
|
||||||
};
|
}
|
||||||
|
|
||||||
const drawBezier = (list: number[], offset: number) => {
|
const drawBezier = (list: number[], offset: number) => {
|
||||||
const points = list.map((y, i) => [
|
const points = list.map((y, i) => [
|
||||||
(dx * (i - 1) - offset + 3) | 0,
|
(dx * (i - 1) - offset + 3) | 0,
|
||||||
countY(y),
|
countY(y),
|
||||||
]);
|
])
|
||||||
|
|
||||||
context.moveTo(points[0][0], points[0][1]);
|
context.moveTo(points[0][0], points[0][1])
|
||||||
|
|
||||||
for (let i = 1; i < points.length; i++) {
|
for (let i = 1; i < points.length; i++) {
|
||||||
const p1 = points[i];
|
const p1 = points[i]
|
||||||
const p2 = points[i + 1] || p1;
|
const p2 = points[i + 1] || p1
|
||||||
|
|
||||||
const x1 = (p1[0] + p2[0]) / 2;
|
const x1 = (p1[0] + p2[0]) / 2
|
||||||
const y1 = (p1[1] + p2[1]) / 2;
|
const y1 = (p1[1] + p2[1]) / 2
|
||||||
|
|
||||||
context.quadraticCurveTo(p1[0], p1[1], x1, y1);
|
context.quadraticCurveTo(p1[0], p1[1], x1, y1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const drawLine = (list: number[], offset: number) => {
|
const drawLine = (list: number[], offset: number) => {
|
||||||
const points = list.map((y, i) => [
|
const points = list.map((y, i) => [
|
||||||
(dx * (i - 1) - offset) | 0,
|
(dx * (i - 1) - offset) | 0,
|
||||||
countY(y),
|
countY(y),
|
||||||
]);
|
])
|
||||||
|
|
||||||
context.moveTo(points[0][0], points[0][1]);
|
context.moveTo(points[0][0], points[0][1])
|
||||||
|
|
||||||
for (let i = 1; i < points.length; i++) {
|
for (let i = 1; i < points.length; i++) {
|
||||||
const p = points[i];
|
const p = points[i]
|
||||||
context.lineTo(p[0], p[1]);
|
context.lineTo(p[0], p[1])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const drawGraph = (lastTime: number) => {
|
const drawGraph = (lastTime: number) => {
|
||||||
const listUp = listRef.current.map((v) => v.up);
|
const listUp = listRef.current.map((v) => v.up)
|
||||||
const listDown = listRef.current.map((v) => v.down);
|
const listDown = listRef.current.map((v) => v.down)
|
||||||
const lineStyle = styleRef.current;
|
const lineStyle = styleRef.current
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now()
|
||||||
const diff = now - lastTime;
|
const diff = now - lastTime
|
||||||
if (diff < 33) {
|
if (diff < 33) {
|
||||||
raf = requestAnimationFrame(() => drawGraph(lastTime));
|
raf = requestAnimationFrame(() => drawGraph(lastTime))
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
const temp = Math.min((diff / 1000) * dx + countRef.current, dx);
|
const temp = Math.min((diff / 1000) * dx + countRef.current, dx)
|
||||||
const offset = countRef.current === 0 ? 0 : temp;
|
const offset = countRef.current === 0 ? 0 : temp
|
||||||
countRef.current = temp;
|
countRef.current = temp
|
||||||
|
|
||||||
context.clearRect(0, 0, width, height);
|
context.clearRect(0, 0, width, height)
|
||||||
|
|
||||||
// Reference lines
|
// Reference lines
|
||||||
context.beginPath();
|
context.beginPath()
|
||||||
context.globalAlpha = refLineAlpha;
|
context.globalAlpha = refLineAlpha
|
||||||
context.lineWidth = refLineWidth;
|
context.lineWidth = refLineWidth
|
||||||
context.strokeStyle = refLineColor;
|
context.strokeStyle = refLineColor
|
||||||
context.moveTo(0, l1);
|
context.moveTo(0, l1)
|
||||||
context.lineTo(width, l1);
|
context.lineTo(width, l1)
|
||||||
context.moveTo(0, l2);
|
context.moveTo(0, l2)
|
||||||
context.lineTo(width, l2);
|
context.lineTo(width, l2)
|
||||||
context.stroke();
|
context.stroke()
|
||||||
context.closePath();
|
context.closePath()
|
||||||
|
|
||||||
context.beginPath();
|
context.beginPath()
|
||||||
context.globalAlpha = upLineAlpha;
|
context.globalAlpha = upLineAlpha
|
||||||
context.lineWidth = upLineWidth;
|
context.lineWidth = upLineWidth
|
||||||
context.strokeStyle = upLineColor;
|
context.strokeStyle = upLineColor
|
||||||
if (lineStyle) {
|
if (lineStyle) {
|
||||||
drawBezier(listUp, offset);
|
drawBezier(listUp, offset)
|
||||||
} else {
|
} else {
|
||||||
drawLine(listUp, offset);
|
drawLine(listUp, offset)
|
||||||
}
|
}
|
||||||
context.stroke();
|
context.stroke()
|
||||||
context.closePath();
|
context.closePath()
|
||||||
|
|
||||||
context.beginPath();
|
context.beginPath()
|
||||||
context.globalAlpha = downLineAlpha;
|
context.globalAlpha = downLineAlpha
|
||||||
context.lineWidth = downLineWidth;
|
context.lineWidth = downLineWidth
|
||||||
context.strokeStyle = downLineColor;
|
context.strokeStyle = downLineColor
|
||||||
if (lineStyle) {
|
if (lineStyle) {
|
||||||
drawBezier(listDown, offset);
|
drawBezier(listDown, offset)
|
||||||
} else {
|
} else {
|
||||||
drawLine(listDown, offset);
|
drawLine(listDown, offset)
|
||||||
}
|
}
|
||||||
context.stroke();
|
context.stroke()
|
||||||
context.closePath();
|
context.closePath()
|
||||||
|
|
||||||
raf = requestAnimationFrame(() => drawGraph(now));
|
raf = requestAnimationFrame(() => drawGraph(now))
|
||||||
};
|
}
|
||||||
|
|
||||||
drawGraph(Date.now());
|
drawGraph(Date.now())
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelAnimationFrame(raf);
|
cancelAnimationFrame(raf)
|
||||||
};
|
}
|
||||||
}, [palette]);
|
}, [palette])
|
||||||
|
|
||||||
return <canvas ref={canvasRef} style={{ width: "100%", height: "100%" }} />;
|
return <canvas ref={canvasRef} style={{ width: '100%', height: '100%' }} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import { Button } from "@mui/material";
|
import { Button } from '@mui/material'
|
||||||
import { useRef } from "react";
|
import { useRef } from 'react'
|
||||||
|
|
||||||
import { DialogRef } from "@/components/base";
|
import { DialogRef } from '@/components/base'
|
||||||
import { useUpdate } from "@/hooks/use-update";
|
import { useUpdate } from '@/hooks/use-update'
|
||||||
|
|
||||||
import { UpdateViewer } from "../setting/mods/update-viewer";
|
import { UpdateViewer } from '../setting/mods/update-viewer'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UpdateButton = (props: Props) => {
|
export const UpdateButton = (props: Props) => {
|
||||||
const { className } = props;
|
const { className } = props
|
||||||
const viewerRef = useRef<DialogRef>(null);
|
const viewerRef = useRef<DialogRef>(null)
|
||||||
|
|
||||||
const { updateInfo } = useUpdate();
|
const { updateInfo } = useUpdate()
|
||||||
|
|
||||||
if (!updateInfo?.available) return null;
|
if (!updateInfo?.available) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -32,5 +32,5 @@ export const UpdateButton = (props: Props) => {
|
|||||||
New
|
New
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
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 { forwardRef, useImperativeHandle } from "react";
|
import { forwardRef, useImperativeHandle } from 'react'
|
||||||
|
|
||||||
import { useWindowControls } from "@/hooks/use-window";
|
import { useWindowControls } from '@/hooks/use-window'
|
||||||
import getSystem from "@/utils/get-system";
|
import getSystem from '@/utils/get-system'
|
||||||
|
|
||||||
export const WindowControls = forwardRef(function WindowControls(props, ref) {
|
export const WindowControls = forwardRef(function WindowControls(props, ref) {
|
||||||
const OS = getSystem();
|
const OS = getSystem()
|
||||||
const {
|
const {
|
||||||
currentWindow,
|
currentWindow,
|
||||||
maximized,
|
maximized,
|
||||||
@@ -14,7 +14,7 @@ export const WindowControls = forwardRef(function WindowControls(props, ref) {
|
|||||||
close,
|
close,
|
||||||
toggleFullscreen,
|
toggleFullscreen,
|
||||||
toggleMaximize,
|
toggleMaximize,
|
||||||
} = useWindowControls();
|
} = useWindowControls()
|
||||||
|
|
||||||
useImperativeHandle(
|
useImperativeHandle(
|
||||||
ref,
|
ref,
|
||||||
@@ -34,7 +34,7 @@ export const WindowControls = forwardRef(function WindowControls(props, ref) {
|
|||||||
toggleFullscreen,
|
toggleFullscreen,
|
||||||
toggleMaximize,
|
toggleMaximize,
|
||||||
],
|
],
|
||||||
);
|
)
|
||||||
|
|
||||||
// 通过前端对 tauri 窗口进行翻转全屏时会短暂地与系统图标重叠渲染。
|
// 通过前端对 tauri 窗口进行翻转全屏时会短暂地与系统图标重叠渲染。
|
||||||
// 这可能是上游缺陷,保险起见跨平台以窗口的最大化翻转为准。
|
// 这可能是上游缺陷,保险起见跨平台以窗口的最大化翻转为准。
|
||||||
@@ -42,15 +42,15 @@ export const WindowControls = forwardRef(function WindowControls(props, ref) {
|
|||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
gap: 1,
|
gap: 1,
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
"> button": {
|
'> button': {
|
||||||
cursor: "default",
|
cursor: 'default',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{OS === "macos" && (
|
{OS === 'macos' && (
|
||||||
<>
|
<>
|
||||||
{/* macOS 风格:关闭 → 最小化 → 全屏 */}
|
{/* macOS 风格:关闭 → 最小化 → 全屏 */}
|
||||||
<IconButton size="small" sx={{ fontSize: 14 }} onClick={close}>
|
<IconButton size="small" sx={{ fontSize: 14 }} onClick={close}>
|
||||||
@@ -73,7 +73,7 @@ export const WindowControls = forwardRef(function WindowControls(props, ref) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{OS === "windows" && (
|
{OS === 'windows' && (
|
||||||
<>
|
<>
|
||||||
{/* Windows 风格:最小化 → 最大化 → 关闭 */}
|
{/* Windows 风格:最小化 → 最大化 → 关闭 */}
|
||||||
<IconButton size="small" sx={{ fontSize: 16 }} onClick={minimize}>
|
<IconButton size="small" sx={{ fontSize: 16 }} onClick={minimize}>
|
||||||
@@ -92,7 +92,7 @@ export const WindowControls = forwardRef(function WindowControls(props, ref) {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ fontSize: 16, ":hover": { bgcolor: "red", color: "white" } }}
|
sx={{ fontSize: 16, ':hover': { bgcolor: 'red', color: 'white' } }}
|
||||||
onClick={close}
|
onClick={close}
|
||||||
>
|
>
|
||||||
<Close fontSize="inherit" color="inherit" />
|
<Close fontSize="inherit" color="inherit" />
|
||||||
@@ -100,7 +100,7 @@ export const WindowControls = forwardRef(function WindowControls(props, ref) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{OS === "linux" && (
|
{OS === 'linux' && (
|
||||||
<>
|
<>
|
||||||
{/* Linux 桌面常见布局(GNOME/KDE 多为:最小化 → 最大化 → 关闭) */}
|
{/* Linux 桌面常见布局(GNOME/KDE 多为:最小化 → 最大化 → 关闭) */}
|
||||||
<IconButton size="small" sx={{ fontSize: 16 }} onClick={minimize}>
|
<IconButton size="small" sx={{ fontSize: 16 }} onClick={minimize}>
|
||||||
@@ -119,7 +119,7 @@ export const WindowControls = forwardRef(function WindowControls(props, ref) {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ fontSize: 16, ":hover": { bgcolor: "red", color: "white" } }}
|
sx={{ fontSize: 16, ':hover': { bgcolor: 'red', color: 'white' } }}
|
||||||
onClick={close}
|
onClick={close}
|
||||||
>
|
>
|
||||||
<Close fontSize="inherit" color="inherit" />
|
<Close fontSize="inherit" color="inherit" />
|
||||||
@@ -127,5 +127,5 @@ export const WindowControls = forwardRef(function WindowControls(props, ref) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
import { styled, Box } from "@mui/material";
|
import { styled, Box } from '@mui/material'
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from 'react'
|
||||||
|
|
||||||
import type { SearchState } from "@/components/base";
|
import type { SearchState } from '@/components/base'
|
||||||
|
|
||||||
const Item = styled(Box)(({ theme: { palette, typography } }) => ({
|
const Item = styled(Box)(({ theme: { palette, typography } }) => ({
|
||||||
padding: "8px 0",
|
padding: '8px 0',
|
||||||
margin: "0 12px",
|
margin: '0 12px',
|
||||||
lineHeight: 1.35,
|
lineHeight: 1.35,
|
||||||
borderBottom: `1px solid ${palette.divider}`,
|
borderBottom: `1px solid ${palette.divider}`,
|
||||||
fontSize: "0.875rem",
|
fontSize: '0.875rem',
|
||||||
fontFamily: typography.fontFamily,
|
fontFamily: typography.fontFamily,
|
||||||
userSelect: "text",
|
userSelect: 'text',
|
||||||
"& .time": {
|
'& .time': {
|
||||||
color: palette.text.secondary,
|
color: palette.text.secondary,
|
||||||
},
|
},
|
||||||
"& .type": {
|
'& .type': {
|
||||||
display: "inline-block",
|
display: 'inline-block',
|
||||||
marginLeft: 8,
|
marginLeft: 8,
|
||||||
textAlign: "center",
|
textAlign: 'center',
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
textTransform: "uppercase",
|
textTransform: 'uppercase',
|
||||||
fontWeight: "600",
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
'& .type[data-type="error"], & .type[data-type="err"]': {
|
'& .type[data-type="error"], & .type[data-type="err"]': {
|
||||||
color: palette.error.main,
|
color: palette.error.main,
|
||||||
@@ -31,84 +31,84 @@ const Item = styled(Box)(({ theme: { palette, typography } }) => ({
|
|||||||
'& .type[data-type="info"], & .type[data-type="inf"]': {
|
'& .type[data-type="info"], & .type[data-type="inf"]': {
|
||||||
color: palette.info.main,
|
color: palette.info.main,
|
||||||
},
|
},
|
||||||
"& .data": {
|
'& .data': {
|
||||||
color: palette.text.primary,
|
color: palette.text.primary,
|
||||||
overflowWrap: "anywhere",
|
overflowWrap: 'anywhere',
|
||||||
},
|
},
|
||||||
"& .highlight": {
|
'& .highlight': {
|
||||||
backgroundColor: palette.mode === "dark" ? "#ffeb3b40" : "#ffeb3b90",
|
backgroundColor: palette.mode === 'dark' ? '#ffeb3b40' : '#ffeb3b90',
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
padding: "0 2px",
|
padding: '0 2px',
|
||||||
},
|
},
|
||||||
}));
|
}))
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: ILogItem;
|
value: ILogItem
|
||||||
searchState?: SearchState;
|
searchState?: SearchState
|
||||||
}
|
}
|
||||||
|
|
||||||
const LogItem = ({ value, searchState }: Props) => {
|
const LogItem = ({ value, searchState }: Props) => {
|
||||||
const renderHighlightText = (text: string) => {
|
const renderHighlightText = (text: string) => {
|
||||||
if (!searchState?.text.trim()) return text;
|
if (!searchState?.text.trim()) return text
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const searchText = searchState.text;
|
const searchText = searchState.text
|
||||||
let pattern: string;
|
let pattern: string
|
||||||
|
|
||||||
if (searchState.useRegularExpression) {
|
if (searchState.useRegularExpression) {
|
||||||
try {
|
try {
|
||||||
new RegExp(searchText);
|
new RegExp(searchText)
|
||||||
pattern = searchText;
|
pattern = searchText
|
||||||
} catch {
|
} catch {
|
||||||
pattern = searchText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
pattern = searchText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const escaped = searchText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
const escaped = searchText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||||
pattern = searchState.matchWholeWord ? `\\b${escaped}\\b` : escaped;
|
pattern = searchState.matchWholeWord ? `\\b${escaped}\\b` : escaped
|
||||||
}
|
}
|
||||||
|
|
||||||
const flags = searchState.matchCase ? "g" : "gi";
|
const flags = searchState.matchCase ? 'g' : 'gi'
|
||||||
const regex = new RegExp(pattern, flags);
|
const regex = new RegExp(pattern, flags)
|
||||||
const elements: ReactNode[] = [];
|
const elements: ReactNode[] = []
|
||||||
let lastIndex = 0;
|
let lastIndex = 0
|
||||||
let match: RegExpExecArray | null;
|
let match: RegExpExecArray | null
|
||||||
|
|
||||||
while ((match = regex.exec(text)) !== null) {
|
while ((match = regex.exec(text)) !== null) {
|
||||||
const start = match.index;
|
const start = match.index
|
||||||
const matchText = match[0];
|
const matchText = match[0]
|
||||||
|
|
||||||
if (matchText === "") {
|
if (matchText === '') {
|
||||||
regex.lastIndex += 1;
|
regex.lastIndex += 1
|
||||||
continue;
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (start > lastIndex) {
|
if (start > lastIndex) {
|
||||||
elements.push(text.slice(lastIndex, start));
|
elements.push(text.slice(lastIndex, start))
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.push(
|
elements.push(
|
||||||
<span key={`highlight-${start}`} className="highlight">
|
<span key={`highlight-${start}`} className="highlight">
|
||||||
{matchText}
|
{matchText}
|
||||||
</span>,
|
</span>,
|
||||||
);
|
)
|
||||||
|
|
||||||
lastIndex = start + matchText.length;
|
lastIndex = start + matchText.length
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastIndex < text.length) {
|
if (lastIndex < text.length) {
|
||||||
elements.push(text.slice(lastIndex));
|
elements.push(text.slice(lastIndex))
|
||||||
}
|
}
|
||||||
|
|
||||||
return elements.length ? elements : text;
|
return elements.length ? elements : text
|
||||||
} catch {
|
} catch {
|
||||||
return text;
|
return text
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Item>
|
<Item>
|
||||||
<div>
|
<div>
|
||||||
<span className="time">{renderHighlightText(value.time || "")}</span>
|
<span className="time">{renderHighlightText(value.time || '')}</span>
|
||||||
<span className="type" data-type={value.type.toLowerCase()}>
|
<span className="type" data-type={value.type.toLowerCase()}>
|
||||||
{renderHighlightText(value.type)}
|
{renderHighlightText(value.type)}
|
||||||
</span>
|
</span>
|
||||||
@@ -117,7 +117,7 @@ const LogItem = ({ value, searchState }: Props) => {
|
|||||||
<span className="data">{renderHighlightText(value.payload)}</span>
|
<span className="data">{renderHighlightText(value.payload)}</span>
|
||||||
</div>
|
</div>
|
||||||
</Item>
|
</Item>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default LogItem;
|
export default LogItem
|
||||||
|
|||||||
@@ -4,43 +4,43 @@ import {
|
|||||||
DialogActions,
|
DialogActions,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@mui/material";
|
} from '@mui/material'
|
||||||
import { useEffect } from "react";
|
import { useEffect } from 'react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean
|
||||||
title: string;
|
title: string
|
||||||
message: string;
|
message: string
|
||||||
onClose: () => void;
|
onClose: () => void
|
||||||
onConfirm: () => void;
|
onConfirm: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConfirmViewer = (props: Props) => {
|
export const ConfirmViewer = (props: Props) => {
|
||||||
const { open, title, message, onClose, onConfirm } = props;
|
const { open, title, message, onClose, onConfirm } = props
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return
|
||||||
}, [open]);
|
}, [open])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onClose} maxWidth="xs" fullWidth>
|
<Dialog open={open} onClose={onClose} maxWidth="xs" fullWidth>
|
||||||
<DialogTitle>{title}</DialogTitle>
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
|
||||||
<DialogContent sx={{ pb: 1, userSelect: "text" }}>
|
<DialogContent sx={{ pb: 1, userSelect: 'text' }}>
|
||||||
{message}
|
{message}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={onClose} variant="outlined">
|
<Button onClick={onClose} variant="outlined">
|
||||||
{t("shared.actions.cancel")}
|
{t('shared.actions.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onConfirm} variant="contained">
|
<Button onClick={onConfirm} variant="contained">
|
||||||
{t("shared.actions.confirm")}
|
{t('shared.actions.confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import MonacoEditor from "@monaco-editor/react";
|
import MonacoEditor from '@monaco-editor/react'
|
||||||
import {
|
import {
|
||||||
CloseFullscreenRounded,
|
CloseFullscreenRounded,
|
||||||
ContentPasteRounded,
|
ContentPasteRounded,
|
||||||
FormatPaintRounded,
|
FormatPaintRounded,
|
||||||
OpenInFullRounded,
|
OpenInFullRounded,
|
||||||
} from "@mui/icons-material";
|
} from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
@@ -13,44 +13,38 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
IconButton,
|
IconButton,
|
||||||
} from "@mui/material";
|
} from '@mui/material'
|
||||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from 'ahooks'
|
||||||
import type { editor } from "monaco-editor";
|
import type { editor } from 'monaco-editor'
|
||||||
import {
|
import { type ReactNode, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
type ReactNode,
|
import { useTranslation } from 'react-i18next'
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
import { BaseLoadingOverlay } from "@/components/base";
|
import { BaseLoadingOverlay } from '@/components/base'
|
||||||
import { beforeEditorMount } from "@/services/monaco";
|
import { beforeEditorMount } from '@/services/monaco'
|
||||||
import { showNotice } from "@/services/notice-service";
|
import { showNotice } from '@/services/notice-service'
|
||||||
import { useThemeMode } from "@/services/states";
|
import { useThemeMode } from '@/services/states'
|
||||||
import debounce from "@/utils/debounce";
|
import debounce from '@/utils/debounce'
|
||||||
import getSystem from "@/utils/get-system";
|
import getSystem from '@/utils/get-system'
|
||||||
|
|
||||||
const appWindow = getCurrentWebviewWindow();
|
const appWindow = getCurrentWebviewWindow()
|
||||||
|
|
||||||
export type EditorLanguage = "yaml" | "javascript" | "css";
|
export type EditorLanguage = 'yaml' | 'javascript' | 'css'
|
||||||
|
|
||||||
export interface EditorViewerProps {
|
export interface EditorViewerProps {
|
||||||
open: boolean;
|
open: boolean
|
||||||
title?: string | ReactNode;
|
title?: string | ReactNode
|
||||||
value: string;
|
value: string
|
||||||
language: EditorLanguage;
|
language: EditorLanguage
|
||||||
path: string;
|
path: string
|
||||||
readOnly?: boolean;
|
readOnly?: boolean
|
||||||
loading?: boolean;
|
loading?: boolean
|
||||||
dirty?: boolean;
|
dirty?: boolean
|
||||||
saveDisabled?: boolean;
|
saveDisabled?: boolean
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void
|
||||||
onSave?: () => void | Promise<void>;
|
onSave?: () => void | Promise<void>
|
||||||
onClose: () => void;
|
onClose: () => void
|
||||||
onValidate?: (markers: editor.IMarker[]) => void;
|
onValidate?: (markers: editor.IMarker[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EditorViewer = ({
|
export const EditorViewer = ({
|
||||||
@@ -68,111 +62,111 @@ export const EditorViewer = ({
|
|||||||
onClose,
|
onClose,
|
||||||
onValidate,
|
onValidate,
|
||||||
}: EditorViewerProps) => {
|
}: EditorViewerProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const themeMode = useThemeMode();
|
const themeMode = useThemeMode()
|
||||||
const [isMaximized, setIsMaximized] = useState(false);
|
const [isMaximized, setIsMaximized] = useState(false)
|
||||||
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
|
||||||
|
|
||||||
const resolvedTitle = title ?? t("profiles.components.menu.editFile");
|
const resolvedTitle = title ?? t('profiles.components.menu.editFile')
|
||||||
const disableSave = loading || saveDisabled || dirty === false;
|
const disableSave = loading || saveDisabled || dirty === false
|
||||||
|
|
||||||
const syncMaximizedState = useCallback(async () => {
|
const syncMaximizedState = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setIsMaximized(await appWindow.isMaximized());
|
setIsMaximized(await appWindow.isMaximized())
|
||||||
} catch {
|
} catch {
|
||||||
setIsMaximized(false);
|
setIsMaximized(false)
|
||||||
}
|
}
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
const handleSave = useLockFn(async () => {
|
const handleSave = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
if (!readOnly) {
|
if (!readOnly) {
|
||||||
await onSave?.();
|
await onSave?.()
|
||||||
}
|
}
|
||||||
onClose();
|
onClose()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showNotice.error(error);
|
showNotice.error(error)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
try {
|
try {
|
||||||
onClose();
|
onClose()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showNotice.error(error);
|
showNotice.error(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handlePaste = useLockFn(async () => {
|
const handlePaste = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
if (readOnly || loading || !editorRef.current) return;
|
if (readOnly || loading || !editorRef.current) return
|
||||||
|
|
||||||
const text = await navigator.clipboard.readText();
|
const text = await navigator.clipboard.readText()
|
||||||
if (!text) return;
|
if (!text) return
|
||||||
|
|
||||||
const editorInstance = editorRef.current;
|
const editorInstance = editorRef.current
|
||||||
const model = editorInstance.getModel();
|
const model = editorInstance.getModel()
|
||||||
const selections = editorInstance.getSelections();
|
const selections = editorInstance.getSelections()
|
||||||
if (!model || !selections || selections.length === 0) return;
|
if (!model || !selections || selections.length === 0) return
|
||||||
|
|
||||||
editorInstance.pushUndoStop();
|
editorInstance.pushUndoStop()
|
||||||
editorInstance.executeEdits(
|
editorInstance.executeEdits(
|
||||||
"explicit-paste",
|
'explicit-paste',
|
||||||
selections.map((selection) => ({
|
selections.map((selection) => ({
|
||||||
range: selection,
|
range: selection,
|
||||||
text,
|
text,
|
||||||
forceMoveMarkers: true,
|
forceMoveMarkers: true,
|
||||||
})),
|
})),
|
||||||
);
|
)
|
||||||
editorInstance.pushUndoStop();
|
editorInstance.pushUndoStop()
|
||||||
editorInstance.focus();
|
editorInstance.focus()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showNotice.error(error);
|
showNotice.error(error)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
const handleFormat = useLockFn(async () => {
|
const handleFormat = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
if (loading) return;
|
if (loading) return
|
||||||
await editorRef.current?.getAction("editor.action.formatDocument")?.run();
|
await editorRef.current?.getAction('editor.action.formatDocument')?.run()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showNotice.error(error);
|
showNotice.error(error)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
const handleToggleMaximize = useLockFn(async () => {
|
const handleToggleMaximize = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
await appWindow.toggleMaximize();
|
await appWindow.toggleMaximize()
|
||||||
await syncMaximizedState();
|
await syncMaximizedState()
|
||||||
editorRef.current?.layout();
|
editorRef.current?.layout()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showNotice.error(error);
|
showNotice.error(error)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return
|
||||||
void syncMaximizedState();
|
void syncMaximizedState()
|
||||||
}, [open, syncMaximizedState]);
|
}, [open, syncMaximizedState])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return
|
||||||
|
|
||||||
const onResized = debounce(() => {
|
const onResized = debounce(() => {
|
||||||
void syncMaximizedState();
|
void syncMaximizedState()
|
||||||
try {
|
try {
|
||||||
editorRef.current?.layout();
|
editorRef.current?.layout()
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore transient layout errors during window transitions.
|
// Ignore transient layout errors during window transitions.
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100)
|
||||||
|
|
||||||
const unlistenResized = appWindow.onResized(onResized);
|
const unlistenResized = appWindow.onResized(onResized)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unlistenResized.then((unlisten) => unlisten());
|
unlistenResized.then((unlisten) => unlisten())
|
||||||
};
|
}
|
||||||
}, [open, syncMaximizedState]);
|
}, [open, syncMaximizedState])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -186,14 +180,14 @@ export const EditorViewer = ({
|
|||||||
|
|
||||||
<DialogContent
|
<DialogContent
|
||||||
sx={{
|
sx={{
|
||||||
width: "auto",
|
width: 'auto',
|
||||||
height: "calc(100vh - 185px)",
|
height: 'calc(100vh - 185px)',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
flexDirection: "column",
|
flexDirection: 'column',
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ position: "relative", flex: "1 1 auto", minHeight: 0 }}>
|
<div style={{ position: 'relative', flex: '1 1 auto', minHeight: 0 }}>
|
||||||
<BaseLoadingOverlay isLoading={loading} />
|
<BaseLoadingOverlay isLoading={loading} />
|
||||||
{!loading && (
|
{!loading && (
|
||||||
<MonacoEditor
|
<MonacoEditor
|
||||||
@@ -201,30 +195,30 @@ export const EditorViewer = ({
|
|||||||
path={path}
|
path={path}
|
||||||
value={value}
|
value={value}
|
||||||
language={language}
|
language={language}
|
||||||
theme={themeMode === "light" ? "light" : "vs-dark"}
|
theme={themeMode === 'light' ? 'light' : 'vs-dark'}
|
||||||
loading={null}
|
loading={null}
|
||||||
saveViewState
|
saveViewState
|
||||||
keepCurrentModel={false}
|
keepCurrentModel={false}
|
||||||
beforeMount={beforeEditorMount}
|
beforeMount={beforeEditorMount}
|
||||||
onMount={(editorInstance) => {
|
onMount={(editorInstance) => {
|
||||||
editorRef.current = editorInstance;
|
editorRef.current = editorInstance
|
||||||
}}
|
}}
|
||||||
onChange={(nextValue) => onChange?.(nextValue ?? "")}
|
onChange={(nextValue) => onChange?.(nextValue ?? '')}
|
||||||
onValidate={onValidate}
|
onValidate={onValidate}
|
||||||
options={{
|
options={{
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
tabSize: 2,
|
tabSize: 2,
|
||||||
minimap: {
|
minimap: {
|
||||||
enabled:
|
enabled:
|
||||||
typeof document !== "undefined" &&
|
typeof document !== 'undefined' &&
|
||||||
document.documentElement.clientWidth >= 1500,
|
document.documentElement.clientWidth >= 1500,
|
||||||
},
|
},
|
||||||
mouseWheelZoom: true,
|
mouseWheelZoom: true,
|
||||||
readOnly,
|
readOnly,
|
||||||
readOnlyMessage: {
|
readOnlyMessage: {
|
||||||
value: t("profiles.modals.editor.messages.readOnly"),
|
value: t('profiles.modals.editor.messages.readOnly'),
|
||||||
},
|
},
|
||||||
renderValidationDecorations: "on",
|
renderValidationDecorations: 'on',
|
||||||
quickSuggestions: {
|
quickSuggestions: {
|
||||||
strings: true,
|
strings: true,
|
||||||
comments: true,
|
comments: true,
|
||||||
@@ -234,7 +228,7 @@ export const EditorViewer = ({
|
|||||||
top: 33,
|
top: 33,
|
||||||
},
|
},
|
||||||
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
|
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
|
||||||
getSystem() === "windows" ? ", twemoji mozilla" : ""
|
getSystem() === 'windows' ? ', twemoji mozilla' : ''
|
||||||
}`,
|
}`,
|
||||||
fontLigatures: false,
|
fontLigatures: false,
|
||||||
smoothScrolling: true,
|
smoothScrolling: true,
|
||||||
@@ -245,16 +239,16 @@ export const EditorViewer = ({
|
|||||||
|
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
variant="contained"
|
variant="contained"
|
||||||
sx={{ position: "absolute", left: "14px", bottom: "8px" }}
|
sx={{ position: 'absolute', left: '14px', bottom: '8px' }}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="medium"
|
size="medium"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
sx={{ display: readOnly ? "none" : "" }}
|
sx={{ display: readOnly ? 'none' : '' }}
|
||||||
title={t("profiles.page.importForm.actions.paste")}
|
title={t('profiles.page.importForm.actions.paste')}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void handlePaste();
|
void handlePaste()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ContentPasteRounded fontSize="inherit" />
|
<ContentPasteRounded fontSize="inherit" />
|
||||||
@@ -262,11 +256,11 @@ export const EditorViewer = ({
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="medium"
|
size="medium"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
sx={{ display: readOnly ? "none" : "" }}
|
sx={{ display: readOnly ? 'none' : '' }}
|
||||||
title={t("profiles.modals.editor.actions.format")}
|
title={t('profiles.modals.editor.actions.format')}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void handleFormat();
|
void handleFormat()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FormatPaintRounded fontSize="inherit" />
|
<FormatPaintRounded fontSize="inherit" />
|
||||||
@@ -275,10 +269,10 @@ export const EditorViewer = ({
|
|||||||
size="medium"
|
size="medium"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
title={t(
|
title={t(
|
||||||
isMaximized ? "shared.window.minimize" : "shared.window.maximize",
|
isMaximized ? 'shared.window.minimize' : 'shared.window.maximize',
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void handleToggleMaximize();
|
void handleToggleMaximize()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isMaximized ? <CloseFullscreenRounded /> : <OpenInFullRounded />}
|
{isMaximized ? <CloseFullscreenRounded /> : <OpenInFullRounded />}
|
||||||
@@ -288,20 +282,20 @@ export const EditorViewer = ({
|
|||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleClose} variant="outlined">
|
<Button onClick={handleClose} variant="outlined">
|
||||||
{t(readOnly ? "shared.actions.close" : "shared.actions.cancel")}
|
{t(readOnly ? 'shared.actions.close' : 'shared.actions.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void handleSave();
|
void handleSave()
|
||||||
}}
|
}}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
disabled={disableSave}
|
disabled={disableSave}
|
||||||
>
|
>
|
||||||
{t("shared.actions.save")}
|
{t('shared.actions.save')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,61 +1,61 @@
|
|||||||
import { Box, Button, Typography } from "@mui/material";
|
import { Box, Button, Typography } from '@mui/material'
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from 'ahooks'
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from 'react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onChange: (file: File, value: string) => void;
|
onChange: (file: File, value: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileInput = (props: Props) => {
|
export const FileInput = (props: Props) => {
|
||||||
const { onChange } = props;
|
const { onChange } = props
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
// file input
|
// file input
|
||||||
const inputRef = useRef<any>(undefined);
|
const inputRef = useRef<any>(undefined)
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false)
|
||||||
const [fileName, setFileName] = useState("");
|
const [fileName, setFileName] = useState('')
|
||||||
|
|
||||||
const onFileInput = useLockFn(async (e: any) => {
|
const onFileInput = useLockFn(async (e: any) => {
|
||||||
const file = e.target.files?.[0] as File;
|
const file = e.target.files?.[0] as File
|
||||||
|
|
||||||
if (!file) return;
|
if (!file) return
|
||||||
|
|
||||||
setFileName(file.name);
|
setFileName(file.name)
|
||||||
setLoading(true);
|
setLoading(true)
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader()
|
||||||
reader.onload = (event) => {
|
reader.onload = (event) => {
|
||||||
resolve(null);
|
resolve(null)
|
||||||
onChange(file, event.target?.result as string);
|
onChange(file, event.target?.result as string)
|
||||||
};
|
}
|
||||||
reader.onerror = reject;
|
reader.onerror = reject
|
||||||
reader.readAsText(file);
|
reader.readAsText(file)
|
||||||
}).finally(() => setLoading(false));
|
}).finally(() => setLoading(false))
|
||||||
});
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ mt: 2, mb: 1, display: "flex", alignItems: "center" }}>
|
<Box sx={{ mt: 2, mb: 1, display: 'flex', alignItems: 'center' }}>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
sx={{ flex: "none" }}
|
sx={{ flex: 'none' }}
|
||||||
onClick={() => inputRef.current?.click()}
|
onClick={() => inputRef.current?.click()}
|
||||||
>
|
>
|
||||||
{t("profiles.components.fileInput.chooseFile")}
|
{t('profiles.components.fileInput.chooseFile')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept=".yaml,.yml"
|
accept=".yaml,.yml"
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
style={{ display: "none" }}
|
style={{ display: 'none' }}
|
||||||
onChange={onFileInput}
|
onChange={onFileInput}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Typography noWrap sx={{ ml: 1 }}>
|
<Typography noWrap sx={{ ml: 1 }}>
|
||||||
{loading ? "Loading..." : fileName}
|
{loading ? 'Loading...' : fileName}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useSortable } from "@dnd-kit/sortable";
|
import { useSortable } from '@dnd-kit/sortable'
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
import { DeleteForeverRounded, UndoRounded } from "@mui/icons-material";
|
import { DeleteForeverRounded, UndoRounded } from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
IconButton,
|
IconButton,
|
||||||
@@ -8,18 +8,18 @@ import {
|
|||||||
ListItemText,
|
ListItemText,
|
||||||
alpha,
|
alpha,
|
||||||
styled,
|
styled,
|
||||||
} from "@mui/material";
|
} from '@mui/material'
|
||||||
|
|
||||||
import { useIconCache } from "@/hooks/use-icon-cache";
|
import { useIconCache } from '@/hooks/use-icon-cache'
|
||||||
interface Props {
|
interface Props {
|
||||||
type: "prepend" | "original" | "delete" | "append";
|
type: 'prepend' | 'original' | 'delete' | 'append'
|
||||||
group: IProxyGroupConfig;
|
group: IProxyGroupConfig
|
||||||
onDelete: () => void;
|
onDelete: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GroupItem = (props: Props) => {
|
export const GroupItem = (props: Props) => {
|
||||||
const { type, group, onDelete } = props;
|
const { type, group, onDelete } = props
|
||||||
const sortable = type === "prepend" || type === "append";
|
const sortable = type === 'prepend' || type === 'append'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
attributes: sortableAttributes,
|
attributes: sortableAttributes,
|
||||||
@@ -31,64 +31,64 @@ export const GroupItem = (props: Props) => {
|
|||||||
} = useSortable({
|
} = useSortable({
|
||||||
id: group.name,
|
id: group.name,
|
||||||
disabled: !sortable,
|
disabled: !sortable,
|
||||||
});
|
})
|
||||||
const dragAttributes = sortable ? sortableAttributes : undefined;
|
const dragAttributes = sortable ? sortableAttributes : undefined
|
||||||
const dragListeners = sortable ? sortableListeners : undefined;
|
const dragListeners = sortable ? sortableListeners : undefined
|
||||||
const dragNodeRef = sortable ? sortableSetNodeRef : undefined;
|
const dragNodeRef = sortable ? sortableSetNodeRef : undefined
|
||||||
|
|
||||||
const iconCachePath = useIconCache({
|
const iconCachePath = useIconCache({
|
||||||
icon: group.icon,
|
icon: group.icon,
|
||||||
cacheKey: group.name.replaceAll(" ", ""),
|
cacheKey: group.name.replaceAll(' ', ''),
|
||||||
});
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
dense
|
dense
|
||||||
sx={({ palette }) => ({
|
sx={({ palette }) => ({
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
background:
|
background:
|
||||||
type === "original"
|
type === 'original'
|
||||||
? palette.mode === "dark"
|
? palette.mode === 'dark'
|
||||||
? alpha(palette.background.paper, 0.3)
|
? alpha(palette.background.paper, 0.3)
|
||||||
: alpha(palette.grey[400], 0.3)
|
: alpha(palette.grey[400], 0.3)
|
||||||
: type === "delete"
|
: type === 'delete'
|
||||||
? alpha(palette.error.main, 0.3)
|
? alpha(palette.error.main, 0.3)
|
||||||
: alpha(palette.success.main, 0.3),
|
: alpha(palette.success.main, 0.3),
|
||||||
height: "100%",
|
height: '100%',
|
||||||
margin: "8px 0",
|
margin: '8px 0',
|
||||||
borderRadius: "8px",
|
borderRadius: '8px',
|
||||||
transform: CSS.Transform.toString(transform),
|
transform: CSS.Transform.toString(transform),
|
||||||
transition,
|
transition,
|
||||||
zIndex: isDragging ? "calc(infinity)" : undefined,
|
zIndex: isDragging ? 'calc(infinity)' : undefined,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{group.icon && group.icon?.trim().startsWith("http") && (
|
{group.icon && group.icon?.trim().startsWith('http') && (
|
||||||
<img
|
<img
|
||||||
src={iconCachePath === "" ? group.icon : iconCachePath}
|
src={iconCachePath === '' ? group.icon : iconCachePath}
|
||||||
width="32px"
|
width="32px"
|
||||||
style={{
|
style={{
|
||||||
marginRight: "12px",
|
marginRight: '12px',
|
||||||
borderRadius: "6px",
|
borderRadius: '6px',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{group.icon && group.icon?.trim().startsWith("data") && (
|
{group.icon && group.icon?.trim().startsWith('data') && (
|
||||||
<img
|
<img
|
||||||
src={group.icon}
|
src={group.icon}
|
||||||
width="32px"
|
width="32px"
|
||||||
style={{
|
style={{
|
||||||
marginRight: "12px",
|
marginRight: '12px',
|
||||||
borderRadius: "6px",
|
borderRadius: '6px',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{group.icon && group.icon?.trim().startsWith("<svg") && (
|
{group.icon && group.icon?.trim().startsWith('<svg') && (
|
||||||
<img
|
<img
|
||||||
src={`data:image/svg+xml;base64,${btoa(group.icon ?? "")}`}
|
src={`data:image/svg+xml;base64,${btoa(group.icon ?? '')}`}
|
||||||
width="32px"
|
width="32px"
|
||||||
style={{
|
style={{
|
||||||
marginRight: "12px",
|
marginRight: '12px',
|
||||||
borderRadius: "6px",
|
borderRadius: '6px',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -96,10 +96,10 @@ export const GroupItem = (props: Props) => {
|
|||||||
{...(dragAttributes ?? {})}
|
{...(dragAttributes ?? {})}
|
||||||
{...(dragListeners ?? {})}
|
{...(dragListeners ?? {})}
|
||||||
ref={dragNodeRef}
|
ref={dragNodeRef}
|
||||||
sx={{ cursor: sortable ? "move" : "" }}
|
sx={{ cursor: sortable ? 'move' : '' }}
|
||||||
primary={
|
primary={
|
||||||
<StyledPrimary
|
<StyledPrimary
|
||||||
sx={{ textDecoration: type === "delete" ? "line-through" : "" }}
|
sx={{ textDecoration: type === 'delete' ? 'line-through' : '' }}
|
||||||
>
|
>
|
||||||
{group.name}
|
{group.name}
|
||||||
</StyledPrimary>
|
</StyledPrimary>
|
||||||
@@ -107,13 +107,13 @@ export const GroupItem = (props: Props) => {
|
|||||||
secondary={
|
secondary={
|
||||||
<ListItemTextChild
|
<ListItemTextChild
|
||||||
sx={{
|
sx={{
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
pt: "2px",
|
pt: '2px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ marginTop: "2px" }}>
|
<Box sx={{ marginTop: '2px' }}>
|
||||||
<StyledTypeBox>{group.type}</StyledTypeBox>
|
<StyledTypeBox>{group.type}</StyledTypeBox>
|
||||||
</Box>
|
</Box>
|
||||||
</ListItemTextChild>
|
</ListItemTextChild>
|
||||||
@@ -121,41 +121,41 @@ export const GroupItem = (props: Props) => {
|
|||||||
slotProps={{
|
slotProps={{
|
||||||
secondary: {
|
secondary: {
|
||||||
sx: {
|
sx: {
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
color: "#ccc",
|
color: '#ccc',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<IconButton onClick={onDelete}>
|
<IconButton onClick={onDelete}>
|
||||||
{type === "delete" ? <UndoRounded /> : <DeleteForeverRounded />}
|
{type === 'delete' ? <UndoRounded /> : <DeleteForeverRounded />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const StyledPrimary = styled("div")`
|
const StyledPrimary = styled('div')`
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`;
|
`
|
||||||
|
|
||||||
const ListItemTextChild = styled("span")`
|
const ListItemTextChild = styled('span')`
|
||||||
display: block;
|
display: block;
|
||||||
`;
|
`
|
||||||
|
|
||||||
const StyledTypeBox = styled(ListItemTextChild)(({ theme }) => ({
|
const StyledTypeBox = styled(ListItemTextChild)(({ theme }) => ({
|
||||||
display: "inline-block",
|
display: 'inline-block',
|
||||||
border: "1px solid #ccc",
|
border: '1px solid #ccc',
|
||||||
borderColor: alpha(theme.palette.primary.main, 0.5),
|
borderColor: alpha(theme.palette.primary.main, 0.5),
|
||||||
color: alpha(theme.palette.primary.main, 0.8),
|
color: alpha(theme.palette.primary.main, 0.8),
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
padding: "0 4px",
|
padding: '0 4px',
|
||||||
lineHeight: 1.5,
|
lineHeight: 1.5,
|
||||||
marginRight: "8px",
|
marginRight: '8px',
|
||||||
}));
|
}))
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,33 +7,33 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
Divider,
|
Divider,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from '@mui/material'
|
||||||
import { Fragment } from "react";
|
import { Fragment } from 'react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { BaseEmpty } from "@/components/base";
|
import { BaseEmpty } from '@/components/base'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean
|
||||||
logInfo: [string, string][];
|
logInfo: [string, string][]
|
||||||
onClose: () => void;
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LogViewer = (props: Props) => {
|
export const LogViewer = (props: Props) => {
|
||||||
const { open, logInfo, onClose } = props;
|
const { open, logInfo, onClose } = props
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onClose}>
|
<Dialog open={open} onClose={onClose}>
|
||||||
<DialogTitle>{t("profiles.modals.logViewer.title")}</DialogTitle>
|
<DialogTitle>{t('profiles.modals.logViewer.title')}</DialogTitle>
|
||||||
|
|
||||||
<DialogContent
|
<DialogContent
|
||||||
sx={{
|
sx={{
|
||||||
width: 400,
|
width: 400,
|
||||||
height: 300,
|
height: 300,
|
||||||
overflowX: "hidden",
|
overflowX: 'hidden',
|
||||||
userSelect: "text",
|
userSelect: 'text',
|
||||||
pb: 1,
|
pb: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -45,9 +45,9 @@ export const LogViewer = (props: Props) => {
|
|||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color={
|
color={
|
||||||
level === "error" || level === "exception"
|
level === 'error' || level === 'exception'
|
||||||
? "error"
|
? 'error'
|
||||||
: "default"
|
: 'default'
|
||||||
}
|
}
|
||||||
sx={{ mr: 1 }}
|
sx={{ mr: 1 }}
|
||||||
/>
|
/>
|
||||||
@@ -62,9 +62,9 @@ export const LogViewer = (props: Props) => {
|
|||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={onClose} variant="outlined">
|
<Button onClick={onClose} variant="outlined">
|
||||||
{t("shared.actions.close")}
|
{t('shared.actions.close')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,58 +1,58 @@
|
|||||||
import { alpha, Box, styled } from "@mui/material";
|
import { alpha, Box, styled } from '@mui/material'
|
||||||
|
|
||||||
export const ProfileBox = styled(Box)(({
|
export const ProfileBox = styled(Box)(({
|
||||||
theme,
|
theme,
|
||||||
"aria-selected": selected,
|
'aria-selected': selected,
|
||||||
}) => {
|
}) => {
|
||||||
const { mode, primary, text } = theme.palette;
|
const { mode, primary, text } = theme.palette
|
||||||
const key = `${mode}-${!!selected}`;
|
const key = `${mode}-${!!selected}`
|
||||||
|
|
||||||
const backgroundColor = mode === "light" ? "#ffffff" : "#282A36";
|
const backgroundColor = mode === 'light' ? '#ffffff' : '#282A36'
|
||||||
|
|
||||||
const color = {
|
const color = {
|
||||||
"light-true": text.secondary,
|
'light-true': text.secondary,
|
||||||
"light-false": text.secondary,
|
'light-false': text.secondary,
|
||||||
"dark-true": alpha(text.secondary, 0.65),
|
'dark-true': alpha(text.secondary, 0.65),
|
||||||
"dark-false": alpha(text.secondary, 0.65),
|
'dark-false': alpha(text.secondary, 0.65),
|
||||||
}[key]!;
|
}[key]!
|
||||||
|
|
||||||
const h2color = {
|
const h2color = {
|
||||||
"light-true": primary.main,
|
'light-true': primary.main,
|
||||||
"light-false": text.primary,
|
'light-false': text.primary,
|
||||||
"dark-true": primary.main,
|
'dark-true': primary.main,
|
||||||
"dark-false": text.primary,
|
'dark-false': text.primary,
|
||||||
}[key]!;
|
}[key]!
|
||||||
|
|
||||||
const borderSelect = {
|
const borderSelect = {
|
||||||
"light-true": {
|
'light-true': {
|
||||||
borderLeft: `3px solid ${primary.main}`,
|
borderLeft: `3px solid ${primary.main}`,
|
||||||
width: `calc(100% + 3px)`,
|
width: `calc(100% + 3px)`,
|
||||||
marginLeft: `-3px`,
|
marginLeft: `-3px`,
|
||||||
},
|
},
|
||||||
"light-false": {
|
'light-false': {
|
||||||
width: "100%",
|
width: '100%',
|
||||||
},
|
},
|
||||||
"dark-true": {
|
'dark-true': {
|
||||||
borderLeft: `3px solid ${primary.main}`,
|
borderLeft: `3px solid ${primary.main}`,
|
||||||
width: `calc(100% + 3px)`,
|
width: `calc(100% + 3px)`,
|
||||||
marginLeft: `-3px`,
|
marginLeft: `-3px`,
|
||||||
},
|
},
|
||||||
"dark-false": {
|
'dark-false': {
|
||||||
width: "100%",
|
width: '100%',
|
||||||
},
|
},
|
||||||
}[key];
|
}[key]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
display: "block",
|
display: 'block',
|
||||||
cursor: "pointer",
|
cursor: 'pointer',
|
||||||
textAlign: "left",
|
textAlign: 'left',
|
||||||
padding: "8px 16px",
|
padding: '8px 16px',
|
||||||
boxSizing: "border-box",
|
boxSizing: 'border-box',
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
...borderSelect,
|
...borderSelect,
|
||||||
borderRadius: "8px",
|
borderRadius: '8px',
|
||||||
color,
|
color,
|
||||||
"& h2": { color: h2color },
|
'& h2': { color: h2color },
|
||||||
};
|
}
|
||||||
});
|
})
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
import { FeaturedPlayListRounded } from "@mui/icons-material";
|
import { FeaturedPlayListRounded } from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Badge,
|
Badge,
|
||||||
@@ -7,99 +7,99 @@ import {
|
|||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from '@mui/material'
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from 'ahooks'
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from 'react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { EditorViewer } from "@/components/profile/editor-viewer";
|
import { EditorViewer } from '@/components/profile/editor-viewer'
|
||||||
import { useEditorDocument } from "@/hooks/use-editor-document";
|
import { useEditorDocument } from '@/hooks/use-editor-document'
|
||||||
import { viewProfile, readProfileFile, saveProfileFile } from "@/services/cmds";
|
import { viewProfile, readProfileFile, saveProfileFile } from '@/services/cmds'
|
||||||
import { showNotice } from "@/services/notice-service";
|
import { showNotice } from '@/services/notice-service'
|
||||||
|
|
||||||
import { LogViewer } from "./log-viewer";
|
import { LogViewer } from './log-viewer'
|
||||||
import { ProfileBox } from "./profile-box";
|
import { ProfileBox } from './profile-box'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
logInfo?: [string, string][];
|
logInfo?: [string, string][]
|
||||||
id: "Merge" | "Script";
|
id: 'Merge' | 'Script'
|
||||||
onSave?: (prev?: string, curr?: string) => void;
|
onSave?: (prev?: string, curr?: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY_LOG_INFO: [string, string][] = [];
|
const EMPTY_LOG_INFO: [string, string][] = []
|
||||||
|
|
||||||
// profile enhanced item
|
// profile enhanced item
|
||||||
export const ProfileMore = (props: Props) => {
|
export const ProfileMore = (props: Props) => {
|
||||||
const { id, logInfo, onSave } = props;
|
const { id, logInfo, onSave } = props
|
||||||
|
|
||||||
const entries = logInfo ?? EMPTY_LOG_INFO;
|
const entries = logInfo ?? EMPTY_LOG_INFO
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
|
||||||
const [position, setPosition] = useState({ left: 0, top: 0 });
|
const [position, setPosition] = useState({ left: 0, top: 0 })
|
||||||
const [fileOpen, setFileOpen] = useState(false);
|
const [fileOpen, setFileOpen] = useState(false)
|
||||||
const [logOpen, setLogOpen] = useState(false);
|
const [logOpen, setLogOpen] = useState(false)
|
||||||
|
|
||||||
const loadDocument = useCallback(() => readProfileFile(id), [id]);
|
const loadDocument = useCallback(() => readProfileFile(id), [id])
|
||||||
const document = useEditorDocument({
|
const document = useEditorDocument({
|
||||||
open: fileOpen,
|
open: fileOpen,
|
||||||
load: loadDocument,
|
load: loadDocument,
|
||||||
});
|
})
|
||||||
|
|
||||||
const onEditFile = () => {
|
const onEditFile = () => {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null)
|
||||||
setFileOpen(true);
|
setFileOpen(true)
|
||||||
};
|
}
|
||||||
|
|
||||||
const onOpenFile = useLockFn(async () => {
|
const onOpenFile = useLockFn(async () => {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null)
|
||||||
try {
|
try {
|
||||||
await viewProfile(id);
|
await viewProfile(id)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showNotice.error(err);
|
showNotice.error(err)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
const hasError = entries.some(([level]) => level === "exception");
|
const hasError = entries.some(([level]) => level === 'exception')
|
||||||
|
|
||||||
const globalTitles: Record<Props["id"], string> = {
|
const globalTitles: Record<Props['id'], string> = {
|
||||||
Merge: "profiles.components.more.global.merge",
|
Merge: 'profiles.components.more.global.merge',
|
||||||
Script: "profiles.components.more.global.script",
|
Script: 'profiles.components.more.global.script',
|
||||||
};
|
}
|
||||||
|
|
||||||
const chipLabels: Record<Props["id"], string> = {
|
const chipLabels: Record<Props['id'], string> = {
|
||||||
Merge: "profiles.components.more.chips.merge",
|
Merge: 'profiles.components.more.chips.merge',
|
||||||
Script: "profiles.components.more.chips.script",
|
Script: 'profiles.components.more.chips.script',
|
||||||
};
|
}
|
||||||
|
|
||||||
const itemMenu = [
|
const itemMenu = [
|
||||||
{ label: "profiles.components.menu.editFile", handler: onEditFile },
|
{ label: 'profiles.components.menu.editFile', handler: onEditFile },
|
||||||
{ label: "profiles.components.menu.openFile", handler: onOpenFile },
|
{ label: 'profiles.components.menu.openFile', handler: onOpenFile },
|
||||||
];
|
]
|
||||||
|
|
||||||
const boxStyle = {
|
const boxStyle = {
|
||||||
height: 26,
|
height: 26,
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "space-between",
|
justifyContent: 'space-between',
|
||||||
lineHeight: 1,
|
lineHeight: 1,
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleSave = useLockFn(async () => {
|
const handleSave = useLockFn(async () => {
|
||||||
const currentValue = document.value;
|
const currentValue = document.value
|
||||||
await saveProfileFile(id, currentValue);
|
await saveProfileFile(id, currentValue)
|
||||||
onSave?.(document.savedValue, currentValue);
|
onSave?.(document.savedValue, currentValue)
|
||||||
document.markSaved(currentValue);
|
document.markSaved(currentValue)
|
||||||
});
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ProfileBox
|
<ProfileBox
|
||||||
onDoubleClick={onEditFile}
|
onDoubleClick={onEditFile}
|
||||||
onContextMenu={(event) => {
|
onContextMenu={(event) => {
|
||||||
const { clientX, clientY } = event;
|
const { clientX, clientY } = event
|
||||||
setPosition({ top: clientY, left: clientX });
|
setPosition({ top: clientY, left: clientX })
|
||||||
setAnchorEl(event.currentTarget as HTMLElement);
|
setAnchorEl(event.currentTarget as HTMLElement)
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
@@ -123,19 +123,19 @@ export const ProfileMore = (props: Props) => {
|
|||||||
color="primary"
|
color="primary"
|
||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
sx={{ height: 20, textTransform: "capitalize" }}
|
sx={{ height: 20, textTransform: 'capitalize' }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={boxStyle}>
|
<Box sx={boxStyle}>
|
||||||
{id === "Script" &&
|
{id === 'Script' &&
|
||||||
(hasError ? (
|
(hasError ? (
|
||||||
<Badge color="error" variant="dot" overlap="circular">
|
<Badge color="error" variant="dot" overlap="circular">
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
edge="start"
|
edge="start"
|
||||||
color="error"
|
color="error"
|
||||||
title={t("profiles.modals.logViewer.title")}
|
title={t('profiles.modals.logViewer.title')}
|
||||||
onClick={() => setLogOpen(true)}
|
onClick={() => setLogOpen(true)}
|
||||||
>
|
>
|
||||||
<FeaturedPlayListRounded fontSize="inherit" />
|
<FeaturedPlayListRounded fontSize="inherit" />
|
||||||
@@ -146,7 +146,7 @@ export const ProfileMore = (props: Props) => {
|
|||||||
size="small"
|
size="small"
|
||||||
edge="start"
|
edge="start"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
title={t("profiles.modals.logViewer.title")}
|
title={t('profiles.modals.logViewer.title')}
|
||||||
onClick={() => setLogOpen(true)}
|
onClick={() => setLogOpen(true)}
|
||||||
>
|
>
|
||||||
<FeaturedPlayListRounded fontSize="inherit" />
|
<FeaturedPlayListRounded fontSize="inherit" />
|
||||||
@@ -164,8 +164,8 @@ export const ProfileMore = (props: Props) => {
|
|||||||
transitionDuration={225}
|
transitionDuration={225}
|
||||||
MenuListProps={{ sx: { py: 0.5 } }}
|
MenuListProps={{ sx: { py: 0.5 } }}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null)
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{itemMenu
|
{itemMenu
|
||||||
@@ -179,10 +179,10 @@ export const ProfileMore = (props: Props) => {
|
|||||||
(theme) => {
|
(theme) => {
|
||||||
return {
|
return {
|
||||||
color:
|
color:
|
||||||
item.label === "Delete"
|
item.label === 'Delete'
|
||||||
? theme.palette.error.main
|
? theme.palette.error.main
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
dense
|
dense
|
||||||
@@ -196,8 +196,8 @@ export const ProfileMore = (props: Props) => {
|
|||||||
open={true}
|
open={true}
|
||||||
title={t(globalTitles[id])}
|
title={t(globalTitles[id])}
|
||||||
value={document.value}
|
value={document.value}
|
||||||
language={id === "Merge" ? "yaml" : "javascript"}
|
language={id === 'Merge' ? 'yaml' : 'javascript'}
|
||||||
path={`profile-more:${id}.${id === "Merge" ? "yaml" : "js"}`}
|
path={`profile-more:${id}.${id === 'Merge' ? 'yaml' : 'js'}`}
|
||||||
loading={document.loading}
|
loading={document.loading}
|
||||||
dirty={document.dirty}
|
dirty={document.dirty}
|
||||||
onChange={document.setValue}
|
onChange={document.setValue}
|
||||||
@@ -213,5 +213,5 @@ export const ProfileMore = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -7,146 +7,146 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
styled,
|
styled,
|
||||||
TextField,
|
TextField,
|
||||||
} from "@mui/material";
|
} from '@mui/material'
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from 'ahooks'
|
||||||
import type { Ref } from "react";
|
import type { Ref } from 'react'
|
||||||
import { useEffect, useImperativeHandle, useRef, useState } from "react";
|
import { useEffect, useImperativeHandle, useRef, useState } from 'react'
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from 'react-hook-form'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { BaseDialog, Switch } from "@/components/base";
|
import { BaseDialog, Switch } from '@/components/base'
|
||||||
import { useProfiles } from "@/hooks/use-profiles";
|
import { useProfiles } from '@/hooks/use-profiles'
|
||||||
import { createProfile, patchProfile } from "@/services/cmds";
|
import { createProfile, patchProfile } from '@/services/cmds'
|
||||||
import { showNotice } from "@/services/notice-service";
|
import { showNotice } from '@/services/notice-service'
|
||||||
import { version } from "@root/package.json";
|
import { version } from '@root/package.json'
|
||||||
|
|
||||||
import { FileInput } from "./file-input";
|
import { FileInput } from './file-input'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onChange: (isActivating?: boolean) => void;
|
onChange: (isActivating?: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProfileViewerRef {
|
export interface ProfileViewerRef {
|
||||||
create: () => void;
|
create: () => void
|
||||||
edit: (item: IProfileItem) => void;
|
edit: (item: IProfileItem) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
// create or edit the profile
|
// create or edit the profile
|
||||||
// remote / local
|
// remote / local
|
||||||
type ProfileViewerProps = Props & { ref?: Ref<ProfileViewerRef> };
|
type ProfileViewerProps = Props & { ref?: Ref<ProfileViewerRef> }
|
||||||
|
|
||||||
export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false)
|
||||||
const [openType, setOpenType] = useState<"new" | "edit">("new");
|
const [openType, setOpenType] = useState<'new' | 'edit'>('new')
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false)
|
||||||
const { profiles } = useProfiles();
|
const { profiles } = useProfiles()
|
||||||
|
|
||||||
// file input
|
// file input
|
||||||
const fileDataRef = useRef<string | null>(null);
|
const fileDataRef = useRef<string | null>(null)
|
||||||
|
|
||||||
const { control, watch, setValue, reset, handleSubmit, getValues } =
|
const { control, watch, setValue, reset, handleSubmit, getValues } =
|
||||||
useForm<IProfileItem>({
|
useForm<IProfileItem>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
type: "remote",
|
type: 'remote',
|
||||||
name: "",
|
name: '',
|
||||||
desc: "",
|
desc: '',
|
||||||
url: "",
|
url: '',
|
||||||
option: {
|
option: {
|
||||||
with_proxy: false,
|
with_proxy: false,
|
||||||
self_proxy: false,
|
self_proxy: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
create: () => {
|
create: () => {
|
||||||
setOpenType("new");
|
setOpenType('new')
|
||||||
setOpen(true);
|
setOpen(true)
|
||||||
},
|
},
|
||||||
edit: (item: IProfileItem) => {
|
edit: (item: IProfileItem) => {
|
||||||
if (item) {
|
if (item) {
|
||||||
Object.entries(item).forEach(([key, value]) => {
|
Object.entries(item).forEach(([key, value]) => {
|
||||||
setValue(key as any, value);
|
setValue(key as any, value)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
setOpenType("edit");
|
setOpenType('edit')
|
||||||
setOpen(true);
|
setOpen(true)
|
||||||
},
|
},
|
||||||
}));
|
}))
|
||||||
|
|
||||||
const selfProxy = watch("option.self_proxy");
|
const selfProxy = watch('option.self_proxy')
|
||||||
const withProxy = watch("option.with_proxy");
|
const withProxy = watch('option.with_proxy')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selfProxy) setValue("option.with_proxy", false);
|
if (selfProxy) setValue('option.with_proxy', false)
|
||||||
}, [selfProxy, setValue]);
|
}, [selfProxy, setValue])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (withProxy) setValue("option.self_proxy", false);
|
if (withProxy) setValue('option.self_proxy', false)
|
||||||
}, [setValue, withProxy]);
|
}, [setValue, withProxy])
|
||||||
|
|
||||||
const handleOk = useLockFn(
|
const handleOk = useLockFn(
|
||||||
handleSubmit(async (form) => {
|
handleSubmit(async (form) => {
|
||||||
setLoading(true);
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
// 基本验证
|
// 基本验证
|
||||||
if (!form.type) throw new Error("`Type` should not be null");
|
if (!form.type) throw new Error('`Type` should not be null')
|
||||||
if (form.type === "remote" && !form.url) {
|
if (form.type === 'remote' && !form.url) {
|
||||||
throw new Error("The URL should not be null");
|
throw new Error('The URL should not be null')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理表单数据
|
// 处理表单数据
|
||||||
const option = form.option ? { ...form.option } : undefined;
|
const option = form.option ? { ...form.option } : undefined
|
||||||
if (option?.timeout_seconds) {
|
if (option?.timeout_seconds) {
|
||||||
option.timeout_seconds = +option.timeout_seconds;
|
option.timeout_seconds = +option.timeout_seconds
|
||||||
}
|
}
|
||||||
if (option?.update_interval) {
|
if (option?.update_interval) {
|
||||||
option.update_interval = +option.update_interval;
|
option.update_interval = +option.update_interval
|
||||||
} else if (option) {
|
} else if (option) {
|
||||||
option.update_interval = undefined;
|
option.update_interval = undefined
|
||||||
}
|
}
|
||||||
if (option?.user_agent === "") {
|
if (option?.user_agent === '') {
|
||||||
option.user_agent = undefined;
|
option.user_agent = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = form.name || `${form.type} file`;
|
const name = form.name || `${form.type} file`
|
||||||
const item = { ...form, name, option };
|
const item = { ...form, name, option }
|
||||||
const isRemote = form.type === "remote";
|
const isRemote = form.type === 'remote'
|
||||||
const isUpdate = openType === "edit";
|
const isUpdate = openType === 'edit'
|
||||||
|
|
||||||
// 判断是否是当前激活的配置
|
// 判断是否是当前激活的配置
|
||||||
const isActivating = isUpdate && form.uid === (profiles?.current ?? "");
|
const isActivating = isUpdate && form.uid === (profiles?.current ?? '')
|
||||||
|
|
||||||
// 保存原始代理设置以便回退成功后恢复
|
// 保存原始代理设置以便回退成功后恢复
|
||||||
const originalOptions = {
|
const originalOptions = {
|
||||||
with_proxy: form.option?.with_proxy,
|
with_proxy: form.option?.with_proxy,
|
||||||
self_proxy: form.option?.self_proxy,
|
self_proxy: form.option?.self_proxy,
|
||||||
};
|
}
|
||||||
|
|
||||||
// 执行创建或更新操作,本地配置不需要回退机制
|
// 执行创建或更新操作,本地配置不需要回退机制
|
||||||
if (!isRemote) {
|
if (!isRemote) {
|
||||||
if (openType === "new") {
|
if (openType === 'new') {
|
||||||
await createProfile(item, fileDataRef.current);
|
await createProfile(item, fileDataRef.current)
|
||||||
} else {
|
} else {
|
||||||
if (!form.uid) throw new Error("UID not found");
|
if (!form.uid) throw new Error('UID not found')
|
||||||
await patchProfile(form.uid, item);
|
await patchProfile(form.uid, item)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 远程配置使用回退机制
|
// 远程配置使用回退机制
|
||||||
try {
|
try {
|
||||||
// 尝试正常操作
|
// 尝试正常操作
|
||||||
if (openType === "new") {
|
if (openType === 'new') {
|
||||||
await createProfile(item, fileDataRef.current);
|
await createProfile(item, fileDataRef.current)
|
||||||
} else {
|
} else {
|
||||||
if (!form.uid) throw new Error("UID not found");
|
if (!form.uid) throw new Error('UID not found')
|
||||||
await patchProfile(form.uid, item);
|
await patchProfile(form.uid, item)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// 首次创建/更新失败,尝试使用自身代理
|
// 首次创建/更新失败,尝试使用自身代理
|
||||||
showNotice.info(
|
showNotice.info(
|
||||||
"profiles.modals.profileForm.feedback.notifications.creationRetry",
|
'profiles.modals.profileForm.feedback.notifications.creationRetry',
|
||||||
);
|
)
|
||||||
|
|
||||||
// 使用自身代理的配置
|
// 使用自身代理的配置
|
||||||
const retryItem = {
|
const retryItem = {
|
||||||
@@ -156,76 +156,76 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
with_proxy: false,
|
with_proxy: false,
|
||||||
self_proxy: true,
|
self_proxy: true,
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
// 使用自身代理再次尝试
|
// 使用自身代理再次尝试
|
||||||
if (openType === "new") {
|
if (openType === 'new') {
|
||||||
await createProfile(retryItem, fileDataRef.current);
|
await createProfile(retryItem, fileDataRef.current)
|
||||||
} else {
|
} else {
|
||||||
if (!form.uid) throw new Error("UID not found");
|
if (!form.uid) throw new Error('UID not found')
|
||||||
await patchProfile(form.uid, retryItem);
|
await patchProfile(form.uid, retryItem)
|
||||||
|
|
||||||
// 编辑模式下恢复原始代理设置
|
// 编辑模式下恢复原始代理设置
|
||||||
await patchProfile(form.uid, { option: originalOptions });
|
await patchProfile(form.uid, { option: originalOptions })
|
||||||
}
|
}
|
||||||
|
|
||||||
showNotice.success(
|
showNotice.success(
|
||||||
"profiles.modals.profileForm.feedback.notifications.creationSuccess",
|
'profiles.modals.profileForm.feedback.notifications.creationSuccess',
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 成功后的操作
|
// 成功后的操作
|
||||||
setOpen(false);
|
setOpen(false)
|
||||||
setTimeout(() => reset(), 500);
|
setTimeout(() => reset(), 500)
|
||||||
fileDataRef.current = null;
|
fileDataRef.current = null
|
||||||
|
|
||||||
// 优化:UI先关闭,异步通知父组件
|
// 优化:UI先关闭,异步通知父组件
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
onChange(isActivating);
|
onChange(isActivating)
|
||||||
}, 0);
|
}, 0)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showNotice.error(err);
|
showNotice.error(err)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
)
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
try {
|
try {
|
||||||
setOpen(false);
|
setOpen(false)
|
||||||
fileDataRef.current = null;
|
fileDataRef.current = null
|
||||||
setTimeout(() => reset(), 500);
|
setTimeout(() => reset(), 500)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("[ProfileViewer] handleClose error:", e);
|
console.warn('[ProfileViewer] handleClose error:', e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const text = {
|
const text = {
|
||||||
fullWidth: true,
|
fullWidth: true,
|
||||||
size: "small",
|
size: 'small',
|
||||||
margin: "normal",
|
margin: 'normal',
|
||||||
variant: "outlined",
|
variant: 'outlined',
|
||||||
autoComplete: "off",
|
autoComplete: 'off',
|
||||||
autoCorrect: "off",
|
autoCorrect: 'off',
|
||||||
} as const;
|
} as const
|
||||||
|
|
||||||
const formType = watch("type");
|
const formType = watch('type')
|
||||||
const isRemote = formType === "remote";
|
const isRemote = formType === 'remote'
|
||||||
const isLocal = formType === "local";
|
const isLocal = formType === 'local'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
open={open}
|
open={open}
|
||||||
title={
|
title={
|
||||||
openType === "new"
|
openType === 'new'
|
||||||
? t("profiles.modals.profileForm.title.create")
|
? t('profiles.modals.profileForm.title.create')
|
||||||
: t("profiles.modals.profileForm.title.edit")
|
: t('profiles.modals.profileForm.title.edit')
|
||||||
}
|
}
|
||||||
contentSx={{ width: 375, pb: 0, maxHeight: "80%" }}
|
contentSx={{ width: 375, pb: 0, maxHeight: '80%' }}
|
||||||
okBtn={t("shared.actions.save")}
|
okBtn={t('shared.actions.save')}
|
||||||
cancelBtn={t("shared.actions.cancel")}
|
cancelBtn={t('shared.actions.cancel')}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
onCancel={handleClose}
|
onCancel={handleClose}
|
||||||
onOk={handleOk}
|
onOk={handleOk}
|
||||||
@@ -237,12 +237,12 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormControl size="small" fullWidth sx={{ mt: 1, mb: 1 }}>
|
<FormControl size="small" fullWidth sx={{ mt: 1, mb: 1 }}>
|
||||||
<InputLabel>
|
<InputLabel>
|
||||||
{t("profiles.modals.profileForm.fields.type")}
|
{t('profiles.modals.profileForm.fields.type')}
|
||||||
</InputLabel>
|
</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
{...field}
|
{...field}
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t("profiles.modals.profileForm.fields.type")}
|
label={t('profiles.modals.profileForm.fields.type')}
|
||||||
>
|
>
|
||||||
<MenuItem value="remote">Remote</MenuItem>
|
<MenuItem value="remote">Remote</MenuItem>
|
||||||
<MenuItem value="local">Local</MenuItem>
|
<MenuItem value="local">Local</MenuItem>
|
||||||
@@ -255,7 +255,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
name="name"
|
name="name"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<TextField {...text} {...field} label={t("shared.labels.name")} />
|
<TextField {...text} {...field} label={t('shared.labels.name')} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -266,7 +266,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
<TextField
|
<TextField
|
||||||
{...text}
|
{...text}
|
||||||
{...field}
|
{...field}
|
||||||
label={t("profiles.modals.profileForm.fields.description")}
|
label={t('profiles.modals.profileForm.fields.description')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -281,7 +281,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
{...text}
|
{...text}
|
||||||
{...field}
|
{...field}
|
||||||
multiline
|
multiline
|
||||||
label={t("profiles.modals.profileForm.fields.subscriptionUrl")}
|
label={t('profiles.modals.profileForm.fields.subscriptionUrl')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -308,12 +308,12 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
{...field}
|
{...field}
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="60"
|
placeholder="60"
|
||||||
label={t("profiles.modals.profileForm.fields.httpTimeout")}
|
label={t('profiles.modals.profileForm.fields.httpTimeout')}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
input: {
|
input: {
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
{t("shared.units.seconds")}
|
{t('shared.units.seconds')}
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -333,12 +333,12 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
{...text}
|
{...text}
|
||||||
{...field}
|
{...field}
|
||||||
type="number"
|
type="number"
|
||||||
label={t("profiles.modals.profileForm.fields.updateInterval")}
|
label={t('profiles.modals.profileForm.fields.updateInterval')}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
input: {
|
input: {
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
{t("shared.units.minutes")}
|
{t('shared.units.minutes')}
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -348,11 +348,11 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isLocal && openType === "new" && (
|
{isLocal && openType === 'new' && (
|
||||||
<FileInput
|
<FileInput
|
||||||
onChange={(file, val) => {
|
onChange={(file, val) => {
|
||||||
setValue("name", getValues("name") || file.name);
|
setValue('name', getValues('name') || file.name)
|
||||||
fileDataRef.current = val;
|
fileDataRef.current = val
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -365,7 +365,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<StyledBox>
|
<StyledBox>
|
||||||
<InputLabel>
|
<InputLabel>
|
||||||
{t("profiles.modals.profileForm.fields.useSystemProxy")}
|
{t('profiles.modals.profileForm.fields.useSystemProxy')}
|
||||||
</InputLabel>
|
</InputLabel>
|
||||||
<Switch checked={field.value} {...field} color="primary" />
|
<Switch checked={field.value} {...field} color="primary" />
|
||||||
</StyledBox>
|
</StyledBox>
|
||||||
@@ -378,7 +378,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<StyledBox>
|
<StyledBox>
|
||||||
<InputLabel>
|
<InputLabel>
|
||||||
{t("profiles.modals.profileForm.fields.useClashProxy")}
|
{t('profiles.modals.profileForm.fields.useClashProxy')}
|
||||||
</InputLabel>
|
</InputLabel>
|
||||||
<Switch checked={field.value} {...field} color="primary" />
|
<Switch checked={field.value} {...field} color="primary" />
|
||||||
</StyledBox>
|
</StyledBox>
|
||||||
@@ -391,7 +391,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<StyledBox>
|
<StyledBox>
|
||||||
<InputLabel>
|
<InputLabel>
|
||||||
{t("profiles.modals.profileForm.fields.acceptInvalidCerts")}
|
{t('profiles.modals.profileForm.fields.acceptInvalidCerts')}
|
||||||
</InputLabel>
|
</InputLabel>
|
||||||
<Switch checked={field.value} {...field} color="primary" />
|
<Switch checked={field.value} {...field} color="primary" />
|
||||||
</StyledBox>
|
</StyledBox>
|
||||||
@@ -404,7 +404,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<StyledBox>
|
<StyledBox>
|
||||||
<InputLabel>
|
<InputLabel>
|
||||||
{t("profiles.modals.profileForm.fields.allowAutoUpdate")}
|
{t('profiles.modals.profileForm.fields.allowAutoUpdate')}
|
||||||
</InputLabel>
|
</InputLabel>
|
||||||
<Switch checked={field.value} {...field} color="primary" />
|
<Switch checked={field.value} {...field} color="primary" />
|
||||||
</StyledBox>
|
</StyledBox>
|
||||||
@@ -413,12 +413,12 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledBox = styled(Box)(() => ({
|
const StyledBox = styled(Box)(() => ({
|
||||||
margin: "8px 0 8px 8px",
|
margin: '8px 0 8px 8px',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "space-between",
|
justifyContent: 'space-between',
|
||||||
}));
|
}))
|
||||||
|
|||||||
@@ -6,16 +6,13 @@ import {
|
|||||||
closestCenter,
|
closestCenter,
|
||||||
useSensor,
|
useSensor,
|
||||||
useSensors,
|
useSensors,
|
||||||
} from "@dnd-kit/core";
|
} from '@dnd-kit/core'
|
||||||
import {
|
import { SortableContext, sortableKeyboardCoordinates } from '@dnd-kit/sortable'
|
||||||
SortableContext,
|
import MonacoEditor from '@monaco-editor/react'
|
||||||
sortableKeyboardCoordinates,
|
|
||||||
} from "@dnd-kit/sortable";
|
|
||||||
import MonacoEditor from "@monaco-editor/react";
|
|
||||||
import {
|
import {
|
||||||
VerticalAlignBottomRounded,
|
VerticalAlignBottomRounded,
|
||||||
VerticalAlignTopRounded,
|
VerticalAlignTopRounded,
|
||||||
} from "@mui/icons-material";
|
} from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -27,62 +24,62 @@ import {
|
|||||||
ListItem,
|
ListItem,
|
||||||
TextField,
|
TextField,
|
||||||
styled,
|
styled,
|
||||||
} from "@mui/material";
|
} from '@mui/material'
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from 'ahooks'
|
||||||
import yaml from "js-yaml";
|
import yaml from 'js-yaml'
|
||||||
import {
|
import {
|
||||||
startTransition,
|
startTransition,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from 'react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Virtuoso } from "react-virtuoso";
|
import { Virtuoso } from 'react-virtuoso'
|
||||||
|
|
||||||
import { BaseSearchBox } from "@/components/base";
|
import { BaseSearchBox } from '@/components/base'
|
||||||
import { ProxyItem } from "@/components/profile/proxy-item";
|
import { ProxyItem } from '@/components/profile/proxy-item'
|
||||||
import { readProfileFile, saveProfileFile } from "@/services/cmds";
|
import { readProfileFile, saveProfileFile } from '@/services/cmds'
|
||||||
import { showNotice } from "@/services/notice-service";
|
import { showNotice } from '@/services/notice-service'
|
||||||
import { useThemeMode } from "@/services/states";
|
import { useThemeMode } from '@/services/states'
|
||||||
import getSystem from "@/utils/get-system";
|
import getSystem from '@/utils/get-system'
|
||||||
import parseUri from "@/utils/uri-parser";
|
import parseUri from '@/utils/uri-parser'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
profileUid: string;
|
profileUid: string
|
||||||
property: string;
|
property: string
|
||||||
open: boolean;
|
open: boolean
|
||||||
onClose: () => void;
|
onClose: () => void
|
||||||
onSave?: (prev?: string, curr?: string) => void;
|
onSave?: (prev?: string, curr?: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProxiesEditorViewer = (props: Props) => {
|
export const ProxiesEditorViewer = (props: Props) => {
|
||||||
const { profileUid, property, open, onClose, onSave } = props;
|
const { profileUid, property, open, onClose, onSave } = props
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const themeMode = useThemeMode();
|
const themeMode = useThemeMode()
|
||||||
const [prevData, setPrevData] = useState("");
|
const [prevData, setPrevData] = useState('')
|
||||||
const [currData, setCurrData] = useState("");
|
const [currData, setCurrData] = useState('')
|
||||||
const [visualization, setVisualization] = useState(true);
|
const [visualization, setVisualization] = useState(true)
|
||||||
const [match, setMatch] = useState(() => (_: string) => true);
|
const [match, setMatch] = useState(() => (_: string) => true)
|
||||||
const [proxyUri, setProxyUri] = useState<string>("");
|
const [proxyUri, setProxyUri] = useState<string>('')
|
||||||
|
|
||||||
const [proxyList, setProxyList] = useState<IProxyConfig[]>([]);
|
const [proxyList, setProxyList] = useState<IProxyConfig[]>([])
|
||||||
const [prependSeq, setPrependSeq] = useState<IProxyConfig[]>([]);
|
const [prependSeq, setPrependSeq] = useState<IProxyConfig[]>([])
|
||||||
const [appendSeq, setAppendSeq] = useState<IProxyConfig[]>([]);
|
const [appendSeq, setAppendSeq] = useState<IProxyConfig[]>([])
|
||||||
const [deleteSeq, setDeleteSeq] = useState<string[]>([]);
|
const [deleteSeq, setDeleteSeq] = useState<string[]>([])
|
||||||
|
|
||||||
const filteredPrependSeq = useMemo(
|
const filteredPrependSeq = useMemo(
|
||||||
() => prependSeq.filter((proxy) => match(proxy.name)),
|
() => prependSeq.filter((proxy) => match(proxy.name)),
|
||||||
[prependSeq, match],
|
[prependSeq, match],
|
||||||
);
|
)
|
||||||
const filteredProxyList = useMemo(
|
const filteredProxyList = useMemo(
|
||||||
() => proxyList.filter((proxy) => match(proxy.name)),
|
() => proxyList.filter((proxy) => match(proxy.name)),
|
||||||
[proxyList, match],
|
[proxyList, match],
|
||||||
);
|
)
|
||||||
const filteredAppendSeq = useMemo(
|
const filteredAppendSeq = useMemo(
|
||||||
() => appendSeq.filter((proxy) => match(proxy.name)),
|
() => appendSeq.filter((proxy) => match(proxy.name)),
|
||||||
[appendSeq, match],
|
[appendSeq, match],
|
||||||
);
|
)
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(PointerSensor, {
|
||||||
@@ -91,138 +88,138 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
useSensor(KeyboardSensor, {
|
useSensor(KeyboardSensor, {
|
||||||
coordinateGetter: sortableKeyboardCoordinates,
|
coordinateGetter: sortableKeyboardCoordinates,
|
||||||
}),
|
}),
|
||||||
);
|
)
|
||||||
const reorder = (
|
const reorder = (
|
||||||
list: IProxyConfig[],
|
list: IProxyConfig[],
|
||||||
startIndex: number,
|
startIndex: number,
|
||||||
endIndex: number,
|
endIndex: number,
|
||||||
) => {
|
) => {
|
||||||
const result = Array.from(list);
|
const result = Array.from(list)
|
||||||
const [removed] = result.splice(startIndex, 1);
|
const [removed] = result.splice(startIndex, 1)
|
||||||
result.splice(endIndex, 0, removed);
|
result.splice(endIndex, 0, removed)
|
||||||
return result;
|
return result
|
||||||
};
|
}
|
||||||
const onPrependDragEnd = async (event: DragEndEvent) => {
|
const onPrependDragEnd = async (event: DragEndEvent) => {
|
||||||
const { active, over } = event;
|
const { active, over } = event
|
||||||
if (over) {
|
if (over) {
|
||||||
if (active.id !== over.id) {
|
if (active.id !== over.id) {
|
||||||
let activeIndex = 0;
|
let activeIndex = 0
|
||||||
let overIndex = 0;
|
let overIndex = 0
|
||||||
prependSeq.forEach((item, index) => {
|
prependSeq.forEach((item, index) => {
|
||||||
if (item.name === active.id) {
|
if (item.name === active.id) {
|
||||||
activeIndex = index;
|
activeIndex = index
|
||||||
}
|
}
|
||||||
if (item.name === over.id) {
|
if (item.name === over.id) {
|
||||||
overIndex = index;
|
overIndex = index
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
setPrependSeq(reorder(prependSeq, activeIndex, overIndex));
|
setPrependSeq(reorder(prependSeq, activeIndex, overIndex))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
const onAppendDragEnd = async (event: DragEndEvent) => {
|
const onAppendDragEnd = async (event: DragEndEvent) => {
|
||||||
const { active, over } = event;
|
const { active, over } = event
|
||||||
if (over) {
|
if (over) {
|
||||||
if (active.id !== over.id) {
|
if (active.id !== over.id) {
|
||||||
let activeIndex = 0;
|
let activeIndex = 0
|
||||||
let overIndex = 0;
|
let overIndex = 0
|
||||||
appendSeq.forEach((item, index) => {
|
appendSeq.forEach((item, index) => {
|
||||||
if (item.name === active.id) {
|
if (item.name === active.id) {
|
||||||
activeIndex = index;
|
activeIndex = index
|
||||||
}
|
}
|
||||||
if (item.name === over.id) {
|
if (item.name === over.id) {
|
||||||
overIndex = index;
|
overIndex = index
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setAppendSeq(reorder(appendSeq, activeIndex, overIndex))
|
||||||
}
|
}
|
||||||
});
|
|
||||||
setAppendSeq(reorder(appendSeq, activeIndex, overIndex));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
// 优化:异步分片解析,避免主线程阻塞,解析完成后批量setState
|
// 优化:异步分片解析,避免主线程阻塞,解析完成后批量setState
|
||||||
const handleParseAsync = (cb: (proxies: IProxyConfig[]) => void) => {
|
const handleParseAsync = (cb: (proxies: IProxyConfig[]) => void) => {
|
||||||
const proxies: IProxyConfig[] = [];
|
const proxies: IProxyConfig[] = []
|
||||||
const names: string[] = [];
|
const names: string[] = []
|
||||||
let uris: string;
|
let uris: string
|
||||||
try {
|
try {
|
||||||
uris = atob(proxyUri);
|
uris = atob(proxyUri)
|
||||||
} catch {
|
} catch {
|
||||||
uris = proxyUri;
|
uris = proxyUri
|
||||||
}
|
}
|
||||||
const lines = uris.trim().split("\n");
|
const lines = uris.trim().split('\n')
|
||||||
let idx = 0;
|
let idx = 0
|
||||||
const batchSize = 50;
|
const batchSize = 50
|
||||||
let parseTimer: number | undefined;
|
let parseTimer: number | undefined
|
||||||
|
|
||||||
const parseBatch = () => {
|
const parseBatch = () => {
|
||||||
const end = Math.min(idx + batchSize, lines.length);
|
const end = Math.min(idx + batchSize, lines.length)
|
||||||
for (; idx < end; idx++) {
|
for (; idx < end; idx++) {
|
||||||
const uri = lines[idx];
|
const uri = lines[idx]
|
||||||
try {
|
try {
|
||||||
const proxy = parseUri(uri.trim());
|
const proxy = parseUri(uri.trim())
|
||||||
if (!names.includes(proxy.name)) {
|
if (!names.includes(proxy.name)) {
|
||||||
proxies.push(proxy);
|
proxies.push(proxy)
|
||||||
names.push(proxy.name);
|
names.push(proxy.name)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"[ProxiesEditorViewer] parseUri failed for line:",
|
'[ProxiesEditorViewer] parseUri failed for line:',
|
||||||
uri,
|
uri,
|
||||||
err,
|
err,
|
||||||
);
|
)
|
||||||
// 不阻塞主流程
|
// 不阻塞主流程
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (idx < lines.length) {
|
if (idx < lines.length) {
|
||||||
parseTimer = window.setTimeout(parseBatch, 0);
|
parseTimer = window.setTimeout(parseBatch, 0)
|
||||||
} else {
|
} else {
|
||||||
if (parseTimer !== undefined) {
|
if (parseTimer !== undefined) {
|
||||||
clearTimeout(parseTimer);
|
clearTimeout(parseTimer)
|
||||||
parseTimer = undefined;
|
parseTimer = undefined
|
||||||
}
|
}
|
||||||
cb(proxies);
|
cb(proxies)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parseBatch()
|
||||||
}
|
}
|
||||||
};
|
|
||||||
parseBatch();
|
|
||||||
};
|
|
||||||
const fetchProfile = useCallback(async () => {
|
const fetchProfile = useCallback(async () => {
|
||||||
const data = await readProfileFile(profileUid);
|
const data = await readProfileFile(profileUid)
|
||||||
|
|
||||||
const originProxiesObj = yaml.load(data) as {
|
const originProxiesObj = yaml.load(data) as {
|
||||||
proxies: IProxyConfig[];
|
proxies: IProxyConfig[]
|
||||||
} | null;
|
} | null
|
||||||
|
|
||||||
setProxyList(originProxiesObj?.proxies || []);
|
setProxyList(originProxiesObj?.proxies || [])
|
||||||
}, [profileUid]);
|
}, [profileUid])
|
||||||
|
|
||||||
const fetchContent = useCallback(async () => {
|
const fetchContent = useCallback(async () => {
|
||||||
const data = await readProfileFile(property);
|
const data = await readProfileFile(property)
|
||||||
const obj = yaml.load(data) as ISeqProfileConfig | null;
|
const obj = yaml.load(data) as ISeqProfileConfig | null
|
||||||
|
|
||||||
setPrependSeq(obj?.prepend || []);
|
setPrependSeq(obj?.prepend || [])
|
||||||
setAppendSeq(obj?.append || []);
|
setAppendSeq(obj?.append || [])
|
||||||
setDeleteSeq(obj?.delete || []);
|
setDeleteSeq(obj?.delete || [])
|
||||||
|
|
||||||
setPrevData(data);
|
setPrevData(data)
|
||||||
setCurrData(data);
|
setCurrData(data)
|
||||||
}, [property]);
|
}, [property])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currData === "" || visualization !== true) {
|
if (currData === '' || visualization !== true) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const obj = yaml.load(currData) as ISeqProfileConfig | null;
|
const obj = yaml.load(currData) as ISeqProfileConfig | null
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
setPrependSeq(obj?.prepend ?? []);
|
setPrependSeq(obj?.prepend ?? [])
|
||||||
setAppendSeq(obj?.append ?? []);
|
setAppendSeq(obj?.append ?? [])
|
||||||
setDeleteSeq(obj?.delete ?? []);
|
setDeleteSeq(obj?.delete ?? [])
|
||||||
});
|
})
|
||||||
}, [currData, visualization]);
|
}, [currData, visualization])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!(prependSeq && appendSeq && deleteSeq)) {
|
if (!(prependSeq && appendSeq && deleteSeq)) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const serialize = () => {
|
const serialize = () => {
|
||||||
@@ -232,45 +229,45 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
|
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
|
||||||
{ forceQuotes: true },
|
{ forceQuotes: true },
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("[ProxiesEditorViewer] yaml.dump failed:", e);
|
console.warn('[ProxiesEditorViewer] yaml.dump failed:', e)
|
||||||
// 防止异常导致UI卡死
|
// 防止异常导致UI卡死
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
let idleId: number | undefined;
|
let idleId: number | undefined
|
||||||
let timeoutId: number | undefined;
|
let timeoutId: number | undefined
|
||||||
if (window.requestIdleCallback) {
|
if (window.requestIdleCallback) {
|
||||||
idleId = window.requestIdleCallback(serialize);
|
idleId = window.requestIdleCallback(serialize)
|
||||||
} else {
|
} else {
|
||||||
timeoutId = window.setTimeout(serialize, 0);
|
timeoutId = window.setTimeout(serialize, 0)
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
if (idleId !== undefined && window.cancelIdleCallback) {
|
if (idleId !== undefined && window.cancelIdleCallback) {
|
||||||
window.cancelIdleCallback(idleId);
|
window.cancelIdleCallback(idleId)
|
||||||
}
|
}
|
||||||
if (timeoutId !== undefined) {
|
if (timeoutId !== undefined) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}, [prependSeq, appendSeq, deleteSeq]);
|
}, [prependSeq, appendSeq, deleteSeq])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return
|
||||||
fetchContent();
|
fetchContent()
|
||||||
fetchProfile();
|
fetchProfile()
|
||||||
}, [fetchContent, fetchProfile, open]);
|
}, [fetchContent, fetchProfile, open])
|
||||||
|
|
||||||
const handleSave = useLockFn(async () => {
|
const handleSave = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
await saveProfileFile(property, currData);
|
await saveProfileFile(property, currData)
|
||||||
showNotice.success("shared.feedback.notifications.saved");
|
showNotice.success('shared.feedback.notifications.saved')
|
||||||
onSave?.(prevData, currData);
|
onSave?.(prevData, currData)
|
||||||
onClose();
|
onClose()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showNotice.error(err);
|
showNotice.error(err)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -283,18 +280,18 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{
|
{
|
||||||
<Box display="flex" justifyContent="space-between">
|
<Box display="flex" justifyContent="space-between">
|
||||||
{t("profiles.modals.proxiesEditor.title")}
|
{t('profiles.modals.proxiesEditor.title')}
|
||||||
<Box>
|
<Box>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setVisualization((prev) => !prev);
|
setVisualization((prev) => !prev)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{visualization
|
{visualization
|
||||||
? t("shared.editorModes.advanced")
|
? t('shared.editorModes.advanced')
|
||||||
: t("shared.editorModes.visualization")}
|
: t('shared.editorModes.visualization')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -302,27 +299,27 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
|
||||||
<DialogContent
|
<DialogContent
|
||||||
sx={{ display: "flex", width: "auto", height: "calc(100vh - 185px)" }}
|
sx={{ display: 'flex', width: 'auto', height: 'calc(100vh - 185px)' }}
|
||||||
>
|
>
|
||||||
{visualization ? (
|
{visualization ? (
|
||||||
<>
|
<>
|
||||||
<List
|
<List
|
||||||
sx={{
|
sx={{
|
||||||
width: "50%",
|
width: '50%',
|
||||||
padding: "0 10px",
|
padding: '0 10px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
height: "calc(100% - 80px)",
|
height: 'calc(100% - 80px)',
|
||||||
overflowY: "auto",
|
overflowY: 'auto',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Item>
|
<Item>
|
||||||
<TextField
|
<TextField
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
placeholder={t(
|
placeholder={t(
|
||||||
"profiles.modals.proxiesEditor.placeholders.multiUri",
|
'profiles.modals.proxiesEditor.placeholders.multiUri',
|
||||||
)}
|
)}
|
||||||
fullWidth
|
fullWidth
|
||||||
rows={9}
|
rows={9}
|
||||||
@@ -339,11 +336,11 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
startIcon={<VerticalAlignTopRounded />}
|
startIcon={<VerticalAlignTopRounded />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleParseAsync((proxies) => {
|
handleParseAsync((proxies) => {
|
||||||
setPrependSeq((prev) => [...proxies, ...prev]);
|
setPrependSeq((prev) => [...proxies, ...prev])
|
||||||
});
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("profiles.modals.proxiesEditor.actions.prepend")}
|
{t('profiles.modals.proxiesEditor.actions.prepend')}
|
||||||
</Button>
|
</Button>
|
||||||
</Item>
|
</Item>
|
||||||
<Item>
|
<Item>
|
||||||
@@ -353,24 +350,24 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
startIcon={<VerticalAlignBottomRounded />}
|
startIcon={<VerticalAlignBottomRounded />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleParseAsync((proxies) => {
|
handleParseAsync((proxies) => {
|
||||||
setAppendSeq((prev) => [...prev, ...proxies]);
|
setAppendSeq((prev) => [...prev, ...proxies])
|
||||||
});
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("profiles.modals.proxiesEditor.actions.append")}
|
{t('profiles.modals.proxiesEditor.actions.append')}
|
||||||
</Button>
|
</Button>
|
||||||
</Item>
|
</Item>
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
<List
|
<List
|
||||||
sx={{
|
sx={{
|
||||||
width: "50%",
|
width: '50%',
|
||||||
padding: "0 10px",
|
padding: '0 10px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
style={{ height: "calc(100% - 24px)", marginTop: "8px" }}
|
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||||
totalCount={
|
totalCount={
|
||||||
filteredProxyList.length +
|
filteredProxyList.length +
|
||||||
(filteredPrependSeq.length > 0 ? 1 : 0) +
|
(filteredPrependSeq.length > 0 ? 1 : 0) +
|
||||||
@@ -378,7 +375,7 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
}
|
}
|
||||||
increaseViewportBy={256}
|
increaseViewportBy={256}
|
||||||
itemContent={(index) => {
|
itemContent={(index) => {
|
||||||
const shift = filteredPrependSeq.length > 0 ? 1 : 0;
|
const shift = filteredPrependSeq.length > 0 ? 1 : 0
|
||||||
if (filteredPrependSeq.length > 0 && index === 0) {
|
if (filteredPrependSeq.length > 0 && index === 0) {
|
||||||
return (
|
return (
|
||||||
<DndContext
|
<DndContext
|
||||||
@@ -388,7 +385,7 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
>
|
>
|
||||||
<SortableContext
|
<SortableContext
|
||||||
items={filteredPrependSeq.map((x) => {
|
items={filteredPrependSeq.map((x) => {
|
||||||
return x.name;
|
return x.name
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{filteredPrependSeq.map((item) => {
|
{filteredPrependSeq.map((item) => {
|
||||||
@@ -402,23 +399,23 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
prependSeq.filter(
|
prependSeq.filter(
|
||||||
(v) => v.name !== item.name,
|
(v) => v.name !== item.name,
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
);
|
)
|
||||||
} else if (index < filteredProxyList.length + shift) {
|
} else if (index < filteredProxyList.length + shift) {
|
||||||
const newIndex = index - shift;
|
const newIndex = index - shift
|
||||||
return (
|
return (
|
||||||
<ProxyItem
|
<ProxyItem
|
||||||
key={filteredProxyList[newIndex].name}
|
key={filteredProxyList[newIndex].name}
|
||||||
type={
|
type={
|
||||||
deleteSeq.includes(filteredProxyList[newIndex].name)
|
deleteSeq.includes(filteredProxyList[newIndex].name)
|
||||||
? "delete"
|
? 'delete'
|
||||||
: "original"
|
: 'original'
|
||||||
}
|
}
|
||||||
proxy={filteredProxyList[newIndex]}
|
proxy={filteredProxyList[newIndex]}
|
||||||
onDelete={() => {
|
onDelete={() => {
|
||||||
@@ -429,16 +426,16 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
deleteSeq.filter(
|
deleteSeq.filter(
|
||||||
(v) => v !== filteredProxyList[newIndex].name,
|
(v) => v !== filteredProxyList[newIndex].name,
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
} else {
|
} else {
|
||||||
setDeleteSeq((prev) => [
|
setDeleteSeq((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
filteredProxyList[newIndex].name,
|
filteredProxyList[newIndex].name,
|
||||||
]);
|
])
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<DndContext
|
<DndContext
|
||||||
@@ -448,7 +445,7 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
>
|
>
|
||||||
<SortableContext
|
<SortableContext
|
||||||
items={filteredAppendSeq.map((x) => {
|
items={filteredAppendSeq.map((x) => {
|
||||||
return x.name;
|
return x.name
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{filteredAppendSeq.map((item) => {
|
{filteredAppendSeq.map((item) => {
|
||||||
@@ -462,14 +459,14 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
appendSeq.filter(
|
appendSeq.filter(
|
||||||
(v) => v.name !== item.name,
|
(v) => v.name !== item.name,
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -480,7 +477,7 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
height="100%"
|
height="100%"
|
||||||
language="yaml"
|
language="yaml"
|
||||||
value={currData}
|
value={currData}
|
||||||
theme={themeMode === "light" ? "light" : "vs-dark"}
|
theme={themeMode === 'light' ? 'light' : 'vs-dark'}
|
||||||
options={{
|
options={{
|
||||||
tabSize: 2, // 根据语言类型设置缩进大小
|
tabSize: 2, // 根据语言类型设置缩进大小
|
||||||
minimap: {
|
minimap: {
|
||||||
@@ -496,29 +493,29 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
top: 33, // 顶部padding防止遮挡snippets
|
top: 33, // 顶部padding防止遮挡snippets
|
||||||
},
|
},
|
||||||
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
|
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
|
||||||
getSystem() === "windows" ? ", twemoji mozilla" : ""
|
getSystem() === 'windows' ? ', twemoji mozilla' : ''
|
||||||
}`,
|
}`,
|
||||||
fontLigatures: false, // 连字符
|
fontLigatures: false, // 连字符
|
||||||
smoothScrolling: true, // 平滑滚动
|
smoothScrolling: true, // 平滑滚动
|
||||||
}}
|
}}
|
||||||
onChange={(value) => setCurrData(value ?? "")}
|
onChange={(value) => setCurrData(value ?? '')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={onClose} variant="outlined">
|
<Button onClick={onClose} variant="outlined">
|
||||||
{t("shared.actions.cancel")}
|
{t('shared.actions.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button onClick={handleSave} variant="contained">
|
<Button onClick={handleSave} variant="contained">
|
||||||
{t("shared.actions.save")}
|
{t('shared.actions.save')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const Item = styled(ListItem)(() => ({
|
const Item = styled(ListItem)(() => ({
|
||||||
padding: "5px 2px",
|
padding: '5px 2px',
|
||||||
}));
|
}))
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useSortable } from "@dnd-kit/sortable";
|
import { useSortable } from '@dnd-kit/sortable'
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
import { DeleteForeverRounded, UndoRounded } from "@mui/icons-material";
|
import { DeleteForeverRounded, UndoRounded } from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
IconButton,
|
IconButton,
|
||||||
@@ -8,17 +8,17 @@ import {
|
|||||||
ListItemText,
|
ListItemText,
|
||||||
alpha,
|
alpha,
|
||||||
styled,
|
styled,
|
||||||
} from "@mui/material";
|
} from '@mui/material'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
type: "prepend" | "original" | "delete" | "append";
|
type: 'prepend' | 'original' | 'delete' | 'append'
|
||||||
proxy: IProxyConfig;
|
proxy: IProxyConfig
|
||||||
onDelete: () => void;
|
onDelete: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProxyItem = (props: Props) => {
|
export const ProxyItem = (props: Props) => {
|
||||||
const { type, proxy, onDelete } = props;
|
const { type, proxy, onDelete } = props
|
||||||
const sortable = type === "prepend" || type === "append";
|
const sortable = type === 'prepend' || type === 'append'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
attributes: sortableAttributes,
|
attributes: sortableAttributes,
|
||||||
@@ -30,41 +30,41 @@ export const ProxyItem = (props: Props) => {
|
|||||||
} = useSortable({
|
} = useSortable({
|
||||||
id: proxy.name,
|
id: proxy.name,
|
||||||
disabled: !sortable,
|
disabled: !sortable,
|
||||||
});
|
})
|
||||||
const dragAttributes = sortable ? sortableAttributes : undefined;
|
const dragAttributes = sortable ? sortableAttributes : undefined
|
||||||
const dragListeners = sortable ? sortableListeners : undefined;
|
const dragListeners = sortable ? sortableListeners : undefined
|
||||||
const dragNodeRef = sortable ? sortableSetNodeRef : undefined;
|
const dragNodeRef = sortable ? sortableSetNodeRef : undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
dense
|
dense
|
||||||
sx={({ palette }) => ({
|
sx={({ palette }) => ({
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
background:
|
background:
|
||||||
type === "original"
|
type === 'original'
|
||||||
? palette.mode === "dark"
|
? palette.mode === 'dark'
|
||||||
? alpha(palette.background.paper, 0.3)
|
? alpha(palette.background.paper, 0.3)
|
||||||
: alpha(palette.grey[400], 0.3)
|
: alpha(palette.grey[400], 0.3)
|
||||||
: type === "delete"
|
: type === 'delete'
|
||||||
? alpha(palette.error.main, 0.3)
|
? alpha(palette.error.main, 0.3)
|
||||||
: alpha(palette.success.main, 0.3),
|
: alpha(palette.success.main, 0.3),
|
||||||
height: "100%",
|
height: '100%',
|
||||||
margin: "8px 0",
|
margin: '8px 0',
|
||||||
borderRadius: "8px",
|
borderRadius: '8px',
|
||||||
transform: CSS.Transform.toString(transform),
|
transform: CSS.Transform.toString(transform),
|
||||||
transition,
|
transition,
|
||||||
zIndex: isDragging ? "calc(infinity)" : undefined,
|
zIndex: isDragging ? 'calc(infinity)' : undefined,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
{...(dragAttributes ?? {})}
|
{...(dragAttributes ?? {})}
|
||||||
{...(dragListeners ?? {})}
|
{...(dragListeners ?? {})}
|
||||||
ref={dragNodeRef}
|
ref={dragNodeRef}
|
||||||
sx={{ cursor: sortable ? "move" : "" }}
|
sx={{ cursor: sortable ? 'move' : '' }}
|
||||||
primary={
|
primary={
|
||||||
<StyledPrimary
|
<StyledPrimary
|
||||||
title={proxy.name}
|
title={proxy.name}
|
||||||
sx={{ textDecoration: type === "delete" ? "line-through" : "" }}
|
sx={{ textDecoration: type === 'delete' ? 'line-through' : '' }}
|
||||||
>
|
>
|
||||||
{proxy.name}
|
{proxy.name}
|
||||||
</StyledPrimary>
|
</StyledPrimary>
|
||||||
@@ -72,13 +72,13 @@ export const ProxyItem = (props: Props) => {
|
|||||||
secondary={
|
secondary={
|
||||||
<ListItemTextChild
|
<ListItemTextChild
|
||||||
sx={{
|
sx={{
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
pt: "2px",
|
pt: '2px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ marginTop: "2px" }}>
|
<Box sx={{ marginTop: '2px' }}>
|
||||||
<StyledTypeBox>{proxy.type}</StyledTypeBox>
|
<StyledTypeBox>{proxy.type}</StyledTypeBox>
|
||||||
</Box>
|
</Box>
|
||||||
</ListItemTextChild>
|
</ListItemTextChild>
|
||||||
@@ -86,41 +86,41 @@ export const ProxyItem = (props: Props) => {
|
|||||||
slotProps={{
|
slotProps={{
|
||||||
secondary: {
|
secondary: {
|
||||||
sx: {
|
sx: {
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
color: "#ccc",
|
color: '#ccc',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<IconButton onClick={onDelete}>
|
<IconButton onClick={onDelete}>
|
||||||
{type === "delete" ? <UndoRounded /> : <DeleteForeverRounded />}
|
{type === 'delete' ? <UndoRounded /> : <DeleteForeverRounded />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const StyledPrimary = styled("div")`
|
const StyledPrimary = styled('div')`
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`;
|
`
|
||||||
|
|
||||||
const ListItemTextChild = styled("span")`
|
const ListItemTextChild = styled('span')`
|
||||||
display: block;
|
display: block;
|
||||||
`;
|
`
|
||||||
|
|
||||||
const StyledTypeBox = styled(ListItemTextChild)(({ theme }) => ({
|
const StyledTypeBox = styled(ListItemTextChild)(({ theme }) => ({
|
||||||
display: "inline-block",
|
display: 'inline-block',
|
||||||
border: "1px solid #ccc",
|
border: '1px solid #ccc',
|
||||||
borderColor: alpha(theme.palette.primary.main, 0.5),
|
borderColor: alpha(theme.palette.primary.main, 0.5),
|
||||||
color: alpha(theme.palette.primary.main, 0.8),
|
color: alpha(theme.palette.primary.main, 0.8),
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
padding: "0 4px",
|
padding: '0 4px',
|
||||||
lineHeight: 1.5,
|
lineHeight: 1.5,
|
||||||
marginRight: "8px",
|
marginRight: '8px',
|
||||||
}));
|
}))
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useSortable } from "@dnd-kit/sortable";
|
import { useSortable } from '@dnd-kit/sortable'
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
import { DeleteForeverRounded, UndoRounded } from "@mui/icons-material";
|
import { DeleteForeverRounded, UndoRounded } from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
IconButton,
|
IconButton,
|
||||||
@@ -8,23 +8,23 @@ import {
|
|||||||
ListItemText,
|
ListItemText,
|
||||||
alpha,
|
alpha,
|
||||||
styled,
|
styled,
|
||||||
} from "@mui/material";
|
} from '@mui/material'
|
||||||
interface Props {
|
interface Props {
|
||||||
type: "prepend" | "original" | "delete" | "append";
|
type: 'prepend' | 'original' | 'delete' | 'append'
|
||||||
ruleRaw: string;
|
ruleRaw: string
|
||||||
onDelete: () => void;
|
onDelete: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RuleItem = (props: Props) => {
|
export const RuleItem = (props: Props) => {
|
||||||
const { type, ruleRaw, onDelete } = props;
|
const { type, ruleRaw, onDelete } = props
|
||||||
const sortable = type === "prepend" || type === "append";
|
const sortable = type === 'prepend' || type === 'append'
|
||||||
const rule = ruleRaw.replace(",no-resolve", "");
|
const rule = ruleRaw.replace(',no-resolve', '')
|
||||||
|
|
||||||
const ruleType = rule.match(/^[^,]+/)?.[0] ?? "";
|
const ruleType = rule.match(/^[^,]+/)?.[0] ?? ''
|
||||||
const proxyPolicy = rule.match(/[^,]+$/)?.[0] ?? "";
|
const proxyPolicy = rule.match(/[^,]+$/)?.[0] ?? ''
|
||||||
const ruleContent = rule.slice(ruleType.length + 1, -proxyPolicy.length - 1);
|
const ruleContent = rule.slice(ruleType.length + 1, -proxyPolicy.length - 1)
|
||||||
|
|
||||||
const $sortable = useSortable({ id: ruleRaw });
|
const $sortable = useSortable({ id: ruleRaw })
|
||||||
|
|
||||||
const {
|
const {
|
||||||
attributes,
|
attributes,
|
||||||
@@ -42,55 +42,55 @@ export const RuleItem = (props: Props) => {
|
|||||||
transform: null,
|
transform: null,
|
||||||
transition: null,
|
transition: null,
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
};
|
}
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
dense
|
dense
|
||||||
sx={({ palette }) => ({
|
sx={({ palette }) => ({
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
background:
|
background:
|
||||||
type === "original"
|
type === 'original'
|
||||||
? palette.mode === "dark"
|
? palette.mode === 'dark'
|
||||||
? alpha(palette.background.paper, 0.3)
|
? alpha(palette.background.paper, 0.3)
|
||||||
: alpha(palette.grey[400], 0.3)
|
: alpha(palette.grey[400], 0.3)
|
||||||
: type === "delete"
|
: type === 'delete'
|
||||||
? alpha(palette.error.main, 0.3)
|
? alpha(palette.error.main, 0.3)
|
||||||
: alpha(palette.success.main, 0.3),
|
: alpha(palette.success.main, 0.3),
|
||||||
height: "100%",
|
height: '100%',
|
||||||
margin: "8px 0",
|
margin: '8px 0',
|
||||||
borderRadius: "8px",
|
borderRadius: '8px',
|
||||||
transform: CSS.Transform.toString(transform),
|
transform: CSS.Transform.toString(transform),
|
||||||
transition,
|
transition,
|
||||||
zIndex: isDragging ? "calc(infinity)" : undefined,
|
zIndex: isDragging ? 'calc(infinity)' : undefined,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
{...attributes}
|
{...attributes}
|
||||||
{...listeners}
|
{...listeners}
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
sx={{ cursor: sortable ? "move" : "" }}
|
sx={{ cursor: sortable ? 'move' : '' }}
|
||||||
primary={
|
primary={
|
||||||
<StyledPrimary
|
<StyledPrimary
|
||||||
title={ruleContent || "-"}
|
title={ruleContent || '-'}
|
||||||
sx={{ textDecoration: type === "delete" ? "line-through" : "" }}
|
sx={{ textDecoration: type === 'delete' ? 'line-through' : '' }}
|
||||||
>
|
>
|
||||||
{ruleContent || "-"}
|
{ruleContent || '-'}
|
||||||
</StyledPrimary>
|
</StyledPrimary>
|
||||||
}
|
}
|
||||||
secondary={
|
secondary={
|
||||||
<ListItemTextChild
|
<ListItemTextChild
|
||||||
sx={{
|
sx={{
|
||||||
width: "62%",
|
width: '62%',
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
justifyContent: "space-between",
|
justifyContent: 'space-between',
|
||||||
pt: "2px",
|
pt: '2px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ marginTop: "2px" }}>
|
<Box sx={{ marginTop: '2px' }}>
|
||||||
<StyledTypeBox>{ruleType}</StyledTypeBox>
|
<StyledTypeBox>{ruleType}</StyledTypeBox>
|
||||||
</Box>
|
</Box>
|
||||||
<StyledSubtitle sx={{ color: "text.secondary" }}>
|
<StyledSubtitle sx={{ color: 'text.secondary' }}>
|
||||||
{proxyPolicy}
|
{proxyPolicy}
|
||||||
</StyledSubtitle>
|
</StyledSubtitle>
|
||||||
</ListItemTextChild>
|
</ListItemTextChild>
|
||||||
@@ -98,49 +98,49 @@ export const RuleItem = (props: Props) => {
|
|||||||
slotProps={{
|
slotProps={{
|
||||||
secondary: {
|
secondary: {
|
||||||
sx: {
|
sx: {
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
color: "#ccc",
|
color: '#ccc',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<IconButton onClick={onDelete}>
|
<IconButton onClick={onDelete}>
|
||||||
{type === "delete" ? <UndoRounded /> : <DeleteForeverRounded />}
|
{type === 'delete' ? <UndoRounded /> : <DeleteForeverRounded />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const StyledPrimary = styled("div")`
|
const StyledPrimary = styled('div')`
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`;
|
`
|
||||||
|
|
||||||
const StyledSubtitle = styled("span")`
|
const StyledSubtitle = styled('span')`
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: text.secondary;
|
color: text.secondary;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`;
|
`
|
||||||
|
|
||||||
const ListItemTextChild = styled("span")`
|
const ListItemTextChild = styled('span')`
|
||||||
display: block;
|
display: block;
|
||||||
`;
|
`
|
||||||
|
|
||||||
const StyledTypeBox = styled(ListItemTextChild)(({ theme }) => ({
|
const StyledTypeBox = styled(ListItemTextChild)(({ theme }) => ({
|
||||||
display: "inline-block",
|
display: 'inline-block',
|
||||||
border: "1px solid #ccc",
|
border: '1px solid #ccc',
|
||||||
borderColor: alpha(theme.palette.primary.main, 0.5),
|
borderColor: alpha(theme.palette.primary.main, 0.5),
|
||||||
color: alpha(theme.palette.primary.main, 0.8),
|
color: alpha(theme.palette.primary.main, 0.8),
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
padding: "0 4px",
|
padding: '0 4px',
|
||||||
lineHeight: 1.5,
|
lineHeight: 1.5,
|
||||||
marginRight: "8px",
|
marginRight: '8px',
|
||||||
}));
|
}))
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
import { RefreshRounded, StorageOutlined } from "@mui/icons-material";
|
import { RefreshRounded, StorageOutlined } from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -15,126 +15,126 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
alpha,
|
alpha,
|
||||||
styled,
|
styled,
|
||||||
} from "@mui/material";
|
} from '@mui/material'
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from 'ahooks'
|
||||||
import dayjs from "dayjs";
|
import dayjs from 'dayjs'
|
||||||
import { useState } from "react";
|
import { useState } from 'react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
import { updateProxyProvider } from "tauri-plugin-mihomo-api";
|
import { updateProxyProvider } from 'tauri-plugin-mihomo-api'
|
||||||
|
|
||||||
import { useAppData } from "@/providers/app-data-context";
|
import { useAppData } from '@/providers/app-data-context'
|
||||||
import { showNotice } from "@/services/notice-service";
|
import { showNotice } from '@/services/notice-service'
|
||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from '@/utils/parse-traffic'
|
||||||
|
|
||||||
// 样式化组件 - 类型框
|
// 样式化组件 - 类型框
|
||||||
const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({
|
const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({
|
||||||
display: "inline-block",
|
display: 'inline-block',
|
||||||
border: "1px solid #ccc",
|
border: '1px solid #ccc',
|
||||||
borderColor: alpha(theme.palette.secondary.main, 0.5),
|
borderColor: alpha(theme.palette.secondary.main, 0.5),
|
||||||
color: alpha(theme.palette.secondary.main, 0.8),
|
color: alpha(theme.palette.secondary.main, 0.8),
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
marginRight: "4px",
|
marginRight: '4px',
|
||||||
padding: "0 2px",
|
padding: '0 2px',
|
||||||
lineHeight: 1.25,
|
lineHeight: 1.25,
|
||||||
}));
|
}))
|
||||||
|
|
||||||
// 解析过期时间
|
// 解析过期时间
|
||||||
const parseExpire = (expire?: number) => {
|
const parseExpire = (expire?: number) => {
|
||||||
if (!expire) return "-";
|
if (!expire) return '-'
|
||||||
return dayjs(expire * 1000).format("YYYY-MM-DD");
|
return dayjs(expire * 1000).format('YYYY-MM-DD')
|
||||||
};
|
}
|
||||||
|
|
||||||
export const ProviderButton = () => {
|
export const ProviderButton = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false)
|
||||||
const { proxyProviders, refreshProxy, refreshProxyProviders } = useAppData();
|
const { proxyProviders, refreshProxy, refreshProxyProviders } = useAppData()
|
||||||
const [updating, setUpdating] = useState<Record<string, boolean>>({});
|
const [updating, setUpdating] = useState<Record<string, boolean>>({})
|
||||||
|
|
||||||
// 检查是否有提供者
|
// 检查是否有提供者
|
||||||
const hasProviders = Object.keys(proxyProviders || {}).length > 0;
|
const hasProviders = Object.keys(proxyProviders || {}).length > 0
|
||||||
|
|
||||||
// 更新单个代理提供者
|
// 更新单个代理提供者
|
||||||
const updateProvider = useLockFn(async (name: string) => {
|
const updateProvider = useLockFn(async (name: string) => {
|
||||||
try {
|
try {
|
||||||
// 设置更新状态
|
// 设置更新状态
|
||||||
setUpdating((prev) => ({ ...prev, [name]: true }));
|
setUpdating((prev) => ({ ...prev, [name]: true }))
|
||||||
|
|
||||||
await updateProxyProvider(name);
|
await updateProxyProvider(name)
|
||||||
|
|
||||||
// 刷新数据
|
// 刷新数据
|
||||||
await refreshProxy();
|
await refreshProxy()
|
||||||
await refreshProxyProviders();
|
await refreshProxyProviders()
|
||||||
|
|
||||||
showNotice.success(
|
showNotice.success(
|
||||||
"proxies.feedback.notifications.provider.updateSuccess",
|
'proxies.feedback.notifications.provider.updateSuccess',
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showNotice.error("proxies.feedback.notifications.provider.updateFailed", {
|
showNotice.error('proxies.feedback.notifications.provider.updateFailed', {
|
||||||
name,
|
name,
|
||||||
message: String(err),
|
message: String(err),
|
||||||
});
|
})
|
||||||
} finally {
|
} finally {
|
||||||
// 清除更新状态
|
// 清除更新状态
|
||||||
setUpdating((prev) => ({ ...prev, [name]: false }));
|
setUpdating((prev) => ({ ...prev, [name]: false }))
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
// 更新所有代理提供者
|
// 更新所有代理提供者
|
||||||
const updateAllProviders = useLockFn(async () => {
|
const updateAllProviders = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
// 获取所有provider的名称
|
// 获取所有provider的名称
|
||||||
const allProviders = Object.keys(proxyProviders || {});
|
const allProviders = Object.keys(proxyProviders || {})
|
||||||
if (allProviders.length === 0) {
|
if (allProviders.length === 0) {
|
||||||
showNotice.info("proxies.feedback.notifications.provider.none");
|
showNotice.info('proxies.feedback.notifications.provider.none')
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置所有provider为更新中状态
|
// 设置所有provider为更新中状态
|
||||||
const newUpdating = allProviders.reduce(
|
const newUpdating = allProviders.reduce(
|
||||||
(acc, key) => {
|
(acc, key) => {
|
||||||
acc[key] = true;
|
acc[key] = true
|
||||||
return acc;
|
return acc
|
||||||
},
|
},
|
||||||
{} as Record<string, boolean>,
|
{} as Record<string, boolean>,
|
||||||
);
|
)
|
||||||
setUpdating(newUpdating);
|
setUpdating(newUpdating)
|
||||||
|
|
||||||
// 改为串行逐个更新所有provider
|
// 改为串行逐个更新所有provider
|
||||||
for (const name of allProviders) {
|
for (const name of allProviders) {
|
||||||
try {
|
try {
|
||||||
await updateProxyProvider(name);
|
await updateProxyProvider(name)
|
||||||
// 每个更新完成后更新状态
|
// 每个更新完成后更新状态
|
||||||
setUpdating((prev) => ({ ...prev, [name]: false }));
|
setUpdating((prev) => ({ ...prev, [name]: false }))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`更新 ${name} 失败`, err);
|
console.error(`更新 ${name} 失败`, err)
|
||||||
// 继续执行下一个,不中断整体流程
|
// 继续执行下一个,不中断整体流程
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 刷新数据
|
// 刷新数据
|
||||||
await refreshProxy();
|
await refreshProxy()
|
||||||
await refreshProxyProviders();
|
await refreshProxyProviders()
|
||||||
|
|
||||||
showNotice.success("proxies.feedback.notifications.provider.allUpdated");
|
showNotice.success('proxies.feedback.notifications.provider.allUpdated')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showNotice.error("proxies.feedback.notifications.provider.genericError", {
|
showNotice.error('proxies.feedback.notifications.provider.genericError', {
|
||||||
message: String(err),
|
message: String(err),
|
||||||
});
|
})
|
||||||
} finally {
|
} finally {
|
||||||
// 清除所有更新状态
|
// 清除所有更新状态
|
||||||
setUpdating({});
|
setUpdating({})
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setOpen(false);
|
setOpen(false)
|
||||||
};
|
}
|
||||||
|
|
||||||
if (!hasProviders) return null;
|
if (!hasProviders) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -145,7 +145,7 @@ export const ProviderButton = () => {
|
|||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
sx={{ mr: 1 }}
|
sx={{ mr: 1 }}
|
||||||
>
|
>
|
||||||
{t("proxies.page.provider.title")}
|
{t('proxies.page.provider.title')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
|
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
|
||||||
@@ -156,16 +156,16 @@ export const ProviderButton = () => {
|
|||||||
alignItems="center"
|
alignItems="center"
|
||||||
>
|
>
|
||||||
<Typography variant="h6">
|
<Typography variant="h6">
|
||||||
{t("proxies.page.provider.title")}
|
{t('proxies.page.provider.title')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box>
|
<Box>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={updateAllProviders}
|
onClick={updateAllProviders}
|
||||||
aria-label={t("proxies.page.provider.actions.updateAll")}
|
aria-label={t('proxies.page.provider.actions.updateAll')}
|
||||||
>
|
>
|
||||||
{t("proxies.page.provider.actions.updateAll")}
|
{t('proxies.page.provider.actions.updateAll')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -176,17 +176,17 @@ export const ProviderButton = () => {
|
|||||||
{Object.entries(proxyProviders || {})
|
{Object.entries(proxyProviders || {})
|
||||||
.sort()
|
.sort()
|
||||||
.map(([key, item]) => {
|
.map(([key, item]) => {
|
||||||
const provider = item;
|
const provider = item
|
||||||
const time = dayjs(provider.updatedAt);
|
const time = dayjs(provider.updatedAt)
|
||||||
const isUpdating = updating[key];
|
const isUpdating = updating[key]
|
||||||
|
|
||||||
// 订阅信息
|
// 订阅信息
|
||||||
const sub = provider.subscriptionInfo;
|
const sub = provider.subscriptionInfo
|
||||||
const hasSubInfo = !!sub;
|
const hasSubInfo = !!sub
|
||||||
const upload = sub?.Upload || 0;
|
const upload = sub?.Upload || 0
|
||||||
const download = sub?.Download || 0;
|
const download = sub?.Download || 0
|
||||||
const total = sub?.Total || 0;
|
const total = sub?.Total || 0
|
||||||
const expire = sub?.Expire || 0;
|
const expire = sub?.Expire || 0
|
||||||
|
|
||||||
// 流量使用进度
|
// 流量使用进度
|
||||||
const progress =
|
const progress =
|
||||||
@@ -195,7 +195,7 @@ export const ProviderButton = () => {
|
|||||||
Math.round(((download + upload) * 100) / total) + 1,
|
Math.round(((download + upload) * 100) / total) + 1,
|
||||||
100,
|
100,
|
||||||
)
|
)
|
||||||
: 0;
|
: 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
@@ -203,25 +203,24 @@ export const ProviderButton = () => {
|
|||||||
sx={[
|
sx={[
|
||||||
{
|
{
|
||||||
p: 0,
|
p: 0,
|
||||||
mb: "8px",
|
mb: '8px',
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
transition: "all 0.2s",
|
transition: 'all 0.2s',
|
||||||
},
|
},
|
||||||
({ palette: { mode, primary } }) => {
|
({ palette: { mode, primary } }) => {
|
||||||
const bgcolor =
|
const bgcolor = mode === 'light' ? '#ffffff' : '#24252f'
|
||||||
mode === "light" ? "#ffffff" : "#24252f";
|
|
||||||
const hoverColor =
|
const hoverColor =
|
||||||
mode === "light"
|
mode === 'light'
|
||||||
? alpha(primary.main, 0.1)
|
? alpha(primary.main, 0.1)
|
||||||
: alpha(primary.main, 0.2);
|
: alpha(primary.main, 0.2)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
backgroundColor: bgcolor,
|
backgroundColor: bgcolor,
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
backgroundColor: hoverColor,
|
backgroundColor: hoverColor,
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -230,9 +229,9 @@ export const ProviderButton = () => {
|
|||||||
primary={
|
primary={
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
justifyContent: "space-between",
|
justifyContent: 'space-between',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
@@ -240,9 +239,9 @@ export const ProviderButton = () => {
|
|||||||
component="div"
|
component="div"
|
||||||
noWrap
|
noWrap
|
||||||
title={key}
|
title={key}
|
||||||
sx={{ display: "flex", alignItems: "center" }}
|
sx={{ display: 'flex', alignItems: 'center' }}
|
||||||
>
|
>
|
||||||
<span style={{ marginRight: "8px" }}>{key}</span>
|
<span style={{ marginRight: '8px' }}>{key}</span>
|
||||||
<TypeBox component="span">
|
<TypeBox component="span">
|
||||||
{provider.proxies.length}
|
{provider.proxies.length}
|
||||||
</TypeBox>
|
</TypeBox>
|
||||||
@@ -256,7 +255,7 @@ export const ProviderButton = () => {
|
|||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
noWrap
|
noWrap
|
||||||
>
|
>
|
||||||
<small>{t("shared.labels.updateAt")}: </small>
|
<small>{t('shared.labels.updateAt')}: </small>
|
||||||
{time.fromNow()}
|
{time.fromNow()}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -269,20 +268,20 @@ export const ProviderButton = () => {
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
mb: 1,
|
mb: 1,
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "space-between",
|
justifyContent: 'space-between',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
title={t("shared.labels.usedTotal") as string}
|
title={t('shared.labels.usedTotal') as string}
|
||||||
>
|
>
|
||||||
{parseTraffic(upload + download)} /{" "}
|
{parseTraffic(upload + download)} /{' '}
|
||||||
{parseTraffic(total)}
|
{parseTraffic(total)}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
title={
|
title={
|
||||||
t("shared.labels.expireTime") as string
|
t('shared.labels.expireTime') as string
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{parseExpire(expire)}
|
{parseExpire(expire)}
|
||||||
@@ -308,45 +307,45 @@ export const ProviderButton = () => {
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
width: 40,
|
width: 40,
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateProvider(key);
|
updateProvider(key)
|
||||||
}}
|
}}
|
||||||
disabled={isUpdating}
|
disabled={isUpdating}
|
||||||
sx={{
|
sx={{
|
||||||
animation: isUpdating
|
animation: isUpdating
|
||||||
? "spin 1s linear infinite"
|
? 'spin 1s linear infinite'
|
||||||
: "none",
|
: 'none',
|
||||||
"@keyframes spin": {
|
'@keyframes spin': {
|
||||||
"0%": { transform: "rotate(0deg)" },
|
'0%': { transform: 'rotate(0deg)' },
|
||||||
"100%": { transform: "rotate(360deg)" },
|
'100%': { transform: 'rotate(360deg)' },
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
title={t("proxies.page.provider.actions.update")}
|
title={t('proxies.page.provider.actions.update')}
|
||||||
aria-label={t("proxies.page.provider.actions.update")}
|
aria-label={t('proxies.page.provider.actions.update')}
|
||||||
>
|
>
|
||||||
<RefreshRounded />
|
<RefreshRounded />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</List>
|
</List>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleClose} variant="outlined">
|
<Button onClick={handleClose} variant="outlined">
|
||||||
{t("shared.actions.close")}
|
{t('shared.actions.close')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -6,22 +6,22 @@ import {
|
|||||||
PointerSensor,
|
PointerSensor,
|
||||||
useSensor,
|
useSensor,
|
||||||
useSensors,
|
useSensors,
|
||||||
} from "@dnd-kit/core";
|
} from '@dnd-kit/core'
|
||||||
import {
|
import {
|
||||||
arrayMove,
|
arrayMove,
|
||||||
SortableContext,
|
SortableContext,
|
||||||
sortableKeyboardCoordinates,
|
sortableKeyboardCoordinates,
|
||||||
useSortable,
|
useSortable,
|
||||||
verticalListSortingStrategy,
|
verticalListSortingStrategy,
|
||||||
} from "@dnd-kit/sortable";
|
} from '@dnd-kit/sortable'
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
import {
|
import {
|
||||||
ArrowDownward,
|
ArrowDownward,
|
||||||
Delete as DeleteIcon,
|
Delete as DeleteIcon,
|
||||||
DragIndicator,
|
DragIndicator,
|
||||||
Link,
|
Link,
|
||||||
LinkOff,
|
LinkOff,
|
||||||
} from "@mui/icons-material";
|
} from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Box,
|
Box,
|
||||||
@@ -31,55 +31,55 @@ import {
|
|||||||
Paper,
|
Paper,
|
||||||
Typography,
|
Typography,
|
||||||
useTheme,
|
useTheme,
|
||||||
} from "@mui/material";
|
} from '@mui/material'
|
||||||
import yaml from "js-yaml";
|
import yaml from 'js-yaml'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import {
|
||||||
closeAllConnections,
|
closeAllConnections,
|
||||||
selectNodeForGroup,
|
selectNodeForGroup,
|
||||||
} from "tauri-plugin-mihomo-api";
|
} from 'tauri-plugin-mihomo-api'
|
||||||
|
|
||||||
import { useAppData } from "@/providers/app-data-context";
|
import { useAppData } from '@/providers/app-data-context'
|
||||||
import { updateProxyChainConfigInRuntime } from "@/services/cmds";
|
import { updateProxyChainConfigInRuntime } from '@/services/cmds'
|
||||||
import { debugLog } from "@/utils/debug";
|
import { debugLog } from '@/utils/debug'
|
||||||
|
|
||||||
interface ProxyChainItem {
|
interface ProxyChainItem {
|
||||||
id: string;
|
id: string
|
||||||
name: string;
|
name: string
|
||||||
type?: string;
|
type?: string
|
||||||
delay?: number;
|
delay?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ParsedChainConfig {
|
interface ParsedChainConfig {
|
||||||
proxies?: Array<{
|
proxies?: Array<{
|
||||||
name: string;
|
name: string
|
||||||
type: string;
|
type: string
|
||||||
[key: string]: any;
|
[key: string]: any
|
||||||
}>;
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProxyChainProps {
|
interface ProxyChainProps {
|
||||||
proxyChain: ProxyChainItem[];
|
proxyChain: ProxyChainItem[]
|
||||||
onUpdateChain: (chain: ProxyChainItem[]) => void;
|
onUpdateChain: (chain: ProxyChainItem[]) => void
|
||||||
chainConfigData?: string | null;
|
chainConfigData?: string | null
|
||||||
onMarkUnsavedChanges?: () => void;
|
onMarkUnsavedChanges?: () => void
|
||||||
mode?: string;
|
mode?: string
|
||||||
selectedGroup?: string | null;
|
selectedGroup?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SortableItemProps {
|
interface SortableItemProps {
|
||||||
proxy: ProxyChainItem;
|
proxy: ProxyChainItem
|
||||||
index: number;
|
index: number
|
||||||
isFirst: boolean;
|
isFirst: boolean
|
||||||
isLast: boolean;
|
isLast: boolean
|
||||||
onRemove: (id: string) => void;
|
onRemove: (id: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const toChainItems = (
|
const toChainItems = (
|
||||||
parsedConfig: ParsedChainConfig | null | undefined,
|
parsedConfig: ParsedChainConfig | null | undefined,
|
||||||
): ProxyChainItem[] => {
|
): ProxyChainItem[] => {
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
parsedConfig?.proxies?.map((proxy, index) => ({
|
parsedConfig?.proxies?.map((proxy, index) => ({
|
||||||
@@ -88,8 +88,8 @@ const toChainItems = (
|
|||||||
type: proxy.type,
|
type: proxy.type,
|
||||||
delay: undefined,
|
delay: undefined,
|
||||||
})) || []
|
})) || []
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const SortableItem = ({
|
const SortableItem = ({
|
||||||
proxy,
|
proxy,
|
||||||
@@ -98,8 +98,8 @@ const SortableItem = ({
|
|||||||
isLast,
|
isLast,
|
||||||
onRemove,
|
onRemove,
|
||||||
}: SortableItemProps) => {
|
}: SortableItemProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme()
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const {
|
const {
|
||||||
attributes,
|
attributes,
|
||||||
listeners,
|
listeners,
|
||||||
@@ -107,25 +107,25 @@ const SortableItem = ({
|
|||||||
transform,
|
transform,
|
||||||
transition,
|
transition,
|
||||||
isDragging,
|
isDragging,
|
||||||
} = useSortable({ id: proxy.id });
|
} = useSortable({ id: proxy.id })
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
transform: CSS.Transform.toString(transform),
|
transform: CSS.Transform.toString(transform),
|
||||||
transition,
|
transition,
|
||||||
opacity: isDragging ? 0.5 : 1,
|
opacity: isDragging ? 0.5 : 1,
|
||||||
};
|
}
|
||||||
|
|
||||||
const roleLabel = isFirst
|
const roleLabel = isFirst
|
||||||
? t("proxies.page.chain.entryNode")
|
? t('proxies.page.chain.entryNode')
|
||||||
: isLast
|
: isLast
|
||||||
? t("proxies.page.chain.exitNode")
|
? t('proxies.page.chain.exitNode')
|
||||||
: undefined;
|
: undefined
|
||||||
|
|
||||||
const roleColor = isFirst
|
const roleColor = isFirst
|
||||||
? theme.palette.success.main
|
? theme.palette.success.main
|
||||||
: isLast
|
: isLast
|
||||||
? theme.palette.warning.main
|
? theme.palette.warning.main
|
||||||
: undefined;
|
: undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -133,8 +133,8 @@ const SortableItem = ({
|
|||||||
style={style}
|
style={style}
|
||||||
sx={{
|
sx={{
|
||||||
mb: 0,
|
mb: 0,
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
p: 1,
|
p: 1,
|
||||||
backgroundColor: isDragging
|
backgroundColor: isDragging
|
||||||
? theme.palette.action.selected
|
? theme.palette.action.selected
|
||||||
@@ -144,20 +144,20 @@ const SortableItem = ({
|
|||||||
? `1.5px solid ${roleColor}`
|
? `1.5px solid ${roleColor}`
|
||||||
: `1px solid ${theme.palette.divider}`,
|
: `1px solid ${theme.palette.divider}`,
|
||||||
boxShadow: isDragging ? theme.shadows[4] : theme.shadows[1],
|
boxShadow: isDragging ? theme.shadows[4] : theme.shadows[1],
|
||||||
transition: "box-shadow 0.2s, background-color 0.2s",
|
transition: 'box-shadow 0.2s, background-color 0.2s',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
{...attributes}
|
{...attributes}
|
||||||
{...listeners}
|
{...listeners}
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
mr: 1,
|
mr: 1,
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
cursor: "grab",
|
cursor: 'grab',
|
||||||
"&:active": {
|
'&:active': {
|
||||||
cursor: "grabbing",
|
cursor: 'grabbing',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -171,7 +171,7 @@ const SortableItem = ({
|
|||||||
sx={{
|
sx={{
|
||||||
mr: 1,
|
mr: 1,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: "#fff",
|
color: '#fff',
|
||||||
backgroundColor: roleColor,
|
backgroundColor: roleColor,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -189,9 +189,9 @@ const SortableItem = ({
|
|||||||
sx={{
|
sx={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
textOverflow: "ellipsis",
|
textOverflow: 'ellipsis',
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{proxy.name}
|
{proxy.name}
|
||||||
@@ -211,17 +211,17 @@ const SortableItem = ({
|
|||||||
label={
|
label={
|
||||||
proxy.delay > 0
|
proxy.delay > 0
|
||||||
? `${proxy.delay}ms`
|
? `${proxy.delay}ms`
|
||||||
: t("shared.labels.timeout") || "超时"
|
: t('shared.labels.timeout') || '超时'
|
||||||
}
|
}
|
||||||
size="small"
|
size="small"
|
||||||
color={
|
color={
|
||||||
proxy.delay > 0 && proxy.delay < 200
|
proxy.delay > 0 && proxy.delay < 200
|
||||||
? "success"
|
? 'success'
|
||||||
: proxy.delay > 0 && proxy.delay < 800
|
: proxy.delay > 0 && proxy.delay < 800
|
||||||
? "warning"
|
? 'warning'
|
||||||
: "error"
|
: 'error'
|
||||||
}
|
}
|
||||||
sx={{ mr: 1, fontSize: "0.7rem", minWidth: 50 }}
|
sx={{ mr: 1, fontSize: '0.7rem', minWidth: 50 }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -230,16 +230,16 @@ const SortableItem = ({
|
|||||||
onClick={() => onRemove(proxy.id)}
|
onClick={() => onRemove(proxy.id)}
|
||||||
sx={{
|
sx={{
|
||||||
color: theme.palette.error.main,
|
color: theme.palette.error.main,
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
backgroundColor: theme.palette.error.light + "20",
|
backgroundColor: theme.palette.error.light + '20',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DeleteIcon fontSize="small" />
|
<DeleteIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export const ProxyChain = ({
|
export const ProxyChain = ({
|
||||||
proxyChain,
|
proxyChain,
|
||||||
@@ -249,48 +249,48 @@ export const ProxyChain = ({
|
|||||||
mode,
|
mode,
|
||||||
selectedGroup,
|
selectedGroup,
|
||||||
}: ProxyChainProps) => {
|
}: ProxyChainProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme()
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const { proxies, refreshProxy } = useAppData();
|
const { proxies, refreshProxy } = useAppData()
|
||||||
const [isConnecting, setIsConnecting] = useState(false);
|
const [isConnecting, setIsConnecting] = useState(false)
|
||||||
const markUnsavedChanges = useCallback(() => {
|
const markUnsavedChanges = useCallback(() => {
|
||||||
onMarkUnsavedChanges?.();
|
onMarkUnsavedChanges?.()
|
||||||
}, [onMarkUnsavedChanges]);
|
}, [onMarkUnsavedChanges])
|
||||||
|
|
||||||
const isConnected = useMemo(() => {
|
const isConnected = useMemo(() => {
|
||||||
if (!proxies || proxyChain.length < 2) {
|
if (!proxies || proxyChain.length < 2) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastNode = proxyChain[proxyChain.length - 1];
|
const lastNode = proxyChain[proxyChain.length - 1]
|
||||||
|
|
||||||
if (mode === "global") {
|
if (mode === 'global') {
|
||||||
return proxies.global?.now === lastNode.name;
|
return proxies.global?.now === lastNode.name
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selectedGroup || !Array.isArray(proxies.groups)) {
|
if (!selectedGroup || !Array.isArray(proxies.groups)) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const proxyChainGroup = proxies.groups.find(
|
const proxyChainGroup = proxies.groups.find(
|
||||||
(group: { name: string }) => group.name === selectedGroup,
|
(group: { name: string }) => group.name === selectedGroup,
|
||||||
);
|
)
|
||||||
|
|
||||||
return proxyChainGroup?.now === lastNode.name;
|
return proxyChainGroup?.now === lastNode.name
|
||||||
}, [proxies, proxyChain, mode, selectedGroup]);
|
}, [proxies, proxyChain, mode, selectedGroup])
|
||||||
|
|
||||||
// 监听链的变化,但排除从配置加载的情况
|
// 监听链的变化,但排除从配置加载的情况
|
||||||
const chainLengthRef = useRef(proxyChain.length);
|
const chainLengthRef = useRef(proxyChain.length)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 只有当链长度发生变化且不是初始加载时,才标记为未保存
|
// 只有当链长度发生变化且不是初始加载时,才标记为未保存
|
||||||
if (
|
if (
|
||||||
chainLengthRef.current !== proxyChain.length &&
|
chainLengthRef.current !== proxyChain.length &&
|
||||||
chainLengthRef.current !== 0
|
chainLengthRef.current !== 0
|
||||||
) {
|
) {
|
||||||
markUnsavedChanges();
|
markUnsavedChanges()
|
||||||
}
|
}
|
||||||
chainLengthRef.current = proxyChain.length;
|
chainLengthRef.current = proxyChain.length
|
||||||
}, [proxyChain.length, markUnsavedChanges]);
|
}, [proxyChain.length, markUnsavedChanges])
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(PointerSensor, {
|
||||||
@@ -299,50 +299,50 @@ export const ProxyChain = ({
|
|||||||
useSensor(KeyboardSensor, {
|
useSensor(KeyboardSensor, {
|
||||||
coordinateGetter: sortableKeyboardCoordinates,
|
coordinateGetter: sortableKeyboardCoordinates,
|
||||||
}),
|
}),
|
||||||
);
|
)
|
||||||
|
|
||||||
const handleDragEnd = useCallback(
|
const handleDragEnd = useCallback(
|
||||||
(event: DragEndEvent) => {
|
(event: DragEndEvent) => {
|
||||||
const { active, over } = event;
|
const { active, over } = event
|
||||||
|
|
||||||
if (active.id !== over?.id) {
|
if (active.id !== over?.id) {
|
||||||
const oldIndex = proxyChain.findIndex((item) => item.id === active.id);
|
const oldIndex = proxyChain.findIndex((item) => item.id === active.id)
|
||||||
const newIndex = proxyChain.findIndex((item) => item.id === over?.id);
|
const newIndex = proxyChain.findIndex((item) => item.id === over?.id)
|
||||||
|
|
||||||
onUpdateChain(arrayMove(proxyChain, oldIndex, newIndex));
|
onUpdateChain(arrayMove(proxyChain, oldIndex, newIndex))
|
||||||
markUnsavedChanges();
|
markUnsavedChanges()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[proxyChain, onUpdateChain, markUnsavedChanges],
|
[proxyChain, onUpdateChain, markUnsavedChanges],
|
||||||
);
|
)
|
||||||
|
|
||||||
const handleRemoveProxy = useCallback(
|
const handleRemoveProxy = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
const newChain = proxyChain.filter((item) => item.id !== id);
|
const newChain = proxyChain.filter((item) => item.id !== id)
|
||||||
onUpdateChain(newChain);
|
onUpdateChain(newChain)
|
||||||
markUnsavedChanges();
|
markUnsavedChanges()
|
||||||
},
|
},
|
||||||
[proxyChain, onUpdateChain, markUnsavedChanges],
|
[proxyChain, onUpdateChain, markUnsavedChanges],
|
||||||
);
|
)
|
||||||
|
|
||||||
const handleConnect = useCallback(async () => {
|
const handleConnect = useCallback(async () => {
|
||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
setIsConnecting(true);
|
setIsConnecting(true)
|
||||||
try {
|
try {
|
||||||
await updateProxyChainConfigInRuntime(null);
|
await updateProxyChainConfigInRuntime(null)
|
||||||
|
|
||||||
const targetGroup =
|
const targetGroup =
|
||||||
mode === "global"
|
mode === 'global'
|
||||||
? "GLOBAL"
|
? 'GLOBAL'
|
||||||
: selectedGroup || localStorage.getItem("proxy-chain-group");
|
: selectedGroup || localStorage.getItem('proxy-chain-group')
|
||||||
|
|
||||||
if (targetGroup) {
|
if (targetGroup) {
|
||||||
try {
|
try {
|
||||||
await selectNodeForGroup(targetGroup, "DIRECT");
|
await selectNodeForGroup(targetGroup, 'DIRECT')
|
||||||
} catch {
|
} catch {
|
||||||
if (proxyChain.length >= 1) {
|
if (proxyChain.length >= 1) {
|
||||||
try {
|
try {
|
||||||
await selectNodeForGroup(targetGroup, proxyChain[0].name);
|
await selectNodeForGroup(targetGroup, proxyChain[0].name)
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
@@ -350,59 +350,59 @@ export const ProxyChain = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.removeItem("proxy-chain-group");
|
localStorage.removeItem('proxy-chain-group')
|
||||||
localStorage.removeItem("proxy-chain-exit-node");
|
localStorage.removeItem('proxy-chain-exit-node')
|
||||||
localStorage.removeItem("proxy-chain-items");
|
localStorage.removeItem('proxy-chain-items')
|
||||||
|
|
||||||
await closeAllConnections();
|
await closeAllConnections()
|
||||||
await refreshProxy();
|
await refreshProxy()
|
||||||
|
|
||||||
onUpdateChain([]);
|
onUpdateChain([])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to disconnect from proxy chain:", error);
|
console.error('Failed to disconnect from proxy chain:', error)
|
||||||
alert(t("proxies.page.chain.disconnectFailed") || "断开链式代理失败");
|
alert(t('proxies.page.chain.disconnectFailed') || '断开链式代理失败')
|
||||||
} finally {
|
} finally {
|
||||||
setIsConnecting(false);
|
setIsConnecting(false)
|
||||||
}
|
}
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (proxyChain.length < 2) {
|
if (proxyChain.length < 2) {
|
||||||
alert(t("proxies.page.chain.minimumNodes") || "链式代理至少需要2个节点");
|
alert(t('proxies.page.chain.minimumNodes') || '链式代理至少需要2个节点')
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsConnecting(true);
|
setIsConnecting(true)
|
||||||
try {
|
try {
|
||||||
// 第一步:保存链式代理配置
|
// 第一步:保存链式代理配置
|
||||||
const chainProxies = proxyChain.map((node) => node.name);
|
const chainProxies = proxyChain.map((node) => node.name)
|
||||||
debugLog("Saving chain config:", chainProxies);
|
debugLog('Saving chain config:', chainProxies)
|
||||||
await updateProxyChainConfigInRuntime(chainProxies);
|
await updateProxyChainConfigInRuntime(chainProxies)
|
||||||
debugLog("Chain configuration saved successfully");
|
debugLog('Chain configuration saved successfully')
|
||||||
|
|
||||||
// 第二步:连接到代理链的最后一个节点
|
// 第二步:连接到代理链的最后一个节点
|
||||||
const lastNode = proxyChain[proxyChain.length - 1];
|
const lastNode = proxyChain[proxyChain.length - 1]
|
||||||
debugLog(`Connecting to proxy chain, last node: ${lastNode.name}`);
|
debugLog(`Connecting to proxy chain, last node: ${lastNode.name}`)
|
||||||
|
|
||||||
// 根据模式确定使用的代理组名称
|
// 根据模式确定使用的代理组名称
|
||||||
if (mode !== "global" && !selectedGroup) {
|
if (mode !== 'global' && !selectedGroup) {
|
||||||
throw new Error("规则模式下必须选择代理组");
|
throw new Error('规则模式下必须选择代理组')
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetGroup = mode === "global" ? "GLOBAL" : selectedGroup;
|
const targetGroup = mode === 'global' ? 'GLOBAL' : selectedGroup
|
||||||
|
|
||||||
await selectNodeForGroup(targetGroup || "GLOBAL", lastNode.name);
|
await selectNodeForGroup(targetGroup || 'GLOBAL', lastNode.name)
|
||||||
localStorage.setItem("proxy-chain-group", targetGroup || "GLOBAL");
|
localStorage.setItem('proxy-chain-group', targetGroup || 'GLOBAL')
|
||||||
localStorage.setItem("proxy-chain-exit-node", lastNode.name);
|
localStorage.setItem('proxy-chain-exit-node', lastNode.name)
|
||||||
|
|
||||||
// 刷新代理信息以更新连接状态
|
// 刷新代理信息以更新连接状态
|
||||||
refreshProxy();
|
refreshProxy()
|
||||||
debugLog("Successfully connected to proxy chain");
|
debugLog('Successfully connected to proxy chain')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to connect to proxy chain:", error);
|
console.error('Failed to connect to proxy chain:', error)
|
||||||
alert(t("proxies.page.chain.connectFailed") || "连接链式代理失败");
|
alert(t('proxies.page.chain.connectFailed') || '连接链式代理失败')
|
||||||
} finally {
|
} finally {
|
||||||
setIsConnecting(false);
|
setIsConnecting(false)
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
proxyChain,
|
proxyChain,
|
||||||
@@ -412,112 +412,112 @@ export const ProxyChain = ({
|
|||||||
mode,
|
mode,
|
||||||
selectedGroup,
|
selectedGroup,
|
||||||
onUpdateChain,
|
onUpdateChain,
|
||||||
]);
|
])
|
||||||
|
|
||||||
const proxyChainRef = useRef(proxyChain);
|
const proxyChainRef = useRef(proxyChain)
|
||||||
const onUpdateChainRef = useRef(onUpdateChain);
|
const onUpdateChainRef = useRef(onUpdateChain)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
proxyChainRef.current = proxyChain;
|
proxyChainRef.current = proxyChain
|
||||||
onUpdateChainRef.current = onUpdateChain;
|
onUpdateChainRef.current = onUpdateChain
|
||||||
}, [proxyChain, onUpdateChain]);
|
}, [proxyChain, onUpdateChain])
|
||||||
|
|
||||||
// 处理链式代理配置数据
|
// 处理链式代理配置数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chainConfigData) {
|
if (chainConfigData) {
|
||||||
try {
|
try {
|
||||||
// JSON is valid YAML, so one parser covers both persisted formats.
|
// JSON is valid YAML, so one parser covers both persisted formats.
|
||||||
const parsedConfig = yaml.load(chainConfigData) as ParsedChainConfig;
|
const parsedConfig = yaml.load(chainConfigData) as ParsedChainConfig
|
||||||
const chainItems = toChainItems(parsedConfig);
|
const chainItems = toChainItems(parsedConfig)
|
||||||
|
|
||||||
if (chainItems.length > 0) {
|
if (chainItems.length > 0) {
|
||||||
onUpdateChain(chainItems);
|
onUpdateChain(chainItems)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to process chain config data:", error);
|
console.error('Failed to process chain config data:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [chainConfigData, onUpdateChain]);
|
}, [chainConfigData, onUpdateChain])
|
||||||
|
|
||||||
// 定时更新延迟数据
|
// 定时更新延迟数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!proxies?.records) return;
|
if (!proxies?.records) return
|
||||||
|
|
||||||
const updateDelays = () => {
|
const updateDelays = () => {
|
||||||
const currentChain = proxyChainRef.current;
|
const currentChain = proxyChainRef.current
|
||||||
if (currentChain.length === 0) return;
|
if (currentChain.length === 0) return
|
||||||
|
|
||||||
const updatedChain = currentChain.map((item) => {
|
const updatedChain = currentChain.map((item) => {
|
||||||
const proxyRecord = proxies.records[item.name];
|
const proxyRecord = proxies.records[item.name]
|
||||||
if (
|
if (
|
||||||
proxyRecord &&
|
proxyRecord &&
|
||||||
proxyRecord.history &&
|
proxyRecord.history &&
|
||||||
proxyRecord.history.length > 0
|
proxyRecord.history.length > 0
|
||||||
) {
|
) {
|
||||||
const latestDelay =
|
const latestDelay =
|
||||||
proxyRecord.history[proxyRecord.history.length - 1].delay;
|
proxyRecord.history[proxyRecord.history.length - 1].delay
|
||||||
return { ...item, delay: latestDelay };
|
return { ...item, delay: latestDelay }
|
||||||
}
|
}
|
||||||
return item;
|
return item
|
||||||
});
|
})
|
||||||
|
|
||||||
// 只有在延迟数据确实发生变化时才更新
|
// 只有在延迟数据确实发生变化时才更新
|
||||||
const hasChanged = updatedChain.some(
|
const hasChanged = updatedChain.some(
|
||||||
(item, index) => item.delay !== currentChain[index]?.delay,
|
(item, index) => item.delay !== currentChain[index]?.delay,
|
||||||
);
|
)
|
||||||
|
|
||||||
if (hasChanged) {
|
if (hasChanged) {
|
||||||
onUpdateChainRef.current(updatedChain);
|
onUpdateChainRef.current(updatedChain)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// 立即更新一次延迟
|
// 立即更新一次延迟
|
||||||
updateDelays();
|
updateDelays()
|
||||||
|
|
||||||
// 设置定时器,每5秒更新一次延迟
|
// 设置定时器,每5秒更新一次延迟
|
||||||
const interval = setInterval(updateDelays, 5000);
|
const interval = setInterval(updateDelays, 5000)
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval)
|
||||||
}, [proxies?.records]); // 只依赖proxies.records
|
}, [proxies?.records]) // 只依赖proxies.records
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
elevation={1}
|
elevation={1}
|
||||||
sx={{
|
sx={{
|
||||||
height: "100%",
|
height: '100%',
|
||||||
p: 2,
|
p: 2,
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
flexDirection: "column",
|
flexDirection: 'column',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "space-between",
|
justifyContent: 'space-between',
|
||||||
mb: 2,
|
mb: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="h6">{t("proxies.page.chain.header")}</Typography>
|
<Typography variant="h6">{t('proxies.page.chain.header')}</Typography>
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
{proxyChain.length > 0 && (
|
{proxyChain.length > 0 && (
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateProxyChainConfigInRuntime(null);
|
updateProxyChainConfigInRuntime(null)
|
||||||
localStorage.removeItem("proxy-chain-group");
|
localStorage.removeItem('proxy-chain-group')
|
||||||
localStorage.removeItem("proxy-chain-exit-node");
|
localStorage.removeItem('proxy-chain-exit-node')
|
||||||
localStorage.removeItem("proxy-chain-items");
|
localStorage.removeItem('proxy-chain-items')
|
||||||
onUpdateChain([]);
|
onUpdateChain([])
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
color: theme.palette.error.main,
|
color: theme.palette.error.main,
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
backgroundColor: theme.palette.error.light + "20",
|
backgroundColor: theme.palette.error.light + '20',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
title={
|
title={
|
||||||
t("proxies.page.actions.clearChainConfig") || "删除链式配置"
|
t('proxies.page.actions.clearChainConfig') || '删除链式配置'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<DeleteIcon fontSize="small" />
|
<DeleteIcon fontSize="small" />
|
||||||
@@ -531,51 +531,51 @@ export const ProxyChain = ({
|
|||||||
disabled={
|
disabled={
|
||||||
isConnecting ||
|
isConnecting ||
|
||||||
proxyChain.length < 2 ||
|
proxyChain.length < 2 ||
|
||||||
(mode !== "global" && !selectedGroup)
|
(mode !== 'global' && !selectedGroup)
|
||||||
}
|
}
|
||||||
color={isConnected ? "error" : "success"}
|
color={isConnected ? 'error' : 'success'}
|
||||||
sx={{
|
sx={{
|
||||||
minWidth: 90,
|
minWidth: 90,
|
||||||
}}
|
}}
|
||||||
title={
|
title={
|
||||||
proxyChain.length < 2
|
proxyChain.length < 2
|
||||||
? t("proxies.page.chain.minimumNodes") ||
|
? t('proxies.page.chain.minimumNodes') ||
|
||||||
"链式代理至少需要2个节点"
|
'链式代理至少需要2个节点'
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isConnecting
|
{isConnecting
|
||||||
? t("proxies.page.actions.connecting") || "连接中..."
|
? t('proxies.page.actions.connecting') || '连接中...'
|
||||||
: isConnected
|
: isConnected
|
||||||
? t("proxies.page.actions.disconnect") || "断开"
|
? t('proxies.page.actions.disconnect') || '断开'
|
||||||
: t("proxies.page.actions.connect") || "连接"}
|
: t('proxies.page.actions.connect') || '连接'}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Alert
|
<Alert
|
||||||
severity={proxyChain.length === 1 ? "warning" : "info"}
|
severity={proxyChain.length === 1 ? 'warning' : 'info'}
|
||||||
sx={{ mb: 2 }}
|
sx={{ mb: 2 }}
|
||||||
>
|
>
|
||||||
{proxyChain.length === 1
|
{proxyChain.length === 1
|
||||||
? t("proxies.page.chain.minimumNodesHint") ||
|
? t('proxies.page.chain.minimumNodesHint') ||
|
||||||
"链式代理至少需要2个节点,请再添加一个节点。"
|
'链式代理至少需要2个节点,请再添加一个节点。'
|
||||||
: t("proxies.page.chain.instruction") ||
|
: t('proxies.page.chain.instruction') ||
|
||||||
"按顺序点击节点添加到代理链中"}
|
'按顺序点击节点添加到代理链中'}
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<Box sx={{ flex: 1, overflow: "auto" }}>
|
<Box sx={{ flex: 1, overflow: 'auto' }}>
|
||||||
{proxyChain.length === 0 ? (
|
{proxyChain.length === 0 ? (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
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>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<DndContext
|
<DndContext
|
||||||
@@ -608,8 +608,8 @@ export const ProxyChain = ({
|
|||||||
{index < proxyChain.length - 1 && (
|
{index < proxyChain.length - 1 && (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
py: 0.25,
|
py: 0.25,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -630,5 +630,5 @@ export const ProxyChain = ({
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import { Box, Button, Tooltip } from "@mui/material";
|
import { Box, Button, Tooltip } from '@mui/material'
|
||||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||||
|
|
||||||
interface ProxyGroupNavigatorProps {
|
interface ProxyGroupNavigatorProps {
|
||||||
proxyGroupNames: string[];
|
proxyGroupNames: string[]
|
||||||
onGroupLocation: (groupName: string) => void;
|
onGroupLocation: (groupName: string) => void
|
||||||
enableHoverJump?: boolean;
|
enableHoverJump?: boolean
|
||||||
hoverDelay?: number;
|
hoverDelay?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_HOVER_DELAY = 280;
|
export const DEFAULT_HOVER_DELAY = 280
|
||||||
|
|
||||||
// 提取代理组名的第一个字符
|
// 提取代理组名的第一个字符
|
||||||
const getGroupDisplayChar = (groupName: string): string => {
|
const getGroupDisplayChar = (groupName: string): string => {
|
||||||
if (!groupName) return "?";
|
if (!groupName) return '?'
|
||||||
|
|
||||||
// 直接返回第一个字符,支持表情符号
|
// 直接返回第一个字符,支持表情符号
|
||||||
const firstChar = Array.from(groupName)[0];
|
const firstChar = Array.from(groupName)[0]
|
||||||
return firstChar || "?";
|
return firstChar || '?'
|
||||||
};
|
}
|
||||||
|
|
||||||
export const ProxyGroupNavigator = ({
|
export const ProxyGroupNavigator = ({
|
||||||
proxyGroupNames,
|
proxyGroupNames,
|
||||||
@@ -25,55 +25,55 @@ export const ProxyGroupNavigator = ({
|
|||||||
enableHoverJump = true,
|
enableHoverJump = true,
|
||||||
hoverDelay = DEFAULT_HOVER_DELAY,
|
hoverDelay = DEFAULT_HOVER_DELAY,
|
||||||
}: ProxyGroupNavigatorProps) => {
|
}: ProxyGroupNavigatorProps) => {
|
||||||
const lastHoveredRef = useRef<string | null>(null);
|
const lastHoveredRef = useRef<string | null>(null)
|
||||||
const hoverTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const hoverTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
||||||
const hoverDelayMs = hoverDelay >= 0 ? hoverDelay : 0;
|
const hoverDelayMs = hoverDelay >= 0 ? hoverDelay : 0
|
||||||
|
|
||||||
const clearHoverTimer = useCallback(() => {
|
const clearHoverTimer = useCallback(() => {
|
||||||
if (hoverTimerRef.current) {
|
if (hoverTimerRef.current) {
|
||||||
clearTimeout(hoverTimerRef.current);
|
clearTimeout(hoverTimerRef.current)
|
||||||
hoverTimerRef.current = null;
|
hoverTimerRef.current = null
|
||||||
}
|
}
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enableHoverJump) {
|
if (!enableHoverJump) {
|
||||||
clearHoverTimer();
|
clearHoverTimer()
|
||||||
lastHoveredRef.current = null;
|
lastHoveredRef.current = null
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
clearHoverTimer();
|
clearHoverTimer()
|
||||||
};
|
}
|
||||||
}, [clearHoverTimer, enableHoverJump]);
|
}, [clearHoverTimer, enableHoverJump])
|
||||||
|
|
||||||
const handleGroupClick = useCallback(
|
const handleGroupClick = useCallback(
|
||||||
(groupName: string) => {
|
(groupName: string) => {
|
||||||
clearHoverTimer();
|
clearHoverTimer()
|
||||||
lastHoveredRef.current = groupName;
|
lastHoveredRef.current = groupName
|
||||||
onGroupLocation(groupName);
|
onGroupLocation(groupName)
|
||||||
},
|
},
|
||||||
[clearHoverTimer, onGroupLocation],
|
[clearHoverTimer, onGroupLocation],
|
||||||
);
|
)
|
||||||
|
|
||||||
const handleGroupHover = useCallback(
|
const handleGroupHover = useCallback(
|
||||||
(groupName: string) => {
|
(groupName: string) => {
|
||||||
if (!enableHoverJump) return;
|
if (!enableHoverJump) return
|
||||||
if (lastHoveredRef.current === groupName) return;
|
if (lastHoveredRef.current === groupName) return
|
||||||
clearHoverTimer();
|
clearHoverTimer()
|
||||||
hoverTimerRef.current = setTimeout(() => {
|
hoverTimerRef.current = setTimeout(() => {
|
||||||
hoverTimerRef.current = null;
|
hoverTimerRef.current = null
|
||||||
lastHoveredRef.current = groupName;
|
lastHoveredRef.current = groupName
|
||||||
onGroupLocation(groupName);
|
onGroupLocation(groupName)
|
||||||
}, hoverDelayMs);
|
}, hoverDelayMs)
|
||||||
},
|
},
|
||||||
[clearHoverTimer, enableHoverJump, hoverDelayMs, onGroupLocation],
|
[clearHoverTimer, enableHoverJump, hoverDelayMs, onGroupLocation],
|
||||||
);
|
)
|
||||||
|
|
||||||
const handleButtonLeave = useCallback(() => {
|
const handleButtonLeave = useCallback(() => {
|
||||||
clearHoverTimer();
|
clearHoverTimer()
|
||||||
lastHoveredRef.current = null;
|
lastHoveredRef.current = null
|
||||||
}, [clearHoverTimer]);
|
}, [clearHoverTimer])
|
||||||
|
|
||||||
// 处理代理组数据,去重和排序
|
// 处理代理组数据,去重和排序
|
||||||
const processedGroups = useMemo(() => {
|
const processedGroups = useMemo(() => {
|
||||||
@@ -82,36 +82,36 @@ export const ProxyGroupNavigator = ({
|
|||||||
.map((name) => ({
|
.map((name) => ({
|
||||||
name,
|
name,
|
||||||
displayChar: getGroupDisplayChar(name),
|
displayChar: getGroupDisplayChar(name),
|
||||||
}));
|
}))
|
||||||
}, [proxyGroupNames]);
|
}, [proxyGroupNames])
|
||||||
|
|
||||||
if (processedGroups.length === 0) {
|
if (processedGroups.length === 0) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
right: 2,
|
right: 2,
|
||||||
top: "50%",
|
top: '50%',
|
||||||
transform: "translateY(-50%)",
|
transform: 'translateY(-50%)',
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
flexDirection: "column",
|
flexDirection: 'column',
|
||||||
gap: 0.25,
|
gap: 0.25,
|
||||||
bgcolor: "transparent",
|
bgcolor: 'transparent',
|
||||||
borderRadius: 0.5,
|
borderRadius: 0.5,
|
||||||
boxShadow: 0,
|
boxShadow: 0,
|
||||||
p: 0.25,
|
p: 0.25,
|
||||||
maxHeight: "70vh",
|
maxHeight: '70vh',
|
||||||
overflowY: "auto",
|
overflowY: 'auto',
|
||||||
scrollbarWidth: "none",
|
scrollbarWidth: 'none',
|
||||||
"&::-webkit-scrollbar": {
|
'&::-webkit-scrollbar': {
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
},
|
},
|
||||||
minWidth: "auto",
|
minWidth: 'auto',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{processedGroups.map(({ name, displayChar }) => (
|
{processedGroups.map(({ name, displayChar }) => (
|
||||||
@@ -129,17 +129,17 @@ export const ProxyGroupNavigator = ({
|
|||||||
minHeight: 28,
|
minHeight: 28,
|
||||||
width: 28,
|
width: 28,
|
||||||
height: 28,
|
height: 28,
|
||||||
fontSize: "12px",
|
fontSize: '12px',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
padding: 0,
|
padding: 0,
|
||||||
borderRadius: 0.25,
|
borderRadius: 0.25,
|
||||||
color: "text.secondary",
|
color: 'text.secondary',
|
||||||
textAlign: "center",
|
textAlign: 'center',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
textTransform: "none",
|
textTransform: 'none',
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
bgcolor: "primary.light",
|
bgcolor: 'primary.light',
|
||||||
color: "primary.contrastText",
|
color: 'primary.contrastText',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -148,5 +148,5 @@ export const ProxyGroupNavigator = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ExpandMoreRounded } from "@mui/icons-material";
|
import { ExpandMoreRounded } from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Box,
|
Box,
|
||||||
@@ -8,255 +8,253 @@ import {
|
|||||||
MenuItem,
|
MenuItem,
|
||||||
Snackbar,
|
Snackbar,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from '@mui/material'
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from 'ahooks'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso";
|
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso'
|
||||||
import { delayGroup, healthcheckProxyProvider } from "tauri-plugin-mihomo-api";
|
import { delayGroup, healthcheckProxyProvider } from 'tauri-plugin-mihomo-api'
|
||||||
|
|
||||||
import { BaseEmpty } from "@/components/base";
|
import { BaseEmpty } from '@/components/base'
|
||||||
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 { useAppData } from "@/providers/app-data-context";
|
import { useAppData } from '@/providers/app-data-context'
|
||||||
import { updateProxyChainConfigInRuntime } from "@/services/cmds";
|
import { updateProxyChainConfigInRuntime } from '@/services/cmds'
|
||||||
import delayManager from "@/services/delay";
|
import delayManager from '@/services/delay'
|
||||||
import { debugLog } from "@/utils/debug";
|
import { debugLog } from '@/utils/debug'
|
||||||
|
|
||||||
import { ScrollTopButton } from "../layout/scroll-top-button";
|
import { ScrollTopButton } from '../layout/scroll-top-button'
|
||||||
|
|
||||||
import { ProxyChain } from "./proxy-chain";
|
import { ProxyChain } from './proxy-chain'
|
||||||
import {
|
import {
|
||||||
DEFAULT_HOVER_DELAY,
|
DEFAULT_HOVER_DELAY,
|
||||||
ProxyGroupNavigator,
|
ProxyGroupNavigator,
|
||||||
} from "./proxy-group-navigator";
|
} from './proxy-group-navigator'
|
||||||
import { ProxyRender } from "./proxy-render";
|
import { ProxyRender } from './proxy-render'
|
||||||
import { useRenderList } from "./use-render-list";
|
import { useRenderList } from './use-render-list'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mode: string;
|
mode: string
|
||||||
isChainMode?: boolean;
|
isChainMode?: boolean
|
||||||
chainConfigData?: string | null;
|
chainConfigData?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProxyChainItem {
|
interface ProxyChainItem {
|
||||||
id: string;
|
id: string
|
||||||
name: string;
|
name: string
|
||||||
type?: string;
|
type?: string
|
||||||
delay?: number;
|
delay?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const VirtuosoFooter = () => <div style={{ height: "8px" }} />;
|
const VirtuosoFooter = () => <div style={{ height: '8px' }} />
|
||||||
|
|
||||||
export const ProxyGroups = (props: Props) => {
|
export const ProxyGroups = (props: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const { mode, isChainMode = false, chainConfigData } = props;
|
const { mode, isChainMode = false, chainConfigData } = props
|
||||||
const [proxyChain, setProxyChain] = useState<ProxyChainItem[]>(() => {
|
const [proxyChain, setProxyChain] = useState<ProxyChainItem[]>(() => {
|
||||||
try {
|
try {
|
||||||
const saved = localStorage.getItem("proxy-chain-items");
|
const saved = localStorage.getItem('proxy-chain-items')
|
||||||
if (saved) {
|
if (saved) {
|
||||||
return JSON.parse(saved);
|
return JSON.parse(saved)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
return [];
|
return []
|
||||||
});
|
})
|
||||||
const [selectedGroup, setSelectedGroup] = useState<string | null>(null);
|
const [selectedGroup, setSelectedGroup] = useState<string | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (proxyChain.length > 0) {
|
if (proxyChain.length > 0) {
|
||||||
localStorage.setItem("proxy-chain-items", JSON.stringify(proxyChain));
|
localStorage.setItem('proxy-chain-items', JSON.stringify(proxyChain))
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem("proxy-chain-items");
|
localStorage.removeItem('proxy-chain-items')
|
||||||
}
|
}
|
||||||
}, [proxyChain]);
|
}, [proxyChain])
|
||||||
const [ruleMenuAnchor, setRuleMenuAnchor] = useState<null | HTMLElement>(
|
const [ruleMenuAnchor, setRuleMenuAnchor] = useState<null | HTMLElement>(null)
|
||||||
null,
|
|
||||||
);
|
|
||||||
const [duplicateWarning, setDuplicateWarning] = useState<{
|
const [duplicateWarning, setDuplicateWarning] = useState<{
|
||||||
open: boolean;
|
open: boolean
|
||||||
message: string;
|
message: string
|
||||||
}>({ open: false, message: "" });
|
}>({ open: false, message: '' })
|
||||||
|
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge()
|
||||||
const { proxies: proxiesData } = useAppData();
|
const { proxies: proxiesData } = useAppData()
|
||||||
const groups = proxiesData?.groups;
|
const groups = proxiesData?.groups
|
||||||
const availableGroups = useMemo(() => {
|
const availableGroups = useMemo(() => {
|
||||||
if (!groups) return [];
|
if (!groups) return []
|
||||||
// 在链式代理模式下,仅显示支持选择节点的 Selector 代理组
|
// 在链式代理模式下,仅显示支持选择节点的 Selector 代理组
|
||||||
return isChainMode
|
return isChainMode
|
||||||
? groups.filter((g: any) => g.type === "Selector")
|
? groups.filter((g: any) => g.type === 'Selector')
|
||||||
: groups;
|
: groups
|
||||||
}, [groups, isChainMode]);
|
}, [groups, isChainMode])
|
||||||
|
|
||||||
const defaultRuleGroup = useMemo(() => {
|
const defaultRuleGroup = useMemo(() => {
|
||||||
if (isChainMode && mode === "rule" && availableGroups.length > 0) {
|
if (isChainMode && mode === 'rule' && availableGroups.length > 0) {
|
||||||
return availableGroups[0].name;
|
return availableGroups[0].name
|
||||||
}
|
}
|
||||||
return null;
|
return null
|
||||||
}, [availableGroups, isChainMode, mode]);
|
}, [availableGroups, isChainMode, mode])
|
||||||
|
|
||||||
const activeSelectedGroup = useMemo(
|
const activeSelectedGroup = useMemo(
|
||||||
() => selectedGroup ?? defaultRuleGroup,
|
() => selectedGroup ?? defaultRuleGroup,
|
||||||
[selectedGroup, defaultRuleGroup],
|
[selectedGroup, defaultRuleGroup],
|
||||||
);
|
)
|
||||||
|
|
||||||
const { renderList, onProxies, onHeadState } = useRenderList(
|
const { renderList, onProxies, onHeadState } = useRenderList(
|
||||||
mode,
|
mode,
|
||||||
isChainMode,
|
isChainMode,
|
||||||
activeSelectedGroup,
|
activeSelectedGroup,
|
||||||
);
|
)
|
||||||
|
|
||||||
const getGroupHeadState = useCallback(
|
const getGroupHeadState = useCallback(
|
||||||
(groupName: string) => {
|
(groupName: string) => {
|
||||||
const headItem = renderList.find(
|
const headItem = renderList.find(
|
||||||
(item) => item.type === 1 && item.group?.name === groupName,
|
(item) => item.type === 1 && item.group?.name === groupName,
|
||||||
);
|
)
|
||||||
return headItem?.headState;
|
return headItem?.headState
|
||||||
},
|
},
|
||||||
[renderList],
|
[renderList],
|
||||||
);
|
)
|
||||||
|
|
||||||
// 统代理选择
|
// 统代理选择
|
||||||
const { handleProxyGroupChange } = useProxySelection({
|
const { handleProxyGroupChange } = useProxySelection({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
onProxies();
|
onProxies()
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.error("代理切换失败", error);
|
console.error('代理切换失败', error)
|
||||||
onProxies();
|
onProxies()
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const timeout = verge?.default_latency_timeout || 10000;
|
const timeout = verge?.default_latency_timeout || 10000
|
||||||
|
|
||||||
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
const virtuosoRef = useRef<VirtuosoHandle>(null)
|
||||||
const scrollPositionRef = useRef<Record<string, number>>({});
|
const scrollPositionRef = useRef<Record<string, number>>({})
|
||||||
const [showScrollTop, setShowScrollTop] = useState(false);
|
const [showScrollTop, setShowScrollTop] = useState(false)
|
||||||
const scrollerRef = useRef<Element | null>(null);
|
const scrollerRef = useRef<Element | null>(null)
|
||||||
|
|
||||||
// 从 localStorage 恢复滚动位置
|
// 从 localStorage 恢复滚动位置
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (renderList.length === 0) return;
|
if (renderList.length === 0) return
|
||||||
|
|
||||||
let restoreTimer: ReturnType<typeof setTimeout> | null = null;
|
let restoreTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const savedPositions = localStorage.getItem("proxy-scroll-positions");
|
const savedPositions = localStorage.getItem('proxy-scroll-positions')
|
||||||
if (savedPositions) {
|
if (savedPositions) {
|
||||||
const positions = JSON.parse(savedPositions);
|
const positions = JSON.parse(savedPositions)
|
||||||
scrollPositionRef.current = positions;
|
scrollPositionRef.current = positions
|
||||||
const savedPosition = positions[mode];
|
const savedPosition = positions[mode]
|
||||||
|
|
||||||
if (savedPosition !== undefined) {
|
if (savedPosition !== undefined) {
|
||||||
restoreTimer = setTimeout(() => {
|
restoreTimer = setTimeout(() => {
|
||||||
virtuosoRef.current?.scrollTo({
|
virtuosoRef.current?.scrollTo({
|
||||||
top: savedPosition,
|
top: savedPosition,
|
||||||
behavior: "auto",
|
behavior: 'auto',
|
||||||
});
|
})
|
||||||
}, 100);
|
}, 100)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error restoring scroll position:", e);
|
console.error('Error restoring scroll position:', e)
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (restoreTimer) {
|
if (restoreTimer) {
|
||||||
clearTimeout(restoreTimer);
|
clearTimeout(restoreTimer)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}, [mode, renderList.length]);
|
}, [mode, renderList.length])
|
||||||
|
|
||||||
// 改为使用节流函数保存滚动位置
|
// 改为使用节流函数保存滚动位置
|
||||||
const saveScrollPosition = useCallback(
|
const saveScrollPosition = useCallback(
|
||||||
(scrollTop: number) => {
|
(scrollTop: number) => {
|
||||||
try {
|
try {
|
||||||
scrollPositionRef.current[mode] = scrollTop;
|
scrollPositionRef.current[mode] = scrollTop
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"proxy-scroll-positions",
|
'proxy-scroll-positions',
|
||||||
JSON.stringify(scrollPositionRef.current),
|
JSON.stringify(scrollPositionRef.current),
|
||||||
);
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error saving scroll position:", e);
|
console.error('Error saving scroll position:', e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[mode],
|
[mode],
|
||||||
);
|
)
|
||||||
|
|
||||||
// 使用改进的滚动处理
|
// 使用改进的滚动处理
|
||||||
const handleScroll = useMemo(
|
const handleScroll = useMemo(
|
||||||
() =>
|
() =>
|
||||||
throttle((event: Event) => {
|
throttle((event: Event) => {
|
||||||
const target = event.target as HTMLElement | null;
|
const target = event.target as HTMLElement | null
|
||||||
const scrollTop = target?.scrollTop ?? 0;
|
const scrollTop = target?.scrollTop ?? 0
|
||||||
setShowScrollTop(scrollTop > 100);
|
setShowScrollTop(scrollTop > 100)
|
||||||
// 使用稳定的节流来保存位置,而不是setTimeout
|
// 使用稳定的节流来保存位置,而不是setTimeout
|
||||||
saveScrollPosition(scrollTop);
|
saveScrollPosition(scrollTop)
|
||||||
}, 500), // 增加到500ms以确保平滑滚动
|
}, 500), // 增加到500ms以确保平滑滚动
|
||||||
[saveScrollPosition],
|
[saveScrollPosition],
|
||||||
);
|
)
|
||||||
|
|
||||||
// 添加和清理滚动事件监听器
|
// 添加和清理滚动事件监听器
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const node = scrollerRef.current;
|
const node = scrollerRef.current
|
||||||
if (!node) return;
|
if (!node) return
|
||||||
|
|
||||||
const listener = handleScroll as EventListener;
|
const listener = handleScroll as EventListener
|
||||||
const options: AddEventListenerOptions = { passive: true };
|
const options: AddEventListenerOptions = { passive: true }
|
||||||
|
|
||||||
node.addEventListener("scroll", listener, options);
|
node.addEventListener('scroll', listener, options)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
node.removeEventListener("scroll", listener, options);
|
node.removeEventListener('scroll', listener, options)
|
||||||
};
|
}
|
||||||
}, [handleScroll]);
|
}, [handleScroll])
|
||||||
|
|
||||||
// 滚动到顶部
|
// 滚动到顶部
|
||||||
const scrollToTop = useCallback(() => {
|
const scrollToTop = useCallback(() => {
|
||||||
virtuosoRef.current?.scrollTo?.({
|
virtuosoRef.current?.scrollTo?.({
|
||||||
top: 0,
|
top: 0,
|
||||||
behavior: "smooth",
|
behavior: 'smooth',
|
||||||
});
|
})
|
||||||
saveScrollPosition(0);
|
saveScrollPosition(0)
|
||||||
}, [saveScrollPosition]);
|
}, [saveScrollPosition])
|
||||||
|
|
||||||
// 关闭重复节点警告
|
// 关闭重复节点警告
|
||||||
const handleCloseDuplicateWarning = useCallback(() => {
|
const handleCloseDuplicateWarning = useCallback(() => {
|
||||||
setDuplicateWarning({ open: false, message: "" });
|
setDuplicateWarning({ open: false, message: '' })
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
const currentGroup = useMemo(() => {
|
const currentGroup = useMemo(() => {
|
||||||
if (!activeSelectedGroup) return null;
|
if (!activeSelectedGroup) return null
|
||||||
return (
|
return (
|
||||||
availableGroups.find(
|
availableGroups.find(
|
||||||
(group: any) => group.name === activeSelectedGroup,
|
(group: any) => group.name === activeSelectedGroup,
|
||||||
) ?? null
|
) ?? null
|
||||||
);
|
)
|
||||||
}, [activeSelectedGroup, availableGroups]);
|
}, [activeSelectedGroup, availableGroups])
|
||||||
|
|
||||||
// 处理代理组选择菜单
|
// 处理代理组选择菜单
|
||||||
const handleGroupMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
const handleGroupMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
setRuleMenuAnchor(event.currentTarget);
|
setRuleMenuAnchor(event.currentTarget)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleGroupMenuClose = () => {
|
const handleGroupMenuClose = () => {
|
||||||
setRuleMenuAnchor(null);
|
setRuleMenuAnchor(null)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleGroupSelect = (groupName: string) => {
|
const handleGroupSelect = (groupName: string) => {
|
||||||
setSelectedGroup(groupName);
|
setSelectedGroup(groupName)
|
||||||
handleGroupMenuClose();
|
handleGroupMenuClose()
|
||||||
|
|
||||||
if (isChainMode && mode === "rule") {
|
if (isChainMode && mode === 'rule') {
|
||||||
updateProxyChainConfigInRuntime(null);
|
updateProxyChainConfigInRuntime(null)
|
||||||
localStorage.removeItem("proxy-chain-group");
|
localStorage.removeItem('proxy-chain-group')
|
||||||
localStorage.removeItem("proxy-chain-exit-node");
|
localStorage.removeItem('proxy-chain-exit-node')
|
||||||
localStorage.removeItem("proxy-chain-items");
|
localStorage.removeItem('proxy-chain-items')
|
||||||
setProxyChain([]);
|
setProxyChain([])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangeProxy = useCallback(
|
const handleChangeProxy = useCallback(
|
||||||
(group: IProxyGroupItem, proxy: IProxyItem) => {
|
(group: IProxyGroupItem, proxy: IProxyItem) => {
|
||||||
@@ -265,69 +263,69 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
setProxyChain((prev) => {
|
setProxyChain((prev) => {
|
||||||
// 检查是否已经存在相同名称的代理,防止重复添加
|
// 检查是否已经存在相同名称的代理,防止重复添加
|
||||||
if (prev.some((item) => item.name === proxy.name)) {
|
if (prev.some((item) => item.name === proxy.name)) {
|
||||||
const warningMessage = t("proxies.page.chain.duplicateNode");
|
const warningMessage = t('proxies.page.chain.duplicateNode')
|
||||||
setDuplicateWarning({
|
setDuplicateWarning({
|
||||||
open: true,
|
open: true,
|
||||||
message: warningMessage,
|
message: warningMessage,
|
||||||
});
|
})
|
||||||
return prev; // 返回原来的状态,不做任何更改
|
return prev // 返回原来的状态,不做任何更改
|
||||||
}
|
}
|
||||||
|
|
||||||
// 安全获取延迟数据,如果没有延迟数据则设为 undefined
|
// 安全获取延迟数据,如果没有延迟数据则设为 undefined
|
||||||
const delay =
|
const delay =
|
||||||
proxy.history && proxy.history.length > 0
|
proxy.history && proxy.history.length > 0
|
||||||
? proxy.history[proxy.history.length - 1].delay
|
? proxy.history[proxy.history.length - 1].delay
|
||||||
: undefined;
|
: undefined
|
||||||
|
|
||||||
const chainItem: ProxyChainItem = {
|
const chainItem: ProxyChainItem = {
|
||||||
id: `${proxy.name}_${Date.now()}`,
|
id: `${proxy.name}_${Date.now()}`,
|
||||||
name: proxy.name,
|
name: proxy.name,
|
||||||
type: proxy.type,
|
type: proxy.type,
|
||||||
delay: delay,
|
delay: delay,
|
||||||
};
|
|
||||||
|
|
||||||
return [...prev, chainItem];
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!["Selector", "URLTest", "Fallback"].includes(group.type)) return;
|
return [...prev, chainItem]
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
handleProxyGroupChange(group, proxy);
|
if (!['Selector', 'URLTest', 'Fallback'].includes(group.type)) return
|
||||||
|
|
||||||
|
handleProxyGroupChange(group, proxy)
|
||||||
},
|
},
|
||||||
[handleProxyGroupChange, isChainMode, t],
|
[handleProxyGroupChange, isChainMode, t],
|
||||||
);
|
)
|
||||||
|
|
||||||
// 测全部延迟
|
// 测全部延迟
|
||||||
const handleCheckAll = useLockFn(async (groupName: string) => {
|
const handleCheckAll = useLockFn(async (groupName: string) => {
|
||||||
debugLog(`[ProxyGroups] 开始测试所有延迟,组: ${groupName}`);
|
debugLog(`[ProxyGroups] 开始测试所有延迟,组: ${groupName}`)
|
||||||
|
|
||||||
const proxies = renderList
|
const proxies = renderList
|
||||||
.filter(
|
.filter(
|
||||||
(e) => e.group?.name === groupName && (e.type === 2 || e.type === 4),
|
(e) => e.group?.name === groupName && (e.type === 2 || e.type === 4),
|
||||||
)
|
)
|
||||||
.flatMap((e) => e.proxyCol || e.proxy!)
|
.flatMap((e) => e.proxyCol || e.proxy!)
|
||||||
.filter(Boolean);
|
.filter(Boolean)
|
||||||
|
|
||||||
debugLog(`[ProxyGroups] 找到代理数量: ${proxies.length}`);
|
debugLog(`[ProxyGroups] 找到代理数量: ${proxies.length}`)
|
||||||
|
|
||||||
const providers = new Set(proxies.map((p) => p!.provider!).filter(Boolean));
|
const providers = new Set(proxies.map((p) => p!.provider!).filter(Boolean))
|
||||||
|
|
||||||
if (providers.size) {
|
if (providers.size) {
|
||||||
debugLog(`[ProxyGroups] 发现提供者,数量: ${providers.size}`);
|
debugLog(`[ProxyGroups] 发现提供者,数量: ${providers.size}`)
|
||||||
Promise.allSettled(
|
Promise.allSettled(
|
||||||
[...providers].map((p) => healthcheckProxyProvider(p)),
|
[...providers].map((p) => healthcheckProxyProvider(p)),
|
||||||
).then(() => {
|
).then(() => {
|
||||||
debugLog(`[ProxyGroups] 提供者健康检查完成`);
|
debugLog(`[ProxyGroups] 提供者健康检查完成`)
|
||||||
onProxies();
|
onProxies()
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const names = proxies.filter((p) => !p!.provider).map((p) => p!.name);
|
const names = proxies.filter((p) => !p!.provider).map((p) => p!.name)
|
||||||
debugLog(`[ProxyGroups] 过滤后需要测试的代理数量: ${names.length}`);
|
debugLog(`[ProxyGroups] 过滤后需要测试的代理数量: ${names.length}`)
|
||||||
|
|
||||||
const url = delayManager.getUrl(groupName);
|
const url = delayManager.getUrl(groupName)
|
||||||
debugLog(`[ProxyGroups] 测试URL: ${url}, 超时: ${timeout}ms`);
|
debugLog(`[ProxyGroups] 测试URL: ${url}, 超时: ${timeout}ms`)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
@@ -336,116 +334,116 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
debugLog(
|
debugLog(
|
||||||
`[ProxyGroups] getGroupProxyDelays返回结果数量:`,
|
`[ProxyGroups] getGroupProxyDelays返回结果数量:`,
|
||||||
Object.keys(result || {}).length,
|
Object.keys(result || {}).length,
|
||||||
);
|
)
|
||||||
}), // 查询group delays 将清除fixed(不关注调用结果)
|
}), // 查询group delays 将清除fixed(不关注调用结果)
|
||||||
]);
|
])
|
||||||
debugLog(`[ProxyGroups] 延迟测试完成,组: ${groupName}`);
|
debugLog(`[ProxyGroups] 延迟测试完成,组: ${groupName}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[ProxyGroups] 延迟测试出错,组: ${groupName}`, error);
|
console.error(`[ProxyGroups] 延迟测试出错,组: ${groupName}`, error)
|
||||||
} finally {
|
} finally {
|
||||||
const headState = getGroupHeadState(groupName);
|
const headState = getGroupHeadState(groupName)
|
||||||
if (headState?.sortType === 1) {
|
if (headState?.sortType === 1) {
|
||||||
onHeadState(groupName, { sortType: headState.sortType });
|
onHeadState(groupName, { sortType: headState.sortType })
|
||||||
}
|
}
|
||||||
onProxies();
|
onProxies()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
// 滚到对应的节点
|
// 滚到对应的节点
|
||||||
const handleLocation = (group: IProxyGroupItem) => {
|
const handleLocation = (group: IProxyGroupItem) => {
|
||||||
if (!group) return;
|
if (!group) return
|
||||||
const { name, now } = group;
|
const { name, now } = group
|
||||||
|
|
||||||
const index = renderList.findIndex(
|
const index = renderList.findIndex(
|
||||||
(e) =>
|
(e) =>
|
||||||
e.group?.name === name &&
|
e.group?.name === name &&
|
||||||
((e.type === 2 && e.proxy?.name === now) ||
|
((e.type === 2 && e.proxy?.name === now) ||
|
||||||
(e.type === 4 && e.proxyCol?.some((p) => p.name === now))),
|
(e.type === 4 && e.proxyCol?.some((p) => p.name === now))),
|
||||||
);
|
)
|
||||||
|
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
virtuosoRef.current?.scrollToIndex?.({
|
virtuosoRef.current?.scrollToIndex?.({
|
||||||
index,
|
index,
|
||||||
align: "center",
|
align: 'center',
|
||||||
behavior: "smooth",
|
behavior: 'smooth',
|
||||||
});
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// 定位到指定的代理组
|
// 定位到指定的代理组
|
||||||
const handleGroupLocationByName = useCallback(
|
const handleGroupLocationByName = useCallback(
|
||||||
(groupName: string) => {
|
(groupName: string) => {
|
||||||
const index = renderList.findIndex(
|
const index = renderList.findIndex(
|
||||||
(item) => item.type === 0 && item.group?.name === groupName,
|
(item) => item.type === 0 && item.group?.name === groupName,
|
||||||
);
|
)
|
||||||
|
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
virtuosoRef.current?.scrollToIndex?.({
|
virtuosoRef.current?.scrollToIndex?.({
|
||||||
index,
|
index,
|
||||||
align: "start",
|
align: 'start',
|
||||||
behavior: "smooth",
|
behavior: 'smooth',
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[renderList],
|
[renderList],
|
||||||
);
|
)
|
||||||
|
|
||||||
const proxyGroupNames = useMemo(() => {
|
const proxyGroupNames = useMemo(() => {
|
||||||
const names = renderList
|
const names = renderList
|
||||||
.filter((item) => item.type === 0 && item.group?.name)
|
.filter((item) => item.type === 0 && item.group?.name)
|
||||||
.map((item) => item.group!.name);
|
.map((item) => item.group!.name)
|
||||||
return Array.from(new Set(names));
|
return Array.from(new Set(names))
|
||||||
}, [renderList]);
|
}, [renderList])
|
||||||
|
|
||||||
if (mode === "direct") {
|
if (mode === 'direct') {
|
||||||
return <BaseEmpty textKey="proxies.page.messages.directMode" />;
|
return <BaseEmpty textKey="proxies.page.messages.directMode" />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isChainMode) {
|
if (isChainMode) {
|
||||||
// 获取所有代理组
|
// 获取所有代理组
|
||||||
const proxyGroups = proxiesData?.groups || [];
|
const proxyGroups = proxiesData?.groups || []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box sx={{ display: "flex", height: "100%", gap: 2 }}>
|
<Box sx={{ display: 'flex', height: '100%', gap: 2 }}>
|
||||||
<Box sx={{ flex: 1, position: "relative" }}>
|
<Box sx={{ flex: 1, position: 'relative' }}>
|
||||||
{/* 代理规则标题和代理组按钮栏 */}
|
{/* 代理规则标题和代理组按钮栏 */}
|
||||||
{mode === "rule" && proxyGroups.length > 0 && (
|
{mode === 'rule' && proxyGroups.length > 0 && (
|
||||||
<Box sx={{ borderBottom: "1px solid", borderColor: "divider" }}>
|
<Box sx={{ borderBottom: '1px solid', borderColor: 'divider' }}>
|
||||||
{/* 代理规则标题 */}
|
{/* 代理规则标题 */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
px: 2,
|
px: 2,
|
||||||
py: 1.5,
|
py: 1.5,
|
||||||
borderBottom: "1px solid",
|
borderBottom: '1px solid',
|
||||||
borderColor: "divider",
|
borderColor: 'divider',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "space-between",
|
justifyContent: 'space-between',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
<Typography
|
<Typography
|
||||||
variant="h6"
|
variant="h6"
|
||||||
sx={{ fontWeight: 600, fontSize: "16px" }}
|
sx={{ fontWeight: 600, fontSize: '16px' }}
|
||||||
>
|
>
|
||||||
{t("proxies.page.rules.title")}
|
{t('proxies.page.rules.title')}
|
||||||
</Typography>
|
</Typography>
|
||||||
{currentGroup && (
|
{currentGroup && (
|
||||||
<Box
|
<Box
|
||||||
sx={{ display: "flex", alignItems: "center", gap: 1 }}
|
sx={{ display: 'flex', alignItems: 'center', gap: 1 }}
|
||||||
>
|
>
|
||||||
<Chip
|
<Chip
|
||||||
size="small"
|
size="small"
|
||||||
label={`${currentGroup.name} (${currentGroup.type})`}
|
label={`${currentGroup.name} (${currentGroup.type})`}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: "12px",
|
fontSize: '12px',
|
||||||
maxWidth: "200px",
|
maxWidth: '200px',
|
||||||
"& .MuiChip-label": {
|
'& .MuiChip-label': {
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
textOverflow: "ellipsis",
|
textOverflow: 'ellipsis',
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: 'nowrap',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -458,17 +456,17 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
size="small"
|
size="small"
|
||||||
onClick={handleGroupMenuOpen}
|
onClick={handleGroupMenuOpen}
|
||||||
sx={{
|
sx={{
|
||||||
border: "1px solid",
|
border: '1px solid',
|
||||||
borderColor: "divider",
|
borderColor: 'divider',
|
||||||
borderRadius: "4px",
|
borderRadius: '4px',
|
||||||
padding: "4px 8px",
|
padding: '4px 8px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
sx={{ mr: 0.5, fontSize: "12px" }}
|
sx={{ mr: 0.5, fontSize: '12px' }}
|
||||||
>
|
>
|
||||||
{t("proxies.page.rules.select")}
|
{t('proxies.page.rules.select')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<ExpandMoreRounded fontSize="small" />
|
<ExpandMoreRounded fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -481,16 +479,16 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
ref={virtuosoRef}
|
ref={virtuosoRef}
|
||||||
style={{
|
style={{
|
||||||
height:
|
height:
|
||||||
mode === "rule" && proxyGroups.length > 0
|
mode === 'rule' && proxyGroups.length > 0
|
||||||
? "calc(100% - 80px)" // 只有标题的高度
|
? 'calc(100% - 80px)' // 只有标题的高度
|
||||||
: "calc(100% - 14px)",
|
: 'calc(100% - 14px)',
|
||||||
}}
|
}}
|
||||||
totalCount={renderList.length}
|
totalCount={renderList.length}
|
||||||
increaseViewportBy={{ top: 200, bottom: 200 }}
|
increaseViewportBy={{ top: 200, bottom: 200 }}
|
||||||
overscan={150}
|
overscan={150}
|
||||||
defaultItemHeight={56}
|
defaultItemHeight={56}
|
||||||
scrollerRef={(ref) => {
|
scrollerRef={(ref) => {
|
||||||
scrollerRef.current = ref as Element;
|
scrollerRef.current = ref as Element
|
||||||
}}
|
}}
|
||||||
components={{
|
components={{
|
||||||
Footer: VirtuosoFooter,
|
Footer: VirtuosoFooter,
|
||||||
@@ -501,7 +499,7 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
<ProxyRender
|
<ProxyRender
|
||||||
key={renderList[index].key}
|
key={renderList[index].key}
|
||||||
item={renderList[index]}
|
item={renderList[index]}
|
||||||
indent={mode === "rule" || mode === "script"}
|
indent={mode === 'rule' || mode === 'script'}
|
||||||
onLocation={handleLocation}
|
onLocation={handleLocation}
|
||||||
onCheckAll={handleCheckAll}
|
onCheckAll={handleCheckAll}
|
||||||
onHeadState={onHeadState}
|
onHeadState={onHeadState}
|
||||||
@@ -513,7 +511,7 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
<ScrollTopButton show={showScrollTop} onClick={scrollToTop} />
|
<ScrollTopButton show={showScrollTop} onClick={scrollToTop} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ width: "400px", minWidth: "300px" }}>
|
<Box sx={{ width: '400px', minWidth: '300px' }}>
|
||||||
<ProxyChain
|
<ProxyChain
|
||||||
proxyChain={proxyChain}
|
proxyChain={proxyChain}
|
||||||
onUpdateChain={setProxyChain}
|
onUpdateChain={setProxyChain}
|
||||||
@@ -528,7 +526,7 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
open={duplicateWarning.open}
|
open={duplicateWarning.open}
|
||||||
autoHideDuration={3000}
|
autoHideDuration={3000}
|
||||||
onClose={handleCloseDuplicateWarning}
|
onClose={handleCloseDuplicateWarning}
|
||||||
anchorOrigin={{ vertical: "top", horizontal: "center" }}
|
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||||
>
|
>
|
||||||
<Alert
|
<Alert
|
||||||
onClose={handleCloseDuplicateWarning}
|
onClose={handleCloseDuplicateWarning}
|
||||||
@@ -559,15 +557,15 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
onClick={() => handleGroupSelect(group.name)}
|
onClick={() => handleGroupSelect(group.name)}
|
||||||
selected={activeSelectedGroup === group.name}
|
selected={activeSelectedGroup === group.name}
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: "14px",
|
fontSize: '14px',
|
||||||
py: 1,
|
py: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
flexDirection: "column",
|
flexDirection: 'column',
|
||||||
alignItems: "flex-start",
|
alignItems: 'flex-start',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="body2" sx={{ fontWeight: 500 }}>
|
<Typography variant="body2" sx={{ fontWeight: 500 }}>
|
||||||
@@ -588,15 +586,15 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
)}
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{ position: "relative", height: "100%", willChange: "transform" }}
|
style={{ position: 'relative', height: '100%', willChange: 'transform' }}
|
||||||
>
|
>
|
||||||
{/* 代理组导航栏 */}
|
{/* 代理组导航栏 */}
|
||||||
{mode === "rule" && (
|
{mode === 'rule' && (
|
||||||
<ProxyGroupNavigator
|
<ProxyGroupNavigator
|
||||||
proxyGroupNames={proxyGroupNames}
|
proxyGroupNames={proxyGroupNames}
|
||||||
onGroupLocation={handleGroupLocationByName}
|
onGroupLocation={handleGroupLocationByName}
|
||||||
@@ -607,13 +605,13 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
|
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
ref={virtuosoRef}
|
ref={virtuosoRef}
|
||||||
style={{ height: "calc(100% - 14px)" }}
|
style={{ height: 'calc(100% - 14px)' }}
|
||||||
totalCount={renderList.length}
|
totalCount={renderList.length}
|
||||||
increaseViewportBy={{ top: 200, bottom: 200 }}
|
increaseViewportBy={{ top: 200, bottom: 200 }}
|
||||||
overscan={150}
|
overscan={150}
|
||||||
defaultItemHeight={56}
|
defaultItemHeight={56}
|
||||||
scrollerRef={(ref) => {
|
scrollerRef={(ref) => {
|
||||||
scrollerRef.current = ref as Element;
|
scrollerRef.current = ref as Element
|
||||||
}}
|
}}
|
||||||
components={{
|
components={{
|
||||||
Footer: VirtuosoFooter,
|
Footer: VirtuosoFooter,
|
||||||
@@ -625,7 +623,7 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
<ProxyRender
|
<ProxyRender
|
||||||
key={renderList[index].key}
|
key={renderList[index].key}
|
||||||
item={renderList[index]}
|
item={renderList[index]}
|
||||||
indent={mode === "rule" || mode === "script"}
|
indent={mode === 'rule' || mode === 'script'}
|
||||||
onLocation={handleLocation}
|
onLocation={handleLocation}
|
||||||
onCheckAll={handleCheckAll}
|
onCheckAll={handleCheckAll}
|
||||||
onHeadState={onHeadState}
|
onHeadState={onHeadState}
|
||||||
@@ -635,34 +633,34 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
<ScrollTopButton show={showScrollTop} onClick={scrollToTop} />
|
<ScrollTopButton show={showScrollTop} onClick={scrollToTop} />
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
// 替换简单防抖函数为更优的节流函数
|
// 替换简单防抖函数为更优的节流函数
|
||||||
function throttle<T extends (...args: any[]) => any>(
|
function throttle<T extends (...args: any[]) => any>(
|
||||||
func: T,
|
func: T,
|
||||||
wait: number,
|
wait: number,
|
||||||
): (...args: Parameters<T>) => void {
|
): (...args: Parameters<T>) => void {
|
||||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
let timer: ReturnType<typeof setTimeout> | null = null
|
||||||
let previous = 0;
|
let previous = 0
|
||||||
|
|
||||||
return function (...args: Parameters<T>) {
|
return function (...args: Parameters<T>) {
|
||||||
const now = Date.now();
|
const now = Date.now()
|
||||||
const remaining = wait - (now - previous);
|
const remaining = wait - (now - previous)
|
||||||
|
|
||||||
if (remaining <= 0 || remaining > wait) {
|
if (remaining <= 0 || remaining > wait) {
|
||||||
if (timer) {
|
if (timer) {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer)
|
||||||
timer = null;
|
timer = null
|
||||||
}
|
}
|
||||||
previous = now;
|
previous = now
|
||||||
func(...args);
|
func(...args)
|
||||||
} else if (!timer) {
|
} else if (!timer) {
|
||||||
timer = setTimeout(() => {
|
timer = setTimeout(() => {
|
||||||
previous = Date.now();
|
previous = Date.now()
|
||||||
timer = null;
|
timer = null
|
||||||
func(...args);
|
func(...args)
|
||||||
}, remaining);
|
}, remaining)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,30 +10,30 @@ import {
|
|||||||
WifiTetheringOffRounded,
|
WifiTetheringOffRounded,
|
||||||
SortByAlphaRounded,
|
SortByAlphaRounded,
|
||||||
SortRounded,
|
SortRounded,
|
||||||
} from "@mui/icons-material";
|
} from '@mui/icons-material'
|
||||||
import { Box, IconButton, TextField, SxProps } from "@mui/material";
|
import { Box, IconButton, TextField, SxProps } from '@mui/material'
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { BaseSearchBox } from "@/components/base";
|
import { BaseSearchBox } from '@/components/base'
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
import delayManager from "@/services/delay";
|
import delayManager from '@/services/delay'
|
||||||
import { debugLog } from "@/utils/debug";
|
import { debugLog } from '@/utils/debug'
|
||||||
|
|
||||||
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'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
sx?: SxProps;
|
sx?: SxProps
|
||||||
url?: string;
|
url?: string
|
||||||
groupName: string;
|
groupName: string
|
||||||
headState: HeadState;
|
headState: HeadState
|
||||||
onLocation: () => void;
|
onLocation: () => void
|
||||||
onCheckDelay: () => void;
|
onCheckDelay: () => void
|
||||||
onHeadState: (val: Partial<HeadState>) => void;
|
onHeadState: (val: Partial<HeadState>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultSx: SxProps = {};
|
const defaultSx: SxProps = {}
|
||||||
|
|
||||||
export const ProxyHead = ({
|
export const ProxyHead = ({
|
||||||
sx = defaultSx,
|
sx = defaultSx,
|
||||||
@@ -53,31 +53,31 @@ export const ProxyHead = ({
|
|||||||
filterMatchCase,
|
filterMatchCase,
|
||||||
filterMatchWholeWord,
|
filterMatchWholeWord,
|
||||||
filterUseRegularExpression,
|
filterUseRegularExpression,
|
||||||
} = headState;
|
} = headState
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const [autoFocus, setAutoFocus] = useState(false);
|
const [autoFocus, setAutoFocus] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// fix the focus conflict
|
// fix the focus conflict
|
||||||
const timer = setTimeout(() => setAutoFocus(true), 100);
|
const timer = setTimeout(() => setAutoFocus(true), 100)
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer)
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge()
|
||||||
const defaultLatencyUrl =
|
const defaultLatencyUrl =
|
||||||
verge?.default_latency_test?.trim() || "http://cp.cloudflare.com";
|
verge?.default_latency_test?.trim() || 'http://cp.cloudflare.com'
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
delayManager.setUrl(groupName, testUrl?.trim() || url || defaultLatencyUrl);
|
delayManager.setUrl(groupName, testUrl?.trim() || url || defaultLatencyUrl)
|
||||||
}, [groupName, testUrl, defaultLatencyUrl, url]);
|
}, [groupName, testUrl, defaultLatencyUrl, url])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5, ...sx }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, ...sx }}>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
title={t("proxies.page.tooltips.locate")}
|
title={t('proxies.page.tooltips.locate')}
|
||||||
onClick={onLocation}
|
onClick={onLocation}
|
||||||
>
|
>
|
||||||
<MyLocationRounded />
|
<MyLocationRounded />
|
||||||
@@ -86,15 +86,15 @@ export const ProxyHead = ({
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
title={t("proxies.page.tooltips.delayCheck")}
|
title={t('proxies.page.tooltips.delayCheck')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
debugLog(`[ProxyHead] 点击延迟测试按钮,组: ${groupName}`);
|
debugLog(`[ProxyHead] 点击延迟测试按钮,组: ${groupName}`)
|
||||||
// Remind the user that it is custom test url
|
// Remind the user that it is custom test url
|
||||||
if (testUrl?.trim() && textState !== "filter") {
|
if (testUrl?.trim() && textState !== 'filter') {
|
||||||
debugLog(`[ProxyHead] 使用自定义测试URL: ${testUrl}`);
|
debugLog(`[ProxyHead] 使用自定义测试URL: ${testUrl}`)
|
||||||
onHeadState({ textState: "url" });
|
onHeadState({ textState: 'url' })
|
||||||
}
|
}
|
||||||
onCheckDelay();
|
onCheckDelay()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<NetworkCheckRounded />
|
<NetworkCheckRounded />
|
||||||
@@ -105,9 +105,9 @@ export const ProxyHead = ({
|
|||||||
color="inherit"
|
color="inherit"
|
||||||
title={
|
title={
|
||||||
[
|
[
|
||||||
t("proxies.page.tooltips.sortDefault"),
|
t('proxies.page.tooltips.sortDefault'),
|
||||||
t("proxies.page.tooltips.sortDelay"),
|
t('proxies.page.tooltips.sortDelay'),
|
||||||
t("proxies.page.tooltips.sortName"),
|
t('proxies.page.tooltips.sortName'),
|
||||||
][sortType]
|
][sortType]
|
||||||
}
|
}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -122,12 +122,12 @@ export const ProxyHead = ({
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
title={t("proxies.page.tooltips.delayCheckUrl")}
|
title={t('proxies.page.tooltips.delayCheckUrl')}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onHeadState({ textState: textState === "url" ? null : "url" })
|
onHeadState({ textState: textState === 'url' ? null : 'url' })
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{textState === "url" ? (
|
{textState === 'url' ? (
|
||||||
<WifiTetheringRounded />
|
<WifiTetheringRounded />
|
||||||
) : (
|
) : (
|
||||||
<WifiTetheringOffRounded />
|
<WifiTetheringOffRounded />
|
||||||
@@ -139,8 +139,8 @@ export const ProxyHead = ({
|
|||||||
color="inherit"
|
color="inherit"
|
||||||
title={
|
title={
|
||||||
showType
|
showType
|
||||||
? t("proxies.page.tooltips.showBasic")
|
? t('proxies.page.tooltips.showBasic')
|
||||||
: t("proxies.page.tooltips.showDetail")
|
: t('proxies.page.tooltips.showDetail')
|
||||||
}
|
}
|
||||||
onClick={() => onHeadState({ showType: !showType })}
|
onClick={() => onHeadState({ showType: !showType })}
|
||||||
>
|
>
|
||||||
@@ -150,20 +150,20 @@ export const ProxyHead = ({
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
title={t("proxies.page.tooltips.filter")}
|
title={t('proxies.page.tooltips.filter')}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onHeadState({ textState: textState === "filter" ? null : "filter" })
|
onHeadState({ textState: textState === 'filter' ? null : 'filter' })
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{textState === "filter" ? (
|
{textState === 'filter' ? (
|
||||||
<FilterAltRounded />
|
<FilterAltRounded />
|
||||||
) : (
|
) : (
|
||||||
<FilterAltOffRounded />
|
<FilterAltOffRounded />
|
||||||
)}
|
)}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
{textState === "filter" && (
|
{textState === 'filter' && (
|
||||||
<Box sx={{ ml: 0.5, flex: "1 1 auto" }}>
|
<Box sx={{ ml: 0.5, flex: '1 1 auto' }}>
|
||||||
<BaseSearchBox
|
<BaseSearchBox
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
value={filterText}
|
value={filterText}
|
||||||
@@ -184,7 +184,7 @@ export const ProxyHead = ({
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{textState === "url" && (
|
{textState === 'url' && (
|
||||||
<TextField
|
<TextField
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
@@ -193,11 +193,11 @@ export const ProxyHead = ({
|
|||||||
value={testUrl}
|
value={testUrl}
|
||||||
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 })}
|
onChange={(e) => onHeadState({ testUrl: e.target.value })}
|
||||||
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 } }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,88 +1,91 @@
|
|||||||
import { CheckCircleOutlineRounded } from "@mui/icons-material";
|
import { CheckCircleOutlineRounded } from '@mui/icons-material'
|
||||||
import { alpha, Box, ListItemButton, styled, Typography } from "@mui/material";
|
import { alpha, Box, ListItemButton, styled, Typography } from '@mui/material'
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from 'ahooks'
|
||||||
import { useCallback, useEffect, useReducer } from "react";
|
import { useCallback, useEffect, useReducer } from 'react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { BaseLoading } from "@/components/base";
|
import { BaseLoading } from '@/components/base'
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
import delayManager, { DelayUpdate } from "@/services/delay";
|
import delayManager, { DelayUpdate } from '@/services/delay'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
group: IProxyGroupItem;
|
group: IProxyGroupItem
|
||||||
proxy: IProxyItem;
|
proxy: IProxyItem
|
||||||
selected: boolean;
|
selected: boolean
|
||||||
showType?: boolean;
|
showType?: boolean
|
||||||
onClick?: (name: string) => void;
|
onClick?: (name: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
// 多列布局
|
// 多列布局
|
||||||
export const ProxyItemMini = (props: Props) => {
|
export const ProxyItemMini = (props: Props) => {
|
||||||
const { group, proxy, selected, showType = true, onClick } = props;
|
const { group, proxy, selected, showType = true, onClick } = props
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const presetList = ["DIRECT", "REJECT", "REJECT-DROP", "PASS", "COMPATIBLE"];
|
const presetList = ['DIRECT', 'REJECT', 'REJECT-DROP', 'PASS', 'COMPATIBLE']
|
||||||
const isPreset = presetList.includes(proxy.name);
|
const isPreset = presetList.includes(proxy.name)
|
||||||
// -1/<=0 为不显示,-2 为 loading
|
// -1/<=0 为不显示,-2 为 loading
|
||||||
const [delayState, setDelayState] = useReducer(
|
const [delayState, setDelayState] = useReducer(
|
||||||
(_: DelayUpdate, next: DelayUpdate) => next,
|
(_: DelayUpdate, next: DelayUpdate) => next,
|
||||||
{ delay: -1, updatedAt: 0 },
|
{
|
||||||
);
|
delay: -1,
|
||||||
const { verge } = useVerge();
|
updatedAt: 0,
|
||||||
const timeout = verge?.default_latency_timeout || 10000;
|
},
|
||||||
|
)
|
||||||
|
const { verge } = useVerge()
|
||||||
|
const timeout = verge?.default_latency_timeout || 10000
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isPreset) return;
|
if (isPreset) return
|
||||||
delayManager.setListener(proxy.name, group.name, setDelayState);
|
delayManager.setListener(proxy.name, group.name, setDelayState)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
delayManager.removeListener(proxy.name, group.name);
|
delayManager.removeListener(proxy.name, group.name)
|
||||||
};
|
}
|
||||||
}, [isPreset, proxy.name, group.name]);
|
}, [isPreset, proxy.name, group.name])
|
||||||
|
|
||||||
const updateDelay = useCallback(() => {
|
const updateDelay = useCallback(() => {
|
||||||
if (!proxy) return;
|
if (!proxy) return
|
||||||
const cachedUpdate = delayManager.getDelayUpdate(proxy.name, group.name);
|
const cachedUpdate = delayManager.getDelayUpdate(proxy.name, group.name)
|
||||||
if (cachedUpdate) {
|
if (cachedUpdate) {
|
||||||
setDelayState({ ...cachedUpdate });
|
setDelayState({ ...cachedUpdate })
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const fallbackDelay = delayManager.getDelayFix(proxy, group.name);
|
const fallbackDelay = delayManager.getDelayFix(proxy, group.name)
|
||||||
if (fallbackDelay === -1) {
|
if (fallbackDelay === -1) {
|
||||||
setDelayState({ delay: -1, updatedAt: 0 });
|
setDelayState({ delay: -1, updatedAt: 0 })
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let updatedAt = 0;
|
let updatedAt = 0
|
||||||
const history = proxy.history;
|
const history = proxy.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)
|
||||||
if (!Number.isNaN(parsed)) {
|
if (!Number.isNaN(parsed)) {
|
||||||
updatedAt = parsed;
|
updatedAt = parsed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setDelayState({
|
setDelayState({
|
||||||
delay: fallbackDelay,
|
delay: fallbackDelay,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
});
|
})
|
||||||
}, [proxy, group.name]);
|
}, [proxy, group.name])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateDelay();
|
updateDelay()
|
||||||
}, [updateDelay]);
|
}, [updateDelay])
|
||||||
|
|
||||||
const onDelay = useLockFn(async () => {
|
const onDelay = useLockFn(async () => {
|
||||||
setDelayState({ delay: -2, updatedAt: Date.now() });
|
setDelayState({ delay: -2, updatedAt: Date.now() })
|
||||||
setDelayState(
|
setDelayState(
|
||||||
await delayManager.checkDelay(proxy.name, group.name, timeout),
|
await delayManager.checkDelay(proxy.name, group.name, timeout),
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
|
|
||||||
const delayValue = delayState.delay;
|
const delayValue = delayState.delay
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
@@ -95,53 +98,53 @@ export const ProxyItemMini = (props: Props) => {
|
|||||||
borderRadius: 1.5,
|
borderRadius: 1.5,
|
||||||
pl: 1.5,
|
pl: 1.5,
|
||||||
pr: 1,
|
pr: 1,
|
||||||
justifyContent: "space-between",
|
justifyContent: 'space-between',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
({ palette: { mode, primary } }) => {
|
({ palette: { mode, primary } }) => {
|
||||||
const bgcolor = mode === "light" ? "#ffffff" : "#24252f";
|
const bgcolor = mode === 'light' ? '#ffffff' : '#24252f'
|
||||||
const showDelay = delayValue > 0;
|
const showDelay = delayValue > 0
|
||||||
const selectColor = mode === "light" ? primary.main : primary.light;
|
const selectColor = mode === 'light' ? primary.main : primary.light
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"&:hover .the-check": { display: !showDelay ? "block" : "none" },
|
'&:hover .the-check': { display: !showDelay ? 'block' : 'none' },
|
||||||
"&:hover .the-delay": { display: showDelay ? "block" : "none" },
|
'&:hover .the-delay': { display: showDelay ? 'block' : 'none' },
|
||||||
"&:hover .the-icon": { display: "none" },
|
'&:hover .the-icon': { display: 'none' },
|
||||||
"& .the-pin, & .the-unpin": {
|
'& .the-pin, & .the-unpin': {
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
fontSize: "12px",
|
fontSize: '12px',
|
||||||
top: "-5px",
|
top: '-5px',
|
||||||
right: "-5px",
|
right: '-5px',
|
||||||
},
|
},
|
||||||
"& .the-unpin": { filter: "grayscale(1)" },
|
'& .the-unpin': { filter: 'grayscale(1)' },
|
||||||
"&.Mui-selected": {
|
'&.Mui-selected': {
|
||||||
width: `calc(100% + 3px)`,
|
width: `calc(100% + 3px)`,
|
||||||
marginLeft: `-3px`,
|
marginLeft: `-3px`,
|
||||||
borderLeft: `3px solid ${selectColor}`,
|
borderLeft: `3px solid ${selectColor}`,
|
||||||
bgcolor:
|
bgcolor:
|
||||||
mode === "light"
|
mode === 'light'
|
||||||
? alpha(primary.main, 0.15)
|
? alpha(primary.main, 0.15)
|
||||||
: alpha(primary.main, 0.35),
|
: alpha(primary.main, 0.35),
|
||||||
},
|
},
|
||||||
backgroundColor: bgcolor,
|
backgroundColor: bgcolor,
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
title={`${proxy.name}\n${proxy.now ?? ""}`}
|
title={`${proxy.name}\n${proxy.now ?? ''}`}
|
||||||
sx={{ overflow: "hidden" }}
|
sx={{ overflow: 'hidden' }}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
component="div"
|
component="div"
|
||||||
color="text.primary"
|
color="text.primary"
|
||||||
sx={{
|
sx={{
|
||||||
display: "block",
|
display: 'block',
|
||||||
textOverflow: "ellipsis",
|
textOverflow: 'ellipsis',
|
||||||
wordBreak: "break-all",
|
wordBreak: 'break-all',
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{proxy.name}
|
{proxy.name}
|
||||||
@@ -150,10 +153,10 @@ export const ProxyItemMini = (props: Props) => {
|
|||||||
{showType && (
|
{showType && (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
flexWrap: "nowrap",
|
flexWrap: 'nowrap',
|
||||||
flex: "none",
|
flex: 'none',
|
||||||
marginTop: "4px",
|
marginTop: '4px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{proxy.now && (
|
{proxy.now && (
|
||||||
@@ -162,12 +165,12 @@ export const ProxyItemMini = (props: Props) => {
|
|||||||
component="div"
|
component="div"
|
||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
sx={{
|
sx={{
|
||||||
display: "block",
|
display: 'block',
|
||||||
textOverflow: "ellipsis",
|
textOverflow: 'ellipsis',
|
||||||
wordBreak: "break-all",
|
wordBreak: 'break-all',
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: 'nowrap',
|
||||||
marginRight: "8px",
|
marginRight: '8px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{proxy.now}
|
{proxy.now}
|
||||||
@@ -210,7 +213,7 @@ export const ProxyItemMini = (props: Props) => {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
sx={{ ml: 0.5, color: "primary.main", display: isPreset ? "none" : "" }}
|
sx={{ ml: 0.5, color: 'primary.main', display: isPreset ? 'none' : '' }}
|
||||||
>
|
>
|
||||||
{delayValue === -2 && (
|
{delayValue === -2 && (
|
||||||
<Widget>
|
<Widget>
|
||||||
@@ -222,13 +225,13 @@ export const ProxyItemMini = (props: Props) => {
|
|||||||
<Widget
|
<Widget
|
||||||
className="the-check"
|
className="the-check"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
e.stopPropagation();
|
e.stopPropagation()
|
||||||
onDelay();
|
onDelay()
|
||||||
}}
|
}}
|
||||||
sx={({ palette }) => ({
|
sx={({ palette }) => ({
|
||||||
display: "none", // hover 时显示
|
display: 'none', // hover 时显示
|
||||||
":hover": { bgcolor: alpha(palette.primary.main, 0.15) },
|
':hover': { bgcolor: alpha(palette.primary.main, 0.15) },
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
Check
|
Check
|
||||||
@@ -240,68 +243,68 @@ export const ProxyItemMini = (props: Props) => {
|
|||||||
<Widget
|
<Widget
|
||||||
className="the-delay"
|
className="the-delay"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (proxy.provider) return;
|
if (proxy.provider) return
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
e.stopPropagation();
|
e.stopPropagation()
|
||||||
onDelay();
|
onDelay()
|
||||||
}}
|
}}
|
||||||
color={delayManager.formatDelayColor(delayValue, timeout)}
|
color={delayManager.formatDelayColor(delayValue, timeout)}
|
||||||
sx={({ palette }) =>
|
sx={({ palette }) =>
|
||||||
!proxy.provider
|
!proxy.provider
|
||||||
? { ":hover": { bgcolor: alpha(palette.primary.main, 0.15) } }
|
? { ':hover': { bgcolor: alpha(palette.primary.main, 0.15) } }
|
||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{delayManager.formatDelay(delayValue, timeout)}
|
{delayManager.formatDelay(delayValue, timeout)}
|
||||||
</Widget>
|
</Widget>
|
||||||
)}
|
)}
|
||||||
{proxy.type !== "Direct" &&
|
{proxy.type !== 'Direct' &&
|
||||||
delayValue !== -2 &&
|
delayValue !== -2 &&
|
||||||
delayValue < 0 &&
|
delayValue < 0 &&
|
||||||
selected && (
|
selected && (
|
||||||
// 展示已选择的 icon
|
// 展示已选择的 icon
|
||||||
<CheckCircleOutlineRounded
|
<CheckCircleOutlineRounded
|
||||||
className="the-icon"
|
className="the-icon"
|
||||||
sx={{ fontSize: 16, mr: 0.5, display: "block" }}
|
sx={{ fontSize: 16, mr: 0.5, display: 'block' }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
{group.fixed && group.fixed === proxy.name && (
|
{group.fixed && group.fixed === proxy.name && (
|
||||||
// 展示 fixed 状态
|
// 展示 fixed 状态
|
||||||
<span
|
<span
|
||||||
className={proxy.name === group.now ? "the-pin" : "the-unpin"}
|
className={proxy.name === group.now ? 'the-pin' : 'the-unpin'}
|
||||||
title={
|
title={
|
||||||
group.type === "URLTest"
|
group.type === 'URLTest'
|
||||||
? t("proxies.page.labels.delayCheckReset")
|
? t('proxies.page.labels.delayCheckReset')
|
||||||
: ""
|
: ''
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
📌
|
📌
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const Widget = styled(Box)(({ theme: { typography } }) => ({
|
const Widget = styled(Box)(({ theme: { typography } }) => ({
|
||||||
padding: "2px 4px",
|
padding: '2px 4px',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontFamily: typography.fontFamily,
|
fontFamily: typography.fontFamily,
|
||||||
borderRadius: "4px",
|
borderRadius: '4px',
|
||||||
}));
|
}))
|
||||||
|
|
||||||
const TypeBox = styled(Box, {
|
const TypeBox = styled(Box, {
|
||||||
shouldForwardProp: (prop) => prop !== "component",
|
shouldForwardProp: (prop) => prop !== 'component',
|
||||||
})<{ component?: React.ElementType }>(({ theme: { typography } }) => ({
|
})<{ component?: React.ElementType }>(({ theme: { typography } }) => ({
|
||||||
display: "inline-block",
|
display: 'inline-block',
|
||||||
border: "1px solid #ccc",
|
border: '1px solid #ccc',
|
||||||
borderColor: "text.secondary",
|
borderColor: 'text.secondary',
|
||||||
color: "text.secondary",
|
color: 'text.secondary',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontFamily: typography.fontFamily,
|
fontFamily: typography.fontFamily,
|
||||||
marginRight: "4px",
|
marginRight: '4px',
|
||||||
marginTop: "auto",
|
marginTop: 'auto',
|
||||||
padding: "0 4px",
|
padding: '0 4px',
|
||||||
lineHeight: 1.5,
|
lineHeight: 1.5,
|
||||||
}));
|
}))
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CheckCircleOutlineRounded } from "@mui/icons-material";
|
import { CheckCircleOutlineRounded } from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
alpha,
|
alpha,
|
||||||
Box,
|
Box,
|
||||||
@@ -9,105 +9,108 @@ import {
|
|||||||
styled,
|
styled,
|
||||||
SxProps,
|
SxProps,
|
||||||
Theme,
|
Theme,
|
||||||
} from "@mui/material";
|
} from '@mui/material'
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from 'ahooks'
|
||||||
import { useCallback, useEffect, useReducer } from "react";
|
import { useCallback, useEffect, useReducer } from 'react'
|
||||||
|
|
||||||
import { BaseLoading } from "@/components/base";
|
import { BaseLoading } from '@/components/base'
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
import delayManager, { DelayUpdate } from "@/services/delay";
|
import delayManager, { DelayUpdate } from '@/services/delay'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
group: IProxyGroupItem;
|
group: IProxyGroupItem
|
||||||
proxy: IProxyItem;
|
proxy: IProxyItem
|
||||||
selected: boolean;
|
selected: boolean
|
||||||
showType?: boolean;
|
showType?: boolean
|
||||||
sx?: SxProps<Theme>;
|
sx?: SxProps<Theme>
|
||||||
onClick?: (name: string) => void;
|
onClick?: (name: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const Widget = styled(Box)(() => ({
|
const Widget = styled(Box)(() => ({
|
||||||
padding: "3px 6px",
|
padding: '3px 6px',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
borderRadius: "4px",
|
borderRadius: '4px',
|
||||||
}));
|
}))
|
||||||
|
|
||||||
const TypeBox = styled("span")(({ theme }) => ({
|
const TypeBox = styled('span')(({ theme }) => ({
|
||||||
display: "inline-block",
|
display: 'inline-block',
|
||||||
border: "1px solid #ccc",
|
border: '1px solid #ccc',
|
||||||
borderColor: alpha(theme.palette.text.secondary, 0.36),
|
borderColor: alpha(theme.palette.text.secondary, 0.36),
|
||||||
color: alpha(theme.palette.text.secondary, 0.42),
|
color: alpha(theme.palette.text.secondary, 0.42),
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
marginRight: "4px",
|
marginRight: '4px',
|
||||||
padding: "0 2px",
|
padding: '0 2px',
|
||||||
lineHeight: 1.25,
|
lineHeight: 1.25,
|
||||||
}));
|
}))
|
||||||
|
|
||||||
export const ProxyItem = (props: Props) => {
|
export const ProxyItem = (props: Props) => {
|
||||||
const { group, proxy, selected, showType = true, sx, onClick } = props;
|
const { group, proxy, selected, showType = true, sx, onClick } = props
|
||||||
|
|
||||||
const presetList = ["DIRECT", "REJECT", "REJECT-DROP", "PASS", "COMPATIBLE"];
|
const presetList = ['DIRECT', 'REJECT', 'REJECT-DROP', 'PASS', 'COMPATIBLE']
|
||||||
const isPreset = presetList.includes(proxy.name);
|
const isPreset = presetList.includes(proxy.name)
|
||||||
// -1/<=0 为不显示,-2 为 loading
|
// -1/<=0 为不显示,-2 为 loading
|
||||||
const [delayState, setDelayState] = useReducer(
|
const [delayState, setDelayState] = useReducer(
|
||||||
(_: DelayUpdate, next: DelayUpdate) => next,
|
(_: DelayUpdate, next: DelayUpdate) => next,
|
||||||
{ delay: -1, updatedAt: 0 },
|
{
|
||||||
);
|
delay: -1,
|
||||||
const { verge } = useVerge();
|
updatedAt: 0,
|
||||||
const timeout = verge?.default_latency_timeout || 10000;
|
},
|
||||||
|
)
|
||||||
|
const { verge } = useVerge()
|
||||||
|
const timeout = verge?.default_latency_timeout || 10000
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isPreset) return;
|
if (isPreset) return
|
||||||
delayManager.setListener(proxy.name, group.name, setDelayState);
|
delayManager.setListener(proxy.name, group.name, setDelayState)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
delayManager.removeListener(proxy.name, group.name);
|
delayManager.removeListener(proxy.name, group.name)
|
||||||
};
|
}
|
||||||
}, [proxy.name, group.name, isPreset]);
|
}, [proxy.name, group.name, isPreset])
|
||||||
|
|
||||||
const updateDelay = useCallback(() => {
|
const updateDelay = useCallback(() => {
|
||||||
if (!proxy) return;
|
if (!proxy) return
|
||||||
const cachedUpdate = delayManager.getDelayUpdate(proxy.name, group.name);
|
const cachedUpdate = delayManager.getDelayUpdate(proxy.name, group.name)
|
||||||
if (cachedUpdate) {
|
if (cachedUpdate) {
|
||||||
setDelayState({ ...cachedUpdate });
|
setDelayState({ ...cachedUpdate })
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const fallbackDelay = delayManager.getDelayFix(proxy, group.name);
|
const fallbackDelay = delayManager.getDelayFix(proxy, group.name)
|
||||||
if (fallbackDelay === -1) {
|
if (fallbackDelay === -1) {
|
||||||
setDelayState({ delay: -1, updatedAt: 0 });
|
setDelayState({ delay: -1, updatedAt: 0 })
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let updatedAt = 0;
|
let updatedAt = 0
|
||||||
const history = proxy.history;
|
const history = proxy.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)
|
||||||
if (!Number.isNaN(parsed)) {
|
if (!Number.isNaN(parsed)) {
|
||||||
updatedAt = parsed;
|
updatedAt = parsed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setDelayState({
|
setDelayState({
|
||||||
delay: fallbackDelay,
|
delay: fallbackDelay,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
});
|
})
|
||||||
}, [proxy, group.name]);
|
}, [proxy, group.name])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateDelay();
|
updateDelay()
|
||||||
}, [updateDelay]);
|
}, [updateDelay])
|
||||||
|
|
||||||
const onDelay = useLockFn(async () => {
|
const onDelay = useLockFn(async () => {
|
||||||
setDelayState({ delay: -2, updatedAt: Date.now() });
|
setDelayState({ delay: -2, updatedAt: Date.now() })
|
||||||
setDelayState(
|
setDelayState(
|
||||||
await delayManager.checkDelay(proxy.name, group.name, timeout),
|
await delayManager.checkDelay(proxy.name, group.name, timeout),
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
|
|
||||||
const delayValue = delayState.delay;
|
const delayValue = delayState.delay
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem sx={sx}>
|
<ListItem sx={sx}>
|
||||||
@@ -118,27 +121,27 @@ export const ProxyItem = (props: Props) => {
|
|||||||
sx={[
|
sx={[
|
||||||
{ borderRadius: 1 },
|
{ borderRadius: 1 },
|
||||||
({ palette: { mode, primary } }) => {
|
({ palette: { mode, primary } }) => {
|
||||||
const bgcolor = mode === "light" ? "#ffffff" : "#24252f";
|
const bgcolor = mode === 'light' ? '#ffffff' : '#24252f'
|
||||||
const selectColor = mode === "light" ? primary.main : primary.light;
|
const selectColor = mode === 'light' ? primary.main : primary.light
|
||||||
const showDelay = delayValue > 0;
|
const showDelay = delayValue > 0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"&:hover .the-check": { display: !showDelay ? "block" : "none" },
|
'&:hover .the-check': { display: !showDelay ? 'block' : 'none' },
|
||||||
"&:hover .the-delay": { display: showDelay ? "block" : "none" },
|
'&:hover .the-delay': { display: showDelay ? 'block' : 'none' },
|
||||||
"&:hover .the-icon": { display: "none" },
|
'&:hover .the-icon': { display: 'none' },
|
||||||
"&.Mui-selected": {
|
'&.Mui-selected': {
|
||||||
width: `calc(100% + 3px)`,
|
width: `calc(100% + 3px)`,
|
||||||
marginLeft: `-3px`,
|
marginLeft: `-3px`,
|
||||||
borderLeft: `3px solid ${selectColor}`,
|
borderLeft: `3px solid ${selectColor}`,
|
||||||
bgcolor:
|
bgcolor:
|
||||||
mode === "light"
|
mode === 'light'
|
||||||
? alpha(primary.main, 0.15)
|
? alpha(primary.main, 0.15)
|
||||||
: alpha(primary.main, 0.35),
|
: alpha(primary.main, 0.35),
|
||||||
},
|
},
|
||||||
backgroundColor: bgcolor,
|
backgroundColor: bgcolor,
|
||||||
marginBottom: "8px",
|
marginBottom: '8px',
|
||||||
height: "40px",
|
height: '40px',
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -148,10 +151,10 @@ export const ProxyItem = (props: Props) => {
|
|||||||
<>
|
<>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "inline-block",
|
display: 'inline-block',
|
||||||
marginRight: "8px",
|
marginRight: '8px',
|
||||||
fontSize: "14px",
|
fontSize: '14px',
|
||||||
color: "text.primary",
|
color: 'text.primary',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{proxy.name}
|
{proxy.name}
|
||||||
@@ -172,9 +175,9 @@ export const ProxyItem = (props: Props) => {
|
|||||||
|
|
||||||
<ListItemIcon
|
<ListItemIcon
|
||||||
sx={{
|
sx={{
|
||||||
justifyContent: "flex-end",
|
justifyContent: 'flex-end',
|
||||||
color: "primary.main",
|
color: 'primary.main',
|
||||||
display: isPreset ? "none" : "",
|
display: isPreset ? 'none' : '',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{delayValue === -2 && (
|
{delayValue === -2 && (
|
||||||
@@ -188,13 +191,13 @@ export const ProxyItem = (props: Props) => {
|
|||||||
<Widget
|
<Widget
|
||||||
className="the-check"
|
className="the-check"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
e.stopPropagation();
|
e.stopPropagation()
|
||||||
onDelay();
|
onDelay()
|
||||||
}}
|
}}
|
||||||
sx={({ palette }) => ({
|
sx={({ palette }) => ({
|
||||||
display: "none", // hover 时显示
|
display: 'none', // hover 时显示
|
||||||
":hover": { bgcolor: alpha(palette.primary.main, 0.15) },
|
':hover': { bgcolor: alpha(palette.primary.main, 0.15) },
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
Check
|
Check
|
||||||
@@ -206,15 +209,15 @@ export const ProxyItem = (props: Props) => {
|
|||||||
<Widget
|
<Widget
|
||||||
className="the-delay"
|
className="the-delay"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (proxy.provider) return;
|
if (proxy.provider) return
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
e.stopPropagation();
|
e.stopPropagation()
|
||||||
onDelay();
|
onDelay()
|
||||||
}}
|
}}
|
||||||
color={delayManager.formatDelayColor(delayValue, timeout)}
|
color={delayManager.formatDelayColor(delayValue, timeout)}
|
||||||
sx={({ palette }) =>
|
sx={({ palette }) =>
|
||||||
!proxy.provider
|
!proxy.provider
|
||||||
? { ":hover": { bgcolor: alpha(palette.primary.main, 0.15) } }
|
? { ':hover': { bgcolor: alpha(palette.primary.main, 0.15) } }
|
||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -232,5 +235,5 @@ export const ProxyItem = (props: Props) => {
|
|||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {
|
|||||||
ExpandLessRounded,
|
ExpandLessRounded,
|
||||||
ExpandMoreRounded,
|
ExpandMoreRounded,
|
||||||
InboxRounded,
|
InboxRounded,
|
||||||
} from "@mui/icons-material";
|
} from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
alpha,
|
alpha,
|
||||||
Box,
|
Box,
|
||||||
@@ -12,35 +12,35 @@ import {
|
|||||||
styled,
|
styled,
|
||||||
Chip,
|
Chip,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@mui/material";
|
} from '@mui/material'
|
||||||
import { useMemo } from "react";
|
import { useMemo } from 'react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { useIconCache } from "@/hooks/use-icon-cache";
|
import { useIconCache } from '@/hooks/use-icon-cache'
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
import { useThemeMode } from "@/services/states";
|
import { useThemeMode } from '@/services/states'
|
||||||
|
|
||||||
import { ProxyHead } from "./proxy-head";
|
import { ProxyHead } from './proxy-head'
|
||||||
import { ProxyItem } from "./proxy-item";
|
import { ProxyItem } from './proxy-item'
|
||||||
import { ProxyItemMini } from "./proxy-item-mini";
|
import { ProxyItemMini } from './proxy-item-mini'
|
||||||
import { HeadState } from "./use-head-state";
|
import { HeadState } from './use-head-state'
|
||||||
import type { IRenderItem } from "./use-render-list";
|
import type { IRenderItem } from './use-render-list'
|
||||||
|
|
||||||
interface RenderProps {
|
interface RenderProps {
|
||||||
item: IRenderItem;
|
item: IRenderItem
|
||||||
indent: boolean;
|
indent: boolean
|
||||||
isChainMode?: boolean;
|
isChainMode?: boolean
|
||||||
onLocation: (group: IRenderItem["group"]) => void;
|
onLocation: (group: IRenderItem['group']) => void
|
||||||
onCheckAll: (groupName: string) => void;
|
onCheckAll: (groupName: string) => void
|
||||||
onHeadState: (groupName: string, patch: Partial<HeadState>) => void;
|
onHeadState: (groupName: string, patch: Partial<HeadState>) => void
|
||||||
onChangeProxy: (
|
onChangeProxy: (
|
||||||
group: IRenderItem["group"],
|
group: IRenderItem['group'],
|
||||||
proxy: IRenderItem["proxy"] & { name: string },
|
proxy: IRenderItem['proxy'] & { name: string },
|
||||||
) => void;
|
) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProxyRender = (props: RenderProps) => {
|
export const ProxyRender = (props: RenderProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const {
|
const {
|
||||||
indent,
|
indent,
|
||||||
item,
|
item,
|
||||||
@@ -49,35 +49,35 @@ export const ProxyRender = (props: RenderProps) => {
|
|||||||
onHeadState,
|
onHeadState,
|
||||||
onChangeProxy,
|
onChangeProxy,
|
||||||
isChainMode: _ = false,
|
isChainMode: _ = false,
|
||||||
} = props;
|
} = props
|
||||||
const { type, group, headState, proxy, proxyCol } = item;
|
const { type, group, headState, proxy, proxyCol } = item
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge()
|
||||||
const enable_group_icon = verge?.enable_group_icon ?? true;
|
const enable_group_icon = verge?.enable_group_icon ?? true
|
||||||
const mode = useThemeMode();
|
const mode = useThemeMode()
|
||||||
const isDark = mode === "light" ? false : true;
|
const isDark = mode === 'light' ? false : true
|
||||||
const itembackgroundcolor = isDark ? "#282A36" : "#ffffff";
|
const itembackgroundcolor = isDark ? '#282A36' : '#ffffff'
|
||||||
const iconCachePath = useIconCache({
|
const iconCachePath = useIconCache({
|
||||||
icon: group.icon,
|
icon: group.icon,
|
||||||
cacheKey: group.name.replaceAll(" ", ""),
|
cacheKey: group.name.replaceAll(' ', ''),
|
||||||
enabled: enable_group_icon,
|
enabled: enable_group_icon,
|
||||||
});
|
})
|
||||||
|
|
||||||
const proxyColItemsMemo = useMemo(() => {
|
const proxyColItemsMemo = useMemo(() => {
|
||||||
if (type !== 4 || !proxyCol) {
|
if (type !== 4 || !proxyCol) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return proxyCol.map((proxyItem) => (
|
return proxyCol.map((proxyItem) => (
|
||||||
<ProxyItemMini
|
<ProxyItemMini
|
||||||
key={`${item.key}-${proxyItem?.name ?? "unknown"}`}
|
key={`${item.key}-${proxyItem?.name ?? 'unknown'}`}
|
||||||
group={group}
|
group={group}
|
||||||
proxy={proxyItem!}
|
proxy={proxyItem!}
|
||||||
selected={group.now === proxyItem?.name}
|
selected={group.now === proxyItem?.name}
|
||||||
showType={headState?.showType}
|
showType={headState?.showType}
|
||||||
onClick={() => onChangeProxy(group, proxyItem!)}
|
onClick={() => onChangeProxy(group, proxyItem!)}
|
||||||
/>
|
/>
|
||||||
));
|
))
|
||||||
}, [type, proxyCol, item.key, group, headState, onChangeProxy]);
|
}, [type, proxyCol, item.key, group, headState, onChangeProxy])
|
||||||
|
|
||||||
if (type === 0) {
|
if (type === 0) {
|
||||||
return (
|
return (
|
||||||
@@ -85,33 +85,33 @@ export const ProxyRender = (props: RenderProps) => {
|
|||||||
dense
|
dense
|
||||||
style={{
|
style={{
|
||||||
background: itembackgroundcolor,
|
background: itembackgroundcolor,
|
||||||
height: "100%",
|
height: '100%',
|
||||||
margin: "8px 8px",
|
margin: '8px 8px',
|
||||||
borderRadius: "8px",
|
borderRadius: '8px',
|
||||||
}}
|
}}
|
||||||
onClick={() => onHeadState(group.name, { open: !headState?.open })}
|
onClick={() => onHeadState(group.name, { open: !headState?.open })}
|
||||||
>
|
>
|
||||||
{enable_group_icon &&
|
{enable_group_icon &&
|
||||||
group.icon &&
|
group.icon &&
|
||||||
group.icon.trim().startsWith("http") && (
|
group.icon.trim().startsWith('http') && (
|
||||||
<img
|
<img
|
||||||
src={iconCachePath === "" ? group.icon : iconCachePath}
|
src={iconCachePath === '' ? group.icon : iconCachePath}
|
||||||
width="32px"
|
width="32px"
|
||||||
style={{ marginRight: "12px", borderRadius: "6px" }}
|
style={{ marginRight: '12px', borderRadius: '6px' }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{enable_group_icon &&
|
{enable_group_icon &&
|
||||||
group.icon &&
|
group.icon &&
|
||||||
group.icon.trim().startsWith("data") && (
|
group.icon.trim().startsWith('data') && (
|
||||||
<img
|
<img
|
||||||
src={group.icon}
|
src={group.icon}
|
||||||
width="32px"
|
width="32px"
|
||||||
style={{ marginRight: "12px", borderRadius: "6px" }}
|
style={{ marginRight: '12px', borderRadius: '6px' }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{enable_group_icon &&
|
{enable_group_icon &&
|
||||||
group.icon &&
|
group.icon &&
|
||||||
group.icon.trim().startsWith("<svg") && (
|
group.icon.trim().startsWith('<svg') && (
|
||||||
<img
|
<img
|
||||||
src={`data:image/svg+xml;base64,${btoa(group.icon)}`}
|
src={`data:image/svg+xml;base64,${btoa(group.icon)}`}
|
||||||
width="32px"
|
width="32px"
|
||||||
@@ -122,15 +122,15 @@ export const ProxyRender = (props: RenderProps) => {
|
|||||||
secondary={
|
secondary={
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
pt: "2px",
|
pt: '2px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box component="span" sx={{ marginTop: "2px" }}>
|
<Box component="span" sx={{ marginTop: '2px' }}>
|
||||||
<StyledTypeBox>{group.type}</StyledTypeBox>
|
<StyledTypeBox>{group.type}</StyledTypeBox>
|
||||||
<StyledSubtitle sx={{ color: "text.secondary" }}>
|
<StyledSubtitle sx={{ color: 'text.secondary' }}>
|
||||||
{group.now}
|
{group.now}
|
||||||
</StyledSubtitle>
|
</StyledSubtitle>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -138,13 +138,13 @@ export const ProxyRender = (props: RenderProps) => {
|
|||||||
}
|
}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
secondary: {
|
secondary: {
|
||||||
component: "div",
|
component: 'div',
|
||||||
sx: { display: "flex", alignItems: "center", color: "#ccc" },
|
sx: { display: 'flex', alignItems: 'center', color: '#ccc' },
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
<Tooltip title={t("proxies.page.labels.proxyCount")} arrow>
|
<Tooltip title={t('proxies.page.labels.proxyCount')} arrow>
|
||||||
<Chip
|
<Chip
|
||||||
size="small"
|
size="small"
|
||||||
label={`${group.all.length}`}
|
label={`${group.all.length}`}
|
||||||
@@ -159,7 +159,7 @@ export const ProxyRender = (props: RenderProps) => {
|
|||||||
{headState?.open ? <ExpandLessRounded /> : <ExpandMoreRounded />}
|
{headState?.open ? <ExpandLessRounded /> : <ExpandMoreRounded />}
|
||||||
</Box>
|
</Box>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 1) {
|
if (type === 1) {
|
||||||
@@ -173,7 +173,7 @@ export const ProxyRender = (props: RenderProps) => {
|
|||||||
onCheckDelay={() => onCheckAll(group.name)}
|
onCheckDelay={() => onCheckAll(group.name)}
|
||||||
onHeadState={(p) => onHeadState(group.name, p)}
|
onHeadState={(p) => onHeadState(group.name, p)}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 2) {
|
if (type === 2) {
|
||||||
@@ -186,7 +186,7 @@ export const ProxyRender = (props: RenderProps) => {
|
|||||||
sx={{ py: 0, pl: 2 }}
|
sx={{ py: 0, pl: 2 }}
|
||||||
onClick={() => onChangeProxy(group, proxy!)}
|
onClick={() => onChangeProxy(group, proxy!)}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 3) {
|
if (type === 3) {
|
||||||
@@ -195,16 +195,16 @@ export const ProxyRender = (props: RenderProps) => {
|
|||||||
sx={{
|
sx={{
|
||||||
py: 2,
|
py: 2,
|
||||||
pl: 0,
|
pl: 0,
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
flexDirection: "column",
|
flexDirection: 'column',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<InboxRounded sx={{ fontSize: "2.5em", color: "inherit" }} />
|
<InboxRounded sx={{ fontSize: '2.5em', color: 'inherit' }} />
|
||||||
<Typography sx={{ color: "inherit" }}>No Proxies</Typography>
|
<Typography sx={{ color: 'inherit' }}>No Proxies</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 4) {
|
if (type === 4) {
|
||||||
@@ -212,7 +212,7 @@ export const ProxyRender = (props: RenderProps) => {
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
height: 56,
|
height: 56,
|
||||||
display: "grid",
|
display: 'grid',
|
||||||
gap: 1,
|
gap: 1,
|
||||||
pl: 2,
|
pl: 2,
|
||||||
pr: 2,
|
pr: 2,
|
||||||
@@ -222,36 +222,36 @@ export const ProxyRender = (props: RenderProps) => {
|
|||||||
>
|
>
|
||||||
{proxyColItemsMemo}
|
{proxyColItemsMemo}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null
|
||||||
};
|
}
|
||||||
|
|
||||||
const StyledPrimary = styled("span")`
|
const StyledPrimary = styled('span')`
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`;
|
`
|
||||||
const StyledSubtitle = styled("span")`
|
const StyledSubtitle = styled('span')`
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: text.secondary;
|
color: text.secondary;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`;
|
`
|
||||||
|
|
||||||
const StyledTypeBox = styled(Box)(({ theme }) => ({
|
const StyledTypeBox = styled(Box)(({ theme }) => ({
|
||||||
display: "inline-block",
|
display: 'inline-block',
|
||||||
border: "1px solid #ccc",
|
border: '1px solid #ccc',
|
||||||
borderColor: alpha(theme.palette.primary.main, 0.5),
|
borderColor: alpha(theme.palette.primary.main, 0.5),
|
||||||
color: alpha(theme.palette.primary.main, 0.8),
|
color: alpha(theme.palette.primary.main, 0.8),
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
padding: "0 4px",
|
padding: '0 4px',
|
||||||
lineHeight: 1.5,
|
lineHeight: 1.5,
|
||||||
marginRight: "8px",
|
marginRight: '8px',
|
||||||
}));
|
}))
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { useEffect, useMemo, useReducer, useRef } from "react";
|
import { useEffect, useMemo, useReducer, useRef } from 'react'
|
||||||
|
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
import delayManager from "@/services/delay";
|
import delayManager from '@/services/delay'
|
||||||
import { compileStringMatcher } from "@/utils/search-matcher";
|
import { compileStringMatcher } from '@/utils/search-matcher'
|
||||||
|
|
||||||
// default | delay | alphabet
|
// default | delay | alphabet
|
||||||
export type ProxySortType = 0 | 1 | 2;
|
export type ProxySortType = 0 | 1 | 2
|
||||||
|
|
||||||
export type ProxySearchState = {
|
export type ProxySearchState = {
|
||||||
matchCase?: boolean;
|
matchCase?: boolean
|
||||||
matchWholeWord?: boolean;
|
matchWholeWord?: boolean
|
||||||
useRegularExpression?: boolean;
|
useRegularExpression?: boolean
|
||||||
};
|
}
|
||||||
|
|
||||||
export default function useFilterSort(
|
export default function useFilterSort(
|
||||||
proxies: IProxyItem[],
|
proxies: IProxyItem[],
|
||||||
@@ -20,39 +20,39 @@ export default function useFilterSort(
|
|||||||
sortType: ProxySortType,
|
sortType: ProxySortType,
|
||||||
searchState?: ProxySearchState,
|
searchState?: ProxySearchState,
|
||||||
) {
|
) {
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge()
|
||||||
const [_, bumpRefresh] = useReducer((count: number) => count + 1, 0);
|
const [_, bumpRefresh] = useReducer((count: number) => count + 1, 0)
|
||||||
const lastInputRef = useRef<{ text: string; sort: ProxySortType } | null>(
|
const lastInputRef = useRef<{ text: string; sort: ProxySortType } | null>(
|
||||||
null,
|
null,
|
||||||
);
|
)
|
||||||
const debounceTimerRef = useRef<number | null>(null);
|
const debounceTimerRef = useRef<number | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let last = 0;
|
let last = 0
|
||||||
|
|
||||||
delayManager.setGroupListener(groupName, () => {
|
delayManager.setGroupListener(groupName, () => {
|
||||||
// 简单节流
|
// 简单节流
|
||||||
const now = Date.now();
|
const now = Date.now()
|
||||||
if (now - last > 666) {
|
if (now - last > 666) {
|
||||||
last = now;
|
last = now
|
||||||
bumpRefresh();
|
bumpRefresh()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
delayManager.removeGroupListener(groupName);
|
delayManager.removeGroupListener(groupName)
|
||||||
};
|
}
|
||||||
}, [groupName]);
|
}, [groupName])
|
||||||
|
|
||||||
const compute = useMemo(() => {
|
const compute = useMemo(() => {
|
||||||
const fp = filterProxies(proxies, groupName, filterText, searchState);
|
const fp = filterProxies(proxies, groupName, filterText, searchState)
|
||||||
const sp = sortProxies(
|
const sp = sortProxies(
|
||||||
fp,
|
fp,
|
||||||
groupName,
|
groupName,
|
||||||
sortType,
|
sortType,
|
||||||
verge?.default_latency_timeout,
|
verge?.default_latency_timeout,
|
||||||
);
|
)
|
||||||
return sp;
|
return sp
|
||||||
}, [
|
}, [
|
||||||
proxies,
|
proxies,
|
||||||
groupName,
|
groupName,
|
||||||
@@ -60,40 +60,40 @@ export default function useFilterSort(
|
|||||||
sortType,
|
sortType,
|
||||||
searchState,
|
searchState,
|
||||||
verge?.default_latency_timeout,
|
verge?.default_latency_timeout,
|
||||||
]);
|
])
|
||||||
|
|
||||||
const [result, setResult] = useReducer(
|
const [result, setResult] = useReducer(
|
||||||
(_prev: IProxyItem[], next: IProxyItem[]) => next,
|
(_prev: IProxyItem[], next: IProxyItem[]) => next,
|
||||||
compute,
|
compute,
|
||||||
);
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (debounceTimerRef.current !== null) {
|
if (debounceTimerRef.current !== null) {
|
||||||
window.clearTimeout(debounceTimerRef.current);
|
window.clearTimeout(debounceTimerRef.current)
|
||||||
debounceTimerRef.current = null;
|
debounceTimerRef.current = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const prev = lastInputRef.current;
|
const prev = lastInputRef.current
|
||||||
const stableInputs =
|
const stableInputs =
|
||||||
prev && prev.text === filterText && prev.sort === sortType;
|
prev && prev.text === filterText && prev.sort === sortType
|
||||||
|
|
||||||
lastInputRef.current = { text: filterText, sort: sortType };
|
lastInputRef.current = { text: filterText, sort: sortType }
|
||||||
|
|
||||||
const delay = stableInputs ? 0 : 150;
|
const delay = stableInputs ? 0 : 150
|
||||||
debounceTimerRef.current = window.setTimeout(() => {
|
debounceTimerRef.current = window.setTimeout(() => {
|
||||||
setResult(compute);
|
setResult(compute)
|
||||||
debounceTimerRef.current = null;
|
debounceTimerRef.current = null
|
||||||
}, delay);
|
}, delay)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (debounceTimerRef.current !== null) {
|
if (debounceTimerRef.current !== null) {
|
||||||
window.clearTimeout(debounceTimerRef.current);
|
window.clearTimeout(debounceTimerRef.current)
|
||||||
debounceTimerRef.current = null;
|
debounceTimerRef.current = null
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}, [compute, filterText, sortType]);
|
}, [compute, filterText, sortType])
|
||||||
|
|
||||||
return result;
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export function filterSort(
|
export function filterSort(
|
||||||
@@ -104,16 +104,16 @@ export function filterSort(
|
|||||||
latencyTimeout?: number,
|
latencyTimeout?: number,
|
||||||
searchState?: ProxySearchState,
|
searchState?: ProxySearchState,
|
||||||
) {
|
) {
|
||||||
const fp = filterProxies(proxies, groupName, filterText, searchState);
|
const fp = filterProxies(proxies, groupName, filterText, searchState)
|
||||||
const sp = sortProxies(fp, groupName, sortType, latencyTimeout);
|
const sp = sortProxies(fp, groupName, sortType, latencyTimeout)
|
||||||
return sp;
|
return sp
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 可以通过延迟数/节点类型 过滤
|
* 可以通过延迟数/节点类型 过滤
|
||||||
*/
|
*/
|
||||||
const regex1 = /delay([=<>])(\d+|timeout|error)/i;
|
const regex1 = /delay([=<>])(\d+|timeout|error)/i
|
||||||
const regex2 = /type=(.*)/i;
|
const regex2 = /type=(.*)/i
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* filter the proxy
|
* filter the proxy
|
||||||
@@ -125,49 +125,49 @@ function filterProxies(
|
|||||||
filterText: string,
|
filterText: string,
|
||||||
searchState?: ProxySearchState,
|
searchState?: ProxySearchState,
|
||||||
) {
|
) {
|
||||||
const query = filterText.trim();
|
const query = filterText.trim()
|
||||||
if (!query) return proxies;
|
if (!query) return proxies
|
||||||
|
|
||||||
const res1 = regex1.exec(query);
|
const res1 = regex1.exec(query)
|
||||||
if (res1) {
|
if (res1) {
|
||||||
const symbol = res1[1];
|
const symbol = res1[1]
|
||||||
const symbol2 = res1[2].toLowerCase();
|
const symbol2 = res1[2].toLowerCase()
|
||||||
const value =
|
const value =
|
||||||
symbol2 === "error" ? 1e5 : symbol2 === "timeout" ? 3000 : +symbol2;
|
symbol2 === 'error' ? 1e5 : symbol2 === 'timeout' ? 3000 : +symbol2
|
||||||
|
|
||||||
return proxies.filter((p) => {
|
return proxies.filter((p) => {
|
||||||
const delay = delayManager.getDelayFix(p, groupName);
|
const delay = delayManager.getDelayFix(p, groupName)
|
||||||
|
|
||||||
if (delay < 0) return false;
|
if (delay < 0) return false
|
||||||
if (symbol === "=" && symbol2 === "error") return delay >= 1e5;
|
if (symbol === '=' && symbol2 === 'error') return delay >= 1e5
|
||||||
if (symbol === "=" && symbol2 === "timeout")
|
if (symbol === '=' && symbol2 === 'timeout')
|
||||||
return delay < 1e5 && delay >= 3000;
|
return delay < 1e5 && delay >= 3000
|
||||||
if (symbol === "=") return delay == value;
|
if (symbol === '=') return delay == value
|
||||||
if (symbol === "<") return delay <= value;
|
if (symbol === '<') return delay <= value
|
||||||
if (symbol === ">") return delay >= value;
|
if (symbol === '>') return delay >= value
|
||||||
return false;
|
return false
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const res2 = regex2.exec(query);
|
const res2 = regex2.exec(query)
|
||||||
if (res2) {
|
if (res2) {
|
||||||
const type = res2[1].toLowerCase();
|
const type = res2[1].toLowerCase()
|
||||||
return proxies.filter((p) => p.type.toLowerCase().includes(type));
|
return proxies.filter((p) => p.type.toLowerCase().includes(type))
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
matchCase = false,
|
matchCase = false,
|
||||||
matchWholeWord = false,
|
matchWholeWord = false,
|
||||||
useRegularExpression = false,
|
useRegularExpression = false,
|
||||||
} = searchState ?? {};
|
} = searchState ?? {}
|
||||||
const compiled = compileStringMatcher(query, {
|
const compiled = compileStringMatcher(query, {
|
||||||
matchCase,
|
matchCase,
|
||||||
matchWholeWord,
|
matchWholeWord,
|
||||||
useRegularExpression,
|
useRegularExpression,
|
||||||
});
|
})
|
||||||
|
|
||||||
if (!compiled.isValid) return [];
|
if (!compiled.isValid) return []
|
||||||
return proxies.filter((p) => compiled.matcher(p.name));
|
return proxies.filter((p) => compiled.matcher(p.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -179,41 +179,41 @@ function sortProxies(
|
|||||||
sortType: ProxySortType,
|
sortType: ProxySortType,
|
||||||
latencyTimeout?: number,
|
latencyTimeout?: number,
|
||||||
) {
|
) {
|
||||||
if (!proxies) return [];
|
if (!proxies) return []
|
||||||
if (sortType === 0) return proxies;
|
if (sortType === 0) return proxies
|
||||||
|
|
||||||
const list = proxies.slice();
|
const list = proxies.slice()
|
||||||
const effectiveTimeout =
|
const effectiveTimeout =
|
||||||
typeof latencyTimeout === "number" && latencyTimeout > 0
|
typeof latencyTimeout === 'number' && latencyTimeout > 0
|
||||||
? latencyTimeout
|
? latencyTimeout
|
||||||
: 10000;
|
: 10000
|
||||||
|
|
||||||
if (sortType === 1) {
|
if (sortType === 1) {
|
||||||
const categorizeDelay = (delay: number): [number, number] => {
|
const categorizeDelay = (delay: number): [number, number] => {
|
||||||
if (!Number.isFinite(delay)) return [3, Number.MAX_SAFE_INTEGER];
|
if (!Number.isFinite(delay)) return [3, Number.MAX_SAFE_INTEGER]
|
||||||
if (delay > 1e5) return [4, delay];
|
if (delay > 1e5) return [4, delay]
|
||||||
if (delay === 0 || (delay >= effectiveTimeout && delay <= 1e5)) {
|
if (delay === 0 || (delay >= effectiveTimeout && delay <= 1e5)) {
|
||||||
return [3, delay || effectiveTimeout];
|
return [3, delay || effectiveTimeout]
|
||||||
}
|
}
|
||||||
if (delay < 0) {
|
if (delay < 0) {
|
||||||
// sentinel delays (-1, -2, etc.) should always sort after real measurements
|
// sentinel delays (-1, -2, etc.) should always sort after real measurements
|
||||||
return [5, Number.MAX_SAFE_INTEGER];
|
return [5, Number.MAX_SAFE_INTEGER]
|
||||||
|
}
|
||||||
|
return [0, delay]
|
||||||
}
|
}
|
||||||
return [0, delay];
|
|
||||||
};
|
|
||||||
|
|
||||||
list.sort((a, b) => {
|
list.sort((a, b) => {
|
||||||
const ad = delayManager.getDelayFix(a, groupName);
|
const ad = delayManager.getDelayFix(a, groupName)
|
||||||
const bd = delayManager.getDelayFix(b, groupName);
|
const bd = delayManager.getDelayFix(b, groupName)
|
||||||
const [ar, av] = categorizeDelay(ad);
|
const [ar, av] = categorizeDelay(ad)
|
||||||
const [br, bv] = categorizeDelay(bd);
|
const [br, bv] = categorizeDelay(bd)
|
||||||
|
|
||||||
if (ar !== br) return ar - br;
|
if (ar !== br) return ar - br
|
||||||
return av - bv;
|
return av - bv
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
list.sort((a, b) => a.name.localeCompare(b.name));
|
list.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
return list;
|
return list
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user