mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-13 14:22:08 +08:00
Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e7cdbfcc0 | ||
|
|
966fd68087 | ||
|
|
334cec3bde | ||
|
|
6e16133393 | ||
|
|
5e976c2fe1 | ||
|
|
d81aa5f233 | ||
|
|
e5fc0de39a | ||
|
|
6c62350cc3 | ||
|
|
d1649e3017 | ||
|
|
2869a35f1e | ||
|
|
98f12a9c72 | ||
|
|
6dc8a2f232 | ||
|
|
6511f3868e | ||
|
|
7da5a804f9 | ||
|
|
20ed7a3abe | ||
|
|
fd98caccd2 | ||
|
|
a5f494bda2 | ||
|
|
d4d8ef3849 | ||
|
|
b16cbd5379 | ||
|
|
9e6689ef08 | ||
|
|
e0c35c5ee3 | ||
|
|
670055aba1 | ||
|
|
a780e44e69 |
@@ -253,10 +253,12 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-24.04
|
# It should be ubuntu-22.04 to match the cross-compilation environment
|
||||||
|
# ortherwise it is hard to resolve the dependencies
|
||||||
|
- os: ubuntu-22.04
|
||||||
target: aarch64-unknown-linux-gnu
|
target: aarch64-unknown-linux-gnu
|
||||||
arch: arm64
|
arch: arm64
|
||||||
- os: ubuntu-24.04
|
- os: ubuntu-22.04
|
||||||
target: armv7-unknown-linux-gnueabihf
|
target: armv7-unknown-linux-gnueabihf
|
||||||
arch: armhf
|
arch: armhf
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
@@ -311,39 +313,35 @@ 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 dpkg --add-architecture ${{ matrix.arch }}
|
sudo ls -lR /etc/apt/
|
||||||
|
|
||||||
sudo rm -f /etc/apt/sources.list.d/ubuntu.sources
|
cat > /tmp/sources.list << EOF
|
||||||
sudo tee /etc/apt/sources.list << EOF
|
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy main multiverse universe restricted
|
||||||
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu noble main restricted universe multiverse
|
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-security main multiverse universe restricted
|
||||||
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu noble-security main restricted universe multiverse
|
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-updates main multiverse universe restricted
|
||||||
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu noble-updates main restricted universe multiverse
|
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-backports main multiverse universe restricted
|
||||||
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu noble-backports main restricted universe multiverse
|
|
||||||
|
|
||||||
deb [arch=${{ matrix.arch }}] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse
|
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy main multiverse universe restricted
|
||||||
deb [arch=${{ matrix.arch }}] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse
|
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main multiverse universe restricted
|
||||||
deb [arch=${{ matrix.arch }}] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse
|
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main multiverse universe restricted
|
||||||
deb [arch=${{ matrix.arch }}] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse
|
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main multiverse universe restricted
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
sudo apt-get update -y
|
sudo mv /etc/apt/sources.list /etc/apt/sources.list.default
|
||||||
|
sudo mv /tmp/sources.list /etc/apt/sources.list
|
||||||
|
|
||||||
sudo apt-get install -y libglib2.0-dev-bin
|
sudo dpkg --add-architecture ${{ matrix.arch }}
|
||||||
|
sudo apt update
|
||||||
|
|
||||||
sudo apt-get install -y \
|
sudo apt install -y \
|
||||||
linux-libc-dev:${{ matrix.arch }} \
|
libxslt1.1:${{ matrix.arch }} \
|
||||||
libc6-dev:${{ matrix.arch }} \
|
|
||||||
libicu-dev:${{ matrix.arch }}
|
|
||||||
|
|
||||||
sudo apt-get install -y --no-install-recommends \
|
|
||||||
libxslt1-dev:${{ matrix.arch }} \
|
|
||||||
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
|
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
|
||||||
libayatana-appindicator3-dev:${{ matrix.arch }} \
|
libayatana-appindicator3-dev:${{ matrix.arch }} \
|
||||||
libssl-dev:${{ matrix.arch }} \
|
libssl-dev:${{ matrix.arch }} \
|
||||||
librsvg2-dev:${{ matrix.arch }} \
|
patchelf:${{ matrix.arch }} \
|
||||||
patchelf
|
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'
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
queue_rules:
|
||||||
|
- name: LetMeMergeForYou
|
||||||
|
batch_size: 3
|
||||||
|
allow_queue_branch_edit: true
|
||||||
|
queue_conditions: []
|
||||||
Generated
+262
-355
File diff suppressed because it is too large
Load Diff
+1
-3
@@ -5,7 +5,6 @@ members = [
|
|||||||
"crates/clash-verge-logging",
|
"crates/clash-verge-logging",
|
||||||
"crates/clash-verge-signal",
|
"crates/clash-verge-signal",
|
||||||
"crates/tauri-plugin-clash-verge-sysinfo",
|
"crates/tauri-plugin-clash-verge-sysinfo",
|
||||||
"crates/clash-verge-types",
|
|
||||||
"crates/clash-verge-i18n",
|
"crates/clash-verge-i18n",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
@@ -44,7 +43,6 @@ strip = false
|
|||||||
clash-verge-draft = { path = "crates/clash-verge-draft" }
|
clash-verge-draft = { path = "crates/clash-verge-draft" }
|
||||||
clash-verge-logging = { path = "crates/clash-verge-logging" }
|
clash-verge-logging = { path = "crates/clash-verge-logging" }
|
||||||
clash-verge-signal = { path = "crates/clash-verge-signal" }
|
clash-verge-signal = { path = "crates/clash-verge-signal" }
|
||||||
clash-verge-types = { path = "crates/clash-verge-types" }
|
|
||||||
clash-verge-i18n = { path = "crates/clash-verge-i18n" }
|
clash-verge-i18n = { path = "crates/clash-verge-i18n" }
|
||||||
tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" }
|
tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" }
|
||||||
|
|
||||||
@@ -59,7 +57,7 @@ tokio = { version = "1.49.0", features = [
|
|||||||
"time",
|
"time",
|
||||||
"sync",
|
"sync",
|
||||||
] }
|
] }
|
||||||
flexi_logger = "0.31.7"
|
flexi_logger = "0.31.8"
|
||||||
log = "0.4.29"
|
log = "0.4.29"
|
||||||
|
|
||||||
smartstring = { version = "1.0.1" }
|
smartstring = { version = "1.0.1" }
|
||||||
|
|||||||
+5
-1
@@ -1,6 +1,6 @@
|
|||||||
## v2.4.5
|
## v2.4.5
|
||||||
|
|
||||||
- **Mihomo(Meta) 内核升级至 v1.19.18**
|
- **Mihomo(Meta) 内核升级至 v1.19.19**
|
||||||
|
|
||||||
### 🐞 修复问题
|
### 🐞 修复问题
|
||||||
|
|
||||||
@@ -12,6 +12,9 @@
|
|||||||
- 修复 Windows 下系统主题同步问题
|
- 修复 Windows 下系统主题同步问题
|
||||||
- 修复 URL Schemes 无法正常导入
|
- 修复 URL Schemes 无法正常导入
|
||||||
- 修复 Linux 下无法安装 TUN 服务
|
- 修复 Linux 下无法安装 TUN 服务
|
||||||
|
- 修复可能的端口被占用误报
|
||||||
|
- 修复设置允许外部控制来源不能立即生效
|
||||||
|
- 修复前端性能回归问题
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||||
@@ -45,5 +48,6 @@
|
|||||||
- 保存 TUN 设置时优化执行流程,避免界面卡顿
|
- 保存 TUN 设置时优化执行流程,避免界面卡顿
|
||||||
- 补充 `deb` / `rpm` 依赖 `libayatana-appindicator`
|
- 补充 `deb` / `rpm` 依赖 `libayatana-appindicator`
|
||||||
- 「连接」表格标题的排序点击区域扩展到整列宽度
|
- 「连接」表格标题的排序点击区域扩展到整列宽度
|
||||||
|
- 备份恢复时显示加载覆盖层,恢复过程无需再手动关闭对话框
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@@ -66,19 +66,6 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
|
|||||||
|
|
||||||
🌐 官网:👉 [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
🌐 官网:👉 [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
#### 本项目的构建与发布环境由 [YXVM](https://yxvm.com/aff.php?aff=827) 独立服务器全力支持,
|
|
||||||
|
|
||||||
感谢提供 独享资源、高性能、高速网络 的强大后端环境。如果你觉得下载够快、使用够爽,那是因为我们用了好服务器!
|
|
||||||
|
|
||||||
🧩 YXVM 独立服务器优势:
|
|
||||||
|
|
||||||
- 🌎 优质网络,回程优化,下载快到飞起
|
|
||||||
- 🔧 物理机独享资源,非VPS可比,性能拉满
|
|
||||||
- 🧠 适合跑代理、搭建 WEB 站 CDN 站 、搞 CI/CD 或任何高负载应用
|
|
||||||
- 💡 支持即开即用,多机房选择,CN2 / IEPL 可选
|
|
||||||
- 📦 本项目使用配置已在售,欢迎同款入手!
|
|
||||||
- 🎯 想要同款构建体验?[立即下单 YXVM 独立服务器!](https://yxvm.com/aff.php?aff=827)
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- 基于性能强劲的 Rust 和 Tauri 2 框架
|
- 基于性能强劲的 Rust 和 Tauri 2 框架
|
||||||
|
|||||||
@@ -1,60 +1,60 @@
|
|||||||
_version: 1
|
_version: 1
|
||||||
notifications:
|
notifications:
|
||||||
dashboardToggled:
|
dashboardToggled:
|
||||||
title: Dashboard
|
title: لوحة التحكم
|
||||||
body: Dashboard visibility has been updated.
|
body: تم تحديث حالة عرض لوحة التحكم.
|
||||||
clashModeChanged:
|
clashModeChanged:
|
||||||
title: Mode Switch
|
title: تبديل الوضع
|
||||||
body: Switched to {mode}.
|
body: تم التبديل إلى {mode}.
|
||||||
systemProxyToggled:
|
systemProxyToggled:
|
||||||
title: System Proxy
|
title: وكيل النظام
|
||||||
body: System proxy status has been updated.
|
body: تم تحديث حالة وكيل النظام.
|
||||||
tunModeToggled:
|
tunModeToggled:
|
||||||
title: TUN Mode
|
title: وضع TUN
|
||||||
body: TUN mode status has been updated.
|
body: تم تحديث حالة وضع TUN.
|
||||||
lightweightModeEntered:
|
lightweightModeEntered:
|
||||||
title: Lightweight Mode
|
title: الوضع الخفيف
|
||||||
body: Entered lightweight mode.
|
body: تم الدخول إلى الوضع الخفيف.
|
||||||
profilesReactivated:
|
profilesReactivated:
|
||||||
title: Profiles
|
title: الملفات التعريفية
|
||||||
body: Profile Reactivated.
|
body: تمت إعادة تفعيل الملف التعريفي.
|
||||||
appQuit:
|
appQuit:
|
||||||
title: About to Exit
|
title: على وشك الخروج
|
||||||
body: Clash Verge is about to exit.
|
body: Clash Verge على وشك الخروج.
|
||||||
appHidden:
|
appHidden:
|
||||||
title: Application Hidden
|
title: تم إخفاء التطبيق
|
||||||
body: Clash Verge is running in the background.
|
body: Clash Verge يعمل في الخلفية.
|
||||||
service:
|
service:
|
||||||
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
|
adminInstallPrompt: يتطلب تثبيت خدمة Clash Verge صلاحيات المسؤول.
|
||||||
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
|
adminUninstallPrompt: يتطلب إلغاء تثبيت خدمة Clash Verge صلاحيات المسؤول.
|
||||||
tray:
|
tray:
|
||||||
dashboard: Dashboard
|
dashboard: لوحة التحكم
|
||||||
ruleMode: Rule Mode
|
ruleMode: وضع القواعد
|
||||||
globalMode: Global Mode
|
globalMode: الوضع العام
|
||||||
directMode: Direct Mode
|
directMode: الوضع المباشر
|
||||||
outboundModes: Outbound Modes
|
outboundModes: أوضاع الخروج
|
||||||
rule: Rule
|
rule: قاعدة
|
||||||
direct: Direct
|
direct: مباشر
|
||||||
global: Global
|
global: عام
|
||||||
profiles: Profiles
|
profiles: الملفات التعريفية
|
||||||
proxies: Proxies
|
proxies: وكلاء
|
||||||
systemProxy: System Proxy
|
systemProxy: وكيل النظام
|
||||||
tunMode: TUN Mode
|
tunMode: وضع TUN
|
||||||
closeAllConnections: Close All Connections
|
closeAllConnections: إغلاق كل الاتصالات
|
||||||
lightweightMode: Lightweight Mode
|
lightweightMode: الوضع الخفيف
|
||||||
copyEnv: Copy Environment Variables
|
copyEnv: نسخ متغيرات البيئة
|
||||||
confDir: Configuration Directory
|
confDir: دليل الإعدادات
|
||||||
coreDir: Core Directory
|
coreDir: دليل النواة
|
||||||
logsDir: Log Directory
|
logsDir: دليل السجلات
|
||||||
openDir: Open Directory
|
openDir: فتح الدليل
|
||||||
appLog: Application Log
|
appLog: سجل التطبيق
|
||||||
coreLog: Core Log
|
coreLog: سجل النواة
|
||||||
restartClash: Restart Clash Core
|
restartClash: إعادة تشغيل نواة Clash
|
||||||
restartApp: Restart Application
|
restartApp: إعادة تشغيل التطبيق
|
||||||
vergeVersion: Verge Version
|
vergeVersion: إصدار Verge
|
||||||
more: More
|
more: المزيد
|
||||||
exit: Exit
|
exit: خروج
|
||||||
tooltip:
|
tooltip:
|
||||||
systemProxy: System Proxy
|
systemProxy: وكيل النظام
|
||||||
tun: TUN
|
tun: TUN
|
||||||
profile: Profile
|
profile: ملف تعريفي
|
||||||
|
|||||||
@@ -1,60 +1,60 @@
|
|||||||
_version: 1
|
_version: 1
|
||||||
notifications:
|
notifications:
|
||||||
dashboardToggled:
|
dashboardToggled:
|
||||||
title: Dashboard
|
title: Übersicht
|
||||||
body: Dashboard visibility has been updated.
|
body: Die Sichtbarkeit der Übersicht wurde aktualisiert.
|
||||||
clashModeChanged:
|
clashModeChanged:
|
||||||
title: Mode Switch
|
title: Moduswechsel
|
||||||
body: Switched to {mode}.
|
body: Auf {mode} umgeschaltet.
|
||||||
systemProxyToggled:
|
systemProxyToggled:
|
||||||
title: System Proxy
|
title: Systemproxy
|
||||||
body: System proxy status has been updated.
|
body: Der Status des Systemproxys wurde aktualisiert.
|
||||||
tunModeToggled:
|
tunModeToggled:
|
||||||
title: TUN Mode
|
title: TUN-Modus
|
||||||
body: TUN mode status has been updated.
|
body: Der Status des TUN-Modus wurde aktualisiert.
|
||||||
lightweightModeEntered:
|
lightweightModeEntered:
|
||||||
title: Lightweight Mode
|
title: Leichtmodus
|
||||||
body: Entered lightweight mode.
|
body: Leichtmodus aktiviert.
|
||||||
profilesReactivated:
|
profilesReactivated:
|
||||||
title: Profiles
|
title: Profile
|
||||||
body: Profile Reactivated.
|
body: Profil reaktiviert.
|
||||||
appQuit:
|
appQuit:
|
||||||
title: About to Exit
|
title: Beenden steht bevor
|
||||||
body: Clash Verge is about to exit.
|
body: Clash Verge wird gleich beendet.
|
||||||
appHidden:
|
appHidden:
|
||||||
title: Application Hidden
|
title: Anwendung ausgeblendet
|
||||||
body: Clash Verge is running in the background.
|
body: Clash Verge läuft im Hintergrund.
|
||||||
service:
|
service:
|
||||||
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
|
adminInstallPrompt: Für die Installation des Clash-Verge-Dienstes sind Administratorrechte erforderlich.
|
||||||
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
|
adminUninstallPrompt: Für die Deinstallation des Clash-Verge-Dienstes sind Administratorrechte erforderlich.
|
||||||
tray:
|
tray:
|
||||||
dashboard: Dashboard
|
dashboard: Übersicht
|
||||||
ruleMode: Rule Mode
|
ruleMode: Regelmodus
|
||||||
globalMode: Global Mode
|
globalMode: Globaler Modus
|
||||||
directMode: Direct Mode
|
directMode: Direktmodus
|
||||||
outboundModes: Outbound Modes
|
outboundModes: Ausgangsmodi
|
||||||
rule: Regel
|
rule: Regel
|
||||||
direct: Direkt
|
direct: Direkt
|
||||||
global: Global
|
global: Global
|
||||||
profiles: Profiles
|
profiles: Profile
|
||||||
proxies: Proxies
|
proxies: Proxy
|
||||||
systemProxy: System Proxy
|
systemProxy: Systemproxy
|
||||||
tunMode: TUN Mode
|
tunMode: TUN-Modus
|
||||||
closeAllConnections: Close All Connections
|
closeAllConnections: Alle Verbindungen schließen
|
||||||
lightweightMode: Lightweight Mode
|
lightweightMode: Leichtmodus
|
||||||
copyEnv: Copy Environment Variables
|
copyEnv: Umgebungsvariablen kopieren
|
||||||
confDir: Configuration Directory
|
confDir: Konfigurationsverzeichnis
|
||||||
coreDir: Core Directory
|
coreDir: Core-Verzeichnis
|
||||||
logsDir: Log Directory
|
logsDir: Log-Verzeichnis
|
||||||
openDir: Open Directory
|
openDir: Verzeichnis öffnen
|
||||||
appLog: Application Log
|
appLog: Anwendungslog
|
||||||
coreLog: Core Log
|
coreLog: Core-Log
|
||||||
restartClash: Restart Clash Core
|
restartClash: Clash-Core neu starten
|
||||||
restartApp: Restart Application
|
restartApp: Anwendung neu starten
|
||||||
vergeVersion: Verge Version
|
vergeVersion: Verge-Version
|
||||||
more: More
|
more: Mehr
|
||||||
exit: Exit
|
exit: Beenden
|
||||||
tooltip:
|
tooltip:
|
||||||
systemProxy: System Proxy
|
systemProxy: Systemproxy
|
||||||
tun: TUN
|
tun: TUN
|
||||||
profile: Profile
|
profile: Profil
|
||||||
|
|||||||
@@ -1,60 +1,60 @@
|
|||||||
_version: 1
|
_version: 1
|
||||||
notifications:
|
notifications:
|
||||||
dashboardToggled:
|
dashboardToggled:
|
||||||
title: Dashboard
|
title: Panel
|
||||||
body: Dashboard visibility has been updated.
|
body: La visibilidad del panel se ha actualizado.
|
||||||
clashModeChanged:
|
clashModeChanged:
|
||||||
title: Mode Switch
|
title: Cambio de modo
|
||||||
body: Switched to {mode}.
|
body: Cambiado a {mode}.
|
||||||
systemProxyToggled:
|
systemProxyToggled:
|
||||||
title: System Proxy
|
title: Proxy del sistema
|
||||||
body: System proxy status has been updated.
|
body: El estado del proxy del sistema se ha actualizado.
|
||||||
tunModeToggled:
|
tunModeToggled:
|
||||||
title: TUN Mode
|
title: Modo TUN
|
||||||
body: TUN mode status has been updated.
|
body: El estado del modo TUN se ha actualizado.
|
||||||
lightweightModeEntered:
|
lightweightModeEntered:
|
||||||
title: Lightweight Mode
|
title: Modo ligero
|
||||||
body: Entered lightweight mode.
|
body: Se ha entrado en el modo ligero.
|
||||||
profilesReactivated:
|
profilesReactivated:
|
||||||
title: Profiles
|
title: Perfiles
|
||||||
body: Profile Reactivated.
|
body: Perfil reactivado.
|
||||||
appQuit:
|
appQuit:
|
||||||
title: About to Exit
|
title: A punto de salir
|
||||||
body: Clash Verge is about to exit.
|
body: Clash Verge está a punto de salir.
|
||||||
appHidden:
|
appHidden:
|
||||||
title: Application Hidden
|
title: Aplicación oculta
|
||||||
body: Clash Verge is running in the background.
|
body: Clash Verge se está ejecutando en segundo plano.
|
||||||
service:
|
service:
|
||||||
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
|
adminInstallPrompt: Instalar el servicio de Clash Verge requiere privilegios de administrador.
|
||||||
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
|
adminUninstallPrompt: Desinstalar el servicio de Clash Verge requiere privilegios de administrador.
|
||||||
tray:
|
tray:
|
||||||
dashboard: Dashboard
|
dashboard: Panel
|
||||||
ruleMode: Rule Mode
|
ruleMode: Modo de reglas
|
||||||
globalMode: Global Mode
|
globalMode: Modo global
|
||||||
directMode: Direct Mode
|
directMode: Modo directo
|
||||||
outboundModes: Outbound Modes
|
outboundModes: Modos de salida
|
||||||
rule: Regla
|
rule: Regla
|
||||||
direct: Directo
|
direct: Directo
|
||||||
global: Global
|
global: Global
|
||||||
profiles: Profiles
|
profiles: Perfiles
|
||||||
proxies: Proxies
|
proxies: Proxies
|
||||||
systemProxy: System Proxy
|
systemProxy: Proxy del sistema
|
||||||
tunMode: TUN Mode
|
tunMode: Modo TUN
|
||||||
closeAllConnections: Close All Connections
|
closeAllConnections: Cerrar todas las conexiones
|
||||||
lightweightMode: Lightweight Mode
|
lightweightMode: Modo ligero
|
||||||
copyEnv: Copy Environment Variables
|
copyEnv: Copiar variables de entorno
|
||||||
confDir: Configuration Directory
|
confDir: Directorio de configuración
|
||||||
coreDir: Core Directory
|
coreDir: Directorio del núcleo
|
||||||
logsDir: Log Directory
|
logsDir: Directorio de registros
|
||||||
openDir: Open Directory
|
openDir: Abrir directorio
|
||||||
appLog: Application Log
|
appLog: Registro de la aplicación
|
||||||
coreLog: Core Log
|
coreLog: Registro del núcleo
|
||||||
restartClash: Restart Clash Core
|
restartClash: Reiniciar el núcleo de Clash
|
||||||
restartApp: Restart Application
|
restartApp: Reiniciar aplicación
|
||||||
vergeVersion: Verge Version
|
vergeVersion: Versión de Verge
|
||||||
more: More
|
more: Más
|
||||||
exit: Exit
|
exit: Salir
|
||||||
tooltip:
|
tooltip:
|
||||||
systemProxy: System Proxy
|
systemProxy: Proxy del sistema
|
||||||
tun: TUN
|
tun: TUN
|
||||||
profile: Profile
|
profile: Perfil
|
||||||
|
|||||||
@@ -1,60 +1,60 @@
|
|||||||
_version: 1
|
_version: 1
|
||||||
notifications:
|
notifications:
|
||||||
dashboardToggled:
|
dashboardToggled:
|
||||||
title: Dashboard
|
title: داشبورد
|
||||||
body: Dashboard visibility has been updated.
|
body: وضعیت نمایش داشبورد بهروزرسانی شد.
|
||||||
clashModeChanged:
|
clashModeChanged:
|
||||||
title: Mode Switch
|
title: تغییر حالت
|
||||||
body: Switched to {mode}.
|
body: به {mode} تغییر کرد.
|
||||||
systemProxyToggled:
|
systemProxyToggled:
|
||||||
title: System Proxy
|
title: پروکسی سیستم
|
||||||
body: System proxy status has been updated.
|
body: وضعیت پروکسی سیستم بهروزرسانی شد.
|
||||||
tunModeToggled:
|
tunModeToggled:
|
||||||
title: TUN Mode
|
title: حالت TUN
|
||||||
body: TUN mode status has been updated.
|
body: وضعیت حالت TUN بهروزرسانی شد.
|
||||||
lightweightModeEntered:
|
lightweightModeEntered:
|
||||||
title: Lightweight Mode
|
title: حالت سبک
|
||||||
body: Entered lightweight mode.
|
body: به حالت سبک وارد شد.
|
||||||
profilesReactivated:
|
profilesReactivated:
|
||||||
title: Profiles
|
title: پروفایلها
|
||||||
body: Profile Reactivated.
|
body: پروفایل دوباره فعال شد.
|
||||||
appQuit:
|
appQuit:
|
||||||
title: About to Exit
|
title: در آستانه خروج
|
||||||
body: Clash Verge is about to exit.
|
body: Clash Verge در آستانه خروج است.
|
||||||
appHidden:
|
appHidden:
|
||||||
title: Application Hidden
|
title: برنامه پنهان شد
|
||||||
body: Clash Verge is running in the background.
|
body: Clash Verge در پسزمینه در حال اجراست.
|
||||||
service:
|
service:
|
||||||
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
|
adminInstallPrompt: نصب سرویس Clash Verge به دسترسی مدیر نیاز دارد.
|
||||||
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
|
adminUninstallPrompt: حذف سرویس Clash Verge به دسترسی مدیر نیاز دارد.
|
||||||
tray:
|
tray:
|
||||||
dashboard: Dashboard
|
dashboard: داشبورد
|
||||||
ruleMode: Rule Mode
|
ruleMode: حالت قوانین
|
||||||
globalMode: Global Mode
|
globalMode: حالت سراسری
|
||||||
directMode: Direct Mode
|
directMode: حالت مستقیم
|
||||||
outboundModes: Outbound Modes
|
outboundModes: حالتهای خروجی
|
||||||
rule: Rule
|
rule: قانون
|
||||||
direct: Direct
|
direct: مستقیم
|
||||||
global: Global
|
global: سراسری
|
||||||
profiles: Profiles
|
profiles: پروفایلها
|
||||||
proxies: Proxies
|
proxies: پروکسیها
|
||||||
systemProxy: System Proxy
|
systemProxy: پروکسی سیستم
|
||||||
tunMode: TUN Mode
|
tunMode: حالت TUN
|
||||||
closeAllConnections: Close All Connections
|
closeAllConnections: بستن همه اتصالها
|
||||||
lightweightMode: Lightweight Mode
|
lightweightMode: حالت سبک
|
||||||
copyEnv: Copy Environment Variables
|
copyEnv: کپی متغیرهای محیطی
|
||||||
confDir: Configuration Directory
|
confDir: پوشه پیکربندی
|
||||||
coreDir: Core Directory
|
coreDir: پوشه هسته
|
||||||
logsDir: Log Directory
|
logsDir: پوشه گزارشها
|
||||||
openDir: Open Directory
|
openDir: باز کردن پوشه
|
||||||
appLog: Application Log
|
appLog: گزارش برنامه
|
||||||
coreLog: Core Log
|
coreLog: گزارش هسته
|
||||||
restartClash: Restart Clash Core
|
restartClash: راهاندازی مجدد هسته Clash
|
||||||
restartApp: Restart Application
|
restartApp: راهاندازی مجدد برنامه
|
||||||
vergeVersion: Verge Version
|
vergeVersion: نسخه Verge
|
||||||
more: More
|
more: بیشتر
|
||||||
exit: Exit
|
exit: خروج
|
||||||
tooltip:
|
tooltip:
|
||||||
systemProxy: System Proxy
|
systemProxy: پروکسی سیستم
|
||||||
tun: TUN
|
tun: TUN
|
||||||
profile: Profile
|
profile: پروفایل
|
||||||
|
|||||||
@@ -1,60 +1,60 @@
|
|||||||
_version: 1
|
_version: 1
|
||||||
notifications:
|
notifications:
|
||||||
dashboardToggled:
|
dashboardToggled:
|
||||||
title: Dashboard
|
title: Dasbor
|
||||||
body: Dashboard visibility has been updated.
|
body: Visibilitas dasbor telah diperbarui.
|
||||||
clashModeChanged:
|
clashModeChanged:
|
||||||
title: Mode Switch
|
title: Peralihan Mode
|
||||||
body: Switched to {mode}.
|
body: Beralih ke {mode}.
|
||||||
systemProxyToggled:
|
systemProxyToggled:
|
||||||
title: System Proxy
|
title: Proksi Sistem
|
||||||
body: System proxy status has been updated.
|
body: Status proksi sistem telah diperbarui.
|
||||||
tunModeToggled:
|
tunModeToggled:
|
||||||
title: TUN Mode
|
title: Mode TUN
|
||||||
body: TUN mode status has been updated.
|
body: Status mode TUN telah diperbarui.
|
||||||
lightweightModeEntered:
|
lightweightModeEntered:
|
||||||
title: Lightweight Mode
|
title: Mode Ringan
|
||||||
body: Entered lightweight mode.
|
body: Masuk ke mode ringan.
|
||||||
profilesReactivated:
|
profilesReactivated:
|
||||||
title: Profiles
|
title: Profil
|
||||||
body: Profile Reactivated.
|
body: Profil diaktifkan kembali.
|
||||||
appQuit:
|
appQuit:
|
||||||
title: About to Exit
|
title: Akan Keluar
|
||||||
body: Clash Verge is about to exit.
|
body: Clash Verge akan keluar.
|
||||||
appHidden:
|
appHidden:
|
||||||
title: Application Hidden
|
title: Aplikasi Disembunyikan
|
||||||
body: Clash Verge is running in the background.
|
body: Clash Verge berjalan di latar belakang.
|
||||||
service:
|
service:
|
||||||
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
|
adminInstallPrompt: Menginstal layanan Clash Verge memerlukan hak administrator.
|
||||||
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
|
adminUninstallPrompt: Menghapus instalasi layanan Clash Verge memerlukan hak administrator.
|
||||||
tray:
|
tray:
|
||||||
dashboard: Dashboard
|
dashboard: Dasbor
|
||||||
ruleMode: Rule Mode
|
ruleMode: Mode Aturan
|
||||||
globalMode: Global Mode
|
globalMode: Mode Global
|
||||||
directMode: Direct Mode
|
directMode: Mode Langsung
|
||||||
outboundModes: Outbound Modes
|
outboundModes: Mode Keluar
|
||||||
rule: Aturan
|
rule: Aturan
|
||||||
direct: Langsung
|
direct: Langsung
|
||||||
global: Global
|
global: Global
|
||||||
profiles: Profiles
|
profiles: Profil
|
||||||
proxies: Proxies
|
proxies: Proksi
|
||||||
systemProxy: System Proxy
|
systemProxy: Proksi Sistem
|
||||||
tunMode: TUN Mode
|
tunMode: Mode TUN
|
||||||
closeAllConnections: Close All Connections
|
closeAllConnections: Tutup Semua Koneksi
|
||||||
lightweightMode: Lightweight Mode
|
lightweightMode: Mode Ringan
|
||||||
copyEnv: Copy Environment Variables
|
copyEnv: Salin Variabel Lingkungan
|
||||||
confDir: Configuration Directory
|
confDir: Direktori Konfigurasi
|
||||||
coreDir: Core Directory
|
coreDir: Direktori Core
|
||||||
logsDir: Log Directory
|
logsDir: Direktori Log
|
||||||
openDir: Open Directory
|
openDir: Buka Direktori
|
||||||
appLog: Application Log
|
appLog: Log Aplikasi
|
||||||
coreLog: Core Log
|
coreLog: Log Core
|
||||||
restartClash: Restart Clash Core
|
restartClash: Mulai Ulang Core Clash
|
||||||
restartApp: Restart Application
|
restartApp: Mulai Ulang Aplikasi
|
||||||
vergeVersion: Verge Version
|
vergeVersion: Versi Verge
|
||||||
more: More
|
more: Lainnya
|
||||||
exit: Exit
|
exit: Keluar
|
||||||
tooltip:
|
tooltip:
|
||||||
systemProxy: System Proxy
|
systemProxy: Proksi Sistem
|
||||||
tun: TUN
|
tun: TUN
|
||||||
profile: Profile
|
profile: Profil
|
||||||
|
|||||||
@@ -1,60 +1,60 @@
|
|||||||
_version: 1
|
_version: 1
|
||||||
notifications:
|
notifications:
|
||||||
dashboardToggled:
|
dashboardToggled:
|
||||||
title: Dashboard
|
title: ダッシュボード
|
||||||
body: Dashboard visibility has been updated.
|
body: ダッシュボードの表示状態が更新されました。
|
||||||
clashModeChanged:
|
clashModeChanged:
|
||||||
title: Mode Switch
|
title: モード切り替え
|
||||||
body: Switched to {mode}.
|
body: "{mode} に切り替えました。"
|
||||||
systemProxyToggled:
|
systemProxyToggled:
|
||||||
title: System Proxy
|
title: システムプロキシ
|
||||||
body: System proxy status has been updated.
|
body: システムプロキシの状態が更新されました。
|
||||||
tunModeToggled:
|
tunModeToggled:
|
||||||
title: TUN Mode
|
title: TUN モード
|
||||||
body: TUN mode status has been updated.
|
body: TUN モードの状態が更新されました。
|
||||||
lightweightModeEntered:
|
lightweightModeEntered:
|
||||||
title: Lightweight Mode
|
title: 軽量モード
|
||||||
body: Entered lightweight mode.
|
body: 軽量モードに入りました。
|
||||||
profilesReactivated:
|
profilesReactivated:
|
||||||
title: Profiles
|
title: プロファイル
|
||||||
body: Profile Reactivated.
|
body: プロファイルが再有効化されました。
|
||||||
appQuit:
|
appQuit:
|
||||||
title: About to Exit
|
title: 終了間近
|
||||||
body: Clash Verge is about to exit.
|
body: Clash Verge はまもなく終了します。
|
||||||
appHidden:
|
appHidden:
|
||||||
title: Application Hidden
|
title: アプリが非表示
|
||||||
body: Clash Verge is running in the background.
|
body: Clash Verge はバックグラウンドで実行中です。
|
||||||
service:
|
service:
|
||||||
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
|
adminInstallPrompt: Clash Verge サービスのインストールには管理者権限が必要です。
|
||||||
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
|
adminUninstallPrompt: Clash Verge サービスのアンインストールには管理者権限が必要です。
|
||||||
tray:
|
tray:
|
||||||
dashboard: Dashboard
|
dashboard: ダッシュボード
|
||||||
ruleMode: Rule Mode
|
ruleMode: ルールモード
|
||||||
globalMode: Global Mode
|
globalMode: グローバルモード
|
||||||
directMode: Direct Mode
|
directMode: ダイレクトモード
|
||||||
outboundModes: Outbound Modes
|
outboundModes: アウトバウンドモード
|
||||||
rule: ルール
|
rule: ルール
|
||||||
direct: ダイレクト
|
direct: ダイレクト
|
||||||
global: グローバル
|
global: グローバル
|
||||||
profiles: Profiles
|
profiles: プロファイル
|
||||||
proxies: Proxies
|
proxies: プロキシ
|
||||||
systemProxy: System Proxy
|
systemProxy: システムプロキシ
|
||||||
tunMode: TUN Mode
|
tunMode: TUN モード
|
||||||
closeAllConnections: Close All Connections
|
closeAllConnections: すべての接続を閉じる
|
||||||
lightweightMode: Lightweight Mode
|
lightweightMode: 軽量モード
|
||||||
copyEnv: Copy Environment Variables
|
copyEnv: 環境変数をコピー
|
||||||
confDir: Configuration Directory
|
confDir: 設定ディレクトリ
|
||||||
coreDir: Core Directory
|
coreDir: コアディレクトリ
|
||||||
logsDir: Log Directory
|
logsDir: ログディレクトリ
|
||||||
openDir: Open Directory
|
openDir: ディレクトリを開く
|
||||||
appLog: Application Log
|
appLog: アプリケーションログ
|
||||||
coreLog: Core Log
|
coreLog: コアログ
|
||||||
restartClash: Restart Clash Core
|
restartClash: Clash コアを再起動
|
||||||
restartApp: Restart Application
|
restartApp: アプリケーションを再起動
|
||||||
vergeVersion: Verge Version
|
vergeVersion: Verge バージョン
|
||||||
more: More
|
more: その他
|
||||||
exit: Exit
|
exit: 終了
|
||||||
tooltip:
|
tooltip:
|
||||||
systemProxy: System Proxy
|
systemProxy: システムプロキシ
|
||||||
tun: TUN
|
tun: TUN
|
||||||
profile: Profile
|
profile: プロファイル
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ notifications:
|
|||||||
title: 경량 모드
|
title: 경량 모드
|
||||||
body: 경량 모드에 진입했습니다.
|
body: 경량 모드에 진입했습니다.
|
||||||
profilesReactivated:
|
profilesReactivated:
|
||||||
title: Profiles
|
title: 프로필
|
||||||
body: Profile Reactivated.
|
body: 프로필이 다시 활성화되었습니다.
|
||||||
appQuit:
|
appQuit:
|
||||||
title: 곧 종료
|
title: 곧 종료
|
||||||
body: Clash Verge가 곧 종료됩니다.
|
body: Clash Verge가 곧 종료됩니다.
|
||||||
@@ -25,14 +25,14 @@ notifications:
|
|||||||
title: 앱이 숨겨짐
|
title: 앱이 숨겨짐
|
||||||
body: Clash Verge가 백그라운드에서 실행 중입니다.
|
body: Clash Verge가 백그라운드에서 실행 중입니다.
|
||||||
service:
|
service:
|
||||||
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
|
adminInstallPrompt: Clash Verge 서비스 설치에는 관리자 권한이 필요합니다.
|
||||||
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
|
adminUninstallPrompt: Clash Verge 서비스 제거에는 관리자 권한이 필요합니다.
|
||||||
tray:
|
tray:
|
||||||
dashboard: 대시보드
|
dashboard: 대시보드
|
||||||
ruleMode: 규칙 모드
|
ruleMode: 규칙 모드
|
||||||
globalMode: 전역 모드
|
globalMode: 전역 모드
|
||||||
directMode: 직접 모드
|
directMode: 직접 모드
|
||||||
outboundModes: Outbound Modes
|
outboundModes: 아웃바운드 모드
|
||||||
rule: 규칙
|
rule: 규칙
|
||||||
direct: 직접
|
direct: 직접
|
||||||
global: 글로벌
|
global: 글로벌
|
||||||
|
|||||||
@@ -1,60 +1,60 @@
|
|||||||
_version: 1
|
_version: 1
|
||||||
notifications:
|
notifications:
|
||||||
dashboardToggled:
|
dashboardToggled:
|
||||||
title: Dashboard
|
title: Панель
|
||||||
body: Dashboard visibility has been updated.
|
body: Видимость панели обновлена.
|
||||||
clashModeChanged:
|
clashModeChanged:
|
||||||
title: Mode Switch
|
title: Смена режима
|
||||||
body: Switched to {mode}.
|
body: Переключено на {mode}.
|
||||||
systemProxyToggled:
|
systemProxyToggled:
|
||||||
title: System Proxy
|
title: Системный прокси
|
||||||
body: System proxy status has been updated.
|
body: Статус системного прокси обновлен.
|
||||||
tunModeToggled:
|
tunModeToggled:
|
||||||
title: TUN Mode
|
title: Режим TUN
|
||||||
body: TUN mode status has been updated.
|
body: Статус режима TUN обновлен.
|
||||||
lightweightModeEntered:
|
lightweightModeEntered:
|
||||||
title: Lightweight Mode
|
title: Легкий режим
|
||||||
body: Entered lightweight mode.
|
body: Включен легкий режим.
|
||||||
profilesReactivated:
|
profilesReactivated:
|
||||||
title: Profiles
|
title: Профили
|
||||||
body: Profile Reactivated.
|
body: Профиль повторно активирован.
|
||||||
appQuit:
|
appQuit:
|
||||||
title: About to Exit
|
title: Скорый выход
|
||||||
body: Clash Verge is about to exit.
|
body: Clash Verge скоро завершит работу.
|
||||||
appHidden:
|
appHidden:
|
||||||
title: Application Hidden
|
title: Приложение скрыто
|
||||||
body: Clash Verge is running in the background.
|
body: Clash Verge работает в фоновом режиме.
|
||||||
service:
|
service:
|
||||||
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
|
adminInstallPrompt: Для установки службы Clash Verge требуются права администратора.
|
||||||
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
|
adminUninstallPrompt: Для удаления службы Clash Verge требуются права администратора.
|
||||||
tray:
|
tray:
|
||||||
dashboard: Dashboard
|
dashboard: Панель
|
||||||
ruleMode: Rule Mode
|
ruleMode: Режим правил
|
||||||
globalMode: Global Mode
|
globalMode: Глобальный режим
|
||||||
directMode: Direct Mode
|
directMode: Прямой режим
|
||||||
outboundModes: Outbound Modes
|
outboundModes: Исходящие режимы
|
||||||
rule: Правило
|
rule: Правило
|
||||||
direct: Прямой
|
direct: Прямой
|
||||||
global: Глобальный
|
global: Глобальный
|
||||||
profiles: Profiles
|
profiles: Профили
|
||||||
proxies: Proxies
|
proxies: Прокси
|
||||||
systemProxy: System Proxy
|
systemProxy: Системный прокси
|
||||||
tunMode: TUN Mode
|
tunMode: Режим TUN
|
||||||
closeAllConnections: Close All Connections
|
closeAllConnections: Закрыть все соединения
|
||||||
lightweightMode: Lightweight Mode
|
lightweightMode: Легкий режим
|
||||||
copyEnv: Copy Environment Variables
|
copyEnv: Копировать переменные среды
|
||||||
confDir: Configuration Directory
|
confDir: Каталог конфигурации
|
||||||
coreDir: Core Directory
|
coreDir: Каталог ядра
|
||||||
logsDir: Log Directory
|
logsDir: Каталог журналов
|
||||||
openDir: Open Directory
|
openDir: Открыть каталог
|
||||||
appLog: Application Log
|
appLog: Журнал приложения
|
||||||
coreLog: Core Log
|
coreLog: Журнал ядра
|
||||||
restartClash: Restart Clash Core
|
restartClash: Перезапустить ядро Clash
|
||||||
restartApp: Restart Application
|
restartApp: Перезапустить приложение
|
||||||
vergeVersion: Verge Version
|
vergeVersion: Версия Verge
|
||||||
more: More
|
more: Еще
|
||||||
exit: Exit
|
exit: Выход
|
||||||
tooltip:
|
tooltip:
|
||||||
systemProxy: System Proxy
|
systemProxy: Системный прокси
|
||||||
tun: TUN
|
tun: TUN
|
||||||
profile: Profile
|
profile: Профиль
|
||||||
|
|||||||
@@ -1,60 +1,60 @@
|
|||||||
_version: 1
|
_version: 1
|
||||||
notifications:
|
notifications:
|
||||||
dashboardToggled:
|
dashboardToggled:
|
||||||
title: Dashboard
|
title: Gösterge Paneli
|
||||||
body: Dashboard visibility has been updated.
|
body: Gösterge panelinin görünürlüğü güncellendi.
|
||||||
clashModeChanged:
|
clashModeChanged:
|
||||||
title: Mode Switch
|
title: Mod Değişimi
|
||||||
body: Switched to {mode}.
|
body: "{mode} moduna geçildi."
|
||||||
systemProxyToggled:
|
systemProxyToggled:
|
||||||
title: System Proxy
|
title: Sistem Vekil'i
|
||||||
body: System proxy status has been updated.
|
body: Sistem vekil'i durumu güncellendi.
|
||||||
tunModeToggled:
|
tunModeToggled:
|
||||||
title: TUN Mode
|
title: TUN Modu
|
||||||
body: TUN mode status has been updated.
|
body: TUN modu durumu güncellendi.
|
||||||
lightweightModeEntered:
|
lightweightModeEntered:
|
||||||
title: Lightweight Mode
|
title: Hafif Mod
|
||||||
body: Entered lightweight mode.
|
body: Hafif moda geçildi.
|
||||||
profilesReactivated:
|
profilesReactivated:
|
||||||
title: Profiles
|
title: Profiller
|
||||||
body: Profile Reactivated.
|
body: Profil yeniden etkinleştirildi.
|
||||||
appQuit:
|
appQuit:
|
||||||
title: About to Exit
|
title: Çıkış Yapılmak Üzere
|
||||||
body: Clash Verge is about to exit.
|
body: Clash Verge kapanmak üzere.
|
||||||
appHidden:
|
appHidden:
|
||||||
title: Application Hidden
|
title: Uygulama Gizlendi
|
||||||
body: Clash Verge is running in the background.
|
body: Clash Verge arka planda çalışıyor.
|
||||||
service:
|
service:
|
||||||
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
|
adminInstallPrompt: Clash Verge hizmetini kurmak için yönetici ayrıcalıkları gerekir.
|
||||||
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
|
adminUninstallPrompt: Clash Verge hizmetini kaldırmak için yönetici ayrıcalıkları gerekir.
|
||||||
tray:
|
tray:
|
||||||
dashboard: Dashboard
|
dashboard: Gösterge Paneli
|
||||||
ruleMode: Rule Mode
|
ruleMode: Kural Modu
|
||||||
globalMode: Global Mode
|
globalMode: Küresel Mod
|
||||||
directMode: Direct Mode
|
directMode: Doğrudan Mod
|
||||||
outboundModes: Outbound Modes
|
outboundModes: Giden Modlar
|
||||||
rule: Kural
|
rule: Kural
|
||||||
direct: Doğrudan
|
direct: Doğrudan
|
||||||
global: Küresel
|
global: Küresel
|
||||||
profiles: Profiles
|
profiles: Profiller
|
||||||
proxies: Proxies
|
proxies: Vekil'ler
|
||||||
systemProxy: System Proxy
|
systemProxy: Sistem Vekil'i
|
||||||
tunMode: TUN Mode
|
tunMode: TUN Modu
|
||||||
closeAllConnections: Close All Connections
|
closeAllConnections: Tüm Bağlantıları Kapat
|
||||||
lightweightMode: Lightweight Mode
|
lightweightMode: Hafif Mod
|
||||||
copyEnv: Copy Environment Variables
|
copyEnv: Ortam Değişkenlerini Kopyala
|
||||||
confDir: Configuration Directory
|
confDir: Yapılandırma Dizini
|
||||||
coreDir: Core Directory
|
coreDir: Çekirdek Dizini
|
||||||
logsDir: Log Directory
|
logsDir: Günlük Dizini
|
||||||
openDir: Open Directory
|
openDir: Dizini Aç
|
||||||
appLog: Application Log
|
appLog: Uygulama Günlüğü
|
||||||
coreLog: Core Log
|
coreLog: Çekirdek Günlüğü
|
||||||
restartClash: Restart Clash Core
|
restartClash: Clash Çekirdeğini Yeniden Başlat
|
||||||
restartApp: Restart Application
|
restartApp: Uygulamayı Yeniden Başlat
|
||||||
vergeVersion: Verge Version
|
vergeVersion: Verge Sürümü
|
||||||
more: More
|
more: Daha Fazla
|
||||||
exit: Exit
|
exit: Çıkış
|
||||||
tooltip:
|
tooltip:
|
||||||
systemProxy: System Proxy
|
systemProxy: Sistem Vekil'i
|
||||||
tun: TUN
|
tun: TUN
|
||||||
profile: Profile
|
profile: Profil
|
||||||
|
|||||||
@@ -1,60 +1,60 @@
|
|||||||
_version: 1
|
_version: 1
|
||||||
notifications:
|
notifications:
|
||||||
dashboardToggled:
|
dashboardToggled:
|
||||||
title: Dashboard
|
title: Идарә панеле
|
||||||
body: Dashboard visibility has been updated.
|
body: Идарә панеленең күренеше яңартылды.
|
||||||
clashModeChanged:
|
clashModeChanged:
|
||||||
title: Mode Switch
|
title: Режим алыштыру
|
||||||
body: Switched to {mode}.
|
body: "{mode} режимына күчтел."
|
||||||
systemProxyToggled:
|
systemProxyToggled:
|
||||||
title: System Proxy
|
title: Системалы прокси
|
||||||
body: System proxy status has been updated.
|
body: Системалы прокси хәле яңартылды.
|
||||||
tunModeToggled:
|
tunModeToggled:
|
||||||
title: TUN Mode
|
title: TUN режимы
|
||||||
body: TUN mode status has been updated.
|
body: TUN режимы хәле яңартылды.
|
||||||
lightweightModeEntered:
|
lightweightModeEntered:
|
||||||
title: Lightweight Mode
|
title: Җиңел режим
|
||||||
body: Entered lightweight mode.
|
body: Җиңел режимга күчелде.
|
||||||
profilesReactivated:
|
profilesReactivated:
|
||||||
title: Profiles
|
title: Профильләр
|
||||||
body: Profile Reactivated.
|
body: Профиль яңадан активлаштырылды.
|
||||||
appQuit:
|
appQuit:
|
||||||
title: About to Exit
|
title: Чыгар алдыннан
|
||||||
body: Clash Verge is about to exit.
|
body: Clash Verge чыгарга җыена.
|
||||||
appHidden:
|
appHidden:
|
||||||
title: Application Hidden
|
title: Кушымта яшерелде
|
||||||
body: Clash Verge is running in the background.
|
body: Clash Verge фон режимында эшли.
|
||||||
service:
|
service:
|
||||||
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
|
adminInstallPrompt: Clash Verge хезмәтен урнаштыру өчен администратор хокуклары кирәк.
|
||||||
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
|
adminUninstallPrompt: Clash Verge хезмәтен бетерү өчен администратор хокуклары кирәк.
|
||||||
tray:
|
tray:
|
||||||
dashboard: Dashboard
|
dashboard: Идарә панеле
|
||||||
ruleMode: Rule Mode
|
ruleMode: Кагыйдә режимы
|
||||||
globalMode: Global Mode
|
globalMode: Глобаль режим
|
||||||
directMode: Direct Mode
|
directMode: Турыдан-туры режим
|
||||||
outboundModes: Outbound Modes
|
outboundModes: Чыгыш режимнары
|
||||||
rule: Rule
|
rule: Кагыйдә
|
||||||
direct: Direct
|
direct: Турыдан-туры
|
||||||
global: Global
|
global: Глобаль
|
||||||
profiles: Profiles
|
profiles: Профильләр
|
||||||
proxies: Proxies
|
proxies: Проксилар
|
||||||
systemProxy: System Proxy
|
systemProxy: Системалы прокси
|
||||||
tunMode: TUN Mode
|
tunMode: TUN режимы
|
||||||
closeAllConnections: Close All Connections
|
closeAllConnections: Барлык тоташуларны ябу
|
||||||
lightweightMode: Lightweight Mode
|
lightweightMode: Җиңел режим
|
||||||
copyEnv: Copy Environment Variables
|
copyEnv: Мохит үзгәрүчәннәрен күчерү
|
||||||
confDir: Configuration Directory
|
confDir: Конфигурация каталогы
|
||||||
coreDir: Core Directory
|
coreDir: Ядро каталогы
|
||||||
logsDir: Log Directory
|
logsDir: Журнал каталогы
|
||||||
openDir: Open Directory
|
openDir: Каталогны ачу
|
||||||
appLog: Application Log
|
appLog: Кушымта журналы
|
||||||
coreLog: Core Log
|
coreLog: Ядро журналы
|
||||||
restartClash: Restart Clash Core
|
restartClash: Clash ядрәсен кабат җибәрү
|
||||||
restartApp: Restart Application
|
restartApp: Кушымтаны кабат җибәрү
|
||||||
vergeVersion: Verge Version
|
vergeVersion: Verge версиясе
|
||||||
more: More
|
more: Күбрәк
|
||||||
exit: Exit
|
exit: Чыгу
|
||||||
tooltip:
|
tooltip:
|
||||||
systemProxy: System Proxy
|
systemProxy: Системалы прокси
|
||||||
tun: TUN
|
tun: TUN
|
||||||
profile: Profile
|
profile: Профиль
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "clash-verge-types"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
rust-version = "1.91"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
serde = { workspace = true }
|
|
||||||
serde_yaml_ng = { workspace = true }
|
|
||||||
smartstring = { workspace = true }
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pub mod runtime;
|
|
||||||
@@ -120,6 +120,12 @@ fn is_binary_admin() -> bool {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub fn current_gid() -> u32 {
|
||||||
|
unsafe { libc::getgid() }
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn list_network_interfaces() -> Vec<String> {
|
pub fn list_network_interfaces() -> Vec<String> {
|
||||||
let mut networks = Networks::new();
|
let mut networks = Networks::new();
|
||||||
|
|||||||
@@ -69,19 +69,6 @@ Join [@clash_verge_rev](https://t.me/clash_verge_re) for update announcements.
|
|||||||
|
|
||||||
🌐 Official Website: 👉 [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
🌐 Official Website: 👉 [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
#### Build Infrastructure Sponsor — [YXVM Dedicated Servers](https://yxvm.com/aff.php?aff=827)
|
|
||||||
|
|
||||||
Our builds and releases run on YXVM dedicated servers that deliver premium resources, strong performance, and high-speed networking. If downloads feel fast and usage feels snappy, it is thanks to robust hardware.
|
|
||||||
|
|
||||||
🧩 Highlights of YXVM Dedicated Servers:
|
|
||||||
|
|
||||||
- 🌎 Optimized global routes for dramatically faster downloads
|
|
||||||
- 🔧 Bare-metal resources instead of shared VPS capacity for maximum performance
|
|
||||||
- 🧠 Great for proxy workloads, hosting web/CDN services, CI/CD pipelines, or any high-load tasks
|
|
||||||
- 💡 Ready to use instantly with multiple datacenter options, including CN2 and IEPL
|
|
||||||
- 📦 The configuration used by this project is on sale—feel free to get the same setup
|
|
||||||
- 🎯 Want the same build environment? [Order a YXVM server today](https://yxvm.com/aff.php?aff=827)
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Built on high-performance Rust with the Tauri 2 framework
|
- Built on high-performance Rust with the Tauri 2 framework
|
||||||
|
|||||||
@@ -63,19 +63,6 @@ Consulta la [documentación del proyecto](https://clash-verge-rev.github.io/) pa
|
|||||||
- Desbloquea servicios de streaming y acceso a ChatGPT
|
- Desbloquea servicios de streaming y acceso a ChatGPT
|
||||||
- Sitio oficial: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
- Sitio oficial: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
#### Patrocinador de la infraestructura de compilación — [Servidores dedicados YXVM](https://yxvm.com/aff.php?aff=827)
|
|
||||||
|
|
||||||
Las compilaciones y lanzamientos del proyecto se ejecutan en servidores dedicados de YXVM, que proporcionan recursos premium, alto rendimiento y redes de alta velocidad. Si las descargas son rápidas y el uso es fluido, es gracias a este hardware robusto.
|
|
||||||
|
|
||||||
🧩 Ventajas de los servidores dedicados YXVM:
|
|
||||||
|
|
||||||
- 🌎 Rutas globales optimizadas para descargas significativamente más rápidas
|
|
||||||
- 🔧 Recursos bare-metal, en lugar de VPS compartidos, para obtener el máximo rendimiento
|
|
||||||
- 🧠 Ideales para proxys, alojamiento de sitios web/CDN, pipelines de CI/CD o cualquier carga elevada
|
|
||||||
- 💡 Listos para usar al instante, con múltiples centros de datos disponibles (incluidos CN2 e IEPL)
|
|
||||||
- 📦 La misma configuración utilizada por este proyecto está disponible para su compra
|
|
||||||
- 🎯 ¿Quieres el mismo entorno de compilación? [Solicita un servidor YXVM hoy](https://yxvm.com/aff.php?aff=827)
|
|
||||||
|
|
||||||
## Funciones
|
## Funciones
|
||||||
|
|
||||||
- Basado en Rust de alto rendimiento y en el framework Tauri 2
|
- Basado en Rust de alto rendimiento y en el framework Tauri 2
|
||||||
|
|||||||
@@ -62,18 +62,6 @@
|
|||||||
- پشتیبانی از سرویسهای استریم و دسترسی به ChatGPT
|
- پشتیبانی از سرویسهای استریم و دسترسی به ChatGPT
|
||||||
- وبسایت رسمی: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
- وبسایت رسمی: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
#### حامی زیرساخت ساخت — [سرورهای اختصاصی YXVM](https://yxvm.com/aff.php?aff=827)
|
|
||||||
|
|
||||||
بیلدها و نسخههای ما روی سرورهای اختصاصی YXVM اجرا میشوند که منابع ممتاز، عملکرد قوی و شبکه پرسرعت را ارائه میدهند. اگر دانلودها سریع و استفاده از آن سریع به نظر میرسد، به لطف سختافزار قوی است.
|
|
||||||
🧩 نکات برجسته سرورهای اختصاصی YXVM:
|
|
||||||
|
|
||||||
- 🌎 مسیرهای جهانی بهینه شده برای دانلودهای بسیار سریعتر
|
|
||||||
- 🔧 منابع فیزیکی به جای ظرفیت VPS مشترک برای حداکثر کارایی
|
|
||||||
- 🧠 عالی برای بارهای کاری پروکسی، میزبانی سرویسهای وب/CDN، خطوط لوله CI/CD یا هرگونه کار با بار بالا
|
|
||||||
- 💡 آماده استفاده فوری با گزینههای متعدد مرکز داده، از جمله CN2 و IEPL
|
|
||||||
- 📦 پیکربندی مورد استفاده در این پروژه در حال فروش است - میتوانید همان تنظیمات را تهیه کنید.
|
|
||||||
- 🎯 آیا محیط ساخت مشابهی میخواهید؟ [همین امروز یک سرور YXVM سفارش دهید](https://yxvm.com/aff.php?aff=827)
|
|
||||||
|
|
||||||
## ویژگیها
|
## ویژگیها
|
||||||
|
|
||||||
- ساخته شده بر اساس Rust با کارایی بالا و فریمورک Tauri 2
|
- ساخته شده بر اساس Rust با کارایی بالا و فریمورک Tauri 2
|
||||||
|
|||||||
@@ -63,19 +63,6 @@ Windows (x64/x86)、Linux (x64/arm64)、macOS 10.15+ (Intel/Apple) をサポー
|
|||||||
- ストリーミングおよび ChatGPT の利用にも対応
|
- ストリーミングおよび ChatGPT の利用にも対応
|
||||||
- 公式サイト: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
- 公式サイト: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
#### ビルド環境スポンサー — [YXVM 専用サーバー](https://yxvm.com/aff.php?aff=827)
|
|
||||||
|
|
||||||
本プロジェクトのビルドとリリースは、YXVM の専用サーバーによって支えられています。高速ダウンロードや快適な操作性は、強力なハードウェアがあってこそです。
|
|
||||||
|
|
||||||
🧩 YXVM 専用サーバーの特長:
|
|
||||||
|
|
||||||
- 🌎 最適化されたグローバル回線で圧倒的なダウンロード速度
|
|
||||||
- 🔧 VPS とは異なるベアメタル資源で最高性能を発揮
|
|
||||||
- 🧠 プロキシ運用、Web/CDN ホスティング、CI/CD など高負荷ワークロードに最適
|
|
||||||
- 💡 複数データセンターから即時利用可能。CN2 や IEPL も選択可
|
|
||||||
- 📦 本プロジェクトが使用している構成も販売中。同じ環境を入手できます
|
|
||||||
- 🎯 同じビルド体験をしたい方は [今すぐ YXVM サーバーを注文](https://yxvm.com/aff.php?aff=827)
|
|
||||||
|
|
||||||
## 機能
|
## 機能
|
||||||
|
|
||||||
- 高性能な Rust と Tauri 2 フレームワークに基づくデスクトップアプリ
|
- 高性能な Rust と Tauri 2 フレームワークに基づくデスクトップアプリ
|
||||||
|
|||||||
@@ -63,19 +63,6 @@ Windows (x64/x86), Linux (x64/arm64), macOS 10.15+ (Intel/Apple)을 지원합니
|
|||||||
- 스트리밍 및 ChatGPT 접근 지원
|
- 스트리밍 및 ChatGPT 접근 지원
|
||||||
- 공식 사이트: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
- 공식 사이트: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
#### 빌드 인프라 스폰서 — [YXVM 전용 서버](https://yxvm.com/aff.php?aff=827)
|
|
||||||
|
|
||||||
본 프로젝트의 빌드 및 릴리스는 YXVM 전용 서버에서 구동됩니다. 빠른 다운로드와 경쾌한 사용감은 탄탄한 하드웨어 덕분입니다.
|
|
||||||
|
|
||||||
🧩 YXVM 전용 서버 하이라이트:
|
|
||||||
|
|
||||||
- 🌎 최적화된 글로벌 라우팅으로 대폭 빨라진 다운로드
|
|
||||||
- 🔧 공유 VPS가 아닌 베어메탈 자원으로 최대 성능 제공
|
|
||||||
- 🧠 프록시 워크로드, Web/CDN 호스팅, CI/CD, 고부하 작업에 적합
|
|
||||||
- 💡 CN2 / IEPL 등 다양한 데이터센터 옵션, 즉시 사용 가능
|
|
||||||
- 📦 본 프로젝트가 사용하는 구성도 판매 중 — 동일한 환경을 사용할 수 있습니다
|
|
||||||
- 🎯 동일한 빌드 환경이 필요하다면 [지금 YXVM 서버 주문](https://yxvm.com/aff.php?aff=827)
|
|
||||||
|
|
||||||
## 기능
|
## 기능
|
||||||
|
|
||||||
- 고성능 Rust와 Tauri 2 프레임워크 기반 데스크톱 앱
|
- 고성능 Rust와 Tauri 2 프레임워크 기반 데스크톱 앱
|
||||||
|
|||||||
@@ -59,19 +59,6 @@ Clash Meta GUI базируется на <a href="https://github.com/tauri-apps/
|
|||||||
- Разблокировка потоковые сервисы и ChatGPT
|
- Разблокировка потоковые сервисы и ChatGPT
|
||||||
- Официальный сайт: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
- Официальный сайт: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
#### Среда сборки и публикации этого проекта полностью поддерживается выделенным сервером [YXVM](https://yxvm.com/aff.php?aff=827)
|
|
||||||
|
|
||||||
Благодарим вас за предоставление надежной бэкэнд-среды с эксклюзивными ресурсами, высокой производительностью и высокоскоростной сетью. Если вы считаете, что загрузка файлов происходит достаточно быстро, а использование — достаточно плавно, то это потому, что мы используем серверы высшего уровня!
|
|
||||||
|
|
||||||
🧩 Преимущества выделенного сервера YXVM:
|
|
||||||
|
|
||||||
- 🌎 Премиум-сеть с оптимизацией обратного пути для молниеносной скорости загрузки
|
|
||||||
- 🔧 Выделенные физические серверные ресурсы, не имеющие аналогов среди VPS, обеспечивающие максимальную производительность
|
|
||||||
- 🧠 Идеально подходит для прокси, хостинга веб-сайтов/CDN-сайтов, рабочих процессов CI/CD или любых приложений с высокой нагрузкой
|
|
||||||
- 💡 Поддержка использования сразу после включения, выбор нескольких дата-центров, CN2 / IEPL на выбор
|
|
||||||
- 📦 Эта конфигурация в настоящее время доступна для покупки — не стесняйтесь заказывать ту же модель!
|
|
||||||
- 🎯 Хотите попробовать такую же сборку? [Закажите выделенный сервер YXVM прямо сейчас!](https://yxvm.com/aff.php?aff=827)
|
|
||||||
|
|
||||||
## Фичи
|
## Фичи
|
||||||
|
|
||||||
- Основан на произвоительном Rust и фреймворке Tauri 2
|
- Основан на произвоительном Rust и фреймворке Tauri 2
|
||||||
|
|||||||
+13
-13
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "clash-verge",
|
"name": "clash-verge",
|
||||||
"version": "2.4.5-rc.1",
|
"version": "2.4.5",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "husky || true",
|
"prepare": "husky || true",
|
||||||
@@ -48,9 +48,9 @@
|
|||||||
"@tanstack/react-virtual": "^3.13.18",
|
"@tanstack/react-virtual": "^3.13.18",
|
||||||
"@tauri-apps/api": "2.9.1",
|
"@tauri-apps/api": "2.9.1",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||||
"@tauri-apps/plugin-dialog": "^2.5.0",
|
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||||
"@tauri-apps/plugin-fs": "^2.4.5",
|
"@tauri-apps/plugin-fs": "^2.4.5",
|
||||||
"@tauri-apps/plugin-http": "~2.5.5",
|
"@tauri-apps/plugin-http": "~2.5.6",
|
||||||
"@tauri-apps/plugin-process": "^2.3.1",
|
"@tauri-apps/plugin-process": "^2.3.1",
|
||||||
"@tauri-apps/plugin-shell": "2.3.4",
|
"@tauri-apps/plugin-shell": "2.3.4",
|
||||||
"@tauri-apps/plugin-updater": "2.9.0",
|
"@tauri-apps/plugin-updater": "2.9.0",
|
||||||
@@ -66,9 +66,9 @@
|
|||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"react-error-boundary": "6.0.3",
|
"react-error-boundary": "6.1.0",
|
||||||
"react-hook-form": "^7.71.0",
|
"react-hook-form": "^7.71.1",
|
||||||
"react-i18next": "16.5.2",
|
"react-i18next": "16.5.3",
|
||||||
"react-markdown": "10.1.0",
|
"react-markdown": "10.1.0",
|
||||||
"react-router": "^7.12.0",
|
"react-router": "^7.12.0",
|
||||||
"react-virtuoso": "^4.18.1",
|
"react-virtuoso": "^4.18.1",
|
||||||
@@ -79,12 +79,12 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/github": "^7.0.0",
|
"@actions/github": "^7.0.0",
|
||||||
"@eslint-react/eslint-plugin": "^2.5.5",
|
"@eslint-react/eslint-plugin": "^2.7.2",
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@tauri-apps/cli": "2.9.6",
|
"@tauri-apps/cli": "2.9.6",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^24.10.7",
|
"@types/node": "^24.10.9",
|
||||||
"@types/react": "19.2.8",
|
"@types/react": "19.2.8",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
"@vitejs/plugin-legacy": "^7.2.1",
|
"@vitejs/plugin-legacy": "^7.2.1",
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
"eslint-config-prettier": "^10.1.8",
|
"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.4",
|
"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.4.26",
|
"eslint-plugin-react-refresh": "^0.4.26",
|
||||||
@@ -109,12 +109,12 @@
|
|||||||
"jiti": "^2.6.1",
|
"jiti": "^2.6.1",
|
||||||
"lint-staged": "^16.2.7",
|
"lint-staged": "^16.2.7",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.8.0",
|
||||||
"sass": "^1.97.2",
|
"sass": "^1.97.2",
|
||||||
"tar": "^7.5.2",
|
"tar": "^7.5.3",
|
||||||
"terser": "^5.44.1",
|
"terser": "^5.46.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.52.0",
|
"typescript-eslint": "^8.53.0",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vite-plugin-svgr": "^4.5.0"
|
"vite-plugin-svgr": "^4.5.0"
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+791
-791
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "clash-verge"
|
name = "clash-verge"
|
||||||
version = "2.4.5-rc.1"
|
version = "2.4.5"
|
||||||
description = "clash verge"
|
description = "clash verge"
|
||||||
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
|
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
@@ -34,7 +34,6 @@ tauri-build = { version = "2.5.3", features = [] }
|
|||||||
clash-verge-draft = { workspace = true }
|
clash-verge-draft = { workspace = true }
|
||||||
clash-verge-logging = { workspace = true }
|
clash-verge-logging = { workspace = true }
|
||||||
clash-verge-signal = { workspace = true }
|
clash-verge-signal = { workspace = true }
|
||||||
clash-verge-types = { workspace = true }
|
|
||||||
clash-verge-i18n = { workspace = true }
|
clash-verge-i18n = { workspace = true }
|
||||||
tauri-plugin-clash-verge-sysinfo = { workspace = true }
|
tauri-plugin-clash-verge-sysinfo = { workspace = true }
|
||||||
tauri-plugin-clipboard-manager = { workspace = true }
|
tauri-plugin-clipboard-manager = { workspace = true }
|
||||||
@@ -60,7 +59,7 @@ warp = { version = "0.4.2", features = ["server"] }
|
|||||||
open = "5.3.3"
|
open = "5.3.3"
|
||||||
dunce = "1.0.5"
|
dunce = "1.0.5"
|
||||||
nanoid = "0.4"
|
nanoid = "0.4"
|
||||||
chrono = "0.4.42"
|
chrono = "0.4.43"
|
||||||
boa_engine = "0.21.0"
|
boa_engine = "0.21.0"
|
||||||
once_cell = { version = "1.21.3", features = ["parking_lot"] }
|
once_cell = { version = "1.21.3", features = ["parking_lot"] }
|
||||||
delay_timer = "0.11.6"
|
delay_timer = "0.11.6"
|
||||||
@@ -72,18 +71,18 @@ reqwest = { version = "0.13.1", features = [
|
|||||||
"form",
|
"form",
|
||||||
] }
|
] }
|
||||||
regex = "1.12.2"
|
regex = "1.12.2"
|
||||||
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", features = [
|
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", branch = "0.4.3", features = [
|
||||||
"guard",
|
"guard",
|
||||||
] }
|
] }
|
||||||
network-interface = { version = "2.0.5", features = ["serde"] }
|
network-interface = { version = "2.0.5", features = ["serde"] }
|
||||||
tauri-plugin-shell = "2.3.4"
|
tauri-plugin-shell = "2.3.4"
|
||||||
tauri-plugin-dialog = "2.5.0"
|
tauri-plugin-dialog = "2.6.0"
|
||||||
tauri-plugin-fs = "2.4.5"
|
tauri-plugin-fs = "2.4.5"
|
||||||
tauri-plugin-process = "2.3.1"
|
tauri-plugin-process = "2.3.1"
|
||||||
tauri-plugin-deep-link = "2.4.6"
|
tauri-plugin-deep-link = "2.4.6"
|
||||||
tauri-plugin-window-state = "2.4.1"
|
tauri-plugin-window-state = "2.4.1"
|
||||||
zip = "7.0.0"
|
zip = "7.1.0"
|
||||||
reqwest_dav = "0.2.2"
|
reqwest_dav = "0.3.1"
|
||||||
aes-gcm = { version = "0.10.3", features = ["std"] }
|
aes-gcm = { version = "0.10.3", features = ["std"] }
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
getrandom = "0.3.4"
|
getrandom = "0.3.4"
|
||||||
@@ -93,13 +92,13 @@ scopeguard = "1.2.0"
|
|||||||
tauri-plugin-notification = "2.3.3"
|
tauri-plugin-notification = "2.3.3"
|
||||||
tokio-stream = "0.1.18"
|
tokio-stream = "0.1.18"
|
||||||
backoff = { version = "0.4.0", features = ["tokio"] }
|
backoff = { version = "0.4.0", features = ["tokio"] }
|
||||||
tauri-plugin-http = "2.5.5"
|
tauri-plugin-http = "2.5.6"
|
||||||
console-subscriber = { version = "0.5.0", optional = true }
|
console-subscriber = { version = "0.5.0", optional = true }
|
||||||
tauri-plugin-devtools = { version = "2.0.1" }
|
tauri-plugin-devtools = { version = "2.0.1" }
|
||||||
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo" }
|
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo" }
|
||||||
clash_verge_logger = { git = "https://github.com/clash-verge-rev/clash-verge-logger" }
|
clash_verge_logger = { git = "https://github.com/clash-verge-rev/clash-verge-logger" }
|
||||||
async-trait = "0.1.89"
|
async-trait = "0.1.89"
|
||||||
clash_verge_service_ipc = { version = "2.0.29", features = [
|
clash_verge_service_ipc = { version = "2.1.1", features = [
|
||||||
"client",
|
"client",
|
||||||
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
|
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
|
||||||
arc-swap = "1.8.0"
|
arc-swap = "1.8.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use super::{IClashTemp, IProfiles, IVerge};
|
use super::{IClashTemp, IProfiles, IVerge};
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{PrfItem, profiles_append_item_safe},
|
config::{PrfItem, profiles_append_item_safe, runtime::IRuntime},
|
||||||
constants::{files, timing},
|
constants::{files, timing},
|
||||||
core::{
|
core::{
|
||||||
CoreManager,
|
CoreManager,
|
||||||
@@ -16,7 +16,6 @@ use anyhow::{Result, anyhow};
|
|||||||
use backoff::{Error as BackoffError, ExponentialBackoff};
|
use backoff::{Error as BackoffError, ExponentialBackoff};
|
||||||
use clash_verge_draft::Draft;
|
use clash_verge_draft::Draft;
|
||||||
use clash_verge_logging::{Type, logging, logging_error};
|
use clash_verge_logging::{Type, logging, logging_error};
|
||||||
use clash_verge_types::runtime::IRuntime;
|
|
||||||
use smartstring::alias::String;
|
use smartstring::alias::String;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tauri_plugin_clash_verge_sysinfo::is_current_app_handle_admin;
|
use tauri_plugin_clash_verge_sysinfo::is_current_app_handle_admin;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ mod config;
|
|||||||
mod encrypt;
|
mod encrypt;
|
||||||
mod prfitem;
|
mod prfitem;
|
||||||
pub mod profiles;
|
pub mod profiles;
|
||||||
|
pub mod runtime;
|
||||||
mod verge;
|
mod verge;
|
||||||
|
|
||||||
pub use self::{clash::*, config::*, encrypt::*, prfitem::*, profiles::*, verge::*};
|
pub use self::{clash::*, config::*, encrypt::*, prfitem::*, profiles::*, verge::*};
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ use serde_yaml_ng::{Mapping, Value};
|
|||||||
use smartstring::alias::String;
|
use smartstring::alias::String;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
use crate::enhance::field::use_keys;
|
||||||
|
|
||||||
const PATCH_CONFIG_INNER: [&str; 4] = ["allow-lan", "ipv6", "log-level", "unified-delay"];
|
const PATCH_CONFIG_INNER: [&str; 4] = ["allow-lan", "ipv6", "log-level", "unified-delay"];
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
@@ -136,13 +138,3 @@ impl IRuntime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO 完整迁移 enhance 行为后移除
|
|
||||||
#[inline]
|
|
||||||
fn use_keys<'a>(config: &'a Mapping) -> impl Iterator<Item = String> + 'a {
|
|
||||||
config.iter().filter_map(|(key, _)| key.as_str()).map(|s: &str| {
|
|
||||||
let mut s: String = s.into();
|
|
||||||
s.make_ascii_lowercase();
|
|
||||||
s
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -14,7 +14,6 @@ use std::{
|
|||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use tauri_plugin_http::reqwest as tauri_reqwest;
|
|
||||||
use tokio::{fs, time::timeout};
|
use tokio::{fs, time::timeout};
|
||||||
use zip::write::SimpleFileOptions;
|
use zip::write::SimpleFileOptions;
|
||||||
|
|
||||||
@@ -111,12 +110,12 @@ impl WebDavClient {
|
|||||||
// 创建新的客户端
|
// 创建新的客户端
|
||||||
let client = reqwest_dav::ClientBuilder::new()
|
let client = reqwest_dav::ClientBuilder::new()
|
||||||
.set_agent(
|
.set_agent(
|
||||||
tauri_reqwest::Client::builder()
|
reqwest::Client::builder()
|
||||||
.use_rustls_tls()
|
.use_rustls_tls()
|
||||||
.danger_accept_invalid_certs(true)
|
.danger_accept_invalid_certs(true)
|
||||||
.timeout(Duration::from_secs(op.timeout()))
|
.timeout(Duration::from_secs(op.timeout()))
|
||||||
.user_agent(format!("clash-verge/{APP_VERSION} ({OS} WebDAV-Client)"))
|
.user_agent(format!("clash-verge/{APP_VERSION} ({OS} WebDAV-Client)"))
|
||||||
.redirect(tauri_reqwest::redirect::Policy::custom(|attempt| {
|
.redirect(reqwest::redirect::Policy::custom(|attempt| {
|
||||||
// 允许所有请求类型的重定向,包括PUT
|
// 允许所有请求类型的重定向,包括PUT
|
||||||
if attempt.previous().len() >= 5 {
|
if attempt.previous().len() >= 5 {
|
||||||
attempt.error("重定向次数过多")
|
attempt.error("重定向次数过多")
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
use super::CoreManager;
|
use super::CoreManager;
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{Config, ConfigType},
|
config::{Config, ConfigType, runtime::IRuntime},
|
||||||
constants::timing,
|
constants::timing,
|
||||||
core::{handle, validate::CoreConfigValidator},
|
core::{handle, validate::CoreConfigValidator},
|
||||||
utils::{dirs, help},
|
utils::{dirs, help},
|
||||||
};
|
};
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
use clash_verge_logging::{Type, logging};
|
use clash_verge_logging::{Type, logging};
|
||||||
use clash_verge_types::runtime::IRuntime;
|
|
||||||
use smartstring::alias::String;
|
use smartstring::alias::String;
|
||||||
use std::{collections::HashSet, path::PathBuf, time::Instant};
|
use std::{collections::HashSet, path::PathBuf, time::Instant};
|
||||||
use tauri_plugin_mihomo::Error as MihomoError;
|
use tauri_plugin_mihomo::Error as MihomoError;
|
||||||
|
|||||||
@@ -256,9 +256,11 @@ fn install_service() -> Result<()> {
|
|||||||
|
|
||||||
// clash_verge_i18n::sync_locale(Config::verge().await.latest_arc().language.as_deref());
|
// clash_verge_i18n::sync_locale(Config::verge().await.latest_arc().language.as_deref());
|
||||||
|
|
||||||
|
let gid = tauri_plugin_clash_verge_sysinfo::current_gid();
|
||||||
let prompt = clash_verge_i18n::t!("service.adminInstallPrompt");
|
let prompt = clash_verge_i18n::t!("service.adminInstallPrompt");
|
||||||
let command =
|
let command = format!(
|
||||||
format!(r#"do shell script "sudo '{install_shell}'" with administrator privileges with prompt "{prompt}""#);
|
r#"do shell script "sudo CLASH_VERGE_SERVICE_GID={gid} '{install_shell}'" with administrator privileges with prompt "{prompt}""#
|
||||||
|
);
|
||||||
|
|
||||||
let status = StdCommand::new("osascript").args(vec!["-e", &command]).status()?;
|
let status = StdCommand::new("osascript").args(vec!["-e", &command]).status()?;
|
||||||
|
|
||||||
@@ -381,7 +383,12 @@ pub(super) async fn stop_core_by_service() -> Result<()> {
|
|||||||
/// 检查服务是否正在运行
|
/// 检查服务是否正在运行
|
||||||
pub async fn is_service_available() -> Result<()> {
|
pub async fn is_service_available() -> Result<()> {
|
||||||
if let Err(e) = Path::metadata(clash_verge_service_ipc::IPC_PATH.as_ref()) {
|
if let Err(e) = Path::metadata(clash_verge_service_ipc::IPC_PATH.as_ref()) {
|
||||||
logging!(warn, Type::Service, "Some issue with service IPC Path: {}", e);
|
let verge = Config::verge().await;
|
||||||
|
let verge_last = verge.latest_arc();
|
||||||
|
let is_enable = verge_last.enable_tun_mode.unwrap_or(false);
|
||||||
|
if is_enable {
|
||||||
|
logging!(warn, Type::Service, "Some issue with service IPC Path: {}", e);
|
||||||
|
}
|
||||||
return Err(e.into());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
clash_verge_service_ipc::connect().await?;
|
clash_verge_service_ipc::connect().await?;
|
||||||
|
|||||||
@@ -61,14 +61,11 @@ pub fn use_sort(config: Mapping) -> Mapping {
|
|||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn use_keys(config: &Mapping) -> Vec<String> {
|
#[inline]
|
||||||
config
|
pub fn use_keys<'a>(config: &'a Mapping) -> impl Iterator<Item = String> + 'a {
|
||||||
.iter()
|
config.iter().filter_map(|(key, _)| key.as_str()).map(|s: &str| {
|
||||||
.filter_map(|(key, _)| key.as_str())
|
let mut s: String = s.into();
|
||||||
.map(|s: &str| {
|
s.make_ascii_lowercase();
|
||||||
let mut s: String = s.into();
|
s
|
||||||
s.make_ascii_lowercase();
|
})
|
||||||
s
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ fn process_global_items(
|
|||||||
profile_name: &String,
|
profile_name: &String,
|
||||||
) -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
) -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||||
let mut result_map = HashMap::new();
|
let mut result_map = HashMap::new();
|
||||||
let mut exists_keys = use_keys(&config);
|
let mut exists_keys = use_keys(&config).collect::<Vec<_>>();
|
||||||
|
|
||||||
if let ChainType::Merge(merge) = global_merge.data {
|
if let ChainType::Merge(merge) = global_merge.data {
|
||||||
exists_keys.extend(use_keys(&merge));
|
exists_keys.extend(use_keys(&merge));
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ use crate::{
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clash_verge_logging::{Type, logging};
|
use clash_verge_logging::{Type, logging};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use std::time::Duration;
|
|
||||||
use tauri::{AppHandle, Manager as _};
|
use tauri::{AppHandle, Manager as _};
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use tauri_plugin_autostart::MacosLauncher;
|
use tauri_plugin_autostart::MacosLauncher;
|
||||||
@@ -61,11 +60,11 @@ mod app_init {
|
|||||||
.socket_path(crate::config::IClashTemp::guard_external_controller_ipc())
|
.socket_path(crate::config::IClashTemp::guard_external_controller_ipc())
|
||||||
.pool_config(
|
.pool_config(
|
||||||
tauri_plugin_mihomo::IpcPoolConfigBuilder::new()
|
tauri_plugin_mihomo::IpcPoolConfigBuilder::new()
|
||||||
.min_connections(1)
|
.min_connections(3)
|
||||||
.max_connections(32)
|
.max_connections(32)
|
||||||
.idle_timeout(std::time::Duration::from_secs(60))
|
.idle_timeout(std::time::Duration::from_secs(60))
|
||||||
.health_check_interval(std::time::Duration::from_secs(60))
|
.health_check_interval(std::time::Duration::from_secs(60))
|
||||||
.reject_policy(RejectPolicy::Timeout(Duration::from_secs(3)))
|
.reject_policy(RejectPolicy::Wait)
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
.build(),
|
.build(),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "2.4.5-rc.1",
|
"version": "2.4.5",
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
|
|||||||
@@ -2,15 +2,18 @@ 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 errorStack = error instanceof Error ? error.stack : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div role="alert" style={{ padding: 16 }}>
|
<div role="alert" style={{ padding: 16 }}>
|
||||||
<h4>Something went wrong:(</h4>
|
<h4>Something went wrong:(</h4>
|
||||||
|
|
||||||
<pre>{error.message}</pre>
|
<pre>{errorMessage}</pre>
|
||||||
|
|
||||||
<details title="Error Stack">
|
<details title="Error Stack">
|
||||||
<summary>Error Stack</summary>
|
<summary>Error Stack</summary>
|
||||||
<pre>{error.stack}</pre>
|
<pre>{errorStack}</pre>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,13 +4,7 @@ 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 {
|
import { useAppData } from "@/providers/app-data-context";
|
||||||
useAppUptime,
|
|
||||||
useClashConfig,
|
|
||||||
useRulesData,
|
|
||||||
useSystemProxyAddress,
|
|
||||||
useSystemProxyData,
|
|
||||||
} from "@/hooks/use-clash-data";
|
|
||||||
|
|
||||||
import { EnhancedCard } from "./enhanced-card";
|
import { EnhancedCard } from "./enhanced-card";
|
||||||
|
|
||||||
@@ -25,14 +19,7 @@ const formatUptime = (uptimeMs: number) => {
|
|||||||
export const ClashInfoCard = () => {
|
export const ClashInfoCard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { version: clashVersion } = useClash();
|
const { version: clashVersion } = useClash();
|
||||||
const { clashConfig } = useClashConfig();
|
const { clashConfig, rules, uptime, systemProxyAddress } = useAppData();
|
||||||
const { sysproxy } = useSystemProxyData();
|
|
||||||
const { rules } = useRulesData();
|
|
||||||
const { uptime } = useAppUptime();
|
|
||||||
const systemProxyAddress = useSystemProxyAddress({
|
|
||||||
clashConfig,
|
|
||||||
sysproxy,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 使用useMemo缓存格式化后的uptime,避免频繁计算
|
// 使用useMemo缓存格式化后的uptime,避免频繁计算
|
||||||
const formattedUptime = useMemo(() => formatUptime(uptime), [uptime]);
|
const formattedUptime = useMemo(() => formatUptime(uptime), [uptime]);
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ 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 { useClashConfig } from "@/hooks/use-clash-data";
|
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
|
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";
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ const MODE_META: Record<
|
|||||||
export const ClashModeCard = () => {
|
export const ClashModeCard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
const { clashConfig, refreshClashConfig } = useClashConfig();
|
const { clashConfig, refreshClashConfig } = useAppData();
|
||||||
|
|
||||||
// 支持的模式列表
|
// 支持的模式列表
|
||||||
const modeList = CLASH_MODES;
|
const modeList = CLASH_MODES;
|
||||||
|
|||||||
@@ -34,14 +34,10 @@ import { useNavigate } from "react-router";
|
|||||||
import { delayGroup, healthcheckProxyProvider } from "tauri-plugin-mihomo-api";
|
import { delayGroup, healthcheckProxyProvider } from "tauri-plugin-mihomo-api";
|
||||||
|
|
||||||
import { EnhancedCard } from "@/components/home/enhanced-card";
|
import { EnhancedCard } from "@/components/home/enhanced-card";
|
||||||
import {
|
|
||||||
useClashConfig,
|
|
||||||
useProxiesData,
|
|
||||||
useRulesData,
|
|
||||||
} from "@/hooks/use-clash-data";
|
|
||||||
import { useProfiles } from "@/hooks/use-profiles";
|
import { useProfiles } from "@/hooks/use-profiles";
|
||||||
import { useProxySelection } from "@/hooks/use-proxy-selection";
|
import { useProxySelection } from "@/hooks/use-proxy-selection";
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
|
import { useAppData } from "@/providers/app-data-context";
|
||||||
import delayManager from "@/services/delay";
|
import delayManager from "@/services/delay";
|
||||||
import { debugLog } from "@/utils/debug";
|
import { debugLog } from "@/utils/debug";
|
||||||
|
|
||||||
@@ -105,9 +101,7 @@ export const CurrentProxyCard = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { proxies, refreshProxy } = useProxiesData();
|
const { proxies, clashConfig, refreshProxy, rules } = useAppData();
|
||||||
const { clashConfig } = useClashConfig();
|
|
||||||
const { rules } = useRulesData();
|
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
const { current: currentProfile } = useProfiles();
|
const { current: currentProfile } = useProfiles();
|
||||||
const autoDelayEnabled = verge?.enable_auto_delay_detection ?? false;
|
const autoDelayEnabled = verge?.enable_auto_delay_detection ?? false;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ 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 { useRefreshAll } from "@/hooks/use-clash-data";
|
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";
|
||||||
@@ -281,7 +281,7 @@ export const HomeProfileCard = ({
|
|||||||
}: HomeProfileCardProps) => {
|
}: HomeProfileCardProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const refreshAll = useRefreshAll();
|
const { refreshAll } = useAppData();
|
||||||
|
|
||||||
// 更新当前订阅
|
// 更新当前订阅
|
||||||
const [updating, setUpdating] = useState(false);
|
const [updating, setUpdating] = useState(false);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ 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 { useProxiesData, useProxyProvidersData } from "@/hooks/use-clash-data";
|
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";
|
||||||
|
|
||||||
@@ -48,8 +48,7 @@ const parseExpire = (expire?: number) => {
|
|||||||
export const ProviderButton = () => {
|
export const ProviderButton = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const { proxyProviders, refreshProxyProviders } = useProxyProvidersData();
|
const { proxyProviders, refreshProxy, refreshProxyProviders } = useAppData();
|
||||||
const { refreshProxy } = useProxiesData();
|
|
||||||
const [updating, setUpdating] = useState<Record<string, boolean>>({});
|
const [updating, setUpdating] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
// 检查是否有提供者
|
// 检查是否有提供者
|
||||||
@@ -176,8 +175,8 @@ export const ProviderButton = () => {
|
|||||||
<List sx={{ py: 0, minHeight: 250 }}>
|
<List sx={{ py: 0, minHeight: 250 }}>
|
||||||
{Object.entries(proxyProviders || {})
|
{Object.entries(proxyProviders || {})
|
||||||
.sort()
|
.sort()
|
||||||
.map(([key, provider]) => {
|
.map(([key, item]) => {
|
||||||
if (!provider) return null;
|
const provider = item;
|
||||||
const time = dayjs(provider.updatedAt);
|
const time = dayjs(provider.updatedAt);
|
||||||
const isUpdating = updating[key];
|
const isUpdating = updating[key];
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import {
|
|||||||
selectNodeForGroup,
|
selectNodeForGroup,
|
||||||
} from "tauri-plugin-mihomo-api";
|
} from "tauri-plugin-mihomo-api";
|
||||||
|
|
||||||
import { useProxiesData } from "@/hooks/use-clash-data";
|
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";
|
||||||
|
|
||||||
@@ -199,7 +199,7 @@ export const ProxyChain = ({
|
|||||||
}: ProxyChainProps) => {
|
}: ProxyChainProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { proxies, refreshProxy } = useProxiesData();
|
const { proxies, refreshProxy } = useAppData();
|
||||||
const [isConnecting, setIsConnecting] = useState(false);
|
const [isConnecting, setIsConnecting] = useState(false);
|
||||||
const markUnsavedChanges = useCallback(() => {
|
const markUnsavedChanges = useCallback(() => {
|
||||||
onMarkUnsavedChanges?.();
|
onMarkUnsavedChanges?.();
|
||||||
@@ -221,7 +221,7 @@ export const ProxyChain = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const proxyChainGroup = proxies.groups.find(
|
const proxyChainGroup = proxies.groups.find(
|
||||||
(group) => group.name === selectedGroup,
|
(group: { name: string }) => group.name === selectedGroup,
|
||||||
);
|
);
|
||||||
|
|
||||||
return proxyChainGroup?.now === lastNode.name;
|
return proxyChainGroup?.now === lastNode.name;
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ 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 { useProxiesData } from "@/hooks/use-clash-data";
|
|
||||||
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 { 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";
|
||||||
@@ -80,7 +80,7 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
}>({ open: false, message: "" });
|
}>({ open: false, message: "" });
|
||||||
|
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
const { proxies: proxiesData } = useProxiesData();
|
const { proxies: proxiesData } = useAppData();
|
||||||
const groups = proxiesData?.groups;
|
const groups = proxiesData?.groups;
|
||||||
const availableGroups = useMemo(() => {
|
const availableGroups = useMemo(() => {
|
||||||
if (!groups) return [];
|
if (!groups) return [];
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
import { useProxiesData } from "@/hooks/use-clash-data";
|
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
|
import { useAppData } from "@/providers/app-data-context";
|
||||||
import { getRuntimeConfig } from "@/services/cmds";
|
import { getRuntimeConfig } from "@/services/cmds";
|
||||||
import delayManager from "@/services/delay";
|
import delayManager from "@/services/delay";
|
||||||
import { debugLog } from "@/utils/debug";
|
import { debugLog } from "@/utils/debug";
|
||||||
@@ -34,8 +34,24 @@ interface IProxyItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 代理组类型
|
// 代理组类型
|
||||||
type ProxyGroup = IProxyGroupItem & {
|
type ProxyGroup = {
|
||||||
now?: string;
|
name: string;
|
||||||
|
type: string;
|
||||||
|
udp: boolean;
|
||||||
|
xudp: boolean;
|
||||||
|
tfo: boolean;
|
||||||
|
mptcp: boolean;
|
||||||
|
smux: boolean;
|
||||||
|
history: {
|
||||||
|
time: string;
|
||||||
|
delay: number;
|
||||||
|
}[];
|
||||||
|
now: string;
|
||||||
|
all: IProxyItem[];
|
||||||
|
hidden?: boolean;
|
||||||
|
icon?: string;
|
||||||
|
testUrl?: string;
|
||||||
|
provider?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IRenderItem {
|
export interface IRenderItem {
|
||||||
@@ -84,7 +100,7 @@ export const useRenderList = (
|
|||||||
selectedGroup?: string | null,
|
selectedGroup?: string | null,
|
||||||
) => {
|
) => {
|
||||||
// 使用全局数据提供者
|
// 使用全局数据提供者
|
||||||
const { proxies: proxiesData, refreshProxy } = useProxiesData();
|
const { proxies: proxiesData, refreshProxy } = useAppData();
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
const { width } = useWindowWidth();
|
const { width } = useWindowWidth();
|
||||||
const [headStates, setHeadState] = useHeadStateNew();
|
const [headStates, setHeadState] = useHeadStateNew();
|
||||||
|
|||||||
@@ -21,10 +21,7 @@ import { useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { updateRuleProvider } from "tauri-plugin-mihomo-api";
|
import { updateRuleProvider } from "tauri-plugin-mihomo-api";
|
||||||
|
|
||||||
import type {
|
import { useAppData } from "@/providers/app-data-context";
|
||||||
useRuleProvidersData,
|
|
||||||
useRulesData,
|
|
||||||
} from "@/hooks/use-clash-data";
|
|
||||||
import { showNotice } from "@/services/notice-service";
|
import { showNotice } from "@/services/notice-service";
|
||||||
|
|
||||||
// 辅助组件 - 类型框
|
// 辅助组件 - 类型框
|
||||||
@@ -40,22 +37,10 @@ const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({
|
|||||||
lineHeight: 1.25,
|
lineHeight: 1.25,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
type RuleProvidersHook = ReturnType<typeof useRuleProvidersData>;
|
export const ProviderButton = () => {
|
||||||
type RulesHook = ReturnType<typeof useRulesData>;
|
|
||||||
|
|
||||||
interface ProviderButtonProps {
|
|
||||||
ruleProviders: RuleProvidersHook["ruleProviders"];
|
|
||||||
refreshRuleProviders: RuleProvidersHook["refreshRuleProviders"];
|
|
||||||
refreshRules: RulesHook["refreshRules"];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ProviderButton = ({
|
|
||||||
ruleProviders,
|
|
||||||
refreshRuleProviders,
|
|
||||||
refreshRules,
|
|
||||||
}: ProviderButtonProps) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const { ruleProviders, refreshRules, refreshRuleProviders } = useAppData();
|
||||||
const [updating, setUpdating] = useState<Record<string, boolean>>({});
|
const [updating, setUpdating] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
// 检查是否有提供者
|
// 检查是否有提供者
|
||||||
@@ -178,8 +163,8 @@ export const ProviderButton = ({
|
|||||||
<List sx={{ py: 0, minHeight: 250 }}>
|
<List sx={{ py: 0, minHeight: 250 }}>
|
||||||
{Object.entries(ruleProviders || {})
|
{Object.entries(ruleProviders || {})
|
||||||
.sort()
|
.sort()
|
||||||
.map(([key, provider]) => {
|
.map(([key, item]) => {
|
||||||
if (!provider) return null;
|
const provider = item;
|
||||||
const time = dayjs(provider.updatedAt);
|
const time = dayjs(provider.updatedAt);
|
||||||
const isUpdating = updating[key];
|
const isUpdating = updating[key];
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export const BackupHistoryViewer = ({
|
|||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
const [rows, setRows] = useState<BackupRow[]>([]);
|
const [rows, setRows] = useState<BackupRow[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [isRestoring, setIsRestoring] = useState(false);
|
||||||
const [isRestarting, setIsRestarting] = useState(false);
|
const [isRestarting, setIsRestarting] = useState(false);
|
||||||
const isLocal = source === "local";
|
const isLocal = source === "local";
|
||||||
const isWebDavConfigured = Boolean(
|
const isWebDavConfigured = Boolean(
|
||||||
@@ -86,7 +87,7 @@ export const BackupHistoryViewer = ({
|
|||||||
);
|
);
|
||||||
const shouldSkipWebDav = !isLocal && !isWebDavConfigured;
|
const shouldSkipWebDav = !isLocal && !isWebDavConfigured;
|
||||||
const pageSize = 8;
|
const pageSize = 8;
|
||||||
const isBusy = loading || isRestarting;
|
const isBusy = loading || isRestoring || isRestarting;
|
||||||
|
|
||||||
const buildRow = useCallback(
|
const buildRow = useCallback(
|
||||||
(item: ILocalBackupFile | IWebDavFile): BackupRow | null => {
|
(item: ILocalBackupFile | IWebDavFile): BackupRow | null => {
|
||||||
@@ -195,24 +196,32 @@ export const BackupHistoryViewer = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleRestore = useLockFn(async (filename: string) => {
|
const handleRestore = useLockFn(async (filename: string) => {
|
||||||
if (isRestarting) return;
|
if (isRestoring || isRestarting) return;
|
||||||
if (
|
if (
|
||||||
!(await confirmAsync(t("settings.modals.backup.messages.confirmRestore")))
|
!(await confirmAsync(t("settings.modals.backup.messages.confirmRestore")))
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
if (isLocal) {
|
setIsRestoring(true);
|
||||||
await restoreLocalBackup(filename);
|
try {
|
||||||
} else {
|
if (isLocal) {
|
||||||
await restoreWebDavBackup(filename);
|
await restoreLocalBackup(filename);
|
||||||
|
} else {
|
||||||
|
await restoreWebDavBackup(filename);
|
||||||
|
}
|
||||||
|
showNotice.success("settings.modals.backup.messages.restoreSuccess");
|
||||||
|
setIsRestarting(true);
|
||||||
|
window.setTimeout(() => {
|
||||||
|
void restartApp().catch((err: unknown) => {
|
||||||
|
setIsRestarting(false);
|
||||||
|
showNotice.error(err);
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
showNotice.error(error);
|
||||||
|
} finally {
|
||||||
|
setIsRestoring(false);
|
||||||
}
|
}
|
||||||
showNotice.success("settings.modals.backup.messages.restoreSuccess");
|
|
||||||
setIsRestarting(true);
|
|
||||||
window.setTimeout(() => {
|
|
||||||
void restartApp().catch((err: unknown) => {
|
|
||||||
setIsRestarting(false);
|
|
||||||
showNotice.error(err);
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleExport = useLockFn(async (filename: string) => {
|
const handleExport = useLockFn(async (filename: string) => {
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
|||||||
close: () => setOpen(false),
|
close: () => setOpen(false),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// TODO 减少代码复杂度,性能开支
|
||||||
const onSave = useLockFn(async () => {
|
const onSave = useLockFn(async () => {
|
||||||
// 端口冲突检测
|
// 端口冲突检测
|
||||||
const portList = [
|
const portList = [
|
||||||
@@ -140,14 +141,26 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const port of portList) {
|
const original = originalPortsRef.current;
|
||||||
|
const changedPorts: number[] = [];
|
||||||
|
|
||||||
|
if (mixedPort !== original?.mixedPort) changedPorts.push(mixedPort);
|
||||||
|
if (socksEnabled && socksPort !== original?.socksPort)
|
||||||
|
changedPorts.push(socksPort);
|
||||||
|
if (httpEnabled && httpPort !== original?.httpPort)
|
||||||
|
changedPorts.push(httpPort);
|
||||||
|
if (redirEnabled && redirPort !== original?.redirPort)
|
||||||
|
changedPorts.push(redirPort);
|
||||||
|
if (tproxyEnabled && tproxyPort !== original?.tproxyPort)
|
||||||
|
changedPorts.push(tproxyPort);
|
||||||
|
|
||||||
|
for (const port of changedPorts) {
|
||||||
try {
|
try {
|
||||||
const inUse = await isPortInUse(port);
|
const inUse = await isPortInUse(port);
|
||||||
if (inUse) {
|
if (inUse) {
|
||||||
showNotice.error("settings.modals.clashPort.messages.portInUse", {
|
showNotice.error("settings.modals.clashPort.messages.portInUse", {
|
||||||
port,
|
port,
|
||||||
});
|
});
|
||||||
const original = originalPortsRef.current;
|
|
||||||
if (original) {
|
if (original) {
|
||||||
setMixedPort(original.mixedPort);
|
setMixedPort(original.mixedPort);
|
||||||
setSocksPort(original.socksPort);
|
setSocksPort(original.socksPort);
|
||||||
@@ -201,7 +214,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 提交保存请求
|
// 提交保存请求
|
||||||
await saveSettings({ clashConfig, vergeConfig });
|
saveSettings({ clashConfig, vergeConfig });
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
import { BaseDialog, Switch } from "@/components/base";
|
import { BaseDialog, Switch } from "@/components/base";
|
||||||
import { useClash } from "@/hooks/use-clash";
|
import { useClash } from "@/hooks/use-clash";
|
||||||
|
import { restartCore } from "@/services/cmds";
|
||||||
import { showNotice } from "@/services/notice-service";
|
import { showNotice } from "@/services/notice-service";
|
||||||
|
|
||||||
// 定义开发环境的URL列表
|
// 定义开发环境的URL列表
|
||||||
@@ -134,6 +135,7 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
await restartCore();
|
||||||
await mutateClash();
|
await mutateClash();
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
|
import { getBaseConfig } from "tauri-plugin-mihomo-api";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BaseDialog,
|
BaseDialog,
|
||||||
@@ -33,12 +34,8 @@ import {
|
|||||||
TooltipIcon,
|
TooltipIcon,
|
||||||
} from "@/components/base";
|
} from "@/components/base";
|
||||||
import { EditorViewer } from "@/components/profile/editor-viewer";
|
import { EditorViewer } from "@/components/profile/editor-viewer";
|
||||||
import {
|
|
||||||
useClashConfig,
|
|
||||||
useSystemProxyAddress,
|
|
||||||
useSystemProxyData,
|
|
||||||
} from "@/hooks/use-clash-data";
|
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
|
import { useAppData } from "@/providers/app-data-context";
|
||||||
import {
|
import {
|
||||||
getAutotemProxy,
|
getAutotemProxy,
|
||||||
getNetworkInterfacesInfo,
|
getNetworkInterfacesInfo,
|
||||||
@@ -110,6 +107,9 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
const { verge, patchVerge, mutateVerge } = useVerge();
|
const { verge, patchVerge, mutateVerge } = useVerge();
|
||||||
const [hostOptions, setHostOptions] = useState<string[]>([]);
|
const [hostOptions, setHostOptions] = useState<string[]>([]);
|
||||||
|
|
||||||
|
type SysProxy = Awaited<ReturnType<typeof getSystemProxy>>;
|
||||||
|
const [sysproxy, setSysproxy] = useState<SysProxy>();
|
||||||
|
|
||||||
type AutoProxy = Awaited<ReturnType<typeof getAutotemProxy>>;
|
type AutoProxy = Awaited<ReturnType<typeof getAutotemProxy>>;
|
||||||
const [autoproxy, setAutoproxy] = useState<AutoProxy>();
|
const [autoproxy, setAutoproxy] = useState<AutoProxy>();
|
||||||
|
|
||||||
@@ -148,8 +148,12 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
return "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>";
|
return "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>";
|
||||||
};
|
};
|
||||||
|
|
||||||
const { clashConfig } = useClashConfig();
|
const { data: clashConfig } = useSWR("getClashConfig", getBaseConfig, {
|
||||||
const { sysproxy, refreshSysproxy } = useSystemProxyData();
|
revalidateOnFocus: false,
|
||||||
|
revalidateIfStale: true,
|
||||||
|
dedupingInterval: 1000,
|
||||||
|
errorRetryInterval: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
const prevMixedPortRef = useRef(clashConfig?.mixedPort);
|
const prevMixedPortRef = useRef(clashConfig?.mixedPort);
|
||||||
|
|
||||||
@@ -183,10 +187,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
updateProxy();
|
updateProxy();
|
||||||
}, [clashConfig?.mixedPort, value.pac]);
|
}, [clashConfig?.mixedPort, value.pac]);
|
||||||
|
|
||||||
const systemProxyAddress = useSystemProxyAddress({
|
const { systemProxyAddress } = useAppData();
|
||||||
clashConfig,
|
|
||||||
sysproxy,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 为当前状态计算系统代理地址
|
// 为当前状态计算系统代理地址
|
||||||
const getSystemProxyAddress = useMemo(() => {
|
const getSystemProxyAddress = useMemo(() => {
|
||||||
@@ -236,7 +237,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
pac_content: pac_file_content ?? DEFAULT_PAC,
|
pac_content: pac_file_content ?? DEFAULT_PAC,
|
||||||
proxy_host: proxy_host ?? "127.0.0.1",
|
proxy_host: proxy_host ?? "127.0.0.1",
|
||||||
});
|
});
|
||||||
void refreshSysproxy();
|
getSystemProxy().then((p) => setSysproxy(p));
|
||||||
getAutotemProxy().then((p) => setAutoproxy(p));
|
getAutotemProxy().then((p) => setAutoproxy(p));
|
||||||
fetchNetworkInterfaces();
|
fetchNetworkInterfaces();
|
||||||
},
|
},
|
||||||
|
|||||||
+151
-146
@@ -19,157 +19,162 @@ export interface TestViewerRef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create or edit the test item
|
// create or edit the test item
|
||||||
export const TestViewer = forwardRef<TestViewerRef, Props>((props, ref) => {
|
export const TestViewer = forwardRef<TestViewerRef, Props>(
|
||||||
const { t } = useTranslation();
|
({ onChange }, ref) => {
|
||||||
const [open, setOpen] = useState(false);
|
const { t } = useTranslation();
|
||||||
const [openType, setOpenType] = useState<"new" | "edit">("new");
|
const [open, setOpen] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [openType, setOpenType] = useState<"new" | "edit">("new");
|
||||||
const { verge, patchVerge } = useVerge();
|
const [loading, setLoading] = useState(false);
|
||||||
const testList = verge?.test_list ?? [];
|
const { verge, patchVerge } = useVerge();
|
||||||
const { control, ...formIns } = useForm<IVergeTestItem>({
|
const testList = verge?.test_list ?? [];
|
||||||
defaultValues: {
|
const { control, ...formIns } = useForm<IVergeTestItem>({
|
||||||
name: "",
|
defaultValues: {
|
||||||
icon: "",
|
name: "",
|
||||||
url: "",
|
icon: "",
|
||||||
},
|
url: "",
|
||||||
});
|
},
|
||||||
|
|
||||||
const patchTestList = async (uid: string, patch: Partial<IVergeTestItem>) => {
|
|
||||||
const newList = testList.map((x) => {
|
|
||||||
if (x.uid === uid) {
|
|
||||||
return { ...x, ...patch };
|
|
||||||
}
|
|
||||||
return x;
|
|
||||||
});
|
});
|
||||||
await patchVerge({ test_list: newList });
|
|
||||||
};
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
const patchTestList = async (
|
||||||
create: () => {
|
uid: string,
|
||||||
setOpenType("new");
|
patch: Partial<IVergeTestItem>,
|
||||||
setOpen(true);
|
) => {
|
||||||
},
|
const newList = testList.map((x) => {
|
||||||
edit: (item) => {
|
if (x.uid === uid) {
|
||||||
if (item) {
|
return { ...x, ...patch };
|
||||||
Object.entries(item).forEach(([key, value]) => {
|
|
||||||
formIns.setValue(key as any, value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setOpenType("edit");
|
|
||||||
setOpen(true);
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const handleOk = useLockFn(
|
|
||||||
formIns.handleSubmit(async (form) => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
if (!form.name) throw new Error("`Name` should not be null");
|
|
||||||
if (!form.url) throw new Error("`Url` should not be null");
|
|
||||||
|
|
||||||
let newList;
|
|
||||||
let uid;
|
|
||||||
|
|
||||||
if (form.icon && form.icon.startsWith("<svg")) {
|
|
||||||
// 移除 icon 中的注释
|
|
||||||
if (form.icon) {
|
|
||||||
form.icon = form.icon.replace(/<!--[\s\S]*?-->/g, "");
|
|
||||||
}
|
|
||||||
const doc = new DOMParser().parseFromString(
|
|
||||||
form.icon,
|
|
||||||
"image/svg+xml",
|
|
||||||
);
|
|
||||||
if (doc.querySelector("parsererror")) {
|
|
||||||
throw new Error("`Icon`svg format error");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return x;
|
||||||
|
});
|
||||||
|
await patchVerge({ test_list: newList });
|
||||||
|
};
|
||||||
|
|
||||||
if (openType === "new") {
|
useImperativeHandle(ref, () => ({
|
||||||
uid = nanoid();
|
create: () => {
|
||||||
const item = { ...form, uid };
|
setOpenType("new");
|
||||||
newList = [...testList, item];
|
setOpen(true);
|
||||||
await patchVerge({ test_list: newList });
|
},
|
||||||
props.onChange(uid);
|
edit: (item) => {
|
||||||
} else {
|
if (item) {
|
||||||
if (!form.uid) throw new Error("UID not found");
|
Object.entries(item).forEach(([key, value]) => {
|
||||||
uid = form.uid;
|
formIns.setValue(key as any, value);
|
||||||
|
});
|
||||||
await patchTestList(uid, form);
|
|
||||||
props.onChange(uid, form);
|
|
||||||
}
|
}
|
||||||
setOpen(false);
|
setOpenType("edit");
|
||||||
setLoading(false);
|
setOpen(true);
|
||||||
setTimeout(() => formIns.reset(), 500);
|
},
|
||||||
} catch (err: any) {
|
}));
|
||||||
showNotice.error(err);
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleOk = useLockFn(
|
||||||
setOpen(false);
|
formIns.handleSubmit(async (form) => {
|
||||||
setTimeout(() => formIns.reset(), 500);
|
setLoading(true);
|
||||||
};
|
try {
|
||||||
|
if (!form.name) throw new Error("`Name` should not be null");
|
||||||
|
if (!form.url) throw new Error("`Url` should not be null");
|
||||||
|
|
||||||
const text = {
|
let newList;
|
||||||
fullWidth: true,
|
let uid;
|
||||||
size: "small",
|
|
||||||
margin: "normal",
|
|
||||||
variant: "outlined",
|
|
||||||
autoComplete: "off",
|
|
||||||
autoCorrect: "off",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
return (
|
if (form.icon && form.icon.startsWith("<svg")) {
|
||||||
<BaseDialog
|
// 移除 icon 中的注释
|
||||||
open={open}
|
if (form.icon) {
|
||||||
title={
|
form.icon = form.icon.replace(/<!--[\s\S]*?-->/g, "");
|
||||||
openType === "new"
|
}
|
||||||
? t("tests.modals.test.title.create")
|
const doc = new DOMParser().parseFromString(
|
||||||
: t("tests.modals.test.title.edit")
|
form.icon,
|
||||||
}
|
"image/svg+xml",
|
||||||
contentSx={{ width: 375, pb: 0, maxHeight: "80%" }}
|
);
|
||||||
okBtn={t("shared.actions.save")}
|
if (doc.querySelector("parsererror")) {
|
||||||
cancelBtn={t("shared.actions.cancel")}
|
throw new Error("`Icon`svg format error");
|
||||||
onClose={handleClose}
|
}
|
||||||
onCancel={handleClose}
|
}
|
||||||
onOk={handleOk}
|
|
||||||
loading={loading}
|
if (openType === "new") {
|
||||||
>
|
uid = nanoid();
|
||||||
<Controller
|
const item = { ...form, uid };
|
||||||
name="name"
|
newList = [...testList, item];
|
||||||
control={control}
|
await patchVerge({ test_list: newList });
|
||||||
render={({ field }) => (
|
onChange(uid);
|
||||||
<TextField {...text} {...field} label={t("shared.labels.name")} />
|
} else {
|
||||||
)}
|
if (!form.uid) throw new Error("UID not found");
|
||||||
/>
|
uid = form.uid;
|
||||||
<Controller
|
|
||||||
name="icon"
|
await patchTestList(uid, form);
|
||||||
control={control}
|
onChange(uid, form);
|
||||||
render={({ field }) => (
|
}
|
||||||
<TextField
|
setOpen(false);
|
||||||
{...text}
|
setLoading(false);
|
||||||
{...field}
|
setTimeout(() => formIns.reset(), 500);
|
||||||
multiline
|
} catch (err: any) {
|
||||||
maxRows={5}
|
showNotice.error(err);
|
||||||
label={t("shared.labels.icon")}
|
setLoading(false);
|
||||||
/>
|
}
|
||||||
)}
|
}),
|
||||||
/>
|
);
|
||||||
<Controller
|
|
||||||
name="url"
|
const handleClose = () => {
|
||||||
control={control}
|
setOpen(false);
|
||||||
render={({ field }) => (
|
setTimeout(() => formIns.reset(), 500);
|
||||||
<TextField
|
};
|
||||||
{...text}
|
|
||||||
{...field}
|
const text = {
|
||||||
multiline
|
fullWidth: true,
|
||||||
maxRows={3}
|
size: "small",
|
||||||
label={t("tests.modals.test.fields.url")}
|
margin: "normal",
|
||||||
/>
|
variant: "outlined",
|
||||||
)}
|
autoComplete: "off",
|
||||||
/>
|
autoCorrect: "off",
|
||||||
</BaseDialog>
|
} as const;
|
||||||
);
|
|
||||||
});
|
return (
|
||||||
|
<BaseDialog
|
||||||
|
open={open}
|
||||||
|
title={
|
||||||
|
openType === "new"
|
||||||
|
? t("tests.modals.test.title.create")
|
||||||
|
: t("tests.modals.test.title.edit")
|
||||||
|
}
|
||||||
|
contentSx={{ width: 375, pb: 0, maxHeight: "80%" }}
|
||||||
|
okBtn={t("shared.actions.save")}
|
||||||
|
cancelBtn={t("shared.actions.cancel")}
|
||||||
|
onClose={handleClose}
|
||||||
|
onCancel={handleClose}
|
||||||
|
onOk={handleOk}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="name"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField {...text} {...field} label={t("shared.labels.name")} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="icon"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
{...text}
|
||||||
|
{...field}
|
||||||
|
multiline
|
||||||
|
maxRows={5}
|
||||||
|
label={t("shared.labels.icon")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="url"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
{...text}
|
||||||
|
{...field}
|
||||||
|
multiline
|
||||||
|
maxRows={3}
|
||||||
|
label={t("tests.modals.test.fields.url")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,206 +0,0 @@
|
|||||||
import { useCallback, useMemo } from "react";
|
|
||||||
import useSWR, { useSWRConfig } from "swr";
|
|
||||||
import {
|
|
||||||
getBaseConfig,
|
|
||||||
getRuleProviders,
|
|
||||||
getRules,
|
|
||||||
} from "tauri-plugin-mihomo-api";
|
|
||||||
|
|
||||||
import {
|
|
||||||
calcuProxies,
|
|
||||||
calcuProxyProviders,
|
|
||||||
getAppUptime,
|
|
||||||
getSystemProxy,
|
|
||||||
} from "@/services/cmds";
|
|
||||||
import { SWR_DEFAULTS, SWR_REALTIME, SWR_SLOW_POLL } from "@/services/config";
|
|
||||||
|
|
||||||
import { useSharedSWRPoller } from "./use-shared-swr-poller";
|
|
||||||
import { useVerge } from "./use-verge";
|
|
||||||
|
|
||||||
export const useProxiesData = () => {
|
|
||||||
const { mutate: globalMutate } = useSWRConfig();
|
|
||||||
const { data, error, isLoading } = useSWR("getProxies", calcuProxies, {
|
|
||||||
...SWR_REALTIME,
|
|
||||||
refreshInterval: 0,
|
|
||||||
onError: (err) => console.warn("[AppData] Proxy fetch failed:", err),
|
|
||||||
});
|
|
||||||
|
|
||||||
const refreshProxy = useCallback(
|
|
||||||
() => globalMutate("getProxies"),
|
|
||||||
[globalMutate],
|
|
||||||
);
|
|
||||||
const pollerRefresh = useCallback(() => {
|
|
||||||
void globalMutate("getProxies");
|
|
||||||
}, [globalMutate]);
|
|
||||||
|
|
||||||
useSharedSWRPoller("getProxies", SWR_REALTIME.refreshInterval, pollerRefresh);
|
|
||||||
|
|
||||||
return {
|
|
||||||
proxies: data,
|
|
||||||
refreshProxy,
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useClashConfig = () => {
|
|
||||||
const { mutate: globalMutate } = useSWRConfig();
|
|
||||||
const { data, error, isLoading } = useSWR("getClashConfig", getBaseConfig, {
|
|
||||||
...SWR_SLOW_POLL,
|
|
||||||
refreshInterval: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const refreshClashConfig = useCallback(
|
|
||||||
() => globalMutate("getClashConfig"),
|
|
||||||
[globalMutate],
|
|
||||||
);
|
|
||||||
const pollerRefresh = useCallback(() => {
|
|
||||||
void globalMutate("getClashConfig");
|
|
||||||
}, [globalMutate]);
|
|
||||||
|
|
||||||
useSharedSWRPoller(
|
|
||||||
"getClashConfig",
|
|
||||||
SWR_SLOW_POLL.refreshInterval,
|
|
||||||
pollerRefresh,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
clashConfig: data,
|
|
||||||
refreshClashConfig,
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useProxyProvidersData = () => {
|
|
||||||
const { data, error, isLoading, mutate } = useSWR(
|
|
||||||
"getProxyProviders",
|
|
||||||
calcuProxyProviders,
|
|
||||||
SWR_DEFAULTS,
|
|
||||||
);
|
|
||||||
|
|
||||||
const refreshProxyProviders = useCallback(() => mutate(), [mutate]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
proxyProviders: data || {},
|
|
||||||
refreshProxyProviders,
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useRuleProvidersData = () => {
|
|
||||||
const { data, error, isLoading, mutate } = useSWR(
|
|
||||||
"getRuleProviders",
|
|
||||||
getRuleProviders,
|
|
||||||
SWR_DEFAULTS,
|
|
||||||
);
|
|
||||||
|
|
||||||
const refreshRuleProviders = useCallback(() => mutate(), [mutate]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
ruleProviders: data?.providers || {},
|
|
||||||
refreshRuleProviders,
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useRulesData = () => {
|
|
||||||
const { data, error, isLoading, mutate } = useSWR(
|
|
||||||
"getRules",
|
|
||||||
getRules,
|
|
||||||
SWR_DEFAULTS,
|
|
||||||
);
|
|
||||||
|
|
||||||
const refreshRules = useCallback(() => mutate(), [mutate]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
rules: data?.rules || [],
|
|
||||||
refreshRules,
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useSystemProxyData = () => {
|
|
||||||
const { data, error, isLoading, mutate } = useSWR(
|
|
||||||
"getSystemProxy",
|
|
||||||
getSystemProxy,
|
|
||||||
SWR_DEFAULTS,
|
|
||||||
);
|
|
||||||
|
|
||||||
const refreshSysproxy = useCallback(() => mutate(), [mutate]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
sysproxy: data,
|
|
||||||
refreshSysproxy,
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type ClashConfig = Awaited<ReturnType<typeof getBaseConfig>>;
|
|
||||||
type SystemProxy = Awaited<ReturnType<typeof getSystemProxy>>;
|
|
||||||
|
|
||||||
interface SystemProxyAddressParams {
|
|
||||||
clashConfig?: ClashConfig | null;
|
|
||||||
sysproxy?: SystemProxy | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useSystemProxyAddress = ({
|
|
||||||
clashConfig,
|
|
||||||
sysproxy,
|
|
||||||
}: SystemProxyAddressParams) => {
|
|
||||||
const { verge } = useVerge();
|
|
||||||
|
|
||||||
return useMemo(() => {
|
|
||||||
if (!verge || !clashConfig) return "-";
|
|
||||||
|
|
||||||
const isPacMode = verge.proxy_auto_config ?? false;
|
|
||||||
|
|
||||||
if (isPacMode) {
|
|
||||||
const proxyHost = verge.proxy_host || "127.0.0.1";
|
|
||||||
const proxyPort = verge.verge_mixed_port || clashConfig.mixedPort || 7897;
|
|
||||||
return [proxyHost, proxyPort].join(":");
|
|
||||||
}
|
|
||||||
|
|
||||||
const systemServer = sysproxy?.server;
|
|
||||||
if (systemServer && systemServer !== "-" && !systemServer.startsWith(":")) {
|
|
||||||
return systemServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
const proxyHost = verge.proxy_host || "127.0.0.1";
|
|
||||||
const proxyPort = verge.verge_mixed_port || clashConfig.mixedPort || 7897;
|
|
||||||
return [proxyHost, proxyPort].join(":");
|
|
||||||
}, [clashConfig, sysproxy, verge]);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useAppUptime = () => {
|
|
||||||
const { data, error, isLoading } = useSWR("appUptime", getAppUptime, {
|
|
||||||
...SWR_DEFAULTS,
|
|
||||||
refreshInterval: 3000,
|
|
||||||
errorRetryCount: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
uptime: data || 0,
|
|
||||||
error,
|
|
||||||
isLoading,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useRefreshAll = () => {
|
|
||||||
const { mutate } = useSWRConfig();
|
|
||||||
|
|
||||||
return useCallback(async () => {
|
|
||||||
await Promise.all([
|
|
||||||
mutate("getProxies"),
|
|
||||||
mutate("getClashConfig"),
|
|
||||||
mutate("getRules"),
|
|
||||||
mutate("getSystemProxy"),
|
|
||||||
mutate("getProxyProviders"),
|
|
||||||
mutate("getRuleProviders"),
|
|
||||||
]);
|
|
||||||
}, [mutate]);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
import { useAppData } from "@/providers/app-data-context";
|
||||||
|
|
||||||
|
// 定义代理组类型
|
||||||
|
interface ProxyGroup {
|
||||||
|
name: string;
|
||||||
|
now: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前代理节点信息的自定义Hook
|
||||||
|
export const useCurrentProxy = () => {
|
||||||
|
// 从AppDataProvider获取数据
|
||||||
|
const { proxies, clashConfig, refreshProxy } = useAppData();
|
||||||
|
|
||||||
|
// 获取当前模式
|
||||||
|
const currentMode = clashConfig?.mode?.toLowerCase() || "rule";
|
||||||
|
|
||||||
|
// 获取当前代理节点信息
|
||||||
|
const currentProxyInfo = useMemo(() => {
|
||||||
|
if (!proxies) return { currentProxy: null, primaryGroupName: null };
|
||||||
|
|
||||||
|
const { global, groups, records } = proxies;
|
||||||
|
|
||||||
|
// 默认信息
|
||||||
|
let primaryGroupName = "GLOBAL";
|
||||||
|
let currentName = global?.now;
|
||||||
|
|
||||||
|
// 在规则模式下,寻找主要代理组(通常是第一个或者名字包含特定关键词的组)
|
||||||
|
if (currentMode === "rule" && groups.length > 0) {
|
||||||
|
// 查找主要的代理组(优先级:包含关键词 > 第一个非GLOBAL组)
|
||||||
|
const primaryKeywords = [
|
||||||
|
"auto",
|
||||||
|
"select",
|
||||||
|
"proxy",
|
||||||
|
"节点选择",
|
||||||
|
"自动选择",
|
||||||
|
];
|
||||||
|
const primaryGroup =
|
||||||
|
groups.find((group: ProxyGroup) =>
|
||||||
|
primaryKeywords.some((keyword) =>
|
||||||
|
group.name.toLowerCase().includes(keyword.toLowerCase()),
|
||||||
|
),
|
||||||
|
) || groups.filter((g: ProxyGroup) => g.name !== "GLOBAL")[0];
|
||||||
|
|
||||||
|
if (primaryGroup) {
|
||||||
|
primaryGroupName = primaryGroup.name;
|
||||||
|
currentName = primaryGroup.now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果找不到当前节点,返回null
|
||||||
|
if (!currentName) return { currentProxy: null, primaryGroupName };
|
||||||
|
|
||||||
|
// 获取完整的节点信息
|
||||||
|
const currentProxy = records[currentName] || {
|
||||||
|
name: currentName,
|
||||||
|
type: "Unknown",
|
||||||
|
udp: false,
|
||||||
|
xudp: false,
|
||||||
|
tfo: false,
|
||||||
|
mptcp: false,
|
||||||
|
smux: false,
|
||||||
|
history: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
return { currentProxy, primaryGroupName };
|
||||||
|
}, [proxies, currentMode]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentProxy: currentProxyInfo.currentProxy,
|
||||||
|
primaryGroupName: currentProxyInfo.primaryGroupName,
|
||||||
|
mode: currentMode,
|
||||||
|
refreshProxy,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
import { useEffect } from "react";
|
|
||||||
import type { Key } from "swr";
|
|
||||||
|
|
||||||
type SharedPollerEntry = {
|
|
||||||
subscribers: number;
|
|
||||||
timer: number | null;
|
|
||||||
interval: number;
|
|
||||||
callback: (() => void) | null;
|
|
||||||
lastFired: number;
|
|
||||||
refreshWhenHidden: boolean;
|
|
||||||
refreshWhenOffline: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const sharedPollers = new Map<string, SharedPollerEntry>();
|
|
||||||
|
|
||||||
const isDocumentHidden = () => {
|
|
||||||
if (typeof document === "undefined") return false;
|
|
||||||
return document.visibilityState === "hidden";
|
|
||||||
};
|
|
||||||
|
|
||||||
const isOffline = () => {
|
|
||||||
if (typeof navigator === "undefined") return false;
|
|
||||||
return navigator.onLine === false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ensureTimer = (key: string, entry: SharedPollerEntry) => {
|
|
||||||
if (typeof window === "undefined") return;
|
|
||||||
|
|
||||||
if (entry.timer !== null) {
|
|
||||||
clearInterval(entry.timer);
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.timer = window.setInterval(() => {
|
|
||||||
if (!entry.refreshWhenHidden && isDocumentHidden()) return;
|
|
||||||
if (!entry.refreshWhenOffline && isOffline()) return;
|
|
||||||
const now = Date.now();
|
|
||||||
if (entry.lastFired && now - entry.lastFired < entry.interval / 2) {
|
|
||||||
// Skip duplicate fire within half interval to coalesce concurrent consumers
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
entry.lastFired = now;
|
|
||||||
entry.callback?.();
|
|
||||||
}, entry.interval);
|
|
||||||
};
|
|
||||||
|
|
||||||
const registerSharedPoller = (
|
|
||||||
key: string,
|
|
||||||
interval: number,
|
|
||||||
callback: () => void,
|
|
||||||
options: { refreshWhenHidden: boolean; refreshWhenOffline: boolean },
|
|
||||||
) => {
|
|
||||||
let entry = sharedPollers.get(key);
|
|
||||||
|
|
||||||
if (!entry) {
|
|
||||||
entry = {
|
|
||||||
subscribers: 0,
|
|
||||||
timer: null,
|
|
||||||
interval,
|
|
||||||
callback,
|
|
||||||
lastFired: 0,
|
|
||||||
refreshWhenHidden: options.refreshWhenHidden,
|
|
||||||
refreshWhenOffline: options.refreshWhenOffline,
|
|
||||||
};
|
|
||||||
sharedPollers.set(key, entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.subscribers += 1;
|
|
||||||
entry.callback = callback;
|
|
||||||
entry.interval = Math.min(entry.interval, interval);
|
|
||||||
entry.refreshWhenHidden =
|
|
||||||
entry.refreshWhenHidden || options.refreshWhenHidden;
|
|
||||||
entry.refreshWhenOffline =
|
|
||||||
entry.refreshWhenOffline || options.refreshWhenOffline;
|
|
||||||
|
|
||||||
ensureTimer(key, entry);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
const current = sharedPollers.get(key);
|
|
||||||
if (!current) return;
|
|
||||||
|
|
||||||
current.subscribers -= 1;
|
|
||||||
if (current.subscribers <= 0) {
|
|
||||||
if (current.timer !== null) {
|
|
||||||
clearInterval(current.timer);
|
|
||||||
}
|
|
||||||
sharedPollers.delete(key);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeKey = (key: Key): string | null => {
|
|
||||||
if (typeof key === "string") return key;
|
|
||||||
if (typeof key === "number" || typeof key === "boolean") return String(key);
|
|
||||||
if (Array.isArray(key)) {
|
|
||||||
try {
|
|
||||||
return JSON.stringify(key);
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface SharedSWRPollerOptions {
|
|
||||||
refreshWhenHidden?: boolean;
|
|
||||||
refreshWhenOffline?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useSharedSWRPoller = (
|
|
||||||
key: Key,
|
|
||||||
interval?: number,
|
|
||||||
callback?: () => void,
|
|
||||||
options?: SharedSWRPollerOptions,
|
|
||||||
) => {
|
|
||||||
const refreshWhenHidden = options?.refreshWhenHidden ?? false;
|
|
||||||
const refreshWhenOffline = options?.refreshWhenOffline ?? false;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!key || !interval || interval <= 0 || !callback) return;
|
|
||||||
|
|
||||||
const serializedKey = normalizeKey(key);
|
|
||||||
if (!serializedKey) return;
|
|
||||||
|
|
||||||
return registerSharedPoller(serializedKey, interval, callback, {
|
|
||||||
refreshWhenHidden,
|
|
||||||
refreshWhenOffline,
|
|
||||||
});
|
|
||||||
}, [key, interval, callback, refreshWhenHidden, refreshWhenOffline]);
|
|
||||||
};
|
|
||||||
@@ -2,14 +2,14 @@ import { useLockFn } from "ahooks";
|
|||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
import { closeAllConnections } from "tauri-plugin-mihomo-api";
|
import { closeAllConnections } from "tauri-plugin-mihomo-api";
|
||||||
|
|
||||||
import { useSystemProxyData } from "@/hooks/use-clash-data";
|
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
|
import { useAppData } from "@/providers/app-data-context";
|
||||||
import { getAutotemProxy } from "@/services/cmds";
|
import { getAutotemProxy } from "@/services/cmds";
|
||||||
|
|
||||||
// 系统代理状态检测统一逻辑
|
// 系统代理状态检测统一逻辑
|
||||||
export const useSystemProxyState = () => {
|
export const useSystemProxyState = () => {
|
||||||
const { verge, mutateVerge, patchVerge } = useVerge();
|
const { verge, mutateVerge, patchVerge } = useVerge();
|
||||||
const { sysproxy } = useSystemProxyData();
|
const { sysproxy } = useAppData();
|
||||||
const { data: autoproxy } = useSWR("getAutotemProxy", getAutotemProxy, {
|
const { data: autoproxy } = useSWR("getAutotemProxy", getAutotemProxy, {
|
||||||
revalidateOnFocus: true,
|
revalidateOnFocus: true,
|
||||||
revalidateOnReconnect: true,
|
revalidateOnReconnect: true,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import useSWR from "swr";
|
|||||||
import { getRunningMode, isAdmin, isServiceAvailable } from "@/services/cmds";
|
import { getRunningMode, isAdmin, isServiceAvailable } from "@/services/cmds";
|
||||||
import { showNotice } from "@/services/notice-service";
|
import { showNotice } from "@/services/notice-service";
|
||||||
|
|
||||||
import { useSharedSWRPoller } from "./use-shared-swr-poller";
|
|
||||||
import { useVerge } from "./use-verge";
|
import { useVerge } from "./use-verge";
|
||||||
|
|
||||||
export interface SystemState {
|
export interface SystemState {
|
||||||
@@ -44,20 +43,11 @@ export function useSystemState() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
suspense: true,
|
suspense: true,
|
||||||
refreshInterval: 0,
|
refreshInterval: 30000,
|
||||||
fallback: defaultSystemState,
|
fallback: defaultSystemState,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
useSharedSWRPoller(
|
|
||||||
"getSystemState",
|
|
||||||
30000,
|
|
||||||
() => {
|
|
||||||
void mutateSystemState();
|
|
||||||
},
|
|
||||||
{ refreshWhenHidden: false, refreshWhenOffline: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
const isSidecarMode = systemState.runningMode === "Sidecar";
|
const isSidecarMode = systemState.runningMode === "Sidecar";
|
||||||
const isServiceMode = systemState.runningMode === "Service";
|
const isServiceMode = systemState.runningMode === "Service";
|
||||||
const isTunModeAvailable = systemState.isAdminMode || systemState.isServiceOk;
|
const isTunModeAvailable = systemState.isAdminMode || systemState.isServiceOk;
|
||||||
|
|||||||
@@ -121,14 +121,14 @@
|
|||||||
"communication": "Core communication error"
|
"communication": "Core communication error"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"rule": "Режим правил",
|
"rule": "Rule",
|
||||||
"global": "Глобальный режим",
|
"global": "Global",
|
||||||
"direct": "Прямой режим"
|
"direct": "Direct"
|
||||||
},
|
},
|
||||||
"descriptions": {
|
"descriptions": {
|
||||||
"rule": "Automatically choose proxies according to the rule set.",
|
"rule": "Автоматически выбирать прокси в зависимости от правил",
|
||||||
"global": "Forward all network requests through the selected proxy.",
|
"global": "Перенаправлять все сетевые запросы через выбранный прокси",
|
||||||
"direct": "Bypass the proxy and connect to the internet directly."
|
"direct": "Подключение к интернету без прокси"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,17 +73,17 @@
|
|||||||
"components": {
|
"components": {
|
||||||
"enums": {
|
"enums": {
|
||||||
"strategies": {
|
"strategies": {
|
||||||
"select": "Выбор прокси вручную",
|
"select": "select",
|
||||||
"url-test": "Выбор прокси на основе задержки теста URL",
|
"url-test": "url-test",
|
||||||
"fallback": "Переключение на другой прокси при ошибке",
|
"fallback": "fallback",
|
||||||
"load-balance": "Распределение прокси на основе балансировки нагрузки",
|
"load-balance": "load-balance",
|
||||||
"relay": "Передача через определенную цепочку прокси"
|
"relay": "relay"
|
||||||
},
|
},
|
||||||
"policies": {
|
"policies": {
|
||||||
"DIRECT": "Данные направляются напрямую наружу",
|
"DIRECT": "DIRECT",
|
||||||
"REJECT": "Перехватывает запросы",
|
"REJECT": "REJECT",
|
||||||
"REJECT-DROP": "Отклоняет запросы",
|
"REJECT-DROP": "REJECT-DROP",
|
||||||
"PASS": "Пропускает это правило при совпадении"
|
"PASS": "PASS"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-33
@@ -42,39 +42,39 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ruleTypes": {
|
"ruleTypes": {
|
||||||
"DOMAIN": "Соответствует полному доменному имени",
|
"DOMAIN": "DOMAIN",
|
||||||
"DOMAIN-SUFFIX": "Соответствует суффиксу домена",
|
"DOMAIN-SUFFIX": "DOMAIN-SUFFIX",
|
||||||
"DOMAIN-KEYWORD": "Соответствует ключевому слову домена",
|
"DOMAIN-KEYWORD": "DOMAIN-KEYWORD",
|
||||||
"DOMAIN-REGEX": "Соответствует домену с использованием регулярных выражений",
|
"DOMAIN-REGEX": "DOMAIN-REGEX",
|
||||||
"GEOSITE": "Соответствует доменам в Geosite",
|
"GEOSITE": "GEOSITE",
|
||||||
"GEOIP": "Соответствует коду страны IP-адреса",
|
"GEOIP": "GEOIP",
|
||||||
"SRC-GEOIP": "Соответствует коду страны исходного IP-адреса",
|
"SRC-GEOIP": "SRC-GEOIP",
|
||||||
"IP-ASN": "Соответствует ASN IP-адреса",
|
"IP-ASN": "IP-ASN",
|
||||||
"SRC-IP-ASN": "Соответствует ASN исходного IP-адреса",
|
"SRC-IP-ASN": "SRC-IP-ASN",
|
||||||
"IP-CIDR": "Соответствует диапазону IP-адресов",
|
"IP-CIDR": "IP-CIDR",
|
||||||
"IP-CIDR6": "Соответствует диапазону IPv6-адресов",
|
"IP-CIDR6": "IP-CIDR6",
|
||||||
"SRC-IP-CIDR": "Соответствует диапазону исходных IP-адресов",
|
"SRC-IP-CIDR": "SRC-IP-CIDR",
|
||||||
"IP-SUFFIX": "Соответствует диапазону суффиксов IP-адресов",
|
"IP-SUFFIX": "IP-SUFFIX",
|
||||||
"SRC-IP-SUFFIX": "Соответствует диапазону суффиксов исходных IP-адресов",
|
"SRC-IP-SUFFIX": "SRC-IP-SUFFIX",
|
||||||
"SRC-PORT": "Соответствует диапазону исходных портов",
|
"SRC-PORT": "SRC-PORT",
|
||||||
"DST-PORT": "Соответствует диапазону целевых портов",
|
"DST-PORT": "DST-PORT",
|
||||||
"IN-PORT": "Соответствует входящему порту",
|
"IN-PORT": "IN-PORT",
|
||||||
"DSCP": "Маркировка DSCP (только для tproxy UDP входящего)",
|
"DSCP": "DSCP",
|
||||||
"PROCESS-NAME": "Соответствует имени процесса (имя пакета Android)",
|
"PROCESS-NAME": "PROCESS-NAME",
|
||||||
"PROCESS-PATH": "Соответствует полному пути процесса",
|
"PROCESS-PATH": "PROCESS-PATH",
|
||||||
"PROCESS-NAME-REGEX": "Соответствует имени процесса с использованием регулярных выражений (имя пакета Android)",
|
"PROCESS-NAME-REGEX": "PROCESS-NAME-REGEX",
|
||||||
"PROCESS-PATH-REGEX": "Соответствует полному пути процесса с использованием регулярных выражений",
|
"PROCESS-PATH-REGEX": "PROCESS-PATH-REGEX",
|
||||||
"NETWORK": "Соответствует транспортному протоколу (tcp/udp)",
|
"NETWORK": "NETWORK",
|
||||||
"UID": "Соответствует USER ID в Linux",
|
"UID": "UID",
|
||||||
"IN-TYPE": "Соответствует типу входящего соединения",
|
"IN-TYPE": "IN-TYPE",
|
||||||
"IN-USER": "Соответствует имени пользователя входящего соединения",
|
"IN-USER": "IN-USER",
|
||||||
"IN-NAME": "Соответствует имени входящего соединения",
|
"IN-NAME": "IN-NAME",
|
||||||
"SUB-RULE": "Подправило",
|
"SUB-RULE": "SUB-RULE",
|
||||||
"RULE-SET": "Соответствует набору правил",
|
"RULE-SET": "RULE-SET",
|
||||||
"AND": "Логическое И",
|
"AND": "AND",
|
||||||
"OR": "Логическое ИЛИ",
|
"OR": "OR",
|
||||||
"NOT": "Логическое НЕ",
|
"NOT": "NOT",
|
||||||
"MATCH": "Соответствует всем запросам"
|
"MATCH": "MATCH"
|
||||||
},
|
},
|
||||||
"title": "Редактировать правила"
|
"title": "Редактировать правила"
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-2
@@ -23,8 +23,8 @@ import {
|
|||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
||||||
import type { CSSProperties } from "react";
|
import type { CSSProperties } from "react";
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Outlet, useNavigate } from "react-router";
|
import { Outlet, useNavigate } from "react-router";
|
||||||
import { SWRConfig } from "swr";
|
import { SWRConfig } from "swr";
|
||||||
@@ -257,12 +257,24 @@ const Layout = () => {
|
|||||||
<SWRConfig
|
<SWRConfig
|
||||||
value={{
|
value={{
|
||||||
errorRetryCount: 3,
|
errorRetryCount: 3,
|
||||||
|
// TODO remove the 5000ms
|
||||||
errorRetryInterval: 5000,
|
errorRetryInterval: 5000,
|
||||||
onError: (error, key) => {
|
onError: (error, key) => {
|
||||||
console.error(`[SWR Error] Key: ${key}, Error:`, error);
|
// FIXME the condition should not be handle gllobally
|
||||||
if (key !== "getAutotemProxy") {
|
if (key !== "getAutotemProxy") {
|
||||||
console.error(`SWR Error for ${key}:`, error);
|
console.error(`SWR Error for ${key}:`, error);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME we need a better way to handle the retry when first booting app
|
||||||
|
const silentKeys = [
|
||||||
|
"getVersion",
|
||||||
|
"getClashConfig",
|
||||||
|
"getAutotemProxy",
|
||||||
|
];
|
||||||
|
if (silentKeys.includes(key)) return;
|
||||||
|
|
||||||
|
console.error(`[SWR Error] Key: ${key}, Error:`, error);
|
||||||
},
|
},
|
||||||
dedupingInterval: 2000,
|
dedupingInterval: 2000,
|
||||||
}}
|
}}
|
||||||
|
|||||||
+3
-8
@@ -7,13 +7,12 @@ import { BaseEmpty, BasePage, BaseSearchBox } from "@/components/base";
|
|||||||
import { ScrollTopButton } from "@/components/layout/scroll-top-button";
|
import { ScrollTopButton } from "@/components/layout/scroll-top-button";
|
||||||
import { ProviderButton } from "@/components/rule/provider-button";
|
import { ProviderButton } from "@/components/rule/provider-button";
|
||||||
import RuleItem from "@/components/rule/rule-item";
|
import RuleItem from "@/components/rule/rule-item";
|
||||||
import { useRuleProvidersData, useRulesData } from "@/hooks/use-clash-data";
|
|
||||||
import { useVisibility } from "@/hooks/use-visibility";
|
import { useVisibility } from "@/hooks/use-visibility";
|
||||||
|
import { useAppData } from "@/providers/app-data-context";
|
||||||
|
|
||||||
const RulesPage = () => {
|
const RulesPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { rules = [], refreshRules } = useRulesData();
|
const { rules = [], refreshRules, refreshRuleProviders } = useAppData();
|
||||||
const { ruleProviders, refreshRuleProviders } = useRuleProvidersData();
|
|
||||||
const [match, setMatch] = useState(() => (_: string) => true);
|
const [match, setMatch] = useState(() => (_: string) => true);
|
||||||
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
||||||
const [showScrollTop, setShowScrollTop] = useState(false);
|
const [showScrollTop, setShowScrollTop] = useState(false);
|
||||||
@@ -57,11 +56,7 @@ const RulesPage = () => {
|
|||||||
}}
|
}}
|
||||||
header={
|
header={
|
||||||
<Box display="flex" alignItems="center" gap={1}>
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
<ProviderButton
|
<ProviderButton />
|
||||||
ruleProviders={ruleProviders}
|
|
||||||
refreshRuleProviders={refreshRuleProviders}
|
|
||||||
refreshRules={refreshRules}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { createContext, use } from "react";
|
||||||
|
import {
|
||||||
|
BaseConfig,
|
||||||
|
ProxyProvider,
|
||||||
|
Rule,
|
||||||
|
RuleProvider,
|
||||||
|
} from "tauri-plugin-mihomo-api";
|
||||||
|
|
||||||
|
export interface AppDataContextType {
|
||||||
|
proxies: any;
|
||||||
|
clashConfig: BaseConfig;
|
||||||
|
rules: Rule[];
|
||||||
|
sysproxy: any;
|
||||||
|
runningMode?: string;
|
||||||
|
uptime: number;
|
||||||
|
proxyProviders: Record<string, ProxyProvider>;
|
||||||
|
ruleProviders: Record<string, RuleProvider>;
|
||||||
|
systemProxyAddress: string;
|
||||||
|
|
||||||
|
refreshProxy: () => Promise<any>;
|
||||||
|
refreshClashConfig: () => Promise<any>;
|
||||||
|
refreshRules: () => Promise<any>;
|
||||||
|
refreshSysproxy: () => Promise<any>;
|
||||||
|
refreshProxyProviders: () => Promise<any>;
|
||||||
|
refreshRuleProviders: () => Promise<any>;
|
||||||
|
refreshAll: () => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConnectionWithSpeed extends IConnectionsItem {
|
||||||
|
curUpload: number;
|
||||||
|
curDownload: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConnectionSpeedData {
|
||||||
|
id: string;
|
||||||
|
upload: number;
|
||||||
|
download: number;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AppDataContext = createContext<AppDataContextType | null>(null);
|
||||||
|
|
||||||
|
export const useAppData = () => {
|
||||||
|
const context = use(AppDataContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useAppData必须在AppDataProvider内使用");
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -1,25 +1,67 @@
|
|||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { PropsWithChildren, useCallback, useEffect } from "react";
|
import React, { useCallback, useEffect, useMemo } from "react";
|
||||||
import { useSWRConfig } from "swr";
|
import useSWR from "swr";
|
||||||
|
import {
|
||||||
|
getBaseConfig,
|
||||||
|
getRuleProviders,
|
||||||
|
getRules,
|
||||||
|
} from "tauri-plugin-mihomo-api";
|
||||||
|
|
||||||
// 负责监听全局事件并驱动 SWR 刷新,避免包裹全局 context 带来的额外渲染
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
export const AppDataProvider = ({ children }: PropsWithChildren) => {
|
import {
|
||||||
useAppDataEventBridge();
|
calcuProxies,
|
||||||
return <>{children}</>;
|
calcuProxyProviders,
|
||||||
};
|
getAppUptime,
|
||||||
|
getRunningMode,
|
||||||
|
getSystemProxy,
|
||||||
|
} from "@/services/cmds";
|
||||||
|
import { SWR_DEFAULTS, SWR_REALTIME, SWR_SLOW_POLL } from "@/services/config";
|
||||||
|
|
||||||
const useAppDataEventBridge = () => {
|
import { AppDataContext, AppDataContextType } from "./app-data-context";
|
||||||
const { mutate } = useSWRConfig();
|
|
||||||
|
|
||||||
const refreshProxy = useCallback(() => mutate("getProxies"), [mutate]);
|
// 全局数据提供者组件
|
||||||
const refreshClashConfig = useCallback(
|
export const AppDataProvider = ({
|
||||||
() => mutate("getClashConfig"),
|
children,
|
||||||
[mutate],
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
const { verge } = useVerge();
|
||||||
|
|
||||||
|
const { data: proxiesData, mutate: refreshProxy } = useSWR(
|
||||||
|
"getProxies",
|
||||||
|
calcuProxies,
|
||||||
|
{
|
||||||
|
...SWR_REALTIME,
|
||||||
|
onError: (_) => {
|
||||||
|
// FIXME when we intially start the app, and core is starting,
|
||||||
|
// there will be error thrown by getProxies API.
|
||||||
|
// We should handle this case properly later.
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
const refreshRules = useCallback(() => mutate("getRules"), [mutate]);
|
|
||||||
const refreshRuleProviders = useCallback(
|
const { data: clashConfig, mutate: refreshClashConfig } = useSWR(
|
||||||
() => mutate("getRuleProviders"),
|
"getClashConfig",
|
||||||
[mutate],
|
getBaseConfig,
|
||||||
|
SWR_SLOW_POLL,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: proxyProviders, mutate: refreshProxyProviders } = useSWR(
|
||||||
|
"getProxyProviders",
|
||||||
|
calcuProxyProviders,
|
||||||
|
SWR_DEFAULTS,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: ruleProviders, mutate: refreshRuleProviders } = useSWR(
|
||||||
|
"getRuleProviders",
|
||||||
|
getRuleProviders,
|
||||||
|
SWR_DEFAULTS,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: rulesData, mutate: refreshRules } = useSWR(
|
||||||
|
"getRules",
|
||||||
|
getRules,
|
||||||
|
SWR_DEFAULTS,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -182,10 +224,125 @@ const useAppDataEventBridge = () => {
|
|||||||
|
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
console.error(
|
console.error(
|
||||||
"[DataProvider] " + errors.length + " errors during cleanup:",
|
`[DataProvider] ${errors.length} errors during cleanup:`,
|
||||||
errors,
|
errors,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [refreshProxy, refreshClashConfig, refreshRules, refreshRuleProviders]);
|
}, [refreshProxy, refreshClashConfig, refreshRules, refreshRuleProviders]);
|
||||||
|
|
||||||
|
const { data: sysproxy, mutate: refreshSysproxy } = useSWR(
|
||||||
|
"getSystemProxy",
|
||||||
|
getSystemProxy,
|
||||||
|
SWR_DEFAULTS,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: runningMode } = useSWR(
|
||||||
|
"getRunningMode",
|
||||||
|
getRunningMode,
|
||||||
|
SWR_DEFAULTS,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: uptimeData } = useSWR("appUptime", getAppUptime, {
|
||||||
|
...SWR_DEFAULTS,
|
||||||
|
refreshInterval: 3000,
|
||||||
|
errorRetryCount: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 提供统一的刷新方法
|
||||||
|
const refreshAll = useCallback(async () => {
|
||||||
|
await Promise.all([
|
||||||
|
refreshProxy(),
|
||||||
|
refreshClashConfig(),
|
||||||
|
refreshRules(),
|
||||||
|
refreshSysproxy(),
|
||||||
|
refreshProxyProviders(),
|
||||||
|
refreshRuleProviders(),
|
||||||
|
]);
|
||||||
|
}, [
|
||||||
|
refreshProxy,
|
||||||
|
refreshClashConfig,
|
||||||
|
refreshRules,
|
||||||
|
refreshSysproxy,
|
||||||
|
refreshProxyProviders,
|
||||||
|
refreshRuleProviders,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 聚合所有数据
|
||||||
|
const value = useMemo(() => {
|
||||||
|
// 计算系统代理地址
|
||||||
|
const calculateSystemProxyAddress = () => {
|
||||||
|
if (!verge || !clashConfig) return "-";
|
||||||
|
|
||||||
|
const isPacMode = verge.proxy_auto_config ?? false;
|
||||||
|
|
||||||
|
if (isPacMode) {
|
||||||
|
// PAC模式:显示我们期望设置的代理地址
|
||||||
|
const proxyHost = verge.proxy_host || "127.0.0.1";
|
||||||
|
const proxyPort =
|
||||||
|
verge.verge_mixed_port || clashConfig.mixedPort || 7897;
|
||||||
|
return `${proxyHost}:${proxyPort}`;
|
||||||
|
} else {
|
||||||
|
// HTTP代理模式:优先使用系统地址,但如果格式不正确则使用期望地址
|
||||||
|
const systemServer = sysproxy?.server;
|
||||||
|
if (
|
||||||
|
systemServer &&
|
||||||
|
systemServer !== "-" &&
|
||||||
|
!systemServer.startsWith(":")
|
||||||
|
) {
|
||||||
|
return systemServer;
|
||||||
|
} else {
|
||||||
|
// 系统地址无效,返回期望的代理地址
|
||||||
|
const proxyHost = verge.proxy_host || "127.0.0.1";
|
||||||
|
const proxyPort =
|
||||||
|
verge.verge_mixed_port || clashConfig.mixedPort || 7897;
|
||||||
|
return `${proxyHost}:${proxyPort}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 数据
|
||||||
|
proxies: proxiesData,
|
||||||
|
clashConfig,
|
||||||
|
rules: rulesData?.rules || [],
|
||||||
|
sysproxy,
|
||||||
|
runningMode,
|
||||||
|
uptime: uptimeData || 0,
|
||||||
|
|
||||||
|
// 提供者数据
|
||||||
|
proxyProviders: proxyProviders || {},
|
||||||
|
ruleProviders: ruleProviders?.providers || {},
|
||||||
|
|
||||||
|
systemProxyAddress: calculateSystemProxyAddress(),
|
||||||
|
|
||||||
|
// 刷新方法
|
||||||
|
refreshProxy,
|
||||||
|
refreshClashConfig,
|
||||||
|
refreshRules,
|
||||||
|
refreshSysproxy,
|
||||||
|
refreshProxyProviders,
|
||||||
|
refreshRuleProviders,
|
||||||
|
refreshAll,
|
||||||
|
} as AppDataContextType;
|
||||||
|
}, [
|
||||||
|
proxiesData,
|
||||||
|
clashConfig,
|
||||||
|
rulesData,
|
||||||
|
sysproxy,
|
||||||
|
runningMode,
|
||||||
|
uptimeData,
|
||||||
|
proxyProviders,
|
||||||
|
ruleProviders,
|
||||||
|
verge,
|
||||||
|
refreshProxy,
|
||||||
|
refreshClashConfig,
|
||||||
|
refreshRules,
|
||||||
|
refreshSysproxy,
|
||||||
|
refreshProxyProviders,
|
||||||
|
refreshRuleProviders,
|
||||||
|
refreshAll,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return <AppDataContext value={value}>{children}</AppDataContext>;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user