mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-15 05:40:47 +08:00
Compare commits
7
Commits
34a45548d3
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6b51171a1 | ||
|
|
d4287970bc | ||
|
|
b20fdc9b96 | ||
|
|
5b10331022 | ||
|
|
3ec858eb9f | ||
|
|
3693efcf6c | ||
|
|
6e08a77a84 |
@@ -6,6 +6,7 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
env:
|
||||
HUSKY: 0
|
||||
@@ -99,14 +100,7 @@ jobs:
|
||||
id: check_inputs
|
||||
uses: dorny/paths-filter@v4
|
||||
with:
|
||||
filters: |
|
||||
contract:
|
||||
- 'src/services/contract.ts'
|
||||
- 'scripts/gen-contract.mjs'
|
||||
- 'src-tauri/src/core/notify/**'
|
||||
- 'src-tauri/src/bin/**'
|
||||
- 'src-tauri/src/lib.rs'
|
||||
- '.github/workflows/frontend-check.yml'
|
||||
filters: scripts/contract-inputs.json
|
||||
|
||||
- name: Skip if contract inputs unchanged
|
||||
if: steps.check_inputs.outputs.contract != 'true'
|
||||
@@ -159,8 +153,6 @@ jobs:
|
||||
touch "src-tauri/sidecar/${bin}-x86_64-unknown-linux-gnu"
|
||||
done
|
||||
|
||||
- name: Regenerate contract and require empty diff
|
||||
- name: Check generated contract without changing the checkout
|
||||
if: steps.check_inputs.outputs.contract == 'true'
|
||||
run: |
|
||||
pnpm gen:contract
|
||||
git diff --exit-code -- src/services/contract.ts
|
||||
run: pnpm gen:contract --check
|
||||
|
||||
@@ -5,4 +5,5 @@ if ! command -v "cargo-make" >/dev/null 2>&1; then
|
||||
cargo install --force cargo-make
|
||||
fi
|
||||
|
||||
node scripts/check-contract-changes.mjs --push "$1" || exit $?
|
||||
cargo make pre-push
|
||||
|
||||
Generated
+95
@@ -2,6 +2,12 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "Inflector"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
@@ -1135,6 +1141,8 @@ dependencies = [
|
||||
"sha2 0.11.0",
|
||||
"smartstring",
|
||||
"socket2",
|
||||
"specta",
|
||||
"specta-typescript",
|
||||
"sysproxy",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
@@ -1155,6 +1163,7 @@ dependencies = [
|
||||
"tauri-plugin-updater",
|
||||
"tauri-plugin-wdio-webdriver",
|
||||
"tauri-plugin-window-state",
|
||||
"tauri-specta",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-stream",
|
||||
@@ -1213,6 +1222,7 @@ dependencies = [
|
||||
"rust_iso3166",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"specta",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -6804,6 +6814,58 @@ dependencies = [
|
||||
"system-deps 6.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "specta"
|
||||
version = "2.0.0-rc.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38f9a30cbcbb7011f1da7d73483983bf838af123883e45f2b36ed76328df9c50"
|
||||
dependencies = [
|
||||
"paste",
|
||||
"rustc_version 0.4.1",
|
||||
"serde_json",
|
||||
"specta-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "specta-macros"
|
||||
version = "2.0.0-rc.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ce14957ecc2897f1f848b8255b6531d13ddf49cbcf506b7c2c9fb1d005593bb"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "specta-serde"
|
||||
version = "0.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee8a72b755ddb8949fd8f17c5db43f0e8a806ea587d9bc602ee3f73240c00029"
|
||||
dependencies = [
|
||||
"specta",
|
||||
"specta-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "specta-typescript"
|
||||
version = "0.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "639404ee95557f2f8b7e4cb773ffefd45304c7ab8ba21ac83b69051595e083c0"
|
||||
dependencies = [
|
||||
"specta",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "specta-util"
|
||||
version = "0.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29b1fc02b446f7244a92924fe68c0555921209f1d342990cd1539e9138e69502"
|
||||
dependencies = [
|
||||
"specta",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
@@ -7159,6 +7221,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"serialize-to-javascript",
|
||||
"specta",
|
||||
"swift-rs",
|
||||
"tauri-build",
|
||||
"tauri-macros",
|
||||
@@ -7276,6 +7339,7 @@ dependencies = [
|
||||
"libc",
|
||||
"parking_lot",
|
||||
"serde",
|
||||
"specta",
|
||||
"sysinfo",
|
||||
"tauri",
|
||||
"tauri-plugin-clipboard-manager",
|
||||
@@ -7655,6 +7719,37 @@ dependencies = [
|
||||
"wry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-specta"
|
||||
version = "2.0.0-rc.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee080f36d2ac17ce2f3a82fb53f02d664e8345457de51b56dad3c394dacc41a2"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"specta",
|
||||
"specta-serde",
|
||||
"specta-typescript",
|
||||
"specta-util",
|
||||
"tauri",
|
||||
"tauri-specta-macros",
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-specta-macros"
|
||||
version = "2.0.0-rc.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a59dfdce06c98d8d211619bea5fdb39486d8a8c558e12b2d2ce255972320012"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-utils"
|
||||
version = "2.10.1"
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
<summary><strong> 🐞 修复问题 </strong></summary>
|
||||
|
||||
- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置的问题
|
||||
- 修复「首页」网站测速失败后卡片一直显示测试中的问题
|
||||
|
||||
**🐧 Linux**
|
||||
|
||||
- Linux:修复 deb/rpm 安装包错误包含多余的 `frontend-contract` 可执行文件,导致下游重新打包失败的问题
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
specta = { version = "=2.0.0-rc.25", features = ["derive", "function"] }
|
||||
clash-verge-logging = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
@@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, specta::Type)]
|
||||
pub struct UnlockItem {
|
||||
pub name: String,
|
||||
pub status: String,
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
//! Linux-only termination signal bridge.
|
||||
//!
|
||||
//! Upstream Tauri delivers session-end and termination events to the app on
|
||||
//! Windows and macOS, but has no equivalent on Linux: a desktop session may
|
||||
//! end without any window or run event reaching the process. Linux builds
|
||||
//! keep this crate to listen for SIGTERM, SIGINT and SIGHUP and forward them
|
||||
//! to the app quit flow, so system proxy and TUN cleanup still runs before
|
||||
//! the process dies. Non-Linux builds compile this crate to nothing and must
|
||||
//! not depend on it.
|
||||
|
||||
#![cfg(target_os = "linux")]
|
||||
|
||||
use std::sync::{
|
||||
OnceLock,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
@@ -5,20 +17,13 @@ use std::sync::{
|
||||
|
||||
use clash_verge_logging::{Type, logging};
|
||||
|
||||
#[cfg(unix)]
|
||||
mod unix;
|
||||
#[cfg(windows)]
|
||||
mod windows;
|
||||
|
||||
pub(crate) static RUNTIME: OnceLock<Option<tokio::runtime::Runtime>> = OnceLock::new();
|
||||
pub(crate) static SHUTDOWN_LATCH: ShutdownLatch = ShutdownLatch::new();
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ShutdownOutcome {
|
||||
Committed,
|
||||
Canceled,
|
||||
}
|
||||
|
||||
/// Serializes signal-triggered cleanups: a signal arriving while a quit
|
||||
/// attempt is still in flight is dropped instead of overlapping it.
|
||||
pub(crate) struct ShutdownLatch {
|
||||
is_cleaning_up: AtomicBool,
|
||||
}
|
||||
@@ -36,17 +41,18 @@ impl ShutdownLatch {
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
fn finish(&self, outcome: ShutdownOutcome) {
|
||||
if matches!(outcome, ShutdownOutcome::Canceled) {
|
||||
self.is_cleaning_up.store(false, Ordering::Release);
|
||||
}
|
||||
fn finish(&self) {
|
||||
self.is_cleaning_up.store(false, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns the Linux signal loop on a dedicated runtime so it survives the
|
||||
/// teardown of any app-owned runtime. The handler runs once per accepted
|
||||
/// signal; the quit flow it invokes owns the exit-or-cancel decision.
|
||||
pub fn register<F, Fut>(f: F)
|
||||
where
|
||||
F: Fn() -> Fut + Send + Sync + 'static,
|
||||
Fut: Future<Output = ShutdownOutcome> + Send + 'static,
|
||||
Fut: Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
RUNTIME.get_or_init(|| match tokio::runtime::Runtime::new() {
|
||||
Ok(rt) => Some(rt),
|
||||
@@ -61,36 +67,22 @@ where
|
||||
}
|
||||
});
|
||||
|
||||
#[cfg(unix)]
|
||||
unix::register(f);
|
||||
|
||||
#[cfg(windows)]
|
||||
windows::register(f);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ShutdownLatch, ShutdownOutcome};
|
||||
use super::ShutdownLatch;
|
||||
|
||||
#[test]
|
||||
fn canceled_shutdown_releases_latch_and_permits_next_attempt() {
|
||||
fn finished_attempt_releases_latch_for_the_next_signal() {
|
||||
let latch = ShutdownLatch::new();
|
||||
|
||||
assert!(latch.try_begin());
|
||||
assert!(!latch.try_begin());
|
||||
|
||||
latch.finish(ShutdownOutcome::Canceled);
|
||||
latch.finish();
|
||||
|
||||
assert!(latch.try_begin());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn committed_shutdown_retains_latch_and_suppresses_repeats() {
|
||||
let latch = ShutdownLatch::new();
|
||||
|
||||
assert!(latch.try_begin());
|
||||
latch.finish(ShutdownOutcome::Committed);
|
||||
|
||||
assert!(!latch.try_begin());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use tokio::signal::unix::{SignalKind, signal};
|
||||
|
||||
use crate::{RUNTIME, SHUTDOWN_LATCH, ShutdownOutcome};
|
||||
use crate::{RUNTIME, SHUTDOWN_LATCH};
|
||||
|
||||
pub fn register<F, Fut>(f: F)
|
||||
where
|
||||
F: Fn() -> Fut + Send + Sync + 'static,
|
||||
Fut: Future<Output = ShutdownOutcome> + Send + 'static,
|
||||
Fut: Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
if let Some(Some(rt)) = RUNTIME.get() {
|
||||
rt.spawn(async move {
|
||||
@@ -60,7 +60,8 @@ where
|
||||
}
|
||||
logging!(info, Type::SystemSignal, "Caught signal {}", signal_name);
|
||||
|
||||
SHUTDOWN_LATCH.finish(f().await);
|
||||
f().await;
|
||||
SHUTDOWN_LATCH.finish();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use tokio::signal::windows;
|
||||
|
||||
use crate::{RUNTIME, SHUTDOWN_LATCH, ShutdownOutcome};
|
||||
|
||||
pub fn register<F, Fut>(f: F)
|
||||
where
|
||||
F: Fn() -> Fut + Send + Sync + 'static,
|
||||
Fut: Future<Output = ShutdownOutcome> + Send + 'static,
|
||||
{
|
||||
if let Some(Some(rt)) = RUNTIME.get() {
|
||||
rt.spawn(async move {
|
||||
let mut ctrl_c = match windows::ctrl_c() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
logging!(error, Type::SystemSignal, "Failed to register Ctrl+C: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut ctrl_close = match windows::ctrl_close() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
logging!(error, Type::SystemSignal, "Failed to register Ctrl+Close: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut ctrl_shutdown = match windows::ctrl_shutdown() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
logging!(error, Type::SystemSignal, "Failed to register Ctrl+Shutdown: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut ctrl_logoff = match windows::ctrl_logoff() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
logging!(error, Type::SystemSignal, "Failed to register Ctrl+Logoff: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
loop {
|
||||
let signal_name;
|
||||
tokio::select! {
|
||||
_ = ctrl_c.recv() => {
|
||||
signal_name = "Ctrl+C";
|
||||
}
|
||||
_ = ctrl_close.recv() => {
|
||||
signal_name = "Ctrl+Close";
|
||||
}
|
||||
_ = ctrl_shutdown.recv() => {
|
||||
signal_name = "Ctrl+Shutdown";
|
||||
}
|
||||
_ = ctrl_logoff.recv() => {
|
||||
signal_name = "Ctrl+Logoff";
|
||||
}
|
||||
}
|
||||
|
||||
if !SHUTDOWN_LATCH.try_begin() {
|
||||
logging!(
|
||||
info,
|
||||
Type::SystemSignal,
|
||||
"Already shutting down, ignoring repeated signal: {}",
|
||||
signal_name
|
||||
);
|
||||
continue;
|
||||
}
|
||||
logging!(info, Type::SystemSignal, "Caught Windows signal: {}", signal_name);
|
||||
|
||||
SHUTDOWN_LATCH.finish(f().await);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
logging!(
|
||||
error,
|
||||
Type::SystemSignal,
|
||||
"register shutdown signal failed, RUNTIME is not available"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ edition = "2024"
|
||||
rust-version = "1.91"
|
||||
|
||||
[dependencies]
|
||||
specta = { version = "=2.0.0-rc.25", features = ["derive", "function"] }
|
||||
tauri = { workspace = true }
|
||||
tauri-plugin-clipboard-manager = { workspace = true }
|
||||
parking_lot = { workspace = true }
|
||||
|
||||
@@ -1,53 +1,46 @@
|
||||
use parking_lot::RwLock;
|
||||
use serde::Serialize;
|
||||
use tauri::{AppHandle, Runtime, State, command};
|
||||
use tauri_plugin_clipboard_manager::{ClipboardExt as _, Error};
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt as _;
|
||||
|
||||
use crate::Platform;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, specta::Type)]
|
||||
pub struct SystemInfo {
|
||||
pub system_name: String,
|
||||
pub system_version: String,
|
||||
pub system_kernel_version: String,
|
||||
pub system_arch: String,
|
||||
pub app_version: String,
|
||||
pub app_core_mode: String,
|
||||
pub app_is_admin: bool,
|
||||
}
|
||||
|
||||
impl From<Platform> for SystemInfo {
|
||||
fn from(platform: Platform) -> Self {
|
||||
impl From<&Platform> for SystemInfo {
|
||||
fn from(platform: &Platform) -> Self {
|
||||
Self {
|
||||
system_name: platform.sysinfo.system_name,
|
||||
system_version: platform.sysinfo.system_version,
|
||||
system_kernel_version: platform.sysinfo.system_kernel_version,
|
||||
system_arch: platform.sysinfo.system_arch,
|
||||
app_version: platform.appinfo.app_version,
|
||||
app_core_mode: platform.appinfo.app_core_mode,
|
||||
app_is_admin: platform.appinfo.app_is_admin,
|
||||
system_name: platform.sysinfo.system_name.clone(),
|
||||
system_version: platform.sysinfo.system_version.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn get_system_info(state: State<'_, RwLock<Platform>>) -> Result<SystemInfo, Error> {
|
||||
#[specta::specta]
|
||||
pub fn get_system_info(state: State<'_, RwLock<Platform>>) -> Result<SystemInfo, String> {
|
||||
let platform = state.inner().read();
|
||||
Ok(SystemInfo::from(platform.clone()))
|
||||
Ok(SystemInfo::from(&*platform))
|
||||
}
|
||||
|
||||
/// 获取应用的运行时间(毫秒)
|
||||
#[command]
|
||||
pub fn get_app_uptime(state: State<'_, RwLock<Platform>>) -> Result<u128, Error> {
|
||||
#[specta::specta]
|
||||
pub fn get_app_uptime(state: State<'_, RwLock<Platform>>) -> Result<u128, String> {
|
||||
Ok(state.inner().read().appinfo.app_startup_time.elapsed().as_millis())
|
||||
}
|
||||
|
||||
#[command]
|
||||
#[specta::specta]
|
||||
pub fn export_diagnostic_info<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
state: State<'_, RwLock<Platform>>,
|
||||
) -> Result<(), Error> {
|
||||
) -> Result<(), String> {
|
||||
let info = state.inner().read().to_string();
|
||||
let clipboard = app_handle.clipboard();
|
||||
clipboard.write_text(info)
|
||||
clipboard.write_text(info).map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
@@ -12,6 +12,92 @@ import pluginUnusedImports from 'eslint-plugin-unused-imports'
|
||||
import globals from 'globals'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
const ipcBoundaryRules = ({
|
||||
allowCommands = false,
|
||||
allowInvoke = false,
|
||||
allowEvents = false,
|
||||
} = {}) => {
|
||||
const paths = [
|
||||
...(!allowInvoke
|
||||
? [
|
||||
{
|
||||
name: '@tauri-apps/api/core',
|
||||
importNames: ['invoke'],
|
||||
allowTypeImports: true,
|
||||
message: 'Invoke backend commands through services/cmds.',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(!allowEvents
|
||||
? [
|
||||
{
|
||||
name: '@tauri-apps/api/event',
|
||||
importNames: ['listen', 'emit', 'emitTo', 'once'],
|
||||
allowTypeImports: true,
|
||||
message:
|
||||
'The event bus owns Tauri event subscriptions and emissions.',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
const patterns = allowCommands
|
||||
? []
|
||||
: [
|
||||
{
|
||||
regex: '(^|/)bindings(\\.ts)?$',
|
||||
importNames: ['commands'],
|
||||
allowTypeImports: true,
|
||||
message:
|
||||
'Only services/cmds may import generated command implementations.',
|
||||
},
|
||||
]
|
||||
const modules = [
|
||||
...(!allowInvoke ? ['@tauri-apps/api/core'] : []),
|
||||
...(!allowEvents ? ['@tauri-apps/api/event'] : []),
|
||||
]
|
||||
const syntax = modules.flatMap((name) => [
|
||||
{
|
||||
selector: `ImportExpression[source.value='${name}']`,
|
||||
message:
|
||||
'Use the owned IPC service boundary instead of dynamically importing Tauri APIs.',
|
||||
},
|
||||
{
|
||||
selector: `CallExpression[callee.name='require'][arguments.0.value='${name}']`,
|
||||
message:
|
||||
'Use the owned IPC service boundary instead of requiring Tauri APIs.',
|
||||
},
|
||||
{
|
||||
selector: `TSImportEqualsDeclaration[moduleReference.expression.value='${name}']`,
|
||||
message: 'Use the owned IPC service boundary for Tauri APIs.',
|
||||
},
|
||||
])
|
||||
if (!allowCommands) {
|
||||
syntax.push(
|
||||
{
|
||||
selector: 'ImportExpression[source.value=/bindings(\\.ts)?$/]',
|
||||
message:
|
||||
'Only services/cmds may load generated command implementations.',
|
||||
},
|
||||
{
|
||||
selector:
|
||||
"CallExpression[callee.name='require'][arguments.0.value=/bindings(\\.ts)?$/]",
|
||||
message:
|
||||
'Only services/cmds may load generated command implementations.',
|
||||
},
|
||||
{
|
||||
selector:
|
||||
'TSImportEqualsDeclaration[moduleReference.expression.value=/bindings(\\.ts)?$/]',
|
||||
message:
|
||||
'Only services/cmds may load generated command implementations.',
|
||||
},
|
||||
)
|
||||
}
|
||||
return {
|
||||
'no-restricted-imports': ['error', { paths, patterns }],
|
||||
'no-restricted-syntax': ['error', ...syntax],
|
||||
}
|
||||
}
|
||||
|
||||
export default defineConfig([
|
||||
pluginESx.configs['flat/restrict-to-es2022'],
|
||||
{
|
||||
@@ -162,4 +248,33 @@ export default defineConfig([
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['src/**/*.{ts,tsx,js,jsx,mts,mjs}'],
|
||||
rules: ipcBoundaryRules(),
|
||||
},
|
||||
{
|
||||
files: ['src/services/cmds.ts'],
|
||||
rules: ipcBoundaryRules({ allowCommands: true }),
|
||||
},
|
||||
{
|
||||
files: ['src/services/bindings.ts'],
|
||||
rules: ipcBoundaryRules({ allowCommands: true, allowInvoke: true }),
|
||||
},
|
||||
{
|
||||
files: ['src/services/bus/**/*.{ts,tsx}'],
|
||||
rules: ipcBoundaryRules({ allowEvents: true }),
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'src/components/proxy/proxy-chain.tsx',
|
||||
'src/components/proxy/proxy-groups-chain.tsx',
|
||||
'src/pages/proxies.tsx',
|
||||
],
|
||||
rules: {
|
||||
'@typescript-eslint/no-floating-promises': [
|
||||
'error',
|
||||
{ ignoreVoid: false },
|
||||
],
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/knip@6/schema.json",
|
||||
"entry": ["src/polyfills/*.js!"],
|
||||
"entry": ["src/polyfills/*.js!", "scripts/check-contract-changes.mjs!"],
|
||||
"project": ["src/**/*.{js,jsx,ts,tsx,sass,scss}!", "scripts/**/*.{js,mjs}!"],
|
||||
"include": [
|
||||
"files",
|
||||
@@ -18,7 +18,7 @@
|
||||
"ignoreDependencies": ["lint-staged"],
|
||||
"ignoreIssues": {
|
||||
"src/types/generated/**": ["exports", "types"],
|
||||
"src/services/contract.ts": ["exports", "types"]
|
||||
"src/services/bindings.ts": ["types"]
|
||||
},
|
||||
"cycles": {
|
||||
"dynamicImports": true
|
||||
|
||||
@@ -1,84 +1,34 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
/**
|
||||
* Architecture grep gates for the frontend contract flow. Select gates via
|
||||
* argv:
|
||||
*
|
||||
* node scripts/check-architecture.mjs listen emit invoke
|
||||
*
|
||||
* - listen/emit: the Tauri event API may only be used under src/services/bus/**
|
||||
* (the event bus). Detected as a bare `listen(`/`emit(` call (member calls
|
||||
* like `this.emit(` are unrelated class methods) or as importing the name
|
||||
* from '@tauri-apps/api/event'.
|
||||
* - invoke: `invoke(` calls may exist only under src/services/**.
|
||||
*/
|
||||
import { ESLint } from 'eslint'
|
||||
|
||||
const ROOT = path.resolve(import.meta.dirname, '..')
|
||||
const SRC = path.join(ROOT, 'src')
|
||||
|
||||
const tauriEventImport = (name) =>
|
||||
new RegExp(
|
||||
`import\\s*\\{[^}]*\\b${name}\\b[^}]*\\}\\s*from\\s*'@tauri-apps/api/event'`,
|
||||
)
|
||||
|
||||
const GATES = {
|
||||
listen: {
|
||||
patterns: [/(?<![.\w])listen\(/, tauriEventImport('listen')],
|
||||
allowed: (file) => file.startsWith('src/services/bus/'),
|
||||
description: 'Tauri listen outside the event bus (src/services/bus)',
|
||||
},
|
||||
emit: {
|
||||
patterns: [/(?<![.\w])emit\(/, tauriEventImport('emit')],
|
||||
allowed: (file) => file.startsWith('src/services/bus/'),
|
||||
description: 'Tauri emit outside the event bus (src/services/bus)',
|
||||
},
|
||||
invoke: {
|
||||
patterns: [/\binvoke\(/],
|
||||
allowed: (file) => file.startsWith('src/services/'),
|
||||
description: 'invoke( outside the services layer (src/services)',
|
||||
},
|
||||
}
|
||||
|
||||
const walk = (dir) => {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||
return entries.flatMap((entry) => {
|
||||
const full = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) return walk(full)
|
||||
return /\.(ts|tsx|js|jsx|mts|mjs)$/.test(entry.name) ? [full] : []
|
||||
})
|
||||
}
|
||||
|
||||
const selected = process.argv.slice(2)
|
||||
if (selected.length === 0) {
|
||||
const gates = process.argv.slice(2)
|
||||
if (
|
||||
!gates.length ||
|
||||
gates.some((gate) => !['listen', 'emit', 'invoke'].includes(gate))
|
||||
) {
|
||||
console.error('usage: check-architecture.mjs <gate>... (listen|emit|invoke)')
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
let violations = 0
|
||||
for (const gate of selected) {
|
||||
const rule = GATES[gate]
|
||||
if (!rule) {
|
||||
console.error(`unknown gate: ${gate}`)
|
||||
process.exit(2)
|
||||
}
|
||||
for (const file of walk(SRC)) {
|
||||
const relative = path.relative(ROOT, file).replaceAll('\\', '/')
|
||||
if (rule.allowed(relative)) continue
|
||||
const lines = fs.readFileSync(file, 'utf8').split('\n')
|
||||
lines.forEach((line, index) => {
|
||||
if (rule.patterns.some((pattern) => pattern.test(line))) {
|
||||
violations += 1
|
||||
console.error(
|
||||
`[arch] ${rule.description}: ${relative}:${index + 1}: ${line.trim()}`,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
// All IPC boundaries use the lint configuration, including when callers request a legacy gate.
|
||||
const eslint = new ESLint({ cwd: ROOT, allowInlineConfig: false })
|
||||
const results = await eslint.lintFiles(['src'])
|
||||
const boundaryRules = new Set([
|
||||
'no-restricted-imports',
|
||||
'no-restricted-syntax',
|
||||
'@typescript-eslint/no-floating-promises',
|
||||
])
|
||||
const violations = results.flatMap((result) =>
|
||||
result.messages
|
||||
.filter((message) => message.fatal || boundaryRules.has(message.ruleId))
|
||||
.map((message) => ({ file: result.filePath, ...message })),
|
||||
)
|
||||
for (const violation of violations) {
|
||||
console.error(
|
||||
`[arch] ${path.relative(ROOT, violation.file)}:${violation.line}: ${violation.message}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (violations > 0) {
|
||||
console.error(`[arch] ${violations} violation(s) found`)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('[arch] no violations')
|
||||
if (violations.length) process.exitCode = 1
|
||||
else console.log('[arch] no violations')
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
const ROOT = path.resolve(import.meta.dirname, '..')
|
||||
const git = (...args) =>
|
||||
execFileSync('git', args, { cwd: ROOT, encoding: 'utf8' }).trim()
|
||||
const { contract } = JSON.parse(
|
||||
fs.readFileSync(path.join(ROOT, 'scripts/contract-inputs.json'), 'utf8'),
|
||||
)
|
||||
const inputs = contract.map((pattern) => `:(glob)${pattern}`)
|
||||
const args = process.argv.slice(2)
|
||||
if (args.length !== 2 || args[0] !== '--push') {
|
||||
console.error('usage: check-contract-changes.mjs --push <remote>')
|
||||
process.exit(2)
|
||||
}
|
||||
const hasChanges = (...range) =>
|
||||
git('diff', '--name-only', ...range, '--', ...inputs) !== ''
|
||||
const assertCleanInputs = (...range) => {
|
||||
if (
|
||||
hasChanges(...range) ||
|
||||
git('ls-files', '--others', '--exclude-standard', '--', ...inputs)
|
||||
) {
|
||||
throw new Error(
|
||||
'Contract inputs differ from the pushed commit. Commit those changes before pushing.',
|
||||
)
|
||||
}
|
||||
}
|
||||
let relevant = false
|
||||
const head = git('rev-parse', 'HEAD')
|
||||
for (const line of fs
|
||||
.readFileSync(0, 'utf8')
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter(Boolean)) {
|
||||
const fields = line.trim().split(/\s+/)
|
||||
if (
|
||||
fields.length !== 4 ||
|
||||
![fields[1], fields[3]].every((sha) => /^[a-f0-9]{40,64}$/.test(sha))
|
||||
) {
|
||||
throw new Error('Invalid pre-push ref update')
|
||||
}
|
||||
const [, local, , remote] = fields
|
||||
if (/^0+$/.test(local) || local === remote) continue
|
||||
// A new remote branch has no upstream; use its fork point with the remote default branch.
|
||||
const base = /^0+$/.test(remote)
|
||||
? git('merge-base', local, `refs/remotes/${args[1]}/HEAD`)
|
||||
: remote
|
||||
if (!hasChanges(base, local)) continue
|
||||
const commit = git('rev-parse', `${local}^{commit}`)
|
||||
if (commit !== head)
|
||||
throw new Error(
|
||||
'Check out the pushed commit before validating its changed IPC contract.',
|
||||
)
|
||||
assertCleanInputs('HEAD')
|
||||
relevant = true
|
||||
}
|
||||
if (relevant) {
|
||||
execFileSync(
|
||||
process.execPath,
|
||||
[path.join(ROOT, 'scripts/gen-contract.mjs'), '--check'],
|
||||
{
|
||||
cwd: ROOT,
|
||||
stdio: 'inherit',
|
||||
},
|
||||
)
|
||||
} else {
|
||||
console.log('[contract] inputs unchanged; skipping Rust generation')
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"contract": [
|
||||
"src-tauri/**/*.rs",
|
||||
"crates/**/*.rs",
|
||||
"Cargo.toml",
|
||||
"Cargo.lock",
|
||||
"src-tauri/Cargo.toml",
|
||||
"crates/**/Cargo.toml",
|
||||
"rust-toolchain.toml",
|
||||
"src-tauri/tauri.conf*.json",
|
||||
"src/services/bindings.ts",
|
||||
"scripts/gen-contract.mjs",
|
||||
"scripts/check-contract-changes.mjs",
|
||||
"biome.json",
|
||||
"biome.jsonc",
|
||||
"package.json",
|
||||
"pnpm-lock.yaml",
|
||||
"pnpm-workspace.yaml",
|
||||
"scripts/contract-inputs.json",
|
||||
".github/workflows/frontend-check.yml",
|
||||
".husky/pre-push"
|
||||
]
|
||||
}
|
||||
+46
-82
@@ -1,91 +1,55 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
|
||||
/**
|
||||
* Regenerates src/services/contract.ts from the backend wire contract.
|
||||
* Source of truth: `cargo run -p clash-verge --bin frontend-contract` inside
|
||||
* src-tauri, which serializes the same enum variants and event payloads the
|
||||
* app emits at runtime. CI reruns this and fails on a non-empty diff.
|
||||
*/
|
||||
|
||||
const ROOT = path.resolve(import.meta.dirname, '..')
|
||||
const OUT = path.join(ROOT, 'src/services/contract.ts')
|
||||
|
||||
const contract = JSON.parse(
|
||||
const BINDINGS = path.join(ROOT, 'src/services/bindings.ts')
|
||||
const args = process.argv.slice(2)
|
||||
if (args.length > 1 || (args.length === 1 && args[0] !== '--check')) {
|
||||
console.error('usage: gen-contract.mjs [--check]')
|
||||
process.exit(2)
|
||||
}
|
||||
const check = args[0] === '--check'
|
||||
const temporary = check
|
||||
? fs.mkdtempSync(path.join(os.tmpdir(), 'verge-contract-'))
|
||||
: null
|
||||
const output = temporary ? path.join(temporary, 'bindings.ts') : BINDINGS
|
||||
try {
|
||||
execFileSync(
|
||||
'cargo',
|
||||
['run', '-p', 'clash-verge', '--bin', 'frontend-contract', '-q'],
|
||||
{
|
||||
cwd: path.join(ROOT, 'src-tauri'),
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
if (contract.version !== 1) {
|
||||
throw new Error(`unsupported wire contract version: ${contract.version}`)
|
||||
}
|
||||
|
||||
/** TS type per event, derived from the serialized sample payload's JSON shape. */
|
||||
const payloadType = (name, sample) => {
|
||||
if (sample === null) return 'null'
|
||||
if (typeof sample === 'string') return 'string'
|
||||
if (Array.isArray(sample)) {
|
||||
if (name === 'verge://notice-message') return '[NoticeStatus, string]'
|
||||
throw new Error(`unexpected array payload for ${name}`)
|
||||
[
|
||||
'run',
|
||||
'-p',
|
||||
'clash-verge',
|
||||
'--example',
|
||||
'frontend-contract',
|
||||
'--locked',
|
||||
'-q',
|
||||
...(check ? ['--', output] : []),
|
||||
],
|
||||
{ cwd: ROOT, stdio: 'inherit' },
|
||||
)
|
||||
const formatted = execFileSync(
|
||||
process.execPath,
|
||||
[
|
||||
path.join(ROOT, 'node_modules/@biomejs/biome/bin/biome'),
|
||||
'format',
|
||||
'--stdin-file-path',
|
||||
BINDINGS,
|
||||
],
|
||||
{ cwd: ROOT, input: fs.readFileSync(output) },
|
||||
)
|
||||
if (check) {
|
||||
if (!formatted.equals(fs.readFileSync(BINDINGS))) {
|
||||
console.error(
|
||||
'IPC bindings are out of date. Run pnpm gen:contract and commit the generated file.',
|
||||
)
|
||||
process.exitCode = 1
|
||||
}
|
||||
} else {
|
||||
fs.writeFileSync(BINDINGS, formatted)
|
||||
}
|
||||
if (typeof sample === 'object') {
|
||||
const keys = Object.keys(sample)
|
||||
if (keys.length === 1 && keys[0] === 'uid') return '{ uid: string }'
|
||||
if (name === 'verge://run-state-changed') return 'RunState'
|
||||
throw new Error(`unexpected object payload for ${name}: ${keys.join(',')}`)
|
||||
}
|
||||
throw new Error(`unexpected payload for ${name}`)
|
||||
} finally {
|
||||
if (temporary) fs.rmSync(temporary, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
const needsRunState = contract.events.some(
|
||||
(e) => e.name === 'verge://run-state-changed',
|
||||
)
|
||||
if (!needsRunState)
|
||||
throw new Error('run-state-changed event missing from contract')
|
||||
|
||||
const lines = []
|
||||
lines.push(
|
||||
'// @generated by scripts/gen-contract.mjs from the backend wire contract. DO NOT EDIT.',
|
||||
)
|
||||
lines.push('// Regen: pnpm gen:contract')
|
||||
lines.push('')
|
||||
lines.push("import type { RunState } from './cmds'")
|
||||
lines.push('')
|
||||
lines.push(
|
||||
'/** Wire status strings from NoticeStatus; keep in sync via `pnpm gen:contract`. */',
|
||||
)
|
||||
lines.push('export type NoticeStatus =')
|
||||
for (const status of contract.noticeStatuses) {
|
||||
lines.push(` | ${JSON.stringify(status)}`)
|
||||
}
|
||||
lines.push('')
|
||||
lines.push('/** Event name → payload type for every backend-emitted event. */')
|
||||
lines.push('export interface VergeEventPayloads {')
|
||||
for (const event of contract.events) {
|
||||
const key = JSON.stringify(event.name)
|
||||
lines.push(` ${key}: ${payloadType(event.name, event.sample)}`)
|
||||
}
|
||||
lines.push('}')
|
||||
lines.push('')
|
||||
|
||||
const content = `${lines.join('\n')}\n`
|
||||
fs.writeFileSync(OUT, content)
|
||||
|
||||
// Match the repo's biome style so CI parity (`git diff --exit-code`) is stable.
|
||||
execFileSync(
|
||||
process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm',
|
||||
['exec', 'biome', 'format', '--write', path.relative(ROOT, OUT)],
|
||||
{ cwd: ROOT, stdio: 'ignore' },
|
||||
)
|
||||
|
||||
console.log(
|
||||
`[gen-contract] wrote ${path.relative(ROOT, OUT)}: ${contract.events.length} events, ${contract.noticeStatuses.length} statuses`,
|
||||
)
|
||||
|
||||
@@ -35,7 +35,6 @@ tauri-build = { version = "2.7.1", features = [] }
|
||||
clash-verge-draft = { workspace = true }
|
||||
clash-verge-media-unlock = { workspace = true }
|
||||
clash-verge-logging = { workspace = true }
|
||||
clash-verge-signal = { workspace = true }
|
||||
clash-verge-i18n = { workspace = true }
|
||||
clash-verge-limiter = { workspace = true }
|
||||
tauri-plugin-clash-verge-sysinfo = { workspace = true }
|
||||
@@ -109,10 +108,17 @@ webpki-roots = "1.0.9"
|
||||
dark-light = "3.0.0"
|
||||
bytes = "1.12.1"
|
||||
tokio-util = { version = "0.7.19", features = ["time"] }
|
||||
specta = { version = "=2.0.0-rc.25", features = ["derive", "function", "serde_json"] }
|
||||
tauri-specta = { version = "=2.0.0-rc.25", features = ["derive", "typescript"] }
|
||||
specta-typescript = "=0.0.12"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2.190"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
# Upstream has no session-end signal support on Linux; see crates/clash-verge-signal.
|
||||
clash-verge-signal = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
tauri-plugin-wdio-webdriver = { version = "=1.4.0", optional = true }
|
||||
objc2 = "0.6.5"
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// Dev-only target: Tauri ships everything under src/bin/ to /usr/bin in deb/rpm
|
||||
// bundles (PR #8131), so this generator must stay an example, never a bin.
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut args = std::env::args_os().skip(1);
|
||||
let output = args
|
||||
.next()
|
||||
.map(std::path::PathBuf::from)
|
||||
.unwrap_or_else(|| std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../src/services/bindings.ts"));
|
||||
if args.next().is_some() {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "expected at most one output path").into());
|
||||
}
|
||||
app_lib::ipc::builder().export(specta_typescript::Typescript::default(), output)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
//! Prints the frontend wire contract as JSON to stdout. Zero new dependencies:
|
||||
//! the payload is derived from the same serializers the app uses at runtime.
|
||||
//! Consumed by scripts/gen-contract.mjs; CI regenerates and diffs the committed
|
||||
//! TypeScript artifact.
|
||||
|
||||
fn main() {
|
||||
println!("{}", app_lib::frontend_wire_contract());
|
||||
}
|
||||
@@ -9,12 +9,14 @@ use smartstring::alias::String;
|
||||
use tauri::{AppHandle, Manager as _, ipc::Channel};
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn open_app_dir() -> CmdResult<()> {
|
||||
let app_dir = dirs::app_home_dir().stringify_err()?;
|
||||
open::that(app_dir).stringify_err()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn open_core_dir() -> CmdResult<()> {
|
||||
let core_dir = tauri::utils::platform::current_exe().stringify_err()?;
|
||||
let core_dir = core_dir.parent().ok_or("failed to get core dir")?;
|
||||
@@ -22,12 +24,14 @@ pub async fn open_core_dir() -> CmdResult<()> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn open_logs_dir() -> CmdResult<()> {
|
||||
let log_dir = dirs::app_logs_dir().stringify_err()?;
|
||||
open::that(log_dir).stringify_err()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn open_devtools(app_handle: AppHandle) {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
if !window.is_devtools_open() {
|
||||
@@ -39,22 +43,26 @@ pub fn open_devtools(app_handle: AppHandle) {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn exit_app() {
|
||||
feat::quit().await;
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn restart_app() -> CmdResult<()> {
|
||||
feat::restart_app().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn install_update(
|
||||
app_handle: AppHandle,
|
||||
version: String,
|
||||
version: std::string::String,
|
||||
on_event: Channel<DownloadEvent>,
|
||||
) -> CmdResult<bool> {
|
||||
let version: String = version.into();
|
||||
SilentUpdater::global()
|
||||
.install_update(&app_handle, &version, |event| {
|
||||
let _ = on_event.send(event);
|
||||
@@ -64,22 +72,32 @@ pub async fn install_update(
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn cancel_update_download() {
|
||||
SilentUpdater::global().cancel_download();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_app_dir() -> CmdResult<String> {
|
||||
#[specta::specta]
|
||||
pub fn get_app_dir() -> CmdResult<std::string::String> {
|
||||
let app_home_dir = dirs::app_home_dir().stringify_err()?.to_string_lossy().into();
|
||||
Ok(app_home_dir)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String> {
|
||||
feat::download_icon_cache(url, name).await
|
||||
#[specta::specta]
|
||||
pub async fn download_icon_cache(
|
||||
url: std::string::String,
|
||||
name: std::string::String,
|
||||
) -> CmdResult<std::string::String> {
|
||||
let url: String = url.into();
|
||||
let name: String = name.into();
|
||||
feat::download_icon_cache(url, name).await.map(Into::into)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn copy_icon_file(path: String, icon_info: feat::IconInfo) -> CmdResult<String> {
|
||||
feat::copy_icon_file(path, icon_info).await
|
||||
#[specta::specta]
|
||||
pub async fn copy_icon_file(path: std::string::String, icon_info: feat::IconInfo) -> CmdResult<std::string::String> {
|
||||
let path: String = path.into();
|
||||
feat::copy_icon_file(path, icon_info).await.map(Into::into)
|
||||
}
|
||||
|
||||
@@ -4,33 +4,44 @@ use feat::LocalBackupFile;
|
||||
use smartstring::alias::String;
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn create_local_backup() -> CmdResult<()> {
|
||||
feat::create_local_backup().await.stringify_err()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn list_local_backup() -> CmdResult<Vec<LocalBackupFile>> {
|
||||
feat::list_local_backup().await.stringify_err()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_local_backup(filename: String) -> CmdResult<()> {
|
||||
#[specta::specta]
|
||||
pub async fn delete_local_backup(filename: std::string::String) -> CmdResult<()> {
|
||||
let filename: String = filename.into();
|
||||
feat::delete_local_backup(filename).await.stringify_err()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn restore_local_backup(filename: String) -> CmdResult<()> {
|
||||
#[specta::specta]
|
||||
pub async fn restore_local_backup(filename: std::string::String) -> CmdResult<()> {
|
||||
let filename: String = filename.into();
|
||||
feat::restore_local_backup(filename)
|
||||
.await
|
||||
.map_err(|error| super::proxy_aware_error(&error))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn import_local_backup(source: String) -> CmdResult<String> {
|
||||
feat::import_local_backup(source).await.stringify_err()
|
||||
#[specta::specta]
|
||||
pub async fn import_local_backup(source: std::string::String) -> CmdResult<std::string::String> {
|
||||
let source: String = source.into();
|
||||
feat::import_local_backup(source).await.map(Into::into).stringify_err()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn export_local_backup(filename: String, destination: String) -> CmdResult<()> {
|
||||
#[specta::specta]
|
||||
pub async fn export_local_backup(filename: std::string::String, destination: std::string::String) -> CmdResult<()> {
|
||||
let filename: String = filename.into();
|
||||
let destination: String = destination.into();
|
||||
feat::export_local_backup(filename, destination).await.stringify_err()
|
||||
}
|
||||
|
||||
+36
-24
@@ -12,30 +12,33 @@ use crate::{
|
||||
},
|
||||
};
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
use serde_yaml_ng::Mapping;
|
||||
use smartstring::alias::String;
|
||||
use tokio::fs;
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn copy_clash_env() -> CmdResult {
|
||||
feat::copy_clash_env().await;
|
||||
Ok(())
|
||||
feat::copy_clash_env().await.stringify_err()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_clash_info() -> CmdResult<ClashInfo> {
|
||||
Ok(Config::clash().await.data_arc().get_client_info())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
|
||||
feat::patch_clash(&payload)
|
||||
#[specta::specta]
|
||||
pub async fn patch_clash_config(payload: crate::ipc::YamlMapping) -> CmdResult {
|
||||
feat::patch_clash(&payload.0)
|
||||
.await
|
||||
.map_err(|error| proxy_aware_coded_error(&error, "CLASH_CONFIG_UPDATE_FAILED"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn patch_clash_mode(payload: String) -> CmdResult {
|
||||
#[specta::specta]
|
||||
pub async fn patch_clash_mode(payload: std::string::String) -> CmdResult {
|
||||
let payload: String = payload.into();
|
||||
feat::change_clash_mode(payload)
|
||||
.await
|
||||
.with_error_code("CLASH_MODE_UPDATE_FAILED")
|
||||
@@ -43,12 +46,15 @@ pub async fn patch_clash_mode(payload: String) -> CmdResult {
|
||||
|
||||
/// Reads the saved mode without depending on strict mihomo `/configs` deserialization.
|
||||
#[tauri::command]
|
||||
pub async fn get_clash_mode() -> CmdResult<Option<String>> {
|
||||
Ok(Config::clash().await.data_arc().get_mode().map(Into::into))
|
||||
#[specta::specta]
|
||||
pub async fn get_clash_mode() -> CmdResult<Option<std::string::String>> {
|
||||
Ok(Config::clash().await.data_arc().get_mode())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<CommandFailure>> {
|
||||
#[specta::specta]
|
||||
pub async fn change_clash_core(clash_core: std::string::String) -> CmdResult<Option<CommandFailure>> {
|
||||
let clash_core: String = clash_core.into();
|
||||
logging!(info, Type::Config, "changing core to {clash_core}");
|
||||
|
||||
match CoreManager::global().change_core(&clash_core).await {
|
||||
@@ -80,6 +86,7 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<CommandFa
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn restart_core() -> CmdResult {
|
||||
logging_error!(Type::Core, profiles_save_file_safe().await);
|
||||
let result = CoreManager::global()
|
||||
@@ -94,6 +101,7 @@ pub async fn restart_core() -> CmdResult {
|
||||
|
||||
/// Replaces the managed core binary atomically instead of letting mihomo overwrite itself.
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn upgrade_clash_core(force: bool) -> CmdResult<feat::CoreUpgradeReport> {
|
||||
let report = feat::upgrade_core(force).await.with_error_code("CORE_UPGRADE_FAILED")?;
|
||||
if report.upgraded {
|
||||
@@ -103,7 +111,9 @@ pub async fn upgrade_clash_core(force: bool) -> CmdResult<feat::CoreUpgradeRepor
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn test_delay(url: String) -> CmdResult<u32> {
|
||||
#[specta::specta]
|
||||
pub async fn test_delay(url: std::string::String) -> CmdResult<u32> {
|
||||
let url: String = url.into();
|
||||
let result = match feat::test_delay(url).await {
|
||||
Ok(delay) => delay,
|
||||
Err(e) => {
|
||||
@@ -115,13 +125,14 @@ pub async fn test_delay(url: String) -> CmdResult<u32> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
|
||||
#[specta::specta]
|
||||
pub async fn save_dns_config(dns_config: crate::ipc::YamlMapping) -> CmdResult {
|
||||
use crate::utils::dirs;
|
||||
use tokio::fs;
|
||||
|
||||
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
|
||||
|
||||
let yaml_str = yaml_emitter::to_mihomo_config_string(&dns_config).stringify_err()?;
|
||||
let yaml_str = yaml_emitter::to_mihomo_config_string(&dns_config.0).stringify_err()?;
|
||||
fs::write(&dns_path, yaml_str).await.stringify_err()?;
|
||||
logging!(info, Type::Config, "DNS config saved to {dns_path:?}");
|
||||
|
||||
@@ -129,22 +140,27 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn take_dns_override_notice() -> bool {
|
||||
crate::config::dns::take_dns_override_notice()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn set_dns_override(
|
||||
profile_uid: String,
|
||||
profile_uid: std::string::String,
|
||||
enabled: bool,
|
||||
confirmation: Option<String>,
|
||||
confirmation: Option<std::string::String>,
|
||||
) -> CmdResult<feat::DnsOverrideOutcome> {
|
||||
let profile_uid: String = profile_uid.into();
|
||||
let confirmation = confirmation.map(String::from);
|
||||
feat::set_dns_override(profile_uid, enabled, confirmation)
|
||||
.await
|
||||
.map_err(|error| proxy_aware_coded_error(&error, "DNS_OVERRIDE_UPDATE_FAILED"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
if apply {
|
||||
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
|
||||
@@ -163,7 +179,7 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
);
|
||||
})?;
|
||||
|
||||
let patch_config = serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml).stringify_err_log(|e| {
|
||||
serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml).stringify_err_log(|e| {
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
@@ -172,13 +188,6 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
);
|
||||
})?;
|
||||
|
||||
let mut patch = serde_yaml_ng::Mapping::new();
|
||||
patch.insert("dns".into(), patch_config.into());
|
||||
|
||||
Config::runtime().await.edit_draft(|d| {
|
||||
d.patch_config(&patch);
|
||||
});
|
||||
|
||||
CoreManager::global().update_config_checked().await.map_err(|error| {
|
||||
logging!(error, Type::Config, "failed to apply the config with DNS: {error:#}");
|
||||
proxy_aware_error(&error)
|
||||
@@ -202,20 +211,22 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_dns_config_content() -> CmdResult<Option<String>> {
|
||||
#[specta::specta]
|
||||
pub async fn get_dns_config_content() -> CmdResult<Option<std::string::String>> {
|
||||
use crate::utils::dirs;
|
||||
use tokio::fs;
|
||||
|
||||
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
|
||||
|
||||
match fs::read_to_string(&dns_path).await {
|
||||
Ok(content) => Ok(Some(content.into())),
|
||||
Ok(content) => Ok(Some(content)),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||
Err(error) => Err(CommandFailure::plain(error)),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn validate_dns_config() -> CmdResult<ValidationOutcome> {
|
||||
let app_dir = dirs::app_home_dir().stringify_err()?;
|
||||
let dns_path = app_dir.join(constants::files::DNS_CONFIG);
|
||||
@@ -232,6 +243,7 @@ pub async fn validate_dns_config() -> CmdResult<ValidationOutcome> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_clash_logs() -> CmdResult<Vec<std::string::String>> {
|
||||
let logs = CoreManager::global().get_clash_logs().await.unwrap_or_default();
|
||||
Ok(logs)
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::core::lightweight;
|
||||
use super::CmdResult;
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn entry_lightweight_mode() -> CmdResult {
|
||||
lightweight::entry_lightweight_mode().await;
|
||||
Ok(())
|
||||
|
||||
@@ -5,11 +5,13 @@ use crate::{
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn probe_listener(request: ListenerProbe) -> CmdResult<ListenerProbeOutcome> {
|
||||
feat::probe_listener(request).await.stringify_err()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn save_proxy_ports(settings: ProxyPortSettings) -> CmdResult<SaveProxyPortsOutcome> {
|
||||
feat::save_proxy_ports(settings)
|
||||
.await
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::time::Duration;
|
||||
use tauri::{command, ipc::Channel};
|
||||
|
||||
#[command]
|
||||
#[specta::specta]
|
||||
pub fn get_unlock_items() -> Vec<UnlockItem> {
|
||||
clash_verge_media_unlock::default_unlock_items()
|
||||
}
|
||||
@@ -26,6 +27,7 @@ async fn create_client() -> Result<Client, String> {
|
||||
}
|
||||
|
||||
#[command]
|
||||
#[specta::specta]
|
||||
pub async fn check_media_unlock(on_complete: Channel<UnlockItem>) -> Result<Vec<UnlockItem>, String> {
|
||||
let client = create_client().await?;
|
||||
Ok(clash_verge_media_unlock::check_media_unlock(&client, |item| {
|
||||
@@ -35,6 +37,7 @@ pub async fn check_media_unlock(on_complete: Channel<UnlockItem>) -> Result<Vec<
|
||||
}
|
||||
|
||||
#[command]
|
||||
#[specta::specta]
|
||||
pub async fn check_media_unlock_item(name: String) -> Result<UnlockItem, String> {
|
||||
let client = create_client().await?;
|
||||
clash_verge_media_unlock::check_media_unlock_item(&client, &name).await
|
||||
|
||||
@@ -4,7 +4,7 @@ use smartstring::alias::String;
|
||||
pub type CmdResult<T = ()> = Result<T, CommandFailure>;
|
||||
|
||||
/// Structured command failure returned to the frontend.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, specta::Type)]
|
||||
pub struct CommandFailure {
|
||||
/// Stable frontend identifier, when classified.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -3,13 +3,12 @@ use crate::cmd::StringifyErr as _;
|
||||
use crate::core::{proxy_control, sysopt::Sysopt};
|
||||
|
||||
use gethostname::gethostname;
|
||||
use network_interface::NetworkInterface;
|
||||
use serde_yaml_ng::Mapping;
|
||||
use sysproxy::{Autoproxy, Sysproxy};
|
||||
use tauri_plugin_clash_verge_sysinfo;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||
#[specta::specta]
|
||||
pub async fn get_sys_proxy() -> CmdResult<SystemProxy> {
|
||||
Sysopt::global().wait_idle().await;
|
||||
// With no network service there is no proxy configured anywhere, which reads as disabled.
|
||||
let sys_proxy = match Sysproxy::get_system_proxy() {
|
||||
@@ -17,40 +16,40 @@ pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||
other => other.stringify_err()?,
|
||||
};
|
||||
let Sysproxy {
|
||||
ref host,
|
||||
ref bypass,
|
||||
ref port,
|
||||
ref enable,
|
||||
host,
|
||||
bypass,
|
||||
port,
|
||||
enable,
|
||||
} = sys_proxy;
|
||||
|
||||
let mut map = Mapping::new();
|
||||
map.insert("enable".into(), (*enable).into());
|
||||
map.insert("server".into(), format!("{}:{}", host, port).into());
|
||||
map.insert("bypass".into(), bypass.as_str().into());
|
||||
Ok(map)
|
||||
Ok(SystemProxy {
|
||||
enable,
|
||||
server: format!("{host}:{port}"),
|
||||
bypass,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_auto_proxy() -> CmdResult<Mapping> {
|
||||
#[specta::specta]
|
||||
pub async fn get_auto_proxy() -> CmdResult<AutoProxy> {
|
||||
Sysopt::global().wait_idle().await;
|
||||
let auto_proxy = match Autoproxy::get_auto_proxy() {
|
||||
Err(error) if proxy_control::is_missing_network_service(&error) => Autoproxy::default(),
|
||||
other => other.stringify_err()?,
|
||||
};
|
||||
let Autoproxy { ref enable, ref url } = auto_proxy;
|
||||
let Autoproxy { enable, url } = auto_proxy;
|
||||
|
||||
let mut map = Mapping::new();
|
||||
map.insert("enable".into(), (*enable).into());
|
||||
map.insert("url".into(), url.as_str().into());
|
||||
Ok(map)
|
||||
Ok(AutoProxy { enable, url })
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_embedded_server_port() -> CmdResult<u16> {
|
||||
crate::utils::server::embedded_server_port().stringify_err()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_system_hostname() -> String {
|
||||
match gethostname().into_string() {
|
||||
Ok(name) => name,
|
||||
@@ -62,12 +61,14 @@ pub fn get_system_hostname() -> String {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_network_interfaces() -> Vec<String> {
|
||||
tauri_plugin_clash_verge_sysinfo::list_network_interfaces()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_network_interfaces_info() -> CmdResult<Vec<NetworkInterface>> {
|
||||
#[specta::specta]
|
||||
pub fn get_network_interfaces_info() -> CmdResult<Vec<crate::ipc::NetworkInterfaceView>> {
|
||||
use network_interface::{NetworkInterface, NetworkInterfaceConfig as _};
|
||||
|
||||
let names = get_network_interfaces();
|
||||
@@ -77,9 +78,22 @@ pub fn get_network_interfaces_info() -> CmdResult<Vec<NetworkInterface>> {
|
||||
|
||||
for interface in interfaces {
|
||||
if names.contains(&interface.name) {
|
||||
result.push(interface);
|
||||
result.push(interface.into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, specta::Type)]
|
||||
pub struct SystemProxy {
|
||||
pub enable: bool,
|
||||
pub server: String,
|
||||
pub bypass: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, specta::Type)]
|
||||
pub struct AutoProxy {
|
||||
pub enable: bool,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
+114
-17
@@ -33,6 +33,7 @@ fn profile_import_error(err: &anyhow::Error) -> std::string::String {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_profiles() -> CmdResult<SharedDraft<IProfiles>> {
|
||||
let draft = Config::profiles().await;
|
||||
let data = draft.data_arc();
|
||||
@@ -40,6 +41,7 @@ pub async fn get_profiles() -> CmdResult<SharedDraft<IProfiles>> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn enhance_profiles() -> CmdResult<ValidationOutcome> {
|
||||
match feat::enhance_profiles().await {
|
||||
Ok(outcome) if outcome.is_valid() => {
|
||||
@@ -65,6 +67,7 @@ pub async fn enhance_profiles() -> CmdResult<ValidationOutcome> {
|
||||
|
||||
#[tauri::command]
|
||||
#[tracing::instrument(skip_all, level = "info", fields(url = %help::mask_url(&url), uid = tracing::field::Empty))]
|
||||
#[specta::specta]
|
||||
pub async fn import_profile(url: std::string::String, option: Option<PrfOption>) -> CmdResult {
|
||||
let item = &mut match PrfItem::from_url(&url, None, None, option.as_ref()).await {
|
||||
Ok(it) => it,
|
||||
@@ -94,7 +97,10 @@ pub async fn import_profile(url: std::string::String, option: Option<PrfOption>)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
|
||||
#[specta::specta]
|
||||
pub async fn reorder_profile(active_id: std::string::String, over_id: std::string::String) -> CmdResult {
|
||||
let active_id: String = active_id.into();
|
||||
let over_id: String = over_id.into();
|
||||
match profiles_reorder_safe(&active_id, &over_id).await {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Cmd, "重新排序配置文件: {} -> {}", active_id, over_id);
|
||||
@@ -114,16 +120,16 @@ pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
|
||||
#[specta::specta]
|
||||
pub async fn create_profile(item: ProfileCreate, file_data: Option<std::string::String>) -> CmdResult {
|
||||
let item: PrfItem = item.into();
|
||||
let file_data = file_data.map(String::from);
|
||||
match profiles_append_item_with_filedata_safe(&item, file_data).await {
|
||||
Ok(_) => {
|
||||
profiles_save_file_safe()
|
||||
.await
|
||||
.with_error_code("PROFILE_CREATE_FAILED")?;
|
||||
logging_error!(Type::Timer, Timer::global().refresh().await);
|
||||
if let Some(uid) = &item.uid {
|
||||
handle::Handle::notify_profile_changed(uid);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(coded_error("PROFILE_CREATE_FAILED", err)),
|
||||
@@ -131,7 +137,9 @@ pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResu
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
|
||||
#[specta::specta]
|
||||
pub async fn update_profile(index: std::string::String, option: Option<PrfOption>) -> CmdResult {
|
||||
let index: String = index.into();
|
||||
match feat::update_profile(&index, option.as_ref(), true).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
@@ -142,7 +150,9 @@ pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResu
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_profile(index: String) -> CmdResult {
|
||||
#[specta::specta]
|
||||
pub async fn delete_profile(index: std::string::String) -> CmdResult {
|
||||
let index: String = index.into();
|
||||
let profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
|
||||
|
||||
let profiles = Config::profiles().await;
|
||||
@@ -313,9 +323,85 @@ async fn perform_config_update(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, serde::Deserialize, specta::Type)]
|
||||
#[serde(default)]
|
||||
pub struct ProfileSelection {
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub current: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, serde::Deserialize, specta::Type)]
|
||||
#[serde(default)]
|
||||
pub struct ProfileCreate {
|
||||
#[serde(rename = "type")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub itype: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub name: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub desc: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub url: Option<String>,
|
||||
pub option: Option<PrfOption>,
|
||||
}
|
||||
|
||||
impl From<ProfileCreate> for PrfItem {
|
||||
fn from(item: ProfileCreate) -> Self {
|
||||
Self {
|
||||
itype: item.itype,
|
||||
name: item.name,
|
||||
desc: item.desc,
|
||||
url: item.url,
|
||||
option: item.option,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, serde::Deserialize, specta::Type)]
|
||||
#[serde(default)]
|
||||
pub struct ProfilePatch {
|
||||
#[serde(rename = "type")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub itype: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub name: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub desc: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub file: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub url: Option<String>,
|
||||
pub selected: Option<Vec<crate::config::PrfSelected>>,
|
||||
#[specta(type = Option<std::collections::BTreeMap<std::string::String, std::string::String>>)]
|
||||
pub latency_test_urls: Option<std::collections::BTreeMap<String, String>>,
|
||||
pub extra: Option<crate::config::PrfExtra>,
|
||||
pub updated: Option<usize>,
|
||||
pub option: Option<PrfOption>,
|
||||
}
|
||||
|
||||
impl From<ProfilePatch> for PrfItem {
|
||||
fn from(patch: ProfilePatch) -> Self {
|
||||
Self {
|
||||
itype: patch.itype,
|
||||
name: patch.name,
|
||||
desc: patch.desc,
|
||||
file: patch.file,
|
||||
url: patch.url,
|
||||
selected: patch.selected,
|
||||
latency_test_urls: patch.latency_test_urls,
|
||||
extra: patch.extra,
|
||||
updated: patch.updated,
|
||||
option: patch.option,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[tracing::instrument(skip_all, level = "info", fields(target = ?profiles.current))]
|
||||
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<ValidationOutcome> {
|
||||
#[specta::specta]
|
||||
pub async fn patch_profiles_config(profiles: ProfileSelection) -> CmdResult<ValidationOutcome> {
|
||||
if CURRENT_SWITCHING_PROFILE
|
||||
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
|
||||
.is_err()
|
||||
@@ -325,11 +411,17 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<ValidationO
|
||||
}
|
||||
let _profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
|
||||
|
||||
let profiles = IProfiles {
|
||||
current: profiles.current,
|
||||
items: None,
|
||||
};
|
||||
let target_profile = profiles.current.as_ref();
|
||||
|
||||
let previous_profile = Config::profiles().await.data_arc().current.clone();
|
||||
|
||||
Config::profiles().await.edit_draft(|d| d.patch_config(&profiles));
|
||||
Config::profiles().await.edit_draft(|d| {
|
||||
d.patch_config(&profiles);
|
||||
});
|
||||
|
||||
perform_config_update(target_profile, previous_profile.as_ref())
|
||||
.await
|
||||
@@ -337,15 +429,17 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<ValidationO
|
||||
}
|
||||
|
||||
pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> CmdResult<ValidationOutcome> {
|
||||
let profiles = IProfiles {
|
||||
let profiles = ProfileSelection {
|
||||
current: Some(profile_index),
|
||||
items: None,
|
||||
};
|
||||
patch_profiles_config(profiles).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||
#[specta::specta]
|
||||
pub async fn patch_profile(index: std::string::String, profile: ProfilePatch) -> CmdResult {
|
||||
let profile: PrfItem = profile.into();
|
||||
let index: String = index.into();
|
||||
let profiles = Config::profiles().await;
|
||||
let should_refresh_timer = if let Ok(old_profile) = profiles.latest_arc().get_item(&index)
|
||||
&& let Some(new_option) = profile.option.as_ref()
|
||||
@@ -385,7 +479,8 @@ pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn view_profile(index: String) -> CmdResult {
|
||||
#[specta::specta]
|
||||
pub async fn view_profile(index: std::string::String) -> CmdResult {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_ref = profiles.latest_arc();
|
||||
let file = profiles_ref
|
||||
@@ -409,7 +504,8 @@ pub async fn view_profile(index: String) -> CmdResult {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn read_profile_file(index: String) -> CmdResult<String> {
|
||||
#[specta::specta]
|
||||
pub async fn read_profile_file(index: std::string::String) -> CmdResult<std::string::String> {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_ref = profiles.latest_arc();
|
||||
@@ -429,7 +525,7 @@ pub async fn read_profile_file(index: String) -> CmdResult<String> {
|
||||
.join(file.as_str());
|
||||
match tokio::fs::try_exists(&path).await {
|
||||
Ok(true) => {}
|
||||
Ok(false) => return Ok(String::new()),
|
||||
Ok(false) => return Ok(std::string::String::new()),
|
||||
Err(err) => {
|
||||
return Err(coded_error(
|
||||
"PROFILE_READ_FAILED",
|
||||
@@ -440,11 +536,12 @@ pub async fn read_profile_file(index: String) -> CmdResult<String> {
|
||||
}
|
||||
|
||||
let data = item.read_file().await.with_error_code("PROFILE_READ_FAILED")?;
|
||||
Ok(data)
|
||||
Ok(data.into())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_next_update_time(uid: String) -> CmdResult<Option<i64>> {
|
||||
#[specta::specta]
|
||||
pub async fn get_next_update_time(uid: std::string::String) -> CmdResult<Option<i64>> {
|
||||
let timer = Timer::global();
|
||||
let next_time = timer.get_next_update_time(&uid).await;
|
||||
Ok(next_time)
|
||||
|
||||
@@ -18,6 +18,7 @@ use std::{
|
||||
|
||||
/// Merges one selection pair into fresh backend state to avoid stale-list overwrites.
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn record_selected_node(group_name: String, node: String) -> CmdResult<()> {
|
||||
crate::config::profiles::record_selected_node(&group_name, &node)
|
||||
.await
|
||||
@@ -25,6 +26,7 @@ pub async fn record_selected_node(group_name: String, node: String) -> CmdResult
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn forget_selected_node(group_name: String) -> CmdResult<()> {
|
||||
crate::config::profiles::forget_selected_node(&group_name)
|
||||
.await
|
||||
@@ -104,6 +106,7 @@ fn runtime_group_scopes(config: Option<&Mapping>) -> BTreeMap<String, GroupScope
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_proxy_view() -> CmdResult<ProxyViewV1> {
|
||||
proxy_view(None).await
|
||||
}
|
||||
@@ -135,6 +138,7 @@ pub(crate) async fn proxy_view(provider_timeout: Option<std::time::Duration>) ->
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn sync_tray_proxy_selection() -> CmdResult<()> {
|
||||
if TRAY_SYNC_RUNNING
|
||||
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
|
||||
|
||||
@@ -1,44 +1,57 @@
|
||||
use super::CmdResult;
|
||||
use crate::{cmd::StringifyErr as _, config::Config, core::CoreManager, utils::yaml_emitter};
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use serde_yaml_ng::Mapping;
|
||||
use smartstring::alias::String;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_runtime_config() -> CmdResult<Option<Mapping>> {
|
||||
Ok(Config::runtime().await.latest_arc().config.clone())
|
||||
#[specta::specta]
|
||||
pub async fn get_runtime_config() -> CmdResult<Option<crate::ipc::YamlMapping>> {
|
||||
Ok(Config::runtime()
|
||||
.await
|
||||
.latest_arc()
|
||||
.config
|
||||
.clone()
|
||||
.map(crate::ipc::YamlMapping))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_runtime_yaml() -> CmdResult<String> {
|
||||
#[specta::specta]
|
||||
pub async fn get_runtime_yaml() -> CmdResult<std::string::String> {
|
||||
let runtime = Config::runtime().await;
|
||||
let runtime = runtime.latest_arc();
|
||||
|
||||
let config = runtime.config.as_ref();
|
||||
config
|
||||
.ok_or_else(|| anyhow!("failed to parse config to yaml file"))
|
||||
.and_then(|config| {
|
||||
yaml_emitter::to_mihomo_config_string(config)
|
||||
.context("failed to convert config to yaml")
|
||||
.map(|s| s.into())
|
||||
})
|
||||
.and_then(|config| yaml_emitter::to_mihomo_config_string(config).context("failed to convert config to yaml"))
|
||||
.stringify_err()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
|
||||
Ok(Config::runtime().await.latest_arc().chain_logs.clone())
|
||||
#[specta::specta]
|
||||
pub async fn get_runtime_logs() -> CmdResult<RuntimeLogs> {
|
||||
Ok(RuntimeLogs(Config::runtime().await.latest_arc().chain_logs.clone()))
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, specta::Type)]
|
||||
#[serde(transparent)]
|
||||
pub struct RuntimeLogs(
|
||||
#[specta(type = HashMap<std::string::String, Vec<(std::string::String, std::string::String)>>)]
|
||||
HashMap<String, Vec<(String, String)>>,
|
||||
);
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn take_discarded_keys_notice() -> Option<std::string::String> {
|
||||
crate::enhance::take_discarded_keys_notice().map(Into::into)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn take_discarded_keys_notice() -> Option<String> {
|
||||
crate::enhance::take_discarded_keys_notice()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> CmdResult<String> {
|
||||
#[specta::specta]
|
||||
pub async fn get_runtime_proxy_chain_config(
|
||||
proxy_chain_exit_node: std::string::String,
|
||||
) -> CmdResult<std::string::String> {
|
||||
let runtime = Config::runtime().await;
|
||||
let runtime = runtime.latest_arc();
|
||||
|
||||
@@ -59,7 +72,7 @@ pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> Cm
|
||||
false
|
||||
}
|
||||
}) {
|
||||
proxies_chain.push(proxy.to_owned());
|
||||
proxies_chain.push(proxy);
|
||||
proxy_name = proxy.get("dialer-proxy").map(|x| x.as_str());
|
||||
}
|
||||
|
||||
@@ -68,18 +81,17 @@ pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> Cm
|
||||
.find(|proxy| proxy.get("name").map(|x| x.as_str()) == proxy_name)
|
||||
&& !proxies_chain.is_empty()
|
||||
{
|
||||
proxies_chain.push(entry_proxy.to_owned());
|
||||
proxies_chain.push(entry_proxy);
|
||||
}
|
||||
|
||||
proxies_chain.reverse();
|
||||
|
||||
let mut config: HashMap<String, Vec<serde_yaml_ng::Value>> = HashMap::new();
|
||||
let mut config: HashMap<String, Vec<&serde_yaml_ng::Value>> = HashMap::new();
|
||||
|
||||
config.insert("proxies".into(), proxies_chain);
|
||||
|
||||
yaml_emitter::to_mihomo_config_string(&config)
|
||||
.context("YAML generation failed")
|
||||
.map(|s| s.into())
|
||||
.stringify_err()
|
||||
} else {
|
||||
Err("failed to get proxies or proxy-groups".into())
|
||||
@@ -87,20 +99,13 @@ pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> Cm
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_proxy_chain_config_in_runtime(proxy_chain_config: Option<serde_yaml_ng::Value>) -> CmdResult<()> {
|
||||
match CoreManager::global()
|
||||
#[specta::specta]
|
||||
pub async fn update_proxy_chain_config_in_runtime(
|
||||
proxy_chain_config: Option<crate::ipc::YamlValue>,
|
||||
) -> CmdResult<crate::core::validate::ValidationOutcome> {
|
||||
let proxy_chain_config = proxy_chain_config.map(|value| value.0);
|
||||
CoreManager::global()
|
||||
.update_runtime_config(|d| d.update_proxy_chain_config(proxy_chain_config))
|
||||
.await
|
||||
{
|
||||
Ok(outcome) if outcome.is_valid() => {}
|
||||
Ok(outcome) => logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
"Failed to apply runtime proxy chain config: {}",
|
||||
outcome
|
||||
),
|
||||
Err(err) => logging!(error, Type::Core, "Failed to apply runtime proxy chain config: {err:#}"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
.stringify_err()
|
||||
}
|
||||
|
||||
@@ -16,7 +16,13 @@ use tokio::fs;
|
||||
|
||||
/// 保存profiles的配置
|
||||
#[tauri::command]
|
||||
pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult<ValidationOutcome> {
|
||||
#[specta::specta]
|
||||
pub async fn save_profile_file(
|
||||
index: std::string::String,
|
||||
file_data: Option<std::string::String>,
|
||||
) -> CmdResult<ValidationOutcome> {
|
||||
let index: String = index.into();
|
||||
let file_data = file_data.map(String::from);
|
||||
let file_data = match file_data {
|
||||
Some(d) => d,
|
||||
None => return Ok(ValidationOutcome::Valid),
|
||||
|
||||
@@ -8,11 +8,11 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, serde::Serialize)]
|
||||
#[derive(Debug, PartialEq, Eq, serde::Serialize, specta::Type)]
|
||||
#[serde(tag = "status", rename_all = "camelCase")]
|
||||
pub enum ServiceInstallOutcome {
|
||||
Installed,
|
||||
#[cfg(any(windows, test))]
|
||||
#[cfg_attr(not(any(windows, test)), allow(dead_code))]
|
||||
Sidecar {
|
||||
reason: String,
|
||||
},
|
||||
@@ -82,11 +82,13 @@ where
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn install_service() -> CmdResult<ServiceInstallOutcome> {
|
||||
execute_service_operation_sync(ServiceStatus::InstallRequired, "SERVICE_INSTALL_FAILED").await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn uninstall_service() -> CmdResult {
|
||||
CoreManager::global()
|
||||
.uninstall_service_and_start_sidecar()
|
||||
@@ -95,16 +97,19 @@ pub async fn uninstall_service() -> CmdResult {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn reinstall_service() -> CmdResult<ServiceInstallOutcome> {
|
||||
execute_service_operation_sync(ServiceStatus::ReinstallRequired, "SERVICE_REINSTALL_FAILED").await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn repair_service() -> CmdResult<ServiceInstallOutcome> {
|
||||
execute_service_operation_sync(ServiceStatus::ForceReinstallRequired, "SERVICE_REPAIR_FAILED").await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn continue_with_sidecar() -> CmdResult {
|
||||
crate::core::CoreManager::global()
|
||||
.continue_with_sidecar(None)
|
||||
@@ -113,31 +118,37 @@ pub async fn continue_with_sidecar() -> CmdResult {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_sidecar_failure() -> crate::core::notification::SidecarFailureSnapshot {
|
||||
crate::core::notification::sidecar_failure_snapshot()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn take_service_fallback_notice() -> Option<crate::core::service::ServiceFallbackNotice> {
|
||||
crate::core::service::take_service_fallback_notice()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_core_startup_error() -> Option<crate::core::manager::CoreFailure> {
|
||||
crate::core::CoreManager::global().get_startup_error()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn take_service_repair_notice() -> bool {
|
||||
crate::core::service::take_service_repair_notice()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn take_service_owner_notice() -> Option<String> {
|
||||
crate::core::service::take_service_owner_notice()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn sync_runtime_providers() {
|
||||
if matches!(*CoreManager::global().get_running_mode(), RunningMode::Service) {
|
||||
request_runtime_provider_sync(timing::RUNTIME_PROVIDER_SETTLE);
|
||||
|
||||
@@ -5,11 +5,13 @@ use crate::core::{
|
||||
|
||||
/// Returns one coherent core/service snapshot instead of independently refreshed state.
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_runtime_state() -> Result<RunStateView, String> {
|
||||
Ok(RUN_STATE.settled().await.to_view())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_pending_failures() -> Vec<PendingFailure> {
|
||||
notification::pending_failures()
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ mod platform {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||
platform::invoke_uwp_tool()
|
||||
}
|
||||
|
||||
@@ -9,12 +9,14 @@ use clash_verge_draft::SharedDraft;
|
||||
|
||||
/// 获取Verge配置
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_verge_config() -> CmdResult<SharedDraft<IVerge>> {
|
||||
feat::fetch_verge_config().await.stringify_err()
|
||||
}
|
||||
|
||||
/// 修改Verge配置
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
|
||||
let operation = system_proxy_operation(&payload);
|
||||
let result = match operation {
|
||||
|
||||
@@ -4,11 +4,18 @@ use crate::{
|
||||
config::{Config, IVerge},
|
||||
core, feat,
|
||||
};
|
||||
use reqwest_dav::list_cmd::ListFile;
|
||||
use smartstring::alias::String;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_webdav_config(url: String, username: String, password: String) -> CmdResult<()> {
|
||||
#[specta::specta]
|
||||
pub async fn save_webdav_config(
|
||||
url: std::string::String,
|
||||
username: std::string::String,
|
||||
password: std::string::String,
|
||||
) -> CmdResult<()> {
|
||||
let url: String = url.into();
|
||||
let username: String = username.into();
|
||||
let password: String = password.into();
|
||||
let patch = IVerge {
|
||||
webdav_url: Some(url),
|
||||
webdav_username: Some(username),
|
||||
@@ -25,22 +32,31 @@ pub async fn save_webdav_config(url: String, username: String, password: String)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn create_webdav_backup() -> CmdResult<()> {
|
||||
feat::create_backup_and_upload_webdav().await.stringify_err()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_webdav_backup() -> CmdResult<Vec<ListFile>> {
|
||||
feat::list_wevdav_backup().await.stringify_err()
|
||||
#[specta::specta]
|
||||
pub async fn list_webdav_backup() -> CmdResult<Vec<crate::ipc::WebDavFile>> {
|
||||
feat::list_wevdav_backup()
|
||||
.await
|
||||
.map(|files| files.into_iter().map(Into::into).collect())
|
||||
.stringify_err()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_webdav_backup(filename: String) -> CmdResult<()> {
|
||||
#[specta::specta]
|
||||
pub async fn delete_webdav_backup(filename: std::string::String) -> CmdResult<()> {
|
||||
let filename: String = filename.into();
|
||||
feat::delete_webdav_backup(filename).await.stringify_err()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn restore_webdav_backup(filename: String) -> CmdResult<()> {
|
||||
#[specta::specta]
|
||||
pub async fn restore_webdav_backup(filename: std::string::String) -> CmdResult<()> {
|
||||
let filename: String = filename.into();
|
||||
feat::restore_webdav_backup(filename)
|
||||
.await
|
||||
.map_err(|error| super::proxy_aware_error(&error))
|
||||
|
||||
@@ -308,7 +308,7 @@ impl IClashTemp {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
|
||||
pub struct ClashInfo {
|
||||
/// clash core port
|
||||
pub mixed_port: u16,
|
||||
@@ -364,14 +364,14 @@ fn test_clash_info() {
|
||||
assert_eq!(get_case(8888, "192.168.1.1:80800"), get_result(8888, "127.0.0.1:9097"));
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct IClashExternalControllerCors {
|
||||
pub allow_origins: Option<Vec<String>>,
|
||||
pub allow_private_network: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct IClash {
|
||||
pub mixed_port: Option<u16>,
|
||||
@@ -387,7 +387,7 @@ pub struct IClash {
|
||||
pub external_controller_cors: Option<IClashExternalControllerCors>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct IClashTUN {
|
||||
pub enable: Option<bool>,
|
||||
@@ -397,7 +397,7 @@ pub struct IClashTUN {
|
||||
pub dns_hijack: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct IClashDNS {
|
||||
pub enable: Option<bool>,
|
||||
@@ -414,7 +414,7 @@ pub struct IClashDNS {
|
||||
pub nameserver_policy: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct IClashFallbackFilter {
|
||||
pub geoip: Option<bool>,
|
||||
|
||||
@@ -106,10 +106,11 @@ impl DnsOverrideState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize, specta::Type)]
|
||||
pub struct ProfileDnsSettings {
|
||||
pub enabled: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub confirmation: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -27,28 +27,34 @@ pub(super) fn normalize_profile_home_url(raw: &str) -> Option<String> {
|
||||
Some(url.to_string().into())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default, specta::Type)]
|
||||
#[serde(default)]
|
||||
pub struct PrfItem {
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub uid: Option<String>,
|
||||
|
||||
/// enum value: remote | local | script | merge
|
||||
#[serde(rename = "type")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub itype: Option<String>,
|
||||
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub name: Option<String>,
|
||||
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub file: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub desc: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub url: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub selected: Option<Vec<PrfSelected>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[specta(type = Option<std::collections::BTreeMap<std::string::String, std::string::String>>)]
|
||||
pub latency_test_urls: Option<std::collections::BTreeMap<String, String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -60,19 +66,24 @@ pub struct PrfItem {
|
||||
pub option: Option<PrfOption>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub home: Option<String>,
|
||||
|
||||
#[serde(skip)]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub file_data: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
|
||||
#[serde(default)]
|
||||
pub struct PrfSelected {
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub name: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub now: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
|
||||
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize, specta::Type)]
|
||||
pub struct PrfExtra {
|
||||
pub upload: u64,
|
||||
pub download: u64,
|
||||
@@ -80,9 +91,11 @@ pub struct PrfExtra {
|
||||
pub expire: u64,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
|
||||
#[serde(default)]
|
||||
pub struct PrfOption {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub user_agent: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -105,15 +118,15 @@ pub struct PrfOption {
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub allow_auto_update: Option<bool>,
|
||||
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub merge: Option<String>,
|
||||
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub script: Option<String>,
|
||||
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub rules: Option<String>,
|
||||
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub proxies: Option<String>,
|
||||
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub groups: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -48,8 +48,10 @@ const SELECTED_NODES_SETTLE_INTERVAL: Duration = Duration::from_secs(1);
|
||||
const SELECTED_NODES_FIRST_PASS_BUDGET: Duration = Duration::from_secs(3);
|
||||
|
||||
/// Define the `profiles.yaml` schema
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
|
||||
#[serde(default)]
|
||||
pub struct IProfiles {
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub current: Option<String>,
|
||||
|
||||
pub items: Option<Vec<PrfItem>>,
|
||||
|
||||
@@ -11,26 +11,30 @@ use serde::{Deserialize, Serialize};
|
||||
use smartstring::alias::String;
|
||||
|
||||
/// ### `verge.yaml` schema
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
|
||||
#[serde(default)]
|
||||
pub struct IVerge {
|
||||
/// silent | error | warn | info | debug | trace
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub app_log_level: Option<String>,
|
||||
|
||||
/// Maximum app log size in KiB.
|
||||
pub app_log_max_size: Option<u64>,
|
||||
|
||||
pub app_log_max_count: Option<usize>,
|
||||
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub language: Option<String>,
|
||||
|
||||
/// `light` or `dark` or `system`
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub theme_mode: Option<String>,
|
||||
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub tray_event: Option<String>,
|
||||
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub env_type: Option<String>,
|
||||
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub start_page: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub startup_script: Option<String>,
|
||||
|
||||
pub traffic_graph: Option<bool>,
|
||||
@@ -48,18 +52,23 @@ pub struct IVerge {
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub tray_icon: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub menu_icon: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[specta(type = Option<Vec<std::string::String>>)]
|
||||
pub menu_order: Option<Vec<String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub proxy_group_tools_position: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub notice_position: Option<String>,
|
||||
|
||||
pub collapse_navbar: Option<bool>,
|
||||
@@ -84,39 +93,43 @@ pub struct IVerge {
|
||||
pub enable_dns_settings: Option<bool>,
|
||||
|
||||
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
|
||||
#[specta(type = std::collections::BTreeMap<std::string::String, super::dns::ProfileDnsSettings>)]
|
||||
pub profile_dns_settings: std::collections::BTreeMap<String, super::dns::ProfileDnsSettings>,
|
||||
|
||||
pub use_default_bypass: Option<bool>,
|
||||
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub system_proxy_bypass: Option<String>,
|
||||
|
||||
pub proxy_guard_duration: Option<u64>,
|
||||
|
||||
pub proxy_auto_config: Option<bool>,
|
||||
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub pac_file_content: Option<String>,
|
||||
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub proxy_host: Option<String>,
|
||||
|
||||
pub theme_setting: Option<IVergeTheme>,
|
||||
|
||||
#[specta(type = Option<Vec<std::string::String>>)]
|
||||
pub web_ui_list: Option<Vec<String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub clash_core: Option<String>,
|
||||
|
||||
/// format: {func},{key}
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[specta(type = Option<Vec<std::string::String>>)]
|
||||
pub hotkeys: Option<Vec<String>>,
|
||||
|
||||
pub enable_global_hotkey: Option<bool>,
|
||||
|
||||
#[specta(type = Option<crate::ipc::JsonValue>)]
|
||||
pub home_cards: Option<serde_json::Value>,
|
||||
|
||||
pub auto_close_connection: Option<bool>,
|
||||
|
||||
pub auto_check_update: Option<bool>,
|
||||
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub default_latency_test: Option<String>,
|
||||
|
||||
pub default_latency_timeout: Option<i16>,
|
||||
@@ -171,6 +184,7 @@ pub struct IVerge {
|
||||
skip_serializing_if = "Option::is_none",
|
||||
default
|
||||
)]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub webdav_url: Option<String>,
|
||||
|
||||
#[serde(
|
||||
@@ -179,6 +193,7 @@ pub struct IVerge {
|
||||
skip_serializing_if = "Option::is_none",
|
||||
default
|
||||
)]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub webdav_username: Option<String>,
|
||||
|
||||
#[serde(
|
||||
@@ -187,12 +202,14 @@ pub struct IVerge {
|
||||
skip_serializing_if = "Option::is_none",
|
||||
default
|
||||
)]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub webdav_password: Option<String>,
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub enable_tray_speed: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub tray_proxy_groups_display_mode: Option<String>,
|
||||
pub tray_inline_outbound_modes: Option<bool>,
|
||||
|
||||
@@ -209,27 +226,41 @@ pub struct IVerge {
|
||||
pub enable_external_controller: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
|
||||
#[serde(default)]
|
||||
pub struct IVergeTestItem {
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub uid: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub name: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub icon: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
|
||||
#[serde(default)]
|
||||
pub struct IVergeTheme {
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub primary_color: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub secondary_color: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub primary_text: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub secondary_text: Option<String>,
|
||||
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub info_color: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub error_color: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub warning_color: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub success_color: Option<String>,
|
||||
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub font_family: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub css_injection: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ pub(crate) fn proxy_listener_keys() -> impl Iterator<Item = &'static str> {
|
||||
PROXY_LISTENERS.iter().map(|(key, _, _)| *key)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, specta::Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProxyPortSettings {
|
||||
pub mixed_port: u16,
|
||||
@@ -48,27 +48,27 @@ pub struct ProxyPortSettings {
|
||||
pub tproxy: ToggleableProxyPort,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize)]
|
||||
#[derive(Clone, Copy, Debug, Deserialize, specta::Type)]
|
||||
pub struct ToggleableProxyPort {
|
||||
pub enabled: bool,
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, specta::Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ListenerProbe {
|
||||
pub address: String,
|
||||
pub transports: Vec<ListenerTransport>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash, Serialize)]
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash, Serialize, specta::Type)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ListenerTransport {
|
||||
Tcp,
|
||||
Udp,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize)]
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
|
||||
#[serde(tag = "status", rename_all = "camelCase")]
|
||||
pub enum ListenerProbeOutcome {
|
||||
Available,
|
||||
@@ -77,7 +77,7 @@ pub enum ListenerProbeOutcome {
|
||||
Indeterminate { message: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize)]
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
|
||||
#[serde(tag = "status", rename_all = "camelCase")]
|
||||
pub enum SaveProxyPortsOutcome {
|
||||
Saved,
|
||||
|
||||
@@ -57,7 +57,7 @@ fn claim_core_readiness_generation(state: &AtomicU64, captured_generation: u64)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, serde::Serialize, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, serde::Serialize, PartialEq, Eq, specta::Type)]
|
||||
pub enum RunningMode {
|
||||
Service,
|
||||
Sidecar,
|
||||
@@ -75,7 +75,7 @@ impl fmt::Display for RunningMode {
|
||||
}
|
||||
|
||||
/// Why the Core is not running, kept until a Core starts again.
|
||||
#[derive(Clone, Debug, serde::Serialize, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, serde::Serialize, PartialEq, Eq, specta::Type)]
|
||||
#[serde(tag = "kind", content = "detail", rename_all = "camelCase")]
|
||||
pub enum CoreFailure {
|
||||
StartFailed(String),
|
||||
|
||||
@@ -52,10 +52,7 @@ impl Handle {
|
||||
/// Sent on every transition, so the frontend does not have to poll to notice that the Core
|
||||
/// stopped or that the Service came back.
|
||||
pub fn notify_run_state(state: &crate::core::runstate::RunStateView) {
|
||||
let Ok(state) = serde_json::to_value(state) else {
|
||||
return;
|
||||
};
|
||||
Self::send_event(FrontendEvent::RunStateChanged { state });
|
||||
Self::send_event(FrontendEvent::RunStateChanged { state: state.clone() });
|
||||
}
|
||||
|
||||
pub fn notify_profile_changed(profile_id: &String) {
|
||||
|
||||
@@ -3,9 +3,8 @@ pub use announce::{Refresh, announce};
|
||||
pub mod desktop;
|
||||
pub mod handle;
|
||||
pub mod notification;
|
||||
pub use notification::frontend_wire_contract;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, specta::Type)]
|
||||
pub enum NoticeStatus {
|
||||
#[serde(rename = "info")]
|
||||
Info,
|
||||
@@ -83,6 +82,7 @@ impl NoticeStatus {
|
||||
/// Every variant in golden order. The wire-contract export and the golden
|
||||
/// test both derive their variant list from here; expected strings stay
|
||||
/// hand-written in the test so renames cannot slip through.
|
||||
#[cfg(test)]
|
||||
pub const ALL: [Self; 34] = [
|
||||
Self::Info,
|
||||
Self::ImportSubUrlOk,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::NoticeStatus;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use parking_lot::Mutex;
|
||||
#[cfg(test)]
|
||||
use serde_json::json;
|
||||
use smartstring::alias::String;
|
||||
use std::sync::Arc;
|
||||
@@ -31,7 +32,7 @@ pub enum FrontendEvent<'a> {
|
||||
uid: &'a String,
|
||||
},
|
||||
RunStateChanged {
|
||||
state: serde_json::Value,
|
||||
state: crate::core::runstate::RunStateView,
|
||||
},
|
||||
PendingFailuresChanged,
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -42,7 +43,7 @@ pub enum FrontendEvent<'a> {
|
||||
|
||||
/// Operation associated with a pending failure.
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, specta::Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum FailedOperation {
|
||||
SystemProxyEnable,
|
||||
@@ -72,12 +73,14 @@ impl FailedOperation {
|
||||
}
|
||||
|
||||
/// Latest unresolved failure for one stable code.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, specta::Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PendingFailure {
|
||||
/// Stable code used as the table key.
|
||||
#[specta(type = std::string::String)]
|
||||
pub code: String,
|
||||
/// Full diagnostic context chain.
|
||||
#[specta(type = std::string::String)]
|
||||
pub detail: String,
|
||||
pub operation: FailedOperation,
|
||||
/// Monotonic identity for repeated failures under the same code.
|
||||
@@ -241,9 +244,10 @@ pub fn what_was_asked() -> FailedOperation {
|
||||
static PENDING_FAILURES: LazyLock<FailureTable> = LazyLock::new(FailureTable::default);
|
||||
|
||||
/// A removal has a revision too, so a delayed read cannot restore a resolved failure.
|
||||
#[derive(Debug, Default, Clone, serde::Serialize)]
|
||||
#[derive(Debug, Default, Clone, serde::Serialize, specta::Type)]
|
||||
pub struct SidecarFailureSnapshot {
|
||||
pub revision: u64,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub detail: Option<String>,
|
||||
}
|
||||
|
||||
@@ -327,24 +331,7 @@ impl NotificationSystem {
|
||||
}
|
||||
|
||||
fn serialize_event(event: FrontendEvent) -> (&'static str, Result<serde_json::Value, serde_json::Error>) {
|
||||
match event {
|
||||
FrontendEvent::RefreshClash => ("verge://refresh-clash-config", Ok(json!("yes"))),
|
||||
FrontendEvent::RefreshVerge => ("verge://refresh-verge-config", Ok(json!("yes"))),
|
||||
FrontendEvent::RefreshProfiles => ("verge://refresh-profiles", Ok(json!("yes"))),
|
||||
FrontendEvent::RefreshProxyConfig => ("verge://refresh-proxy-config", Ok(serde_json::Value::Null)),
|
||||
FrontendEvent::NoticeMessage { status, message } => (
|
||||
"verge://notice-message",
|
||||
serde_json::to_value((status, message.as_ref())),
|
||||
),
|
||||
FrontendEvent::ProfileChanged { current_profile_id } => ("profile-changed", Ok(json!(current_profile_id))),
|
||||
FrontendEvent::TimerUpdated { profile_index } => ("verge://timer-updated", Ok(json!(profile_index))),
|
||||
FrontendEvent::ProfileUpdateStarted { uid } => ("profile-update-started", Ok(json!({ "uid": uid }))),
|
||||
FrontendEvent::ProfileUpdateCompleted { uid } => ("profile-update-completed", Ok(json!({ "uid": uid }))),
|
||||
FrontendEvent::RunStateChanged { state } => ("verge://run-state-changed", Ok(state)),
|
||||
FrontendEvent::PendingFailuresChanged => ("verge://pending-failures-changed", Ok(serde_json::Value::Null)),
|
||||
#[cfg(target_os = "linux")]
|
||||
FrontendEvent::ThemeChanged { theme } => ("tauri://theme-changed", serde_json::to_value(theme)),
|
||||
}
|
||||
serialize_frontend_event(event)
|
||||
}
|
||||
|
||||
pub(crate) fn send_event(app_handle: AppHandle, event: FrontendEvent) {
|
||||
@@ -381,20 +368,21 @@ mod wire_tests {
|
||||
}
|
||||
|
||||
/// One sample per `FrontendEvent` variant, paired with its wire name and payload.
|
||||
/// Shared by the wire golden test and the contract export so the two cannot drift.
|
||||
#[cfg(test)]
|
||||
fn sample_frontend_events(uid: &String) -> Vec<(FrontendEvent<'_>, &'static str, serde_json::Value)> {
|
||||
let state = crate::core::runstate::RunState {
|
||||
health: Default::default(),
|
||||
pending: None,
|
||||
sidecar_allowed: false,
|
||||
mode: crate::core::manager::RunningMode::NotRunning,
|
||||
is_admin: false,
|
||||
op_in_flight: false,
|
||||
}
|
||||
.to_view();
|
||||
vec![
|
||||
(
|
||||
FrontendEvent::RefreshClash,
|
||||
"verge://refresh-clash-config",
|
||||
json!("yes"),
|
||||
),
|
||||
(
|
||||
FrontendEvent::RefreshVerge,
|
||||
"verge://refresh-verge-config",
|
||||
json!("yes"),
|
||||
),
|
||||
(FrontendEvent::RefreshProfiles, "verge://refresh-profiles", json!("yes")),
|
||||
(FrontendEvent::RefreshClash, "verge://refresh-clash-config", json!(null)),
|
||||
(FrontendEvent::RefreshVerge, "verge://refresh-verge-config", json!(null)),
|
||||
(FrontendEvent::RefreshProfiles, "verge://refresh-profiles", json!(null)),
|
||||
(
|
||||
FrontendEvent::RefreshProxyConfig,
|
||||
"verge://refresh-proxy-config",
|
||||
@@ -414,11 +402,9 @@ fn sample_frontend_events(uid: &String) -> Vec<(FrontendEvent<'_>, &'static str,
|
||||
json!("profile"),
|
||||
),
|
||||
(
|
||||
FrontendEvent::RunStateChanged {
|
||||
state: json!({"running": true}),
|
||||
},
|
||||
FrontendEvent::RunStateChanged { state: state.clone() },
|
||||
"verge://run-state-changed",
|
||||
json!({"running": true}),
|
||||
serde_json::to_value(state).unwrap_or(serde_json::Value::Null),
|
||||
),
|
||||
(
|
||||
FrontendEvent::PendingFailuresChanged,
|
||||
@@ -445,24 +431,41 @@ fn sample_frontend_events(uid: &String) -> Vec<(FrontendEvent<'_>, &'static str,
|
||||
]
|
||||
}
|
||||
|
||||
/// Machine-readable form of the frontend wire contract: every event name with a
|
||||
/// serialized sample payload, plus every notice status string. The sample
|
||||
/// payload's JSON shape (string / null / array / object) is what the frontend
|
||||
/// generator maps to TypeScript types. `ThemeChanged` is a Tauri built-in
|
||||
/// linux-only window event, not part of this contract.
|
||||
pub fn frontend_wire_contract() -> serde_json::Value {
|
||||
let uid = String::from("sample");
|
||||
let events: Vec<_> = sample_frontend_events(&uid)
|
||||
.into_iter()
|
||||
.map(|(event, _, _)| {
|
||||
let (name, payload) = NotificationSystem::serialize_event(event);
|
||||
json!({ "name": name, "sample": payload.unwrap_or(serde_json::Value::Null) })
|
||||
})
|
||||
.collect();
|
||||
let statuses: Vec<_> = NoticeStatus::ALL
|
||||
.iter()
|
||||
// A unit variant with a string rename cannot fail to serialize.
|
||||
.filter_map(|status| serde_json::to_value(status).ok())
|
||||
.collect();
|
||||
json!({ "version": 1, "events": events, "noticeStatuses": statuses })
|
||||
macro_rules! frontend_events {
|
||||
($( $field:ident: $payload:ty = $name:literal, $pattern:pat => $value:expr; )*) => {
|
||||
#[derive(serde::Serialize, specta::Type)]
|
||||
pub struct VergeEventPayloads {
|
||||
$(#[serde(rename = $name)] pub $field: $payload,)*
|
||||
}
|
||||
|
||||
fn serialize_frontend_event(event: FrontendEvent) -> (&'static str, Result<serde_json::Value, serde_json::Error>) {
|
||||
match event {
|
||||
$($pattern => {
|
||||
let payload: $payload = $value;
|
||||
($name, serde_json::to_value(payload))
|
||||
},)*
|
||||
#[cfg(target_os = "linux")]
|
||||
FrontendEvent::ThemeChanged { theme } => ("tauri://theme-changed", serde_json::to_value(theme)),
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
frontend_events! {
|
||||
refresh_clash: () = "verge://refresh-clash-config", FrontendEvent::RefreshClash => ();
|
||||
refresh_verge: () = "verge://refresh-verge-config", FrontendEvent::RefreshVerge => ();
|
||||
refresh_profiles: () = "verge://refresh-profiles", FrontendEvent::RefreshProfiles => ();
|
||||
refresh_proxy: () = "verge://refresh-proxy-config", FrontendEvent::RefreshProxyConfig => ();
|
||||
notice: (NoticeStatus, std::string::String) = "verge://notice-message", FrontendEvent::NoticeMessage { status, ref message } => (status, message.to_string());
|
||||
profile_changed: std::string::String = "profile-changed", FrontendEvent::ProfileChanged { current_profile_id } => current_profile_id.to_string();
|
||||
timer_updated: std::string::String = "verge://timer-updated", FrontendEvent::TimerUpdated { profile_index } => profile_index.to_string();
|
||||
profile_started: ProfileUpdatePayload = "profile-update-started", FrontendEvent::ProfileUpdateStarted { uid } => ProfileUpdatePayload { uid: uid.to_string() };
|
||||
profile_completed: ProfileUpdatePayload = "profile-update-completed", FrontendEvent::ProfileUpdateCompleted { uid } => ProfileUpdatePayload { uid: uid.to_string() };
|
||||
run_state: crate::core::runstate::RunStateView = "verge://run-state-changed", FrontendEvent::RunStateChanged { state } => state;
|
||||
pending_failures: () = "verge://pending-failures-changed", FrontendEvent::PendingFailuresChanged => ();
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, specta::Type)]
|
||||
pub struct ProfileUpdatePayload {
|
||||
pub uid: std::string::String,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
|
||||
use serde::Serialize;
|
||||
use serde::{Serialize, Serializer, ser::SerializeMap as _};
|
||||
use tauri_plugin_mihomo::models::{
|
||||
DelayHistory, Extra, Proxies, Proxy, ProxyProvider, ProxyProviders, ProxyType, VehicleType,
|
||||
};
|
||||
@@ -20,7 +20,7 @@ pub struct GroupScope {
|
||||
|
||||
pub struct ProxyViewBuilder;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize)]
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProxyViewV1 {
|
||||
pub schema_version: u8,
|
||||
@@ -34,21 +34,21 @@ pub struct ProxyViewV1 {
|
||||
pub providers: Vec<ProxyProviderView>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, specta::Type)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ProxyViewOrderSource {
|
||||
Runtime,
|
||||
Fallback,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, specta::Type)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ProxyViewProviderState {
|
||||
Ready,
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize)]
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
|
||||
pub struct ProxyCapabilities {
|
||||
pub udp: bool,
|
||||
pub xudp: bool,
|
||||
@@ -57,13 +57,35 @@ pub struct ProxyCapabilities {
|
||||
pub smux: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize)]
|
||||
#[derive(Serialize, specta::Type)]
|
||||
struct ProxyLatencyHistory<'a> {
|
||||
#[specta(type = Vec<crate::ipc::DelayHistory>)]
|
||||
history: &'a [DelayHistory],
|
||||
}
|
||||
|
||||
fn serialize_proxy_latency_history<S: Serializer>(
|
||||
extra: &HashMap<String, Extra>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
let mut map = serializer.serialize_map(Some(extra.len()))?;
|
||||
for (url, extra) in extra {
|
||||
map.serialize_entry(
|
||||
url,
|
||||
&ProxyLatencyHistory {
|
||||
history: &extra.history,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
map.end()
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProxyGroupView {
|
||||
pub name: String,
|
||||
#[serde(rename = "type")]
|
||||
#[specta(type = String)]
|
||||
pub proxy_type: ProxyType,
|
||||
pub alive: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub now: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -74,22 +96,28 @@ pub struct ProxyGroupView {
|
||||
pub icon: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub test_url: Option<String>,
|
||||
#[specta(type = Vec<crate::ipc::DelayHistory>)]
|
||||
pub history: Vec<DelayHistory>,
|
||||
#[serde(serialize_with = "serialize_proxy_latency_history")]
|
||||
#[specta(type = HashMap<String, ProxyLatencyHistory<'static>>)]
|
||||
pub extra: HashMap<String, Extra>,
|
||||
#[serde(flatten)]
|
||||
pub capabilities: ProxyCapabilities,
|
||||
pub members: Vec<ProxyMemberRef>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize)]
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProxyNodeView {
|
||||
pub record_id: String,
|
||||
pub name: String,
|
||||
#[serde(rename = "type")]
|
||||
#[specta(type = String)]
|
||||
pub proxy_type: ProxyType,
|
||||
pub alive: bool,
|
||||
#[specta(type = Vec<crate::ipc::DelayHistory>)]
|
||||
pub history: Vec<DelayHistory>,
|
||||
#[serde(serialize_with = "serialize_proxy_latency_history")]
|
||||
#[specta(type = HashMap<String, ProxyLatencyHistory<'static>>)]
|
||||
pub extra: HashMap<String, Extra>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<String>,
|
||||
@@ -104,7 +132,7 @@ pub struct ProxyNodeView {
|
||||
pub source: ProxyNodeSource,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize)]
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
|
||||
#[serde(tag = "kind", rename_all = "camelCase")]
|
||||
pub enum ProxyNodeSource {
|
||||
Core {
|
||||
@@ -119,7 +147,7 @@ pub enum ProxyNodeSource {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize)]
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
|
||||
#[serde(tag = "kind", rename_all = "camelCase")]
|
||||
pub enum ProxyMemberRef {
|
||||
Group {
|
||||
@@ -136,7 +164,7 @@ pub enum ProxyMemberRef {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, specta::Type)]
|
||||
pub enum ProxyMemberUnresolvedReason {
|
||||
#[serde(rename = "missing")]
|
||||
Missing,
|
||||
@@ -146,7 +174,7 @@ pub enum ProxyMemberUnresolvedReason {
|
||||
ProviderUnavailable,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize)]
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProxyProviderView {
|
||||
pub name: String,
|
||||
@@ -158,7 +186,7 @@ pub struct ProxyProviderView {
|
||||
pub proxy_record_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, specta::Type)]
|
||||
pub enum ProxyProviderVehicleType {
|
||||
#[serde(rename = "HTTP")]
|
||||
Http,
|
||||
@@ -168,7 +196,7 @@ pub enum ProxyProviderVehicleType {
|
||||
Inline,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize)]
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
|
||||
pub struct ProxySubscriptionInfo {
|
||||
pub upload: i64,
|
||||
pub download: i64,
|
||||
@@ -402,7 +430,6 @@ fn build_group(name: String, proxy: Proxy, resolver: &MemberResolver<'_>) -> Pro
|
||||
icon,
|
||||
now,
|
||||
test_url,
|
||||
alive,
|
||||
history,
|
||||
extra,
|
||||
udp,
|
||||
@@ -431,7 +458,6 @@ fn build_group(name: String, proxy: Proxy, resolver: &MemberResolver<'_>) -> Pro
|
||||
ProxyGroupView {
|
||||
name,
|
||||
proxy_type,
|
||||
alive,
|
||||
now,
|
||||
fixed,
|
||||
hidden,
|
||||
@@ -456,7 +482,6 @@ fn build_node(record_id: String, name: String, proxy: Proxy, source: ProxyNodeSo
|
||||
hidden,
|
||||
icon,
|
||||
test_url,
|
||||
alive,
|
||||
history,
|
||||
extra,
|
||||
udp,
|
||||
@@ -472,7 +497,6 @@ fn build_node(record_id: String, name: String, proxy: Proxy, source: ProxyNodeSo
|
||||
record_id,
|
||||
name,
|
||||
proxy_type,
|
||||
alive,
|
||||
history,
|
||||
extra,
|
||||
id,
|
||||
|
||||
@@ -14,13 +14,13 @@ pub enum ServiceHealth {
|
||||
}
|
||||
|
||||
impl ServiceHealth {
|
||||
const fn kind(&self) -> &'static str {
|
||||
const fn kind(&self) -> ServiceHealthKind {
|
||||
match self {
|
||||
Self::Unknown => "unknown",
|
||||
Self::Ready => "ready",
|
||||
Self::NotInstalled => "notInstalled",
|
||||
Self::VersionMismatch => "versionMismatch",
|
||||
Self::Unavailable(_) => "unavailable",
|
||||
Self::Unknown => ServiceHealthKind::Unknown,
|
||||
Self::Ready => ServiceHealthKind::Ready,
|
||||
Self::NotInstalled => ServiceHealthKind::NotInstalled,
|
||||
Self::VersionMismatch => ServiceHealthKind::VersionMismatch,
|
||||
Self::Unavailable(_) => ServiceHealthKind::Unavailable,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ impl ServiceHealth {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, specta::Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum PendingAction {
|
||||
Install,
|
||||
@@ -118,11 +118,11 @@ impl RunState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
#[derive(Debug, Clone, serde::Serialize, specta::Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RunStateView {
|
||||
pub mode: RunningMode,
|
||||
pub service: &'static str,
|
||||
pub service: ServiceHealthKind,
|
||||
pub service_unavailable_reason: Option<String>,
|
||||
pub pending_action: Option<PendingAction>,
|
||||
pub sidecar_allowed: bool,
|
||||
@@ -232,7 +232,7 @@ mod tests {
|
||||
|
||||
let view = run_state.to_view();
|
||||
|
||||
assert_eq!(view.service, "unavailable");
|
||||
assert_eq!(view.service, ServiceHealthKind::Unavailable);
|
||||
assert_eq!(view.service_unavailable_reason.as_deref(), Some("socket refused"));
|
||||
assert_eq!(view.pending_action, Some(PendingAction::Reinstall));
|
||||
assert!(view.tun_capable, "elevation alone makes TUN possible");
|
||||
@@ -244,7 +244,7 @@ mod tests {
|
||||
fn a_healthy_view_reports_no_unavailable_reason() {
|
||||
let view = state(ServiceHealth::Ready, false, false).to_view();
|
||||
|
||||
assert_eq!(view.service, "ready");
|
||||
assert_eq!(view.service, ServiceHealthKind::Ready);
|
||||
assert_eq!(view.service_unavailable_reason, None);
|
||||
assert!(view.service_usable);
|
||||
assert!(!view.service_needs_attention);
|
||||
@@ -350,3 +350,13 @@ mod tests {
|
||||
assert!(settled.tun_should_be_disabled(true));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, specta::Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ServiceHealthKind {
|
||||
Unknown,
|
||||
Ready,
|
||||
NotInstalled,
|
||||
VersionMismatch,
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ static PENDING_SERVICE_REPAIR_NOTICE: AtomicBool = AtomicBool::new(false);
|
||||
static PENDING_SERVICE_OWNER_NOTICE: Mutex<Option<String>> = Mutex::new(None);
|
||||
|
||||
/// Why the Service was unavailable when the core fell back to Sidecar.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, specta::Type)]
|
||||
#[serde(tag = "kind", content = "reason", rename_all = "camelCase")]
|
||||
pub enum ServiceFallbackNotice {
|
||||
Unavailable,
|
||||
|
||||
@@ -1160,7 +1160,15 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
logging!(error, Type::Tray, "Failed to close all connections from tray: {err}");
|
||||
}
|
||||
}
|
||||
MenuIds::COPY_ENV => feat::copy_clash_env().await,
|
||||
MenuIds::COPY_ENV => {
|
||||
if let Err(err) = feat::copy_clash_env().await {
|
||||
logging!(
|
||||
error,
|
||||
Type::Tray,
|
||||
"Failed to copy proxy environment variables from tray: {err:#}"
|
||||
);
|
||||
}
|
||||
}
|
||||
MenuIds::CONF_DIR => {
|
||||
let _ = cmd::open_app_dir().await;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ impl SilentUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, specta::Type)]
|
||||
struct UpdateCacheMeta {
|
||||
version: String,
|
||||
downloaded_at: String,
|
||||
@@ -214,7 +214,7 @@ fn nsis_language_id(app_language: &str) -> &'static str {
|
||||
}
|
||||
|
||||
/// Same wire shape as the updater plugin's JS `DownloadEvent`.
|
||||
#[derive(Clone, Serialize)]
|
||||
#[derive(Clone, Serialize, specta::Type)]
|
||||
#[serde(tag = "event", content = "data")]
|
||||
pub enum DownloadEvent {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
||||
@@ -51,7 +51,7 @@ pub struct CoreConfigValidator {
|
||||
is_processing: AtomicBool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, specta::Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ValidationErrorKind {
|
||||
FileMissing,
|
||||
@@ -94,7 +94,7 @@ impl ValidationErrorKind {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, specta::Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ValidationSkipReason {
|
||||
Exiting,
|
||||
@@ -110,12 +110,18 @@ impl fmt::Display for ValidationSkipReason {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, specta::Type)]
|
||||
#[serde(tag = "status", rename_all = "camelCase")]
|
||||
pub enum ValidationOutcome {
|
||||
Valid,
|
||||
Invalid { kind: ValidationErrorKind, message: String },
|
||||
Skipped { reason: ValidationSkipReason },
|
||||
Invalid {
|
||||
kind: ValidationErrorKind,
|
||||
#[specta(type = std::string::String)]
|
||||
message: String,
|
||||
},
|
||||
Skipped {
|
||||
reason: ValidationSkipReason,
|
||||
},
|
||||
Busy,
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,13 @@ use smartstring::alias::String;
|
||||
use std::path::PathBuf;
|
||||
use tokio::fs;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Serialize, specta::Type)]
|
||||
pub struct LocalBackupFile {
|
||||
#[specta(type = std::string::String)]
|
||||
pub filename: String,
|
||||
#[specta(type = std::string::String)]
|
||||
pub path: String,
|
||||
#[specta(type = std::string::String)]
|
||||
pub last_modified: String,
|
||||
pub content_length: u64,
|
||||
}
|
||||
|
||||
@@ -34,12 +34,10 @@ static STAGING_GENERATION: AtomicU64 = AtomicU64::new(0);
|
||||
/// `.rollback` and `.old` are fixed paths, so two upgrades must not overlap.
|
||||
static UPGRADE_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
#[derive(Debug, serde::Serialize, specta::Type)]
|
||||
pub struct CoreUpgradeReport {
|
||||
/// False when the managed core was already at the latest version.
|
||||
pub upgraded: bool,
|
||||
pub from: std::string::String,
|
||||
pub to: std::string::String,
|
||||
}
|
||||
|
||||
pub async fn upgrade_core(force: bool) -> Result<CoreUpgradeReport> {
|
||||
@@ -62,11 +60,7 @@ pub async fn upgrade_core(force: bool) -> Result<CoreUpgradeReport> {
|
||||
logging!(debug, Type::Core, "core upgrade: latest version resolved via {proxy:?}");
|
||||
|
||||
if !force && installed == latest {
|
||||
return Ok(CoreUpgradeReport {
|
||||
upgraded: false,
|
||||
from: installed,
|
||||
to: latest,
|
||||
});
|
||||
return Ok(CoreUpgradeReport { upgraded: false });
|
||||
}
|
||||
|
||||
let package = download_package(proxy, alpha, &latest).await?;
|
||||
@@ -150,11 +144,7 @@ pub async fn upgrade_core(force: bool) -> Result<CoreUpgradeReport> {
|
||||
#[cfg(windows)]
|
||||
let _ = std::fs::remove_file(target.with_extension("old"));
|
||||
|
||||
Ok(CoreUpgradeReport {
|
||||
upgraded: true,
|
||||
from: installed,
|
||||
to: latest,
|
||||
})
|
||||
Ok(CoreUpgradeReport { upgraded: true })
|
||||
}
|
||||
|
||||
/// Whether this upgrade handed bytes to the Service's approved core directory.
|
||||
|
||||
@@ -15,11 +15,14 @@ use crate::{
|
||||
utils::dirs,
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, specta::Type)]
|
||||
#[serde(tag = "status", rename_all = "snake_case")]
|
||||
pub enum DnsOverrideOutcome {
|
||||
Applied,
|
||||
ConfirmationRequired { source: String },
|
||||
ConfirmationRequired {
|
||||
#[specta(type = std::string::String)]
|
||||
source: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub async fn set_dns_override(
|
||||
|
||||
@@ -8,10 +8,13 @@ use std::path::{Component, Path, PathBuf};
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncWriteExt as _;
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub struct IconInfo {
|
||||
#[specta(type = std::string::String)]
|
||||
name: String,
|
||||
#[specta(type = std::string::String)]
|
||||
previous_t: String,
|
||||
#[specta(type = std::string::String)]
|
||||
current_t: String,
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::{
|
||||
window_manager::WindowManager,
|
||||
},
|
||||
};
|
||||
use anyhow::Context as _;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use std::env;
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt as _;
|
||||
@@ -95,7 +96,7 @@ pub async fn toggle_tun_mode(not_save_file: Option<bool>) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn copy_clash_env() {
|
||||
pub async fn copy_clash_env() -> anyhow::Result<()> {
|
||||
let env_ip = env::var("CLASH_VERGE_REV_IP").ok();
|
||||
let verge_cfg = Config::verge().await.latest_arc();
|
||||
let ip = env_ip
|
||||
@@ -133,12 +134,11 @@ pub async fn copy_clash_env() {
|
||||
}
|
||||
"fish" => format!("set -x http_proxy {http_proxy}; set -x https_proxy {http_proxy}"),
|
||||
_ => {
|
||||
logging!(error, Type::ProxyMode, "copy_clash_env: Invalid env type! {env_type}");
|
||||
return;
|
||||
anyhow::bail!("Invalid environment variable format: {env_type}");
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = clipboard.write_text(&export_text) {
|
||||
logging!(error, Type::ProxyMode, "Failed to write to clipboard: {err}");
|
||||
}
|
||||
clipboard
|
||||
.write_text(export_text)
|
||||
.context("Failed to write proxy environment variables to clipboard")
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ pub async fn open_or_close_dashboard() {
|
||||
logging!(info, Type::Window, "Window toggle result: {result:?}");
|
||||
}
|
||||
|
||||
pub async fn quit() -> clash_verge_signal::ShutdownOutcome {
|
||||
pub async fn quit() {
|
||||
logging!(debug, Type::System, "启动退出流程");
|
||||
// 设置退出标志
|
||||
handle::Handle::global().set_is_exiting();
|
||||
@@ -165,13 +165,11 @@ pub async fn quit() -> clash_verge_signal::ShutdownOutcome {
|
||||
NoticeStatus::AppQuitCoreStopFailed,
|
||||
cleanup_result.stop_error.unwrap_or_default(),
|
||||
);
|
||||
return clash_verge_signal::ShutdownOutcome::Canceled;
|
||||
}
|
||||
|
||||
utils::server::shutdown_embedded_server();
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
app_handle.exit(if cleanup_result.all_success { 0 } else { 1 });
|
||||
clash_verge_signal::ShutdownOutcome::Committed
|
||||
}
|
||||
|
||||
pub async fn clean_async() -> CleanupResult {
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
use crate::cmd;
|
||||
|
||||
pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
|
||||
tauri_specta::Builder::new()
|
||||
.types(&platform_types())
|
||||
.commands(tauri_specta::collect_commands![
|
||||
tauri_plugin_clash_verge_sysinfo::commands::get_system_info,
|
||||
tauri_plugin_clash_verge_sysinfo::commands::get_app_uptime,
|
||||
tauri_plugin_clash_verge_sysinfo::commands::export_diagnostic_info::<tauri::Wry>,
|
||||
cmd::probe_listener,
|
||||
cmd::save_proxy_ports,
|
||||
cmd::get_sys_proxy,
|
||||
cmd::get_auto_proxy,
|
||||
cmd::get_embedded_server_port,
|
||||
cmd::open_app_dir,
|
||||
cmd::open_logs_dir,
|
||||
cmd::open_core_dir,
|
||||
cmd::get_network_interfaces,
|
||||
cmd::get_system_hostname,
|
||||
cmd::restart_app,
|
||||
cmd::install_update,
|
||||
cmd::cancel_update_download,
|
||||
cmd::restart_core,
|
||||
cmd::upgrade_clash_core,
|
||||
cmd::get_runtime_state,
|
||||
cmd::get_pending_failures,
|
||||
cmd::get_sidecar_failure,
|
||||
cmd::entry_lightweight_mode,
|
||||
cmd::install_service,
|
||||
cmd::uninstall_service,
|
||||
cmd::reinstall_service,
|
||||
cmd::repair_service,
|
||||
cmd::continue_with_sidecar,
|
||||
cmd::sync_runtime_providers,
|
||||
cmd::get_clash_info,
|
||||
cmd::patch_clash_config,
|
||||
cmd::patch_clash_mode,
|
||||
cmd::get_clash_mode,
|
||||
cmd::change_clash_core,
|
||||
cmd::get_runtime_config,
|
||||
cmd::get_proxy_view,
|
||||
cmd::get_runtime_yaml,
|
||||
cmd::get_runtime_logs,
|
||||
cmd::get_runtime_proxy_chain_config,
|
||||
cmd::update_proxy_chain_config_in_runtime,
|
||||
cmd::invoke_uwp_tool,
|
||||
cmd::copy_clash_env,
|
||||
cmd::sync_tray_proxy_selection,
|
||||
cmd::record_selected_node,
|
||||
cmd::forget_selected_node,
|
||||
cmd::save_dns_config,
|
||||
cmd::apply_dns_config,
|
||||
cmd::set_dns_override,
|
||||
cmd::take_dns_override_notice,
|
||||
cmd::take_service_fallback_notice,
|
||||
cmd::get_core_startup_error,
|
||||
cmd::take_service_repair_notice,
|
||||
cmd::take_service_owner_notice,
|
||||
cmd::take_discarded_keys_notice,
|
||||
cmd::get_dns_config_content,
|
||||
cmd::validate_dns_config,
|
||||
cmd::get_clash_logs,
|
||||
cmd::get_verge_config,
|
||||
cmd::patch_verge_config,
|
||||
cmd::test_delay,
|
||||
cmd::get_app_dir,
|
||||
cmd::copy_icon_file,
|
||||
cmd::download_icon_cache,
|
||||
cmd::open_devtools,
|
||||
cmd::exit_app,
|
||||
cmd::get_network_interfaces_info,
|
||||
cmd::get_profiles,
|
||||
cmd::enhance_profiles,
|
||||
cmd::patch_profiles_config,
|
||||
cmd::view_profile,
|
||||
cmd::patch_profile,
|
||||
cmd::create_profile,
|
||||
cmd::import_profile,
|
||||
cmd::reorder_profile,
|
||||
cmd::update_profile,
|
||||
cmd::delete_profile,
|
||||
cmd::read_profile_file,
|
||||
cmd::save_profile_file,
|
||||
cmd::get_next_update_time,
|
||||
cmd::create_local_backup,
|
||||
cmd::list_local_backup,
|
||||
cmd::delete_local_backup,
|
||||
cmd::restore_local_backup,
|
||||
cmd::import_local_backup,
|
||||
cmd::export_local_backup,
|
||||
cmd::create_webdav_backup,
|
||||
cmd::save_webdav_config,
|
||||
cmd::list_webdav_backup,
|
||||
cmd::delete_webdav_backup,
|
||||
cmd::restore_webdav_backup,
|
||||
cmd::get_unlock_items,
|
||||
cmd::check_media_unlock,
|
||||
cmd::check_media_unlock_item,
|
||||
])
|
||||
.error_handling(tauri_specta::ErrorHandlingMode::Throw)
|
||||
.dangerously_cast_bigints_to_number()
|
||||
.typ::<crate::core::notify::notification::VergeEventPayloads>()
|
||||
}
|
||||
|
||||
// Tauri transports YAML values as JSON; keep YAML storage out of the IPC type graph.
|
||||
#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
#[serde(transparent)]
|
||||
pub struct YamlMapping(#[specta(type = std::collections::HashMap<String, JsonValue>)] pub serde_yaml_ng::Mapping);
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
#[serde(transparent)]
|
||||
pub struct YamlValue(#[specta(type = JsonValue)] pub serde_yaml_ng::Value);
|
||||
|
||||
#[derive(serde::Serialize, specta::Type)]
|
||||
pub struct WebDavFile {
|
||||
pub href: String,
|
||||
#[specta(type = String)]
|
||||
pub last_modified: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl From<reqwest_dav::list_cmd::ListFile> for WebDavFile {
|
||||
fn from(file: reqwest_dav::list_cmd::ListFile) -> Self {
|
||||
Self {
|
||||
href: file.href,
|
||||
last_modified: file.last_modified,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, specta::Type)]
|
||||
pub struct NetworkInterfaceView {
|
||||
pub name: String,
|
||||
pub addr: Vec<InterfaceAddress>,
|
||||
pub mac_addr: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum InterfaceAddress {
|
||||
V4(IpAddress),
|
||||
V6(IpAddress),
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub struct IpAddress {
|
||||
pub ip: String,
|
||||
}
|
||||
|
||||
impl From<network_interface::NetworkInterface> for NetworkInterfaceView {
|
||||
fn from(interface: network_interface::NetworkInterface) -> Self {
|
||||
Self {
|
||||
name: interface.name,
|
||||
addr: interface
|
||||
.addr
|
||||
.into_iter()
|
||||
.map(|address| match address {
|
||||
network_interface::Addr::V4(address) => InterfaceAddress::V4(IpAddress {
|
||||
ip: address.ip.to_string(),
|
||||
}),
|
||||
network_interface::Addr::V6(address) => InterfaceAddress::V6(IpAddress {
|
||||
ip: address.ip.to_string(),
|
||||
}),
|
||||
})
|
||||
.collect(),
|
||||
mac_addr: interface.mac_addr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub struct DelayHistory {
|
||||
pub time: String,
|
||||
pub delay: u16,
|
||||
}
|
||||
|
||||
fn platform_types() -> specta::Types {
|
||||
use specta::datatype::{DataType, Fields};
|
||||
let mut types = specta::Types::default().register::<crate::config::IVerge>();
|
||||
<PlatformVergeFields as specta::Type>::definition(&mut types);
|
||||
let platform = types
|
||||
.into_unsorted_iter()
|
||||
.find(|ty| ty.name == "PlatformVergeFields")
|
||||
.and_then(|ty| ty.ty.clone());
|
||||
let Some(DataType::Struct(platform)) = platform else {
|
||||
unreachable!()
|
||||
};
|
||||
let Fields::Named(platform) = platform.fields else {
|
||||
unreachable!()
|
||||
};
|
||||
types.iter_mut(|ty| {
|
||||
if ty.name != "IVerge" {
|
||||
return;
|
||||
}
|
||||
let Some(DataType::Struct(config)) = &mut ty.ty else {
|
||||
return;
|
||||
};
|
||||
let Fields::Named(fields) = &mut config.fields else {
|
||||
return;
|
||||
};
|
||||
fields
|
||||
.fields
|
||||
.retain(|(name, _)| !platform.fields.iter().any(|(platform_name, _)| name == platform_name));
|
||||
fields.fields.extend(platform.fields.iter().cloned());
|
||||
});
|
||||
types
|
||||
}
|
||||
|
||||
// These fields exist only on some hosts; IPC generation covers every desktop target.
|
||||
#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
struct PlatformVergeFields {
|
||||
#[specta(optional)]
|
||||
tray_icon: Option<String>,
|
||||
#[specta(optional)]
|
||||
verge_redir_port: Option<u16>,
|
||||
#[specta(optional)]
|
||||
verge_redir_enabled: Option<bool>,
|
||||
#[specta(optional)]
|
||||
verge_tproxy_port: Option<u16>,
|
||||
#[specta(optional)]
|
||||
verge_tproxy_enabled: Option<bool>,
|
||||
#[specta(optional)]
|
||||
enable_tray_speed: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
#[serde(untagged)]
|
||||
pub enum JsonValue {
|
||||
Null(()),
|
||||
Bool(bool),
|
||||
Number(f64),
|
||||
String(String),
|
||||
// Specta resolves fields in nested functions, where Self is unavailable.
|
||||
Array(#[specta(type = Vec<JsonValue>)] Vec<Self>),
|
||||
Object(#[specta(type = std::collections::HashMap<String, JsonValue>)] std::collections::HashMap<String, Self>),
|
||||
}
|
||||
+6
-97
@@ -7,6 +7,7 @@ mod constants;
|
||||
mod core;
|
||||
mod enhance;
|
||||
mod feat;
|
||||
pub mod ipc;
|
||||
mod process;
|
||||
pub mod utils;
|
||||
|
||||
@@ -26,9 +27,6 @@ use tauri_plugin_deep_link::DeepLinkExt as _;
|
||||
|
||||
pub static APP_HANDLE: OnceLock<AppHandle> = OnceLock::new();
|
||||
|
||||
// Re-exported for src/bin/frontend-contract.rs; the wire contract stays owned
|
||||
// by core::notify.
|
||||
pub use crate::core::notify::frontend_wire_contract;
|
||||
/// Application initialization helper functions
|
||||
mod app_init {
|
||||
use super::*;
|
||||
@@ -127,100 +125,10 @@ mod app_init {
|
||||
}
|
||||
|
||||
pub fn generate_handlers() -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
|
||||
tauri::generate_handler![
|
||||
tauri_plugin_clash_verge_sysinfo::commands::get_system_info,
|
||||
tauri_plugin_clash_verge_sysinfo::commands::get_app_uptime,
|
||||
tauri_plugin_clash_verge_sysinfo::commands::export_diagnostic_info,
|
||||
cmd::probe_listener,
|
||||
cmd::save_proxy_ports,
|
||||
cmd::get_sys_proxy,
|
||||
cmd::get_auto_proxy,
|
||||
cmd::get_embedded_server_port,
|
||||
cmd::open_app_dir,
|
||||
cmd::open_logs_dir,
|
||||
cmd::open_core_dir,
|
||||
cmd::get_network_interfaces,
|
||||
cmd::get_system_hostname,
|
||||
cmd::restart_app,
|
||||
cmd::install_update,
|
||||
cmd::cancel_update_download,
|
||||
cmd::restart_core,
|
||||
cmd::upgrade_clash_core,
|
||||
cmd::get_runtime_state,
|
||||
cmd::get_pending_failures,
|
||||
cmd::get_sidecar_failure,
|
||||
cmd::entry_lightweight_mode,
|
||||
cmd::install_service,
|
||||
cmd::uninstall_service,
|
||||
cmd::reinstall_service,
|
||||
cmd::repair_service,
|
||||
cmd::continue_with_sidecar,
|
||||
cmd::sync_runtime_providers,
|
||||
cmd::get_clash_info,
|
||||
cmd::patch_clash_config,
|
||||
cmd::patch_clash_mode,
|
||||
cmd::get_clash_mode,
|
||||
cmd::change_clash_core,
|
||||
cmd::get_runtime_config,
|
||||
cmd::get_proxy_view,
|
||||
cmd::get_runtime_yaml,
|
||||
cmd::get_runtime_logs,
|
||||
cmd::get_runtime_proxy_chain_config,
|
||||
cmd::update_proxy_chain_config_in_runtime,
|
||||
cmd::invoke_uwp_tool,
|
||||
cmd::copy_clash_env,
|
||||
cmd::sync_tray_proxy_selection,
|
||||
cmd::record_selected_node,
|
||||
cmd::forget_selected_node,
|
||||
cmd::save_dns_config,
|
||||
cmd::apply_dns_config,
|
||||
cmd::set_dns_override,
|
||||
cmd::take_dns_override_notice,
|
||||
cmd::take_service_fallback_notice,
|
||||
cmd::get_core_startup_error,
|
||||
cmd::take_service_repair_notice,
|
||||
cmd::take_service_owner_notice,
|
||||
cmd::take_discarded_keys_notice,
|
||||
cmd::get_dns_config_content,
|
||||
cmd::validate_dns_config,
|
||||
cmd::get_clash_logs,
|
||||
cmd::get_verge_config,
|
||||
cmd::patch_verge_config,
|
||||
cmd::test_delay,
|
||||
cmd::get_app_dir,
|
||||
cmd::copy_icon_file,
|
||||
cmd::download_icon_cache,
|
||||
cmd::open_devtools,
|
||||
cmd::exit_app,
|
||||
cmd::get_network_interfaces_info,
|
||||
cmd::get_profiles,
|
||||
cmd::enhance_profiles,
|
||||
cmd::patch_profiles_config,
|
||||
cmd::view_profile,
|
||||
cmd::patch_profile,
|
||||
cmd::create_profile,
|
||||
cmd::import_profile,
|
||||
cmd::reorder_profile,
|
||||
cmd::update_profile,
|
||||
cmd::delete_profile,
|
||||
cmd::read_profile_file,
|
||||
cmd::save_profile_file,
|
||||
cmd::get_next_update_time,
|
||||
cmd::create_local_backup,
|
||||
cmd::list_local_backup,
|
||||
cmd::delete_local_backup,
|
||||
cmd::restore_local_backup,
|
||||
cmd::import_local_backup,
|
||||
cmd::export_local_backup,
|
||||
cmd::create_webdav_backup,
|
||||
cmd::save_webdav_config,
|
||||
cmd::list_webdav_backup,
|
||||
cmd::delete_webdav_backup,
|
||||
cmd::restore_webdav_backup,
|
||||
cmd::get_unlock_items,
|
||||
cmd::check_media_unlock,
|
||||
cmd::check_media_unlock_item,
|
||||
]
|
||||
{
|
||||
let builder = crate::ipc::builder();
|
||||
move |invoke| builder.invoke_handler()(invoke)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,6 +230,7 @@ pub fn run() -> std::process::ExitCode {
|
||||
if let Err(panic) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
resolve::resolve_setup_async();
|
||||
resolve::resolve_setup_sync();
|
||||
#[cfg(target_os = "linux")]
|
||||
resolve::init_signal();
|
||||
})) {
|
||||
log_setup_panic("window-core", panic);
|
||||
|
||||
@@ -16,7 +16,7 @@ fn main() -> ExitCode {
|
||||
#[cfg(not(all(feature = "perf-harness", target_os = "macos")))]
|
||||
fn main() -> ExitCode {
|
||||
let default_parallelism = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
|
||||
let worker_limit = std::cmp::min(default_parallelism, 8);
|
||||
let worker_limit = std::cmp::min(default_parallelism, 4);
|
||||
let blocking_limit = 2 * worker_limit;
|
||||
|
||||
#[allow(clippy::unwrap_used)]
|
||||
|
||||
@@ -20,7 +20,6 @@ use crate::{
|
||||
utils::{init, server, window_manager::WindowManager},
|
||||
};
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
use clash_verge_signal;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod dns;
|
||||
@@ -189,6 +188,8 @@ async fn init_silent_updater() {
|
||||
logging!(info, Type::Setup, "Silent updater initialized");
|
||||
}
|
||||
|
||||
// Upstream has no session-end signal support on Linux; see crates/clash-verge-signal.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) fn init_signal() {
|
||||
logging!(debug, Type::Setup, "Initializing signal handlers...");
|
||||
clash_verge_signal::register(feat::quit);
|
||||
|
||||
@@ -39,8 +39,6 @@ static INSTANCE_LOCK: OnceLock<std::fs::File> = OnceLock::new();
|
||||
const PAC_INITIAL_AVAILABLE: bool = false;
|
||||
static PAC_AVAILABLE: AtomicBool = AtomicBool::new(PAC_INITIAL_AVAILABLE);
|
||||
static COMMANDS_READY: AtomicBool = AtomicBool::new(false);
|
||||
#[cfg(feature = "verge-dev")]
|
||||
static DEV_QUIT_REQUESTED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SingletonDisposition {
|
||||
@@ -164,55 +162,12 @@ fn instance_auth(token: String) -> impl warp::Filter<Extract = (), Error = warp:
|
||||
.untuple_one()
|
||||
}
|
||||
|
||||
#[cfg(feature = "verge-dev")]
|
||||
fn dev_quit_route<F>(
|
||||
token: String,
|
||||
commands_ready: &'static AtomicBool,
|
||||
quit_requested: &'static AtomicBool,
|
||||
trigger: F,
|
||||
) -> impl warp::Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone
|
||||
where
|
||||
F: Fn() + Clone + Send + Sync + 'static,
|
||||
{
|
||||
instance_auth(token)
|
||||
.and(warp::path!("commands" / "dev" / "quit"))
|
||||
.and(warp::post())
|
||||
.and_then(move || {
|
||||
let trigger = trigger.clone();
|
||||
async move {
|
||||
if !commands_ready.load(Ordering::Acquire) {
|
||||
return Ok::<_, warp::Rejection>(warp::reply::with_status(
|
||||
String::from("starting"),
|
||||
warp::http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
));
|
||||
}
|
||||
if quit_requested
|
||||
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
|
||||
.is_ok()
|
||||
{
|
||||
trigger();
|
||||
}
|
||||
Ok(warp::reply::with_status(
|
||||
String::from("accepted"),
|
||||
warp::http::StatusCode::ACCEPTED,
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "verge-dev")]
|
||||
fn release_dev_quit_latch(quit_requested: &AtomicBool, outcome: clash_verge_signal::ShutdownOutcome) {
|
||||
if matches!(outcome, clash_verge_signal::ShutdownOutcome::Canceled) {
|
||||
quit_requested.store(false, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
fn start_embedded_server(listener: tokio::net::TcpListener, token: String) {
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel();
|
||||
let _ = SHUTDOWN_SENDER.set(Mutex::new(Some(shutdown_tx)));
|
||||
|
||||
#[cfg(feature = "verge-dev")]
|
||||
let auth = instance_auth(token.clone());
|
||||
let auth = instance_auth(token);
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
let auth = instance_auth(token);
|
||||
|
||||
@@ -278,12 +233,6 @@ fn start_embedded_server(listener: tokio::net::TcpListener, token: String) {
|
||||
});
|
||||
|
||||
let commands = auth.clone().and(visible).or(auth.and(scheme)).or(pac);
|
||||
#[cfg(feature = "verge-dev")]
|
||||
let commands = commands.or(dev_quit_route(token, &COMMANDS_READY, &DEV_QUIT_REQUESTED, || {
|
||||
AsyncHandler::spawn(|| async {
|
||||
release_dev_quit_latch(&DEV_QUIT_REQUESTED, crate::feat::quit().await);
|
||||
});
|
||||
}));
|
||||
AsyncHandler::spawn(move || async move {
|
||||
warp::serve(commands)
|
||||
.incoming(listener)
|
||||
|
||||
@@ -27,8 +27,12 @@ const TRAY_STATUS_ITEM_EXTRA_WIDTH: f64 = 30.0;
|
||||
const TRAY_STATUS_ITEM_MIN_LENGTH: f64 = 58.0;
|
||||
const NS_VARIABLE_STATUS_ITEM_LENGTH: f64 = -1.0;
|
||||
|
||||
/// (button_height, mono_advance, attributes).
|
||||
type CachedAttrs = (f64, f64, Retained<NSDictionary<NSString, AnyObject>>);
|
||||
|
||||
thread_local! {
|
||||
static LAST_DISPLAY_STR: RefCell<String> = const { RefCell::new(String::new()) };
|
||||
static CACHED_ATTRS: RefCell<Option<CachedAttrs>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
fn format_tray_speed(up: u64, down: u64) -> String {
|
||||
@@ -74,8 +78,26 @@ fn create_attributed_string(
|
||||
}
|
||||
}
|
||||
|
||||
fn status_item_length_for_speed(attr_str: &NSAttributedString) -> f64 {
|
||||
(attr_str.size().width.ceil() + TRAY_STATUS_ITEM_EXTRA_WIDTH).max(TRAY_STATUS_ITEM_MIN_LENGTH)
|
||||
/// Attributes and per-glyph advance, rebuilt only when the button height changes.
|
||||
fn cached_attributes(button_height: f64) -> (f64, Retained<NSDictionary<NSString, AnyObject>>) {
|
||||
CACHED_ATTRS.with(|cache| {
|
||||
if let Some((height, advance, attrs)) = cache.borrow().as_ref()
|
||||
&& *height == button_height
|
||||
{
|
||||
return (*advance, attrs.clone());
|
||||
}
|
||||
let attrs = build_attributes(button_height);
|
||||
let probe = NSString::from_str("0");
|
||||
let advance = create_attributed_string(&probe, Some(&attrs)).size().width;
|
||||
*cache.borrow_mut() = Some((button_height, advance, attrs.clone()));
|
||||
(advance, attrs)
|
||||
})
|
||||
}
|
||||
|
||||
/// Longest line times the monospaced advance; matches `size().width` for the ASCII-only speed text.
|
||||
fn arithmetic_width(text: &str, advance: f64) -> f64 {
|
||||
let glyphs = text.split('\n').map(str::len).max().unwrap_or(0) as f64;
|
||||
glyphs * advance
|
||||
}
|
||||
|
||||
fn sync_click_target_frame(button: &NSStatusBarButton) {
|
||||
@@ -89,7 +111,7 @@ fn sync_click_target_frame(button: &NSStatusBarButton) {
|
||||
}
|
||||
|
||||
/// `with_inner_tray_icon` should run this on the main thread; fail safely if it does not.
|
||||
fn apply_status_item_attributed_title(status_item: &NSStatusItem, text: &NSString, show_speed: bool) {
|
||||
fn apply_status_item_attributed_title(status_item: &NSStatusItem, text: &str, show_speed: bool) {
|
||||
let Some(mtm) = MainThreadMarker::new() else {
|
||||
logging!(warn, Type::Tray, "托盘速率富文本设置跳过:非主线程调用");
|
||||
return;
|
||||
@@ -97,17 +119,19 @@ fn apply_status_item_attributed_title(status_item: &NSStatusItem, text: &NSStrin
|
||||
let Some(button) = status_item.button(mtm) else {
|
||||
return;
|
||||
};
|
||||
let ns_string = NSString::from_str(text);
|
||||
let attr_str = if show_speed {
|
||||
let attrs = build_attributes(button.bounds().size.height);
|
||||
let (advance, attrs) = cached_attributes(button.bounds().size.height);
|
||||
let attrs: &NSDictionary<NSString, AnyObject> = &attrs;
|
||||
let attr_str = create_attributed_string(text, Some(attrs));
|
||||
let length = status_item_length_for_speed(&attr_str);
|
||||
let attr_str = create_attributed_string(&ns_string, Some(attrs));
|
||||
let width = arithmetic_width(text, advance);
|
||||
let length = (width.ceil() + TRAY_STATUS_ITEM_EXTRA_WIDTH).max(TRAY_STATUS_ITEM_MIN_LENGTH);
|
||||
if status_item.length() != length {
|
||||
status_item.setLength(length);
|
||||
}
|
||||
attr_str
|
||||
} else {
|
||||
create_attributed_string(text, None)
|
||||
create_attributed_string(&ns_string, None)
|
||||
};
|
||||
button.setAttributedTitle(&attr_str);
|
||||
sync_click_target_frame(&button);
|
||||
@@ -121,8 +145,7 @@ pub fn set_speed_attributed_title(status_item: &NSStatusItem, up: u64, down: u64
|
||||
return;
|
||||
}
|
||||
*last = speed_text;
|
||||
let ns_string = NSString::from_str(last.as_str());
|
||||
apply_status_item_attributed_title(status_item, &ns_string, true);
|
||||
apply_status_item_attributed_title(status_item, last.as_str(), true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -130,7 +153,6 @@ pub fn clear_speed_attributed_title(status_item: &NSStatusItem) {
|
||||
LAST_DISPLAY_STR.with(|last| {
|
||||
last.borrow_mut().clear();
|
||||
});
|
||||
let empty = NSString::from_str("");
|
||||
status_item.setLength(NS_VARIABLE_STATUS_ITEM_LENGTH);
|
||||
apply_status_item_attributed_title(status_item, &empty, false);
|
||||
apply_status_item_attributed_title(status_item, "", false);
|
||||
}
|
||||
|
||||
@@ -423,7 +423,7 @@ interface PersistentProxySelectProps
|
||||
value: string
|
||||
selectedName: string
|
||||
groupName: string
|
||||
fixed?: string
|
||||
fixed?: string | null
|
||||
selectedDelay: number
|
||||
options: ProxyOption[]
|
||||
onChange: (value: string) => void
|
||||
@@ -785,8 +785,8 @@ export const CurrentProxyCard = () => {
|
||||
// The profile selection is the durable source across core restarts and run modes.
|
||||
handleSelectChange(
|
||||
selectedGroup.name,
|
||||
previousProxy,
|
||||
selectedGroup.fixed,
|
||||
previousProxy!,
|
||||
selectedGroup.fixed!,
|
||||
)({
|
||||
target: { value: nextName },
|
||||
})
|
||||
@@ -940,7 +940,7 @@ export const CurrentProxyCard = () => {
|
||||
unsortedProxyOptions,
|
||||
sortType,
|
||||
delays.of,
|
||||
defaultLatencyTimeout,
|
||||
defaultLatencyTimeout!,
|
||||
),
|
||||
[
|
||||
delays,
|
||||
|
||||
@@ -56,26 +56,7 @@ const openProfileHome = (url: string) => {
|
||||
void openExternalUrl(url).catch(showNotice.error)
|
||||
}
|
||||
|
||||
// 使用类型定义,而不是导入
|
||||
interface ProfileExtra {
|
||||
upload: number
|
||||
download: number
|
||||
total: number
|
||||
expire: number
|
||||
}
|
||||
|
||||
interface ProfileItem {
|
||||
uid: string
|
||||
type?: 'local' | 'remote' | 'merge' | 'script'
|
||||
name?: string
|
||||
desc?: string
|
||||
file?: string
|
||||
url?: string
|
||||
updated?: number
|
||||
extra?: ProfileExtra
|
||||
home?: string
|
||||
option?: any
|
||||
}
|
||||
type ProfileItem = IProfileItem
|
||||
|
||||
interface HomeProfileCardProps {
|
||||
current: ProfileItem | null | undefined
|
||||
@@ -298,10 +279,13 @@ export const HomeProfileCard = ({
|
||||
|
||||
setUpdating(true)
|
||||
try {
|
||||
await mutate(() => updateProfile(current.uid, current.option), {
|
||||
id: `update-profile:${current.uid}`,
|
||||
errorNotice: false,
|
||||
})
|
||||
await mutate(
|
||||
() => updateProfile(current.uid!, current.option ?? undefined),
|
||||
{
|
||||
id: `update-profile:${current.uid}`,
|
||||
errorNotice: false,
|
||||
},
|
||||
)
|
||||
onProfileUpdated?.()
|
||||
|
||||
// 刷新首页数据
|
||||
@@ -345,7 +329,7 @@ export const HomeProfileCard = ({
|
||||
flex: 1,
|
||||
},
|
||||
}}
|
||||
title={current.name}
|
||||
title={current.name!}
|
||||
>
|
||||
<span>{current.name}</span>
|
||||
<LaunchOutlined
|
||||
|
||||
@@ -201,7 +201,7 @@ export const ProxyTunCard: FC = () => {
|
||||
onClick={() => handleTabChange('tun')}
|
||||
icon={TroubleshootRounded}
|
||||
label={t('settings.sections.system.toggles.tunMode')}
|
||||
hasIndicator={enable_tun_mode && isTunModeAvailable}
|
||||
hasIndicator={enable_tun_mode! && isTunModeAvailable}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@ export const TestCard = () => {
|
||||
}}
|
||||
>
|
||||
{testList.map((item, index) => (
|
||||
<SortableItem key={item.uid} id={item.uid} index={index}>
|
||||
<SortableItem key={item.uid} id={item.uid!} index={index}>
|
||||
<TestItem
|
||||
itemData={item}
|
||||
onEdit={() => viewerRef.current?.edit(item)}
|
||||
|
||||
@@ -120,7 +120,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
)
|
||||
const setLoading = useCallback(
|
||||
(loading: boolean) => {
|
||||
setProfileLoading([itemData.uid], loading)
|
||||
setProfileLoading([itemData.uid!], loading)
|
||||
},
|
||||
[itemData.uid, setProfileLoading],
|
||||
)
|
||||
@@ -140,7 +140,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
debugLog(`强制刷新定时器任务`)
|
||||
}
|
||||
|
||||
const nextUpdate = await getNextUpdateTime(itemData.uid)
|
||||
const nextUpdate = await getNextUpdateTime(itemData.uid!)
|
||||
debugLog(`获取到下次更新时间结果:`, nextUpdate)
|
||||
|
||||
if (nextUpdate) {
|
||||
@@ -245,7 +245,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
const hasHome = !!itemData.home // only subscription url has home page
|
||||
|
||||
const { upload = 0, download = 0, total = 0 } = extra ?? {}
|
||||
const from = parseUrl(itemData.url)
|
||||
const from = parseUrl(itemData.url!)
|
||||
const description = itemData.desc
|
||||
const expire = parseExpire(extra?.expire)
|
||||
const progress = Math.min(
|
||||
@@ -253,7 +253,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
100,
|
||||
)
|
||||
|
||||
const loading = loadingCache.has(itemData.uid)
|
||||
const loading = loadingCache.has(itemData.uid!)
|
||||
|
||||
const [, forceRefresh] = useReducer((value: number) => value + 1, 0)
|
||||
useEffect(() => {
|
||||
@@ -263,7 +263,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
|
||||
const handler = () => {
|
||||
const now = Date.now()
|
||||
const lastUpdate = updated * 1000
|
||||
const lastUpdate = updated! * 1000
|
||||
if (now - lastUpdate >= 24 * 36e5) return
|
||||
|
||||
const wait = now - lastUpdate >= 36e5 ? 30e5 : 5e4
|
||||
@@ -293,7 +293,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
const [confirmOpen, setConfirmOpen] = useState(false)
|
||||
const [qrOpen, setQrOpen] = useState(false)
|
||||
|
||||
const loadProfileDocument = useCallback(() => readProfileFile(uid), [uid])
|
||||
const loadProfileDocument = useCallback(() => readProfileFile(uid!), [uid])
|
||||
const loadMergeDocument = useCallback(
|
||||
() => readProfileFile(option?.merge ?? ''),
|
||||
[option?.merge],
|
||||
@@ -370,7 +370,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
const onOpenFile = useLockFn(async () => {
|
||||
setAnchorEl(null)
|
||||
try {
|
||||
await mutate(() => viewProfile(itemData.uid), {
|
||||
await mutate(() => viewProfile(itemData.uid!), {
|
||||
id: 'view-profile',
|
||||
errorNotice: false,
|
||||
})
|
||||
@@ -402,7 +402,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
|
||||
try {
|
||||
const payload = Object.keys(option).length > 0 ? option : undefined
|
||||
await mutate(() => updateProfile(itemData.uid, payload), {
|
||||
await mutate(() => updateProfile(itemData.uid!, payload), {
|
||||
id: `update-profile:${itemData.uid}`,
|
||||
errorNotice: false,
|
||||
})
|
||||
@@ -593,7 +593,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
|
||||
const handleSaveProfileDocument = useLockFn(async () => {
|
||||
const currentValue = profileDocument.value
|
||||
const result = await mutate(() => saveProfileFile(uid, currentValue), {
|
||||
const result = await mutate(() => saveProfileFile(uid!, currentValue), {
|
||||
id: `save-profile-file:${uid}`,
|
||||
errorNotice: false,
|
||||
})
|
||||
@@ -728,7 +728,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
variant="h6"
|
||||
component="h2"
|
||||
noWrap
|
||||
title={name}
|
||||
title={name!}
|
||||
>
|
||||
{name}
|
||||
</Typography>
|
||||
@@ -794,7 +794,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
title={
|
||||
showNextUpdate
|
||||
? t('profiles.components.profileItem.tooltips.showLast')
|
||||
: `${t('shared.labels.updateTime')}: ${parseExpire(updated)}\n${t('profiles.components.profileItem.tooltips.showNext')}`
|
||||
: `${t('shared.labels.updateTime')}: ${parseExpire(updated!)}\n${t('profiles.components.profileItem.tooltips.showNext')}`
|
||||
}
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
@@ -812,8 +812,8 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
>
|
||||
{showNextUpdate
|
||||
? nextUpdateTime
|
||||
: updated > 0
|
||||
? dayjs(updated * 1000).fromNow()
|
||||
: updated! > 0
|
||||
? dayjs(updated! * 1000).fromNow()
|
||||
: ''}
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -831,7 +831,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
) : (
|
||||
<Box sx={{ ...boxStyle, fontSize: 12, justifyContent: 'flex-end' }}>
|
||||
<span title={t('shared.labels.updateTime')}>
|
||||
{parseExpire(updated)}
|
||||
{parseExpire(updated!)}
|
||||
</span>
|
||||
</Box>
|
||||
)}
|
||||
@@ -896,7 +896,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
<RulesEditorViewer
|
||||
groupsUid={option?.groups ?? ''}
|
||||
mergeUid={option?.merge ?? ''}
|
||||
profileUid={uid}
|
||||
profileUid={uid!}
|
||||
property={option?.rules ?? ''}
|
||||
open={true}
|
||||
onSave={onSave}
|
||||
@@ -905,7 +905,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
)}
|
||||
{proxiesOpen && (
|
||||
<ProxiesEditorViewer
|
||||
profileUid={uid}
|
||||
profileUid={uid!}
|
||||
property={option?.proxies ?? ''}
|
||||
open={true}
|
||||
onSave={onSave}
|
||||
@@ -916,7 +916,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
<GroupsEditorViewer
|
||||
mergeUid={option?.merge ?? ''}
|
||||
proxiesUid={option?.proxies ?? ''}
|
||||
profileUid={uid}
|
||||
profileUid={uid!}
|
||||
property={option?.groups ?? ''}
|
||||
open={true}
|
||||
onSave={onSave}
|
||||
@@ -976,7 +976,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
{qrOpen && itemData.url && (
|
||||
<QrViewer
|
||||
open={true}
|
||||
value={`${itemData.url}${itemData.url.includes('?') ? '&' : '?'}name=${encodeURIComponent(name)}`}
|
||||
value={`${itemData.url}${itemData.url.includes('?') ? '&' : '?'}name=${encodeURIComponent(name!)}`}
|
||||
onClose={() => setQrOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -115,7 +115,13 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
}
|
||||
|
||||
const name = form.name || `${form.type} file`
|
||||
const item = { ...form, name, option }
|
||||
const item = {
|
||||
type: form.type,
|
||||
name,
|
||||
desc: form.desc,
|
||||
option,
|
||||
...(form.type === 'remote' ? { url: form.url } : {}),
|
||||
}
|
||||
const isRemote = form.type === 'remote'
|
||||
const isUpdate = openType === 'edit'
|
||||
|
||||
@@ -139,7 +145,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
t('profiles.modals.profileForm.errors.uidMissing'),
|
||||
)
|
||||
}
|
||||
await mutate(() => patchProfile(form.uid, item), {
|
||||
await mutate(() => patchProfile(form.uid!, item), {
|
||||
id: 'patch-profile',
|
||||
errorNotice: false,
|
||||
})
|
||||
@@ -157,7 +163,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
t('profiles.modals.profileForm.errors.uidMissing'),
|
||||
)
|
||||
}
|
||||
await mutate(() => patchProfile(form.uid, item), {
|
||||
await mutate(() => patchProfile(form.uid!, item), {
|
||||
id: 'patch-profile',
|
||||
errorNotice: false,
|
||||
})
|
||||
@@ -190,13 +196,13 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
t('profiles.modals.profileForm.errors.uidMissing'),
|
||||
)
|
||||
}
|
||||
await mutate(() => patchProfile(form.uid, retryItem), {
|
||||
await mutate(() => patchProfile(form.uid!, retryItem), {
|
||||
id: 'patch-profile',
|
||||
errorNotice: false,
|
||||
})
|
||||
|
||||
await mutate(
|
||||
() => patchProfile(form.uid, { option: originalOptions }),
|
||||
() => patchProfile(form.uid!, { option: originalOptions }),
|
||||
{
|
||||
id: 'patch-profile',
|
||||
errorNotice: false,
|
||||
@@ -413,7 +419,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
<InputLabel>
|
||||
{t('profiles.modals.profileForm.fields.useSystemProxy')}
|
||||
</InputLabel>
|
||||
<Switch checked={field.value} {...field} color="primary" />
|
||||
<Switch checked={field.value!} {...field} color="primary" />
|
||||
</StyledBox>
|
||||
)}
|
||||
/>
|
||||
@@ -426,7 +432,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
<InputLabel>
|
||||
{t('profiles.modals.profileForm.fields.useClashProxy')}
|
||||
</InputLabel>
|
||||
<Switch checked={field.value} {...field} color="primary" />
|
||||
<Switch checked={field.value!} {...field} color="primary" />
|
||||
</StyledBox>
|
||||
)}
|
||||
/>
|
||||
@@ -439,7 +445,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
<InputLabel>
|
||||
{t('profiles.modals.profileForm.fields.acceptInvalidCerts')}
|
||||
</InputLabel>
|
||||
<Switch checked={field.value} {...field} color="primary" />
|
||||
<Switch checked={field.value!} {...field} color="primary" />
|
||||
</StyledBox>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -445,10 +445,14 @@ export const ProxyChain = ({
|
||||
if (isConnected) {
|
||||
setIsConnecting(true)
|
||||
try {
|
||||
await mutate(() => updateProxyChainConfigInRuntime(null), {
|
||||
id: 'update-proxy-chain-runtime',
|
||||
errorNotice: false,
|
||||
})
|
||||
const result = await mutate(
|
||||
() => updateProxyChainConfigInRuntime(null),
|
||||
{
|
||||
id: 'update-proxy-chain-runtime',
|
||||
errorNotice: false,
|
||||
},
|
||||
)
|
||||
if (!result.ok) return
|
||||
|
||||
const targetGroup =
|
||||
mode === 'global'
|
||||
@@ -461,7 +465,7 @@ export const ProxyChain = ({
|
||||
id: `select-proxy-node:${targetGroup}`,
|
||||
errorNotice: false,
|
||||
})
|
||||
recordSelection(targetGroup, 'DIRECT')
|
||||
await recordSelection(targetGroup, 'DIRECT')
|
||||
} catch {
|
||||
if (currentProxyChain.length >= 1) {
|
||||
try {
|
||||
@@ -473,7 +477,7 @@ export const ProxyChain = ({
|
||||
errorNotice: false,
|
||||
},
|
||||
)
|
||||
recordSelection(targetGroup, currentProxyChain[0].name)
|
||||
await recordSelection(targetGroup, currentProxyChain[0].name)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -519,10 +523,14 @@ export const ProxyChain = ({
|
||||
// 第一步:保存链式代理配置
|
||||
const chainProxies = currentProxyChain.map((node) => node.name)
|
||||
debugLog('Saving chain config:', chainProxies)
|
||||
await mutate(() => updateProxyChainConfigInRuntime(chainProxies), {
|
||||
id: 'update-proxy-chain-runtime',
|
||||
errorNotice: false,
|
||||
})
|
||||
const result = await mutate(
|
||||
() => updateProxyChainConfigInRuntime(chainProxies),
|
||||
{
|
||||
id: 'update-proxy-chain-runtime',
|
||||
errorNotice: false,
|
||||
},
|
||||
)
|
||||
if (!result.ok) return
|
||||
debugLog('Chain configuration saved successfully')
|
||||
|
||||
// 第二步:连接到代理链的最后一个节点
|
||||
@@ -545,12 +553,12 @@ export const ProxyChain = ({
|
||||
)
|
||||
// The chain moves the group like any other selection, so the profile has to learn about
|
||||
// it: what the profile holds is what gets re-applied the next time the core starts.
|
||||
recordSelection(targetGroup || 'GLOBAL', lastNode.name)
|
||||
await recordSelection(targetGroup || 'GLOBAL', lastNode.name)
|
||||
localStorage.setItem('proxy-chain-group', targetGroup || 'GLOBAL')
|
||||
localStorage.setItem('proxy-chain-exit-node', lastNode.name)
|
||||
|
||||
// 刷新代理信息以更新连接状态
|
||||
refreshProxy()
|
||||
await refreshProxy()
|
||||
debugLog('Successfully connected to proxy chain')
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to proxy chain:', error)
|
||||
@@ -619,14 +627,15 @@ export const ProxyChain = ({
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
mutate(() => updateProxyChainConfigInRuntime(null), {
|
||||
void mutate(() => updateProxyChainConfigInRuntime(null), {
|
||||
id: 'update-proxy-chain-runtime',
|
||||
errorNotice: false,
|
||||
})
|
||||
localStorage.removeItem('proxy-chain-group')
|
||||
localStorage.removeItem('proxy-chain-exit-node')
|
||||
localStorage.removeItem('proxy-chain-items')
|
||||
onUpdateChain([])
|
||||
onFulfilled: () => {
|
||||
localStorage.removeItem('proxy-chain-group')
|
||||
localStorage.removeItem('proxy-chain-exit-node')
|
||||
localStorage.removeItem('proxy-chain-items')
|
||||
onUpdateChain([])
|
||||
},
|
||||
}).catch(() => {})
|
||||
}}
|
||||
sx={{
|
||||
color: theme.palette.error.main,
|
||||
|
||||
@@ -5,11 +5,11 @@ export type ProxyEmptyStateReason =
|
||||
| 'no-proxy-info'
|
||||
|
||||
type ProfileSummary = {
|
||||
current?: string
|
||||
current?: string | null
|
||||
items?: Array<{
|
||||
uid?: string
|
||||
type?: string
|
||||
}>
|
||||
uid?: string | null
|
||||
type?: string | null
|
||||
}> | null
|
||||
}
|
||||
|
||||
interface ProxyListStateInput {
|
||||
@@ -20,7 +20,7 @@ interface ProxyListStateInput {
|
||||
isRunningModePending: boolean
|
||||
}
|
||||
|
||||
const isSubscriptionProfile = (item: { type?: string }): boolean =>
|
||||
const isSubscriptionProfile = (item: { type?: string | null }): boolean =>
|
||||
item.type === 'remote' || item.type === 'local'
|
||||
|
||||
/** Decides only pre-render states; the built list owns whether it has renderable content. */
|
||||
|
||||
@@ -400,20 +400,26 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
|
||||
setRuleMenuAnchor(null)
|
||||
}
|
||||
|
||||
const handleGroupSelect = (groupName: string) => {
|
||||
onGroupSelect(groupName)
|
||||
handleGroupMenuClose()
|
||||
|
||||
const handleGroupSelect = async (groupName: string) => {
|
||||
if (mode === 'rule') {
|
||||
mutate(() => updateProxyChainConfigInRuntime(null), {
|
||||
id: 'update-proxy-chain-runtime',
|
||||
errorNotice: false,
|
||||
})
|
||||
try {
|
||||
const result = await mutate(
|
||||
() => updateProxyChainConfigInRuntime(null),
|
||||
{
|
||||
id: 'update-proxy-chain-runtime',
|
||||
},
|
||||
)
|
||||
if (!result.ok) return
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
localStorage.removeItem('proxy-chain-group')
|
||||
localStorage.removeItem('proxy-chain-exit-node')
|
||||
localStorage.removeItem('proxy-chain-items')
|
||||
setProxyChain([])
|
||||
}
|
||||
onGroupSelect(groupName)
|
||||
handleGroupMenuClose()
|
||||
}
|
||||
|
||||
const handleCloseDuplicateWarning = useCallback(() => {
|
||||
|
||||
@@ -198,7 +198,7 @@ export const ProxyRender = memo(function ProxyRender(props: RenderProps) {
|
||||
{toolsOnLeft && proxyCount}
|
||||
<ProxyGroupTools
|
||||
side={toolsOnLeft ? 'left' : 'right'}
|
||||
url={group.testUrl}
|
||||
url={group.testUrl!}
|
||||
groupName={group.name}
|
||||
headState={headState!}
|
||||
onLocation={() => onLocation(group)}
|
||||
@@ -247,7 +247,7 @@ export const ProxyRender = memo(function ProxyRender(props: RenderProps) {
|
||||
return (
|
||||
<ProxyHead
|
||||
sx={{ pl: 2, pr: 3, mt: 0.5, mb: 1 }}
|
||||
url={group.testUrl}
|
||||
url={group.testUrl!}
|
||||
groupName={group.name}
|
||||
headState={headState!}
|
||||
onLocation={() => onLocation(group)}
|
||||
|
||||
@@ -17,7 +17,6 @@ const node = (memberIndex: number, delay: number, name = `${memberIndex}`) =>
|
||||
history: [{ delay }],
|
||||
extra: {
|
||||
'http://cp.cloudflare.com/generate_204': {
|
||||
alive: true,
|
||||
history: [{ time: new Date(0).toISOString(), delay }],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -110,13 +110,13 @@ const CHAIN_DELAY_GROUP = 'chain-mode'
|
||||
const virtualGroup = (members: ProxyGroupView['members']): ProxyGroupView => ({
|
||||
name: CHAIN_DELAY_GROUP,
|
||||
type: 'Selector',
|
||||
alive: true,
|
||||
udp: false,
|
||||
xudp: false,
|
||||
tfo: false,
|
||||
mptcp: false,
|
||||
smux: false,
|
||||
history: [],
|
||||
extra: {},
|
||||
members,
|
||||
})
|
||||
|
||||
@@ -237,7 +237,7 @@ export const useRenderList = (
|
||||
selected?.name ?? CHAIN_DELAY_GROUP,
|
||||
'',
|
||||
0,
|
||||
latencyTimeout,
|
||||
latencyTimeout!,
|
||||
)
|
||||
if (col > 1) {
|
||||
return groupOccurrences(occurrences, col).map((memberCol) => ({
|
||||
@@ -289,8 +289,8 @@ export const useRenderList = (
|
||||
key: group.name,
|
||||
group,
|
||||
headState,
|
||||
icon: group.icon,
|
||||
testUrl: group.testUrl,
|
||||
icon: group.icon!,
|
||||
testUrl: group.testUrl!,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -300,7 +300,7 @@ export const useRenderList = (
|
||||
group.name,
|
||||
headState.filterText,
|
||||
headState.sortType,
|
||||
latencyTimeout,
|
||||
latencyTimeout!,
|
||||
{
|
||||
matchCase: headState.filterMatchCase,
|
||||
matchWholeWord: headState.filterMatchWholeWord,
|
||||
@@ -337,11 +337,11 @@ export const useRenderList = (
|
||||
}
|
||||
|
||||
cache.set(group.name, {
|
||||
now: group.now,
|
||||
now: group.now!,
|
||||
members: group.members,
|
||||
headState,
|
||||
col,
|
||||
latencyTimeout,
|
||||
latencyTimeout: latencyTimeout!,
|
||||
delays: groupDelays.get(group.name),
|
||||
items: ret,
|
||||
})
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
import Visibility from '@mui/icons-material/Visibility'
|
||||
import VisibilityOff from '@mui/icons-material/VisibilityOff'
|
||||
import {
|
||||
TextField,
|
||||
Button,
|
||||
Grid,
|
||||
Stack,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
} from '@mui/material'
|
||||
import { Button, Grid, Stack, TextField } from '@mui/material'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import { useState, useRef, memo, useEffect } from 'react'
|
||||
import { memo, useEffect, useRef } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import { saveWebdavConfig, createWebdavBackup } from '@/services/cmds'
|
||||
import { createWebdavBackup, saveWebdavConfig } from '@/services/cmds'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import { errorDetail, showNotice } from '@/services/notice-service'
|
||||
import {
|
||||
@@ -43,16 +34,15 @@ export const BackupConfigViewer = memo(
|
||||
const { t } = useTranslation()
|
||||
const { verge, mutateVerge } = useVerge()
|
||||
const { webdav_url, webdav_username, webdav_password } = verge || {}
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const usernameRef = useRef<HTMLInputElement>(null)
|
||||
const passwordRef = useRef<HTMLInputElement>(null)
|
||||
const urlRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const { register, handleSubmit, watch } = useForm<IWebDavConfig>({
|
||||
defaultValues: {
|
||||
url: webdav_url,
|
||||
username: webdav_username,
|
||||
password: webdav_password,
|
||||
url: webdav_url!,
|
||||
username: webdav_username!,
|
||||
password: webdav_password!,
|
||||
},
|
||||
})
|
||||
const url = watch('url')
|
||||
@@ -68,10 +58,6 @@ export const BackupConfigViewer = memo(
|
||||
const webdavStatus = getWebdavStatus(webdavSignature)
|
||||
const shouldAutoInit = webdavStatus !== 'failed'
|
||||
|
||||
const handleClickShowPassword = () => {
|
||||
setShowPassword((prev) => !prev)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!webdav_url || !webdav_username || !webdav_password) {
|
||||
return
|
||||
@@ -215,7 +201,7 @@ export const BackupConfigViewer = memo(
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<TextField
|
||||
label={t('shared.labels.password')}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
type="password"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
autoCorrect="off"
|
||||
@@ -223,20 +209,6 @@ export const BackupConfigViewer = memo(
|
||||
spellCheck="false"
|
||||
inputRef={passwordRef}
|
||||
{...register('password')}
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
onClick={handleClickShowPassword}
|
||||
edge="end"
|
||||
>
|
||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -196,7 +196,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
const { verge } = useVerge()
|
||||
const { current: currentProfile } = useProfiles()
|
||||
const dnsEnabled = currentProfile
|
||||
? (verge?.profile_dns_settings?.[currentProfile.uid]?.enabled ??
|
||||
? (verge?.profile_dns_settings?.[currentProfile.uid!]?.enabled ??
|
||||
verge?.enable_dns_settings ??
|
||||
false)
|
||||
: false
|
||||
|
||||
@@ -438,7 +438,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
<Switch
|
||||
edge="end"
|
||||
disabled={!enabled}
|
||||
checked={value.pac}
|
||||
checked={value.pac!}
|
||||
onChange={(_, e) => setValue((v) => ({ ...v, pac: e }))}
|
||||
/>
|
||||
</ListItem>
|
||||
@@ -455,7 +455,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
<Switch
|
||||
edge="end"
|
||||
disabled={!enabled}
|
||||
checked={value.guard}
|
||||
checked={value.guard!}
|
||||
onChange={(_, e) => setValue((v) => ({ ...v, guard: e }))}
|
||||
sx={{ marginLeft: 'auto' }}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { alpha, Box, Button, LinearProgress } from '@mui/material'
|
||||
import type { DownloadEvent } from '@tauri-apps/plugin-updater'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import type { Ref } from 'react'
|
||||
import {
|
||||
@@ -15,6 +14,7 @@ import type { Options as ReactMarkdownOptions } from 'react-markdown'
|
||||
|
||||
import { BaseDialog, DialogRef } from '@/components/base'
|
||||
import { useUpdate } from '@/hooks/use-update'
|
||||
import type { DownloadEvent } from '@/services/bindings'
|
||||
import {
|
||||
cancelUpdateDownload,
|
||||
installUpdate,
|
||||
|
||||
@@ -40,7 +40,7 @@ const SettingClash = ({ onError }: Props) => {
|
||||
const { verge, mutateVerge } = useVerge()
|
||||
const { current: currentProfile } = useProfiles()
|
||||
const dnsEnabled = currentProfile
|
||||
? (verge?.profile_dns_settings?.[currentProfile.uid]?.enabled ??
|
||||
? (verge?.profile_dns_settings?.[currentProfile.uid!]?.enabled ??
|
||||
verge?.enable_dns_settings ??
|
||||
false)
|
||||
: false
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useVerge } from '@/hooks/use-verge'
|
||||
import { navigationItems } from '@/pages/_navigation-meta'
|
||||
import { copyClashEnv } from '@/services/cmds'
|
||||
import { supportedLanguages } from '@/services/i18n'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import getSystem from '@/utils/get-system'
|
||||
|
||||
@@ -75,8 +76,18 @@ const SettingVergeBasic = ({ onError }: Props) => {
|
||||
}
|
||||
|
||||
const onCopyClashEnv = useCallback(async () => {
|
||||
await copyClashEnv()
|
||||
showNotice.success('shared.feedback.notifications.common.copySuccess', 1000)
|
||||
try {
|
||||
await mutate(copyClashEnv, {
|
||||
id: 'copy-clash-env',
|
||||
onFulfilled: () =>
|
||||
showNotice.success(
|
||||
'shared.feedback.notifications.common.copySuccess',
|
||||
1000,
|
||||
),
|
||||
})
|
||||
} catch {
|
||||
// The shared mutation boundary reports the failure.
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LanguageRounded } from '@mui/icons-material'
|
||||
import { Box, Divider, MenuItem, Menu, styled, alpha } from '@mui/material'
|
||||
import { alpha, Box, Divider, Menu, MenuItem, styled } from '@mui/material'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -25,12 +25,18 @@ export const TestItem = ({ itemData, onEdit, onDelete: removeTest }: Props) => {
|
||||
const [position, setPosition] = useState({ left: 0, top: 0 })
|
||||
const [delay, setDelay] = useState(-1)
|
||||
const { uid, name, icon, url } = itemData
|
||||
const iconCachePath = useIconCache({ icon, cacheKey: uid })
|
||||
const iconCachePath = useIconCache({ icon: icon!, cacheKey: uid! })
|
||||
|
||||
const onDelay = useCallback(async () => {
|
||||
setDelay(-2)
|
||||
const result = await cmdTestDelay(url)
|
||||
setDelay(result)
|
||||
try {
|
||||
const result = await cmdTestDelay(url!)
|
||||
setDelay(result)
|
||||
} catch (err) {
|
||||
// A failed test must not leave the item stuck in the loading state.
|
||||
setDelay(-1)
|
||||
showNotice.error(err)
|
||||
}
|
||||
}, [url])
|
||||
|
||||
const onEditTest = () => {
|
||||
@@ -41,7 +47,7 @@ export const TestItem = ({ itemData, onEdit, onDelete: removeTest }: Props) => {
|
||||
const onDelete = useLockFn(async () => {
|
||||
setAnchorEl(null)
|
||||
try {
|
||||
removeTest(uid)
|
||||
removeTest(uid!)
|
||||
} catch (err: any) {
|
||||
showNotice.error(err)
|
||||
}
|
||||
@@ -81,17 +87,17 @@ export const TestItem = ({ itemData, onEdit, onDelete: removeTest }: Props) => {
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
{icon.trim().startsWith('http') && (
|
||||
<img
|
||||
alt={name}
|
||||
alt={name!}
|
||||
src={iconCachePath === '' ? icon : iconCachePath}
|
||||
height="40px"
|
||||
/>
|
||||
)}
|
||||
{icon.trim().startsWith('data') && (
|
||||
<img alt={name} src={icon} height="40px" />
|
||||
<img alt={name!} src={icon!} height="40px" />
|
||||
)}
|
||||
{icon.trim().startsWith('<svg') && (
|
||||
<img
|
||||
alt={name}
|
||||
alt={name!}
|
||||
src={`data:image/svg+xml;charset=utf-8,${encodeURIComponent(icon)}`}
|
||||
height="40px"
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
|
||||
import type { ProfilePatch, ProfileSelection } from '@/services/bindings'
|
||||
import { getProfiles, patchProfile, patchProfilesConfig } from '@/services/cmds'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import { migrateProfileTestUrls } from '@/services/proxy-test-url'
|
||||
@@ -46,7 +47,7 @@ export const useProfiles = () => {
|
||||
}, [])
|
||||
|
||||
const patchProfiles = useCallback(
|
||||
async (value: Partial<IProfilesConfig>) => {
|
||||
async (value: ProfileSelection) => {
|
||||
try {
|
||||
const result = await mutate(() => patchProfilesConfig(value), {
|
||||
id: 'patch-profiles-config',
|
||||
@@ -77,7 +78,7 @@ export const useProfiles = () => {
|
||||
)
|
||||
|
||||
const patchCurrent = useCallback(
|
||||
async (value: Partial<IProfileItem>) => {
|
||||
async (value: ProfilePatch) => {
|
||||
if (profiles?.current) {
|
||||
const uid = profiles.current
|
||||
await mutate(() => patchProfile(uid, value), {
|
||||
|
||||
@@ -166,10 +166,10 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
|
||||
|
||||
const handleProxyGroupChange = useCallback(
|
||||
(
|
||||
group: { name: string; now?: string; fixed?: string },
|
||||
group: { name: string; now?: string | null; fixed?: string | null },
|
||||
proxy: { name: string },
|
||||
) => {
|
||||
changeProxy(group.name, proxy.name, group.now, group.fixed)
|
||||
changeProxy(group.name, proxy.name, group.now!, group.fixed!)
|
||||
},
|
||||
[changeProxy],
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useDisplayedMixedPort } from '@/hooks/use-displayed-mixed-port'
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import { useSystemData } from '@/providers/app-data-context'
|
||||
import {
|
||||
getAutotemProxy,
|
||||
getAutoProxy,
|
||||
getEmbeddedServerPort,
|
||||
patchVergeConfig,
|
||||
} from '@/services/cmds'
|
||||
@@ -21,8 +21,8 @@ export const useSystemProxyState = () => {
|
||||
const { sysproxy } = useSystemData()
|
||||
const displayedMixedPort = useDisplayedMixedPort()
|
||||
const { data: autoproxy } = useQuery({
|
||||
queryKey: ['getAutotemProxy'],
|
||||
queryFn: getAutotemProxy,
|
||||
queryKey: ['getAutoProxy'],
|
||||
queryFn: getAutoProxy,
|
||||
refetchOnWindowFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
})
|
||||
@@ -99,7 +99,7 @@ export const useSystemProxyState = () => {
|
||||
}
|
||||
// Kept separate so an unreadable config cannot discard OS state that did read.
|
||||
try {
|
||||
await revalidateQueries([['getSystemProxy'], ['getAutotemProxy']])
|
||||
await revalidateQueries([['getSystemProxy'], ['getAutoProxy']])
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'[system-proxy] rereading the OS proxy after a toggle failed:',
|
||||
@@ -109,14 +109,14 @@ export const useSystemProxyState = () => {
|
||||
// rather than stay live from a stale cache.
|
||||
await Promise.all([
|
||||
removeCacheData(['getSystemProxy']),
|
||||
removeCacheData(['getAutotemProxy']),
|
||||
removeCacheData(['getAutoProxy']),
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const invalidateProxyState = () =>
|
||||
revalidateQueries([['getSystemProxy'], ['getAutotemProxy']])
|
||||
revalidateQueries([['getSystemProxy'], ['getAutoProxy']])
|
||||
|
||||
return {
|
||||
indicator,
|
||||
|
||||
+13
-13
@@ -117,7 +117,7 @@ const ProfilePage = () => {
|
||||
} = useProfiles()
|
||||
|
||||
useEffect(() => {
|
||||
currentProfileRef.current = profiles.current
|
||||
currentProfileRef.current = profiles.current!
|
||||
}, [profiles])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -263,7 +263,7 @@ const ProfilePage = () => {
|
||||
}
|
||||
|
||||
// `useProfiles` already retries three times; add only one business-level retry.
|
||||
const performRobustRefresh = async (prevCurrent?: string) => {
|
||||
const performRobustRefresh = async (prevCurrent?: string | null) => {
|
||||
let retryCount = 0
|
||||
const maxRetries = 1
|
||||
const baseDelay = 200
|
||||
@@ -580,7 +580,7 @@ const ProfilePage = () => {
|
||||
const onUpdateAll = useLockFn(async () => {
|
||||
const items = profileItems.filter((e) => e.type === 'remote')
|
||||
const target = items
|
||||
.map((item) => item.uid)
|
||||
.map((item) => item.uid!)
|
||||
.filter((uid) => !loadingCache.has(uid))
|
||||
|
||||
setLoadingProfiles(target, true)
|
||||
@@ -612,7 +612,7 @@ const ProfilePage = () => {
|
||||
}
|
||||
|
||||
const selectAllProfiles = () => {
|
||||
setSelectedProfiles(new Set(profileItems.map((item) => item.uid)))
|
||||
setSelectedProfiles(new Set(profileItems.map((item) => item.uid!)))
|
||||
}
|
||||
|
||||
const clearAllSelections = () => {
|
||||
@@ -904,20 +904,20 @@ const ProfilePage = () => {
|
||||
{profileItems.map((item, index) => (
|
||||
<ProfileItem
|
||||
key={item.uid}
|
||||
id={item.uid}
|
||||
id={item.uid!}
|
||||
index={index}
|
||||
selected={(switchTarget ?? profiles.current) === item.uid}
|
||||
activating={
|
||||
activatings.includes(item.uid) ||
|
||||
activatings.includes(item.uid!) ||
|
||||
visibleSwitchingProfile === item.uid
|
||||
}
|
||||
itemData={item}
|
||||
timerUpdateRevision={timerRevisions.get(item.uid) ?? 0}
|
||||
timerUpdateRevision={timerRevisions.get(item.uid!) ?? 0}
|
||||
completedUpdateRevision={
|
||||
completedUpdateRevisions.get(item.uid) ?? 0
|
||||
completedUpdateRevisions.get(item.uid!) ?? 0
|
||||
}
|
||||
mutateProfiles={mutateProfiles}
|
||||
onSelect={(f) => onSelect(item.uid, f)}
|
||||
onSelect={(f) => onSelect(item.uid!, f)}
|
||||
onEdit={() => viewerRef.current?.edit(item)}
|
||||
onSave={async (prev, curr) => {
|
||||
if (prev !== curr && profiles.current === item.uid) {
|
||||
@@ -926,14 +926,14 @@ const ProfilePage = () => {
|
||||
}}
|
||||
onDelete={() => {
|
||||
if (batchMode) {
|
||||
toggleProfileSelection(item.uid)
|
||||
toggleProfileSelection(item.uid!)
|
||||
} else {
|
||||
onDelete(item.uid)
|
||||
onDelete(item.uid!)
|
||||
}
|
||||
}}
|
||||
batchMode={batchMode}
|
||||
isSelected={selectedProfiles.has(item.uid)}
|
||||
onSelectionChange={() => toggleProfileSelection(item.uid)}
|
||||
isSelected={selectedProfiles.has(item.uid!)}
|
||||
onSelectionChange={() => toggleProfileSelection(item.uid!)}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
+15
-11
@@ -62,7 +62,7 @@ const ProxyPage = () => {
|
||||
id: 'patch-clash-mode',
|
||||
errorNotice: false,
|
||||
})
|
||||
refreshClashConfig()
|
||||
await refreshClashConfig()
|
||||
} catch (error) {
|
||||
showNotice.error(error)
|
||||
}
|
||||
@@ -71,23 +71,27 @@ const ProxyPage = () => {
|
||||
const onToggleChainMode = useLockFn(async () => {
|
||||
const newChainMode = !isChainMode
|
||||
|
||||
setIsChainMode(newChainMode)
|
||||
// 保存链式代理按钮状态到 localStorage
|
||||
localStorage.setItem('proxy-chain-mode-enabled', newChainMode.toString())
|
||||
|
||||
if (!newChainMode) {
|
||||
// 退出链式代理模式时,清除链式代理配置
|
||||
try {
|
||||
debugLog('Exiting chain mode, clearing chain configuration')
|
||||
await mutate(() => updateProxyChainConfigInRuntime(null), {
|
||||
id: 'update-proxy-chain-runtime',
|
||||
errorNotice: false,
|
||||
})
|
||||
const result = await mutate(
|
||||
() => updateProxyChainConfigInRuntime(null),
|
||||
{
|
||||
id: 'update-proxy-chain-runtime',
|
||||
errorNotice: false,
|
||||
},
|
||||
)
|
||||
if (!result.ok) return
|
||||
debugLog('Chain configuration cleared successfully')
|
||||
} catch (error) {
|
||||
console.error('Failed to clear chain configuration:', error)
|
||||
showNotice.error(error)
|
||||
return
|
||||
}
|
||||
}
|
||||
setIsChainMode(newChainMode)
|
||||
localStorage.setItem('proxy-chain-mode-enabled', newChainMode.toString())
|
||||
})
|
||||
|
||||
// 当开启链式代理模式时,获取配置数据
|
||||
@@ -123,7 +127,7 @@ const ProxyPage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
fetchChainConfig()
|
||||
fetchChainConfig().catch(() => {})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
@@ -132,7 +136,7 @@ const ProxyPage = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (normalizedMode && !isMode(normalizedMode)) {
|
||||
onChangeMode('rule')
|
||||
onChangeMode('rule').catch(() => {})
|
||||
}
|
||||
}, [normalizedMode, onChangeMode])
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -72,11 +72,7 @@ export const createEventBus = (deps: BusHandlerDeps) => {
|
||||
'getRuleProviders',
|
||||
]),
|
||||
'verge://refresh-verge-config': () =>
|
||||
deps.revalidateKeys([
|
||||
'getVergeConfig',
|
||||
'getSystemProxy',
|
||||
'getAutotemProxy',
|
||||
]),
|
||||
deps.revalidateKeys(['getVergeConfig', 'getSystemProxy', 'getAutoProxy']),
|
||||
'verge://refresh-profiles': () => deps.revalidateKeys(['getProfiles']),
|
||||
'verge://refresh-proxy-config': handleRefreshProxyConfig,
|
||||
'verge://notice-message': (payload) => deps.handleNotice(payload),
|
||||
|
||||
+178
-269
@@ -1,141 +1,183 @@
|
||||
import { Channel, invoke } from '@tauri-apps/api/core'
|
||||
import type { DownloadEvent } from '@tauri-apps/plugin-updater'
|
||||
import { Channel } from '@tauri-apps/api/core'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import type { CommandFailure } from '@/services/notice-service'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import type { ProxyViewV1 } from '@/types/proxy-view'
|
||||
import { debugLog } from '@/utils/debug'
|
||||
|
||||
import { commands } from './bindings'
|
||||
import type {
|
||||
DownloadEvent,
|
||||
JsonValue,
|
||||
ListenerProbe,
|
||||
ProxyPortSettings,
|
||||
UnlockItem,
|
||||
ProxyViewV1_Serialize as ProxyViewV1,
|
||||
ProfileSelection,
|
||||
ProfilePatch,
|
||||
ProfileCreate,
|
||||
} from './bindings'
|
||||
|
||||
export type {
|
||||
RunningMode,
|
||||
RunStateView as RunState,
|
||||
FailedOperation,
|
||||
PendingFailure,
|
||||
SidecarFailureSnapshot,
|
||||
ServiceInstallOutcome,
|
||||
ProxyPortSettings,
|
||||
ListenerTransport,
|
||||
ListenerProbe,
|
||||
UnlockItem,
|
||||
} from './bindings'
|
||||
|
||||
export async function copyClashEnv() {
|
||||
return invoke<void>('copy_clash_env')
|
||||
return commands.copyClashEnv()
|
||||
}
|
||||
|
||||
export async function getProfiles() {
|
||||
return invoke<IProfilesConfig>('get_profiles')
|
||||
export async function getProfiles(): Promise<IProfilesConfig> {
|
||||
return commands.getProfiles()
|
||||
}
|
||||
|
||||
export async function enhanceProfiles() {
|
||||
return (
|
||||
(await invoke<ValidationOutcome>('enhance_profiles')).status === 'valid'
|
||||
)
|
||||
return (await commands.enhanceProfiles()).status === 'valid'
|
||||
}
|
||||
|
||||
export async function patchProfilesConfig(profiles: IProfilesConfig) {
|
||||
return invoke<ValidationOutcome>('patch_profiles_config', { profiles })
|
||||
export async function patchProfilesConfig(profiles: ProfileSelection) {
|
||||
return commands.patchProfilesConfig({ current: profiles.current })
|
||||
}
|
||||
|
||||
export async function createProfile(
|
||||
item: Partial<IProfileItem>,
|
||||
item: ProfileCreate,
|
||||
fileData?: string | null,
|
||||
) {
|
||||
return invoke<void>('create_profile', { item, fileData })
|
||||
}
|
||||
|
||||
export async function viewProfile(index: string) {
|
||||
return invoke<void>('view_profile', { index })
|
||||
}
|
||||
|
||||
export async function readProfileFile(index: string) {
|
||||
return invoke<string>('read_profile_file', { index })
|
||||
}
|
||||
|
||||
export async function saveProfileFile(index: string, fileData: string) {
|
||||
return (
|
||||
(
|
||||
await invoke<ValidationOutcome>('save_profile_file', {
|
||||
index,
|
||||
fileData,
|
||||
})
|
||||
).status === 'valid'
|
||||
return commands.createProfile(
|
||||
{
|
||||
type: item.type,
|
||||
name: item.name,
|
||||
desc: item.desc,
|
||||
...(item.type === 'remote' ? { url: item.url } : {}),
|
||||
option: item.option,
|
||||
},
|
||||
fileData ?? null,
|
||||
)
|
||||
}
|
||||
|
||||
export async function viewProfile(index: string) {
|
||||
return commands.viewProfile(index)
|
||||
}
|
||||
|
||||
export async function readProfileFile(index: string) {
|
||||
return commands.readProfileFile(index)
|
||||
}
|
||||
|
||||
export async function saveProfileFile(index: string, fileData: string) {
|
||||
return (await commands.saveProfileFile(index, fileData)).status === 'valid'
|
||||
}
|
||||
|
||||
export async function importProfile(url: string, option?: IProfileOption) {
|
||||
return invoke<void>('import_profile', {
|
||||
url,
|
||||
option: option || { with_proxy: true },
|
||||
})
|
||||
return commands.importProfile(url, option || { with_proxy: true })
|
||||
}
|
||||
|
||||
export async function reorderProfile(activeId: string, overId: string) {
|
||||
return invoke<void>('reorder_profile', {
|
||||
activeId,
|
||||
overId,
|
||||
})
|
||||
return commands.reorderProfile(activeId, overId)
|
||||
}
|
||||
|
||||
export async function updateProfile(index: string, option?: IProfileOption) {
|
||||
return invoke<void>('update_profile', { index, option })
|
||||
return commands.updateProfile(index, option ?? null)
|
||||
}
|
||||
|
||||
export async function deleteProfile(index: string) {
|
||||
return invoke<void>('delete_profile', { index })
|
||||
return commands.deleteProfile(index)
|
||||
}
|
||||
|
||||
export async function patchProfile(
|
||||
index: string,
|
||||
profile: Partial<IProfileItem>,
|
||||
) {
|
||||
return invoke<void>('patch_profile', { index, profile })
|
||||
export async function patchProfile(index: string, profile: ProfilePatch) {
|
||||
const {
|
||||
type,
|
||||
name,
|
||||
desc,
|
||||
file,
|
||||
url,
|
||||
selected,
|
||||
latency_test_urls,
|
||||
extra,
|
||||
updated,
|
||||
option,
|
||||
} = profile
|
||||
return commands.patchProfile(index, {
|
||||
type,
|
||||
name,
|
||||
desc,
|
||||
file,
|
||||
url,
|
||||
selected,
|
||||
latency_test_urls,
|
||||
extra,
|
||||
updated,
|
||||
option,
|
||||
})
|
||||
}
|
||||
|
||||
export async function getClashInfo() {
|
||||
return invoke<IClashInfo | null>('get_clash_info')
|
||||
return commands.getClashInfo()
|
||||
}
|
||||
|
||||
// Fallback mode read independent of strict mihomo `/configs` deserialization.
|
||||
export async function getClashMode() {
|
||||
return invoke<string | null>('get_clash_mode')
|
||||
return commands.getClashMode()
|
||||
}
|
||||
|
||||
export async function getRuntimeConfig() {
|
||||
return invoke<IConfigData | null>('get_runtime_config')
|
||||
return (await commands.getRuntimeConfig()) as IConfigData | null
|
||||
}
|
||||
|
||||
export async function getRuntimeYaml() {
|
||||
return invoke<string | null>('get_runtime_yaml')
|
||||
return commands.getRuntimeYaml()
|
||||
}
|
||||
|
||||
export async function getRuntimeLogs() {
|
||||
return invoke<Record<string, [string, string][]>>('get_runtime_logs')
|
||||
return commands.getRuntimeLogs()
|
||||
}
|
||||
|
||||
export async function getRuntimeProxyChainConfig(proxyChainExitNode: string) {
|
||||
return invoke<string>('get_runtime_proxy_chain_config', {
|
||||
proxyChainExitNode,
|
||||
})
|
||||
return commands.getRuntimeProxyChainConfig(proxyChainExitNode)
|
||||
}
|
||||
|
||||
export async function updateProxyChainConfigInRuntime(proxyChainConfig: any) {
|
||||
return invoke<void>('update_proxy_chain_config_in_runtime', {
|
||||
proxyChainConfig,
|
||||
})
|
||||
export async function updateProxyChainConfigInRuntime(
|
||||
proxyChainConfig: unknown,
|
||||
) {
|
||||
const outcome = await commands.updateProxyChainConfigInRuntime(
|
||||
proxyChainConfig == null ? null : (proxyChainConfig as JsonValue),
|
||||
)
|
||||
if (outcome.status === 'invalid') throw new Error(outcome.message)
|
||||
if (outcome.status === 'skipped') {
|
||||
throw new Error(`Proxy chain validation skipped: ${outcome.reason}`)
|
||||
}
|
||||
return outcome
|
||||
}
|
||||
|
||||
export async function patchClashConfig(payload: Partial<IConfigData>) {
|
||||
return invoke<void>('patch_clash_config', { payload })
|
||||
return commands.patchClashConfig(payload as Record<string, JsonValue>)
|
||||
}
|
||||
|
||||
export async function patchClashMode(payload: string) {
|
||||
return invoke<void>('patch_clash_mode', { payload })
|
||||
return commands.patchClashMode(payload)
|
||||
}
|
||||
|
||||
export async function syncTrayProxySelection() {
|
||||
return invoke<void>('sync_tray_proxy_selection')
|
||||
return commands.syncTrayProxySelection()
|
||||
}
|
||||
|
||||
/** Sends one selection pair so the backend can merge against current profile state. */
|
||||
export async function recordSelectedNode(groupName: string, node: string) {
|
||||
return invoke<void>('record_selected_node', { groupName, node })
|
||||
return commands.recordSelectedNode(groupName, node)
|
||||
}
|
||||
|
||||
export async function forgetSelectedNode(groupName: string) {
|
||||
return invoke<void>('forget_selected_node', { groupName })
|
||||
return commands.forgetSelectedNode(groupName)
|
||||
}
|
||||
|
||||
export async function getProxyView(): Promise<ProxyViewV1> {
|
||||
const view = await invoke<ProxyViewV1>('get_proxy_view')
|
||||
const view = await commands.getProxyView()
|
||||
if (view.schemaVersion !== 1) {
|
||||
throw new Error('Unsupported proxy view schema: ' + view.schemaVersion)
|
||||
}
|
||||
@@ -145,7 +187,7 @@ export async function getProxyView(): Promise<ProxyViewV1> {
|
||||
export async function getClashLogs() {
|
||||
const regex = /time="(.+?)"\s+level=(.+?)\s+msg="(.+?)"/
|
||||
const newRegex = /(.+?)\s+(.+?)\s+(.+)/
|
||||
const logs = await invoke<string[]>('get_clash_logs')
|
||||
const logs = await commands.getClashLogs()
|
||||
|
||||
return logs.reduce<ILogItem[]>((acc, log) => {
|
||||
const result = log.match(regex)
|
||||
@@ -165,12 +207,12 @@ export async function getClashLogs() {
|
||||
}, [])
|
||||
}
|
||||
|
||||
export async function getVergeConfig() {
|
||||
return invoke<IVergeConfig>('get_verge_config')
|
||||
export async function getVergeConfig(): Promise<IVergeConfig> {
|
||||
return commands.getVergeConfig()
|
||||
}
|
||||
|
||||
export async function patchVergeConfig(payload: IVergeConfig) {
|
||||
return invoke<void>('patch_verge_config', { payload })
|
||||
return commands.patchVergeConfig(payload)
|
||||
}
|
||||
|
||||
export async function setDnsOverride(
|
||||
@@ -178,76 +220,57 @@ export async function setDnsOverride(
|
||||
enabled: boolean,
|
||||
confirmation?: string,
|
||||
) {
|
||||
return invoke<
|
||||
{ status: 'applied' } | { status: 'confirmation_required'; source: string }
|
||||
>('set_dns_override', { profileUid, enabled, confirmation })
|
||||
return commands.setDnsOverride(profileUid, enabled, confirmation ?? null)
|
||||
}
|
||||
|
||||
export async function getDnsConfigContent() {
|
||||
return invoke<string | null>('get_dns_config_content')
|
||||
return commands.getDnsConfigContent()
|
||||
}
|
||||
|
||||
export async function saveDnsConfig(dnsConfig: Record<string, unknown>) {
|
||||
return invoke<void>('save_dns_config', { dnsConfig })
|
||||
return commands.saveDnsConfig(dnsConfig as Record<string, JsonValue>)
|
||||
}
|
||||
|
||||
export async function validateDnsConfig() {
|
||||
return invoke<ValidationOutcome>('validate_dns_config')
|
||||
return commands.validateDnsConfig()
|
||||
}
|
||||
|
||||
export async function applyDnsConfig(apply: boolean) {
|
||||
return invoke<void>('apply_dns_config', { apply })
|
||||
return commands.applyDnsConfig(apply)
|
||||
}
|
||||
|
||||
export async function takeDnsOverrideNotice() {
|
||||
return invoke<boolean>('take_dns_override_notice')
|
||||
return commands.takeDnsOverrideNotice()
|
||||
}
|
||||
|
||||
export type ServiceFallbackNotice =
|
||||
| { kind: 'unavailable' }
|
||||
| { kind: 'coreRejected'; reason: string }
|
||||
| { kind: 'notAutoStarted' }
|
||||
|
||||
export async function takeServiceFallbackNotice() {
|
||||
return invoke<ServiceFallbackNotice | null>('take_service_fallback_notice')
|
||||
}
|
||||
|
||||
export interface CoreFailure {
|
||||
kind: 'startFailed' | 'serviceCoreStopped'
|
||||
detail: string
|
||||
return commands.takeServiceFallbackNotice()
|
||||
}
|
||||
|
||||
export async function getCoreStartupError() {
|
||||
return invoke<CoreFailure | null>('get_core_startup_error')
|
||||
return commands.getCoreStartupError()
|
||||
}
|
||||
|
||||
export async function takeServiceRepairNotice() {
|
||||
return invoke<boolean>('take_service_repair_notice')
|
||||
return commands.takeServiceRepairNotice()
|
||||
}
|
||||
|
||||
export async function takeServiceOwnerNotice() {
|
||||
return invoke<string | null>('take_service_owner_notice')
|
||||
return commands.takeServiceOwnerNotice()
|
||||
}
|
||||
|
||||
export async function takeDiscardedKeysNotice() {
|
||||
return invoke<string | null>('take_discarded_keys_notice')
|
||||
return commands.takeDiscardedKeysNotice()
|
||||
}
|
||||
|
||||
export async function getSystemProxy() {
|
||||
return invoke<{
|
||||
enable: boolean
|
||||
server: string
|
||||
bypass: string
|
||||
}>('get_sys_proxy')
|
||||
return commands.getSysProxy()
|
||||
}
|
||||
|
||||
export async function getAutotemProxy() {
|
||||
export async function getAutoProxy() {
|
||||
try {
|
||||
debugLog('[API] 开始调用 get_auto_proxy')
|
||||
const result = await invoke<{
|
||||
enable: boolean
|
||||
url: string
|
||||
}>('get_auto_proxy')
|
||||
const result = await commands.getAutoProxy()
|
||||
debugLog('[API] get_auto_proxy 调用成功:', result)
|
||||
return result
|
||||
} catch (error) {
|
||||
@@ -260,30 +283,23 @@ export async function getAutotemProxy() {
|
||||
}
|
||||
|
||||
export async function getEmbeddedServerPort() {
|
||||
return invoke<number>('get_embedded_server_port')
|
||||
return commands.getEmbeddedServerPort()
|
||||
}
|
||||
|
||||
export async function changeClashCore(clashCore: string) {
|
||||
return invoke<CommandFailure | null>('change_clash_core', { clashCore })
|
||||
return commands.changeClashCore(clashCore)
|
||||
}
|
||||
|
||||
export async function restartCore() {
|
||||
return invoke<void>('restart_core')
|
||||
}
|
||||
|
||||
export interface CoreUpgradeReport {
|
||||
/** False when the managed core was already at the latest version. */
|
||||
upgraded: boolean
|
||||
from: string
|
||||
to: string
|
||||
return commands.restartCore()
|
||||
}
|
||||
|
||||
export async function upgradeClashCore(force = false) {
|
||||
return invoke<CoreUpgradeReport>('upgrade_clash_core', { force })
|
||||
return commands.upgradeClashCore(force)
|
||||
}
|
||||
|
||||
export async function restartApp() {
|
||||
return invoke<void>('restart_app')
|
||||
return commands.restartApp()
|
||||
}
|
||||
|
||||
export async function installUpdate(
|
||||
@@ -292,69 +308,57 @@ export async function installUpdate(
|
||||
) {
|
||||
const channel = new Channel<DownloadEvent>()
|
||||
channel.onmessage = onEvent
|
||||
return invoke<boolean>('install_update', { version, onEvent: channel })
|
||||
return commands.installUpdate(version, channel)
|
||||
}
|
||||
|
||||
export async function cancelUpdateDownload() {
|
||||
return invoke<void>('cancel_update_download')
|
||||
return commands.cancelUpdateDownload()
|
||||
}
|
||||
|
||||
export async function getAppDir() {
|
||||
return invoke<string>('get_app_dir')
|
||||
return commands.getAppDir()
|
||||
}
|
||||
|
||||
export async function openAppDir() {
|
||||
return invoke<void>('open_app_dir').catch((err) => showNotice.error(err))
|
||||
return commands.openAppDir().catch((err) => showNotice.error(err))
|
||||
}
|
||||
|
||||
export async function openCoreDir() {
|
||||
return invoke<void>('open_core_dir').catch((err) => showNotice.error(err))
|
||||
return commands.openCoreDir().catch((err) => showNotice.error(err))
|
||||
}
|
||||
|
||||
export async function openLogsDir() {
|
||||
return invoke<void>('open_logs_dir').catch((err) => showNotice.error(err))
|
||||
return commands.openLogsDir().catch((err) => showNotice.error(err))
|
||||
}
|
||||
|
||||
export async function syncRuntimeProviders() {
|
||||
return invoke<void>('sync_runtime_providers').catch((err) => {
|
||||
return commands.syncRuntimeProviders().catch((err) => {
|
||||
console.warn('failed to queue the provider cache sync', err)
|
||||
})
|
||||
}
|
||||
|
||||
export async function cmdTestDelay(url: string) {
|
||||
return invoke<number>('test_delay', { url })
|
||||
return commands.testDelay(url)
|
||||
}
|
||||
|
||||
export async function invoke_uwp_tool() {
|
||||
return invoke<void>('invoke_uwp_tool').catch((err) =>
|
||||
showNotice.error(err, 1500),
|
||||
)
|
||||
return commands.invokeUwpTool().catch((err) => showNotice.error(err, 1500))
|
||||
}
|
||||
|
||||
export async function openDevTools() {
|
||||
return invoke('open_devtools')
|
||||
return commands.openDevtools()
|
||||
}
|
||||
|
||||
export async function exitApp() {
|
||||
return invoke('exit_app')
|
||||
return commands.exitApp()
|
||||
}
|
||||
|
||||
export async function exportDiagnosticInfo() {
|
||||
return invoke('export_diagnostic_info')
|
||||
}
|
||||
|
||||
interface SystemInfo {
|
||||
system_name: string
|
||||
system_version: string
|
||||
system_kernel_version: string
|
||||
system_arch: string
|
||||
app_version: string
|
||||
app_core_mode: string
|
||||
app_is_admin: boolean
|
||||
return commands.exportDiagnosticInfo()
|
||||
}
|
||||
|
||||
export async function getSystemInfo() {
|
||||
return invoke<SystemInfo>('get_system_info')
|
||||
return commands.getSystemInfo()
|
||||
}
|
||||
|
||||
export async function copyIconFile(
|
||||
@@ -373,55 +377,55 @@ export async function copyIconFile(
|
||||
current_t: currentTime,
|
||||
}
|
||||
|
||||
return invoke<void>('copy_icon_file', { path, iconInfo })
|
||||
return commands.copyIconFile(path, iconInfo)
|
||||
}
|
||||
|
||||
export async function downloadIconCache(url: string, name: string) {
|
||||
return invoke<string>('download_icon_cache', { url, name })
|
||||
return commands.downloadIconCache(url, name)
|
||||
}
|
||||
|
||||
export async function getNetworkInterfaces() {
|
||||
return invoke<string[]>('get_network_interfaces')
|
||||
return commands.getNetworkInterfaces()
|
||||
}
|
||||
|
||||
export async function getSystemHostname() {
|
||||
return invoke<string>('get_system_hostname')
|
||||
return commands.getSystemHostname()
|
||||
}
|
||||
|
||||
export async function getNetworkInterfacesInfo() {
|
||||
return invoke<INetworkInterface[]>('get_network_interfaces_info')
|
||||
return commands.getNetworkInterfacesInfo()
|
||||
}
|
||||
|
||||
export async function createWebdavBackup() {
|
||||
return invoke<void>('create_webdav_backup')
|
||||
return commands.createWebdavBackup()
|
||||
}
|
||||
|
||||
export async function createLocalBackup() {
|
||||
return invoke<void>('create_local_backup')
|
||||
return commands.createLocalBackup()
|
||||
}
|
||||
|
||||
export async function deleteWebdavBackup(filename: string) {
|
||||
return invoke<void>('delete_webdav_backup', { filename })
|
||||
return commands.deleteWebdavBackup(filename)
|
||||
}
|
||||
|
||||
export async function deleteLocalBackup(filename: string) {
|
||||
return invoke<void>('delete_local_backup', { filename })
|
||||
return commands.deleteLocalBackup(filename)
|
||||
}
|
||||
|
||||
export async function restoreWebDavBackup(filename: string) {
|
||||
return invoke<void>('restore_webdav_backup', { filename })
|
||||
return commands.restoreWebdavBackup(filename)
|
||||
}
|
||||
|
||||
export async function restoreLocalBackup(filename: string) {
|
||||
return invoke<void>('restore_local_backup', { filename })
|
||||
return commands.restoreLocalBackup(filename)
|
||||
}
|
||||
|
||||
export async function importLocalBackup(source: string) {
|
||||
return invoke<string>('import_local_backup', { source })
|
||||
return commands.importLocalBackup(source)
|
||||
}
|
||||
|
||||
export async function exportLocalBackup(filename: string, destination: string) {
|
||||
return invoke<void>('export_local_backup', { filename, destination })
|
||||
return commands.exportLocalBackup(filename, destination)
|
||||
}
|
||||
|
||||
export async function saveWebdavConfig(
|
||||
@@ -429,177 +433,82 @@ export async function saveWebdavConfig(
|
||||
username: string,
|
||||
password: string,
|
||||
) {
|
||||
return invoke<void>('save_webdav_config', {
|
||||
url,
|
||||
username,
|
||||
password,
|
||||
})
|
||||
return commands.saveWebdavConfig(url, username, password)
|
||||
}
|
||||
|
||||
export async function listWebDavBackup() {
|
||||
const list: IWebDavFile[] = await invoke<IWebDavFile[]>('list_webdav_backup')
|
||||
list.forEach((item) => {
|
||||
item.filename = item.href.split('/').pop() as string
|
||||
})
|
||||
return list
|
||||
return (await commands.listWebdavBackup()).map((item) => ({
|
||||
...item,
|
||||
filename: item.href.split('/').pop() ?? '',
|
||||
}))
|
||||
}
|
||||
|
||||
export async function listLocalBackup() {
|
||||
return invoke<ILocalBackupFile[]>('list_local_backup')
|
||||
return commands.listLocalBackup()
|
||||
}
|
||||
|
||||
export type RunningMode = 'Service' | 'Sidecar' | 'NotRunning'
|
||||
|
||||
type ServiceHealth =
|
||||
| 'unknown'
|
||||
| 'ready'
|
||||
| 'notInstalled'
|
||||
| 'versionMismatch'
|
||||
| 'unavailable'
|
||||
|
||||
type PendingServiceAction =
|
||||
| 'install'
|
||||
| 'uninstall'
|
||||
| 'reinstall'
|
||||
| 'forceReinstall'
|
||||
|
||||
/** Consistent core/service snapshot with backend-derived availability flags. */
|
||||
export interface RunState {
|
||||
mode: RunningMode
|
||||
service: ServiceHealth
|
||||
serviceUnavailableReason: string | null
|
||||
pendingAction: PendingServiceAction | null
|
||||
sidecarAllowed: boolean
|
||||
isAdmin: boolean
|
||||
opInFlight: boolean
|
||||
serviceUsable: boolean
|
||||
tunCapable: boolean
|
||||
serviceNeedsAttention: boolean
|
||||
}
|
||||
|
||||
export const getRuntimeState = async () => {
|
||||
return invoke<RunState>('get_runtime_state')
|
||||
}
|
||||
|
||||
export type FailedOperation =
|
||||
| 'systemProxyEnable'
|
||||
| 'systemProxyDisable'
|
||||
| 'systemProxyRestore'
|
||||
| 'systemProxyGuard'
|
||||
|
||||
export interface PendingFailure {
|
||||
code: string
|
||||
detail: string
|
||||
operation: FailedOperation
|
||||
sequence: number
|
||||
return commands.getRuntimeState()
|
||||
}
|
||||
|
||||
export const getPendingFailures = async () => {
|
||||
return invoke<PendingFailure[]>('get_pending_failures')
|
||||
}
|
||||
|
||||
export interface SidecarFailureSnapshot {
|
||||
revision: number
|
||||
detail: string | null
|
||||
return commands.getPendingFailures()
|
||||
}
|
||||
|
||||
export const getSidecarFailure = async () => {
|
||||
return invoke<SidecarFailureSnapshot>('get_sidecar_failure')
|
||||
return commands.getSidecarFailure()
|
||||
}
|
||||
|
||||
export const getAppUptime = async () => {
|
||||
return invoke<number>('get_app_uptime')
|
||||
return commands.getAppUptime()
|
||||
}
|
||||
|
||||
export type ServiceInstallOutcome =
|
||||
| { status: 'installed' }
|
||||
| { status: 'sidecar'; reason: string }
|
||||
|
||||
export const installService = async () => {
|
||||
return invoke<ServiceInstallOutcome>('install_service')
|
||||
return commands.installService()
|
||||
}
|
||||
|
||||
export const uninstallService = async () => {
|
||||
return invoke<void>('uninstall_service')
|
||||
return commands.uninstallService()
|
||||
}
|
||||
|
||||
export const reinstallService = async () => {
|
||||
return invoke<ServiceInstallOutcome>('reinstall_service')
|
||||
return commands.reinstallService()
|
||||
}
|
||||
|
||||
export const repairService = async () => {
|
||||
return invoke<ServiceInstallOutcome>('repair_service')
|
||||
return commands.repairService()
|
||||
}
|
||||
|
||||
export const continueWithSidecar = async () => {
|
||||
return invoke<void>('continue_with_sidecar')
|
||||
return commands.continueWithSidecar()
|
||||
}
|
||||
|
||||
export const entry_lightweight_mode = async () => {
|
||||
return invoke<void>('entry_lightweight_mode')
|
||||
return commands.entryLightweightMode()
|
||||
}
|
||||
|
||||
export async function getNextUpdateTime(uid: string) {
|
||||
return invoke<number | null>('get_next_update_time', { uid })
|
||||
return commands.getNextUpdateTime(uid)
|
||||
}
|
||||
|
||||
interface ToggleableProxyPort {
|
||||
enabled: boolean
|
||||
port: number
|
||||
}
|
||||
|
||||
export interface ProxyPortSettings {
|
||||
mixedPort: number
|
||||
socks: ToggleableProxyPort
|
||||
http: ToggleableProxyPort
|
||||
redir: ToggleableProxyPort
|
||||
tproxy: ToggleableProxyPort
|
||||
}
|
||||
|
||||
export type ListenerTransport = 'tcp' | 'udp'
|
||||
|
||||
export interface ListenerProbe {
|
||||
address: string
|
||||
transports: ListenerTransport[]
|
||||
}
|
||||
|
||||
export type ListenerProbeOutcome =
|
||||
| { status: 'available' }
|
||||
| {
|
||||
status: 'conflict'
|
||||
port: number
|
||||
transport: ListenerTransport
|
||||
}
|
||||
| { status: 'invalid'; message: string }
|
||||
| { status: 'indeterminate'; message: string }
|
||||
|
||||
export type SaveProxyPortsOutcome =
|
||||
| { status: 'saved' }
|
||||
| { status: 'conflict'; port: number; transport: ListenerTransport }
|
||||
|
||||
export const probeListener = async (request: ListenerProbe) => {
|
||||
return invoke<ListenerProbeOutcome>('probe_listener', { request })
|
||||
return commands.probeListener(request)
|
||||
}
|
||||
|
||||
export const saveProxyPorts = async (settings: ProxyPortSettings) => {
|
||||
return invoke<SaveProxyPortsOutcome>('save_proxy_ports', { settings })
|
||||
}
|
||||
|
||||
export interface UnlockItem {
|
||||
name: string
|
||||
status: string
|
||||
region?: string | null
|
||||
check_time?: string | null
|
||||
return commands.saveProxyPorts(settings)
|
||||
}
|
||||
|
||||
export async function getUnlockItems() {
|
||||
return invoke<UnlockItem[]>('get_unlock_items')
|
||||
return commands.getUnlockItems()
|
||||
}
|
||||
|
||||
export async function checkMediaUnlock(onComplete: Channel<UnlockItem>) {
|
||||
return invoke<UnlockItem[]>('check_media_unlock', { onComplete })
|
||||
return commands.checkMediaUnlock(onComplete)
|
||||
}
|
||||
|
||||
export async function checkMediaUnlockItem(name: string) {
|
||||
return invoke<UnlockItem>('check_media_unlock_item', { name })
|
||||
return commands.checkMediaUnlockItem(name)
|
||||
}
|
||||
|
||||
@@ -1,56 +1 @@
|
||||
// @generated by scripts/gen-contract.mjs from the backend wire contract. DO NOT EDIT.
|
||||
// Regen: pnpm gen:contract
|
||||
|
||||
import type { RunState } from './cmds'
|
||||
|
||||
/** Wire status strings from NoticeStatus; keep in sync via `pnpm gen:contract`. */
|
||||
export type NoticeStatus =
|
||||
| 'info'
|
||||
| 'import_sub_url::ok'
|
||||
| 'import_sub_url::error'
|
||||
| 'set_config::error'
|
||||
| 'core_start::error'
|
||||
| 'service_core::repair_required'
|
||||
| 'service_core::app_data_not_owned'
|
||||
| 'service_core::sidecar_fallback'
|
||||
| 'dns_override::auto_disabled'
|
||||
| 'enhance::discarded_keys'
|
||||
| 'tun_mode::auto_disabled'
|
||||
| 'tun_mode::auto_disable_failed'
|
||||
| 'app_restart::core_stop_failed'
|
||||
| 'app_quit::core_stop_failed'
|
||||
| 'update_with_clash_proxy'
|
||||
| 'update_failed_even_with_clash'
|
||||
| 'reactivate_profiles::error'
|
||||
| 'update_failed'
|
||||
| 'config_validate::boot_error'
|
||||
| 'config_validate::error'
|
||||
| 'config_validate::process_terminated'
|
||||
| 'config_validate::script_error'
|
||||
| 'config_validate::script_syntax_error'
|
||||
| 'config_validate::script_missing_main'
|
||||
| 'config_validate::file_not_found'
|
||||
| 'config_validate::yaml_syntax_error'
|
||||
| 'config_validate::yaml_read_error'
|
||||
| 'config_validate::yaml_mapping_error'
|
||||
| 'config_validate::merge_syntax_error'
|
||||
| 'config_validate::merge_mapping_error'
|
||||
| 'config_core::change_success'
|
||||
| 'config_core::change_error'
|
||||
| 'mixed_port::fallback'
|
||||
| 'mixed_port::fallback_error'
|
||||
|
||||
/** Event name → payload type for every backend-emitted event. */
|
||||
export interface VergeEventPayloads {
|
||||
'verge://refresh-clash-config': string
|
||||
'verge://refresh-verge-config': string
|
||||
'verge://refresh-profiles': string
|
||||
'verge://refresh-proxy-config': null
|
||||
'verge://notice-message': [NoticeStatus, string]
|
||||
'verge://timer-updated': string
|
||||
'verge://run-state-changed': RunState
|
||||
'verge://pending-failures-changed': null
|
||||
'profile-changed': string
|
||||
'profile-update-started': { uid: string }
|
||||
'profile-update-completed': { uid: string }
|
||||
}
|
||||
export type { NoticeStatus, VergeEventPayloads } from './bindings'
|
||||
|
||||
@@ -66,7 +66,6 @@ test('uses newer core history after a tray test without hiding an active page te
|
||||
const cached = delayManager.setDelay(member, 'tray-group', 50)
|
||||
member.node.extra = {
|
||||
[delayManager.getUrl('tray-group')]: {
|
||||
alive: true,
|
||||
history: [
|
||||
{ time: new Date(cached.updatedAt + 1000).toISOString(), delay: 300 },
|
||||
],
|
||||
@@ -107,13 +106,11 @@ test('another URL cannot replace a group measurement, but a newer matching URL c
|
||||
history: [{ time: newer, delay: 126 }],
|
||||
extra: {
|
||||
'http://localhost/a': {
|
||||
alive: true,
|
||||
history: [
|
||||
{ time: new Date(cached.updatedAt - 1000).toISOString(), delay: 49 },
|
||||
],
|
||||
},
|
||||
'http://localhost/b': {
|
||||
alive: true,
|
||||
history: [{ time: newer, delay: 126 }],
|
||||
},
|
||||
},
|
||||
@@ -122,7 +119,6 @@ test('another URL cannot replace a group measurement, but a newer matching URL c
|
||||
Object.assign(member.node, {
|
||||
extra: {
|
||||
'http://localhost/a': {
|
||||
alive: true,
|
||||
history: [{ time: newer, delay: 75 }],
|
||||
},
|
||||
},
|
||||
@@ -190,7 +186,7 @@ test('unmeasured selectors show automatic history without replacing a manual URL
|
||||
{ time: new Date(Date.now() + 1000).toISOString(), delay: 126 },
|
||||
]
|
||||
member.node.extra = {
|
||||
'http://localhost/automatic': { alive: true, history: member.node.history },
|
||||
'http://localhost/automatic': { history: member.node.history },
|
||||
}
|
||||
expect(delayManager.getDelayFix(member, 'selector')).toBe(126)
|
||||
delayManager.setDelay(member, 'selector', 50)
|
||||
|
||||
@@ -305,11 +305,7 @@ function extractDisplayText(input: unknown): string | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
export interface CommandFailure {
|
||||
code?: string
|
||||
operation?: FailedOperation
|
||||
detail: string
|
||||
}
|
||||
import type { CommandFailure_Serialize as CommandFailure } from './bindings'
|
||||
|
||||
function isCommandFailure(input: unknown): input is CommandFailure {
|
||||
if (typeof input !== 'object' || input === null) return false
|
||||
@@ -495,11 +491,11 @@ const baseShowNotice = (
|
||||
}
|
||||
|
||||
function failureCode(input: unknown): string | undefined {
|
||||
return isCommandFailure(input) ? input.code : undefined
|
||||
return isCommandFailure(input) ? input.code! : undefined
|
||||
}
|
||||
|
||||
function failureOperation(input: unknown): FailedOperation | undefined {
|
||||
return isCommandFailure(input) ? input.operation : undefined
|
||||
return isCommandFailure(input) ? input.operation! : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,6 +32,10 @@ test('committed edits retain their profile and merge with fresh saved URLs', asy
|
||||
items: [
|
||||
{
|
||||
uid: 'old-profile',
|
||||
type: null,
|
||||
name: null,
|
||||
file: null,
|
||||
updated: null,
|
||||
latency_test_urls: { untouched: 'https://example.com/' },
|
||||
},
|
||||
],
|
||||
@@ -49,7 +53,16 @@ test('committed edits retain their profile and merge with fresh saved URLs', asy
|
||||
|
||||
test('migration cannot overwrite a previously saved or cleared override', async () => {
|
||||
vi.mocked(getProfiles).mockResolvedValue({
|
||||
items: [{ uid: 'p', latency_test_urls: { A: '' } }],
|
||||
items: [
|
||||
{
|
||||
uid: 'p',
|
||||
type: null,
|
||||
name: null,
|
||||
file: null,
|
||||
updated: null,
|
||||
latency_test_urls: { A: '' },
|
||||
},
|
||||
],
|
||||
})
|
||||
await saveProfileTestUrls('p', { A: 'http://localhost/stale' }, true)
|
||||
expect(patchProfile).not.toHaveBeenCalled()
|
||||
@@ -59,14 +72,18 @@ test('pending edits survive a reload and older save acknowledgements', async ()
|
||||
vi.useFakeTimers()
|
||||
const profile = {
|
||||
uid: 'pending-profile',
|
||||
type: null,
|
||||
name: null,
|
||||
file: null,
|
||||
updated: null,
|
||||
latency_test_urls: { A: 'http://localhost/old' },
|
||||
}
|
||||
vi.mocked(getProfiles).mockResolvedValue({ items: [profile] })
|
||||
let finish!: () => void
|
||||
vi.mocked(patchProfile).mockImplementationOnce(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
finish = resolve
|
||||
new Promise<null>((resolve) => {
|
||||
finish = () => resolve(null)
|
||||
}),
|
||||
)
|
||||
const first = commitProfileTestUrl(profile.uid, 'A', 'http://localhost/new')
|
||||
@@ -90,6 +107,10 @@ test('pending edits survive a reload and older save acknowledgements', async ()
|
||||
test('a failed save preserves the committed input across profile refreshes', async () => {
|
||||
const profile = {
|
||||
uid: 'failed-profile',
|
||||
type: null,
|
||||
name: null,
|
||||
file: null,
|
||||
updated: null,
|
||||
latency_test_urls: { A: 'http://localhost/old' },
|
||||
}
|
||||
vi.mocked(getProfiles).mockResolvedValue({ items: [profile] })
|
||||
|
||||
@@ -53,16 +53,17 @@ const migrating = new Set<string>()
|
||||
|
||||
export function migrateProfileTestUrls(profiles: IProfilesConfig) {
|
||||
for (const profile of profiles.items || []) {
|
||||
if (profile.latency_test_urls !== undefined || migrating.has(profile.uid))
|
||||
continue
|
||||
const urls = readLegacyTestUrls(profile.uid)
|
||||
const uid = profile.uid
|
||||
if (!uid) continue
|
||||
if (profile.latency_test_urls !== undefined || migrating.has(uid)) continue
|
||||
const urls = readLegacyTestUrls(uid)
|
||||
if (!Object.keys(urls).length) continue
|
||||
migrating.add(profile.uid)
|
||||
void saveProfileTestUrls(profile.uid, urls, true)
|
||||
migrating.add(uid)
|
||||
void saveProfileTestUrls(uid, urls, true)
|
||||
.catch((error) => {
|
||||
console.error('Failed to migrate latency test URLs:', error)
|
||||
})
|
||||
.finally(() => migrating.delete(profile.uid))
|
||||
.finally(() => migrating.delete(uid))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,10 +93,9 @@ export function profileTestUrls(profile: IProfileItem): Record<string, string> {
|
||||
return {
|
||||
...profile.latency_test_urls,
|
||||
...Object.fromEntries(
|
||||
Object.entries(pending.get(profile.uid) ?? {}).map(([group, edit]) => [
|
||||
group,
|
||||
edit.url,
|
||||
]),
|
||||
Object.entries(
|
||||
(profile.uid ? pending.get(profile.uid) : undefined) ?? {},
|
||||
).map(([group, edit]) => [group, edit.url]),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+23
-185
@@ -13,10 +13,7 @@ type Platform =
|
||||
|
||||
declare const OS_PLATFORM: Platform
|
||||
|
||||
type ValidationOutcome =
|
||||
| { status: 'valid' | 'busy' }
|
||||
| { status: 'invalid'; kind: string; message: string }
|
||||
| { status: 'skipped'; reason: string }
|
||||
type ValidationOutcome = import('@/services/bindings').ValidationOutcome
|
||||
|
||||
interface IConfigData {
|
||||
port: number
|
||||
@@ -207,83 +204,19 @@ interface IConnectionSetting {
|
||||
layout: 'table' | 'list'
|
||||
}
|
||||
|
||||
interface IClashInfo {
|
||||
mixed_port?: number // clash mixed port
|
||||
socks_port?: number // clash socks port
|
||||
redir_port?: number // clash redir port
|
||||
tproxy_port?: number // clash tproxy port
|
||||
port?: number // clash http port
|
||||
server?: string // external-controller
|
||||
secret?: string
|
||||
}
|
||||
type IClashInfo = import('@/services/bindings').ClashInfo
|
||||
|
||||
interface IProfileItem {
|
||||
latency_test_urls?: Record<string, string>
|
||||
uid: string
|
||||
type?: 'local' | 'remote' | 'merge' | 'script'
|
||||
name?: string
|
||||
desc?: string
|
||||
file?: string
|
||||
url?: string
|
||||
updated?: number
|
||||
selected?: {
|
||||
name?: string
|
||||
now?: string
|
||||
}[]
|
||||
extra?: {
|
||||
upload: number
|
||||
download: number
|
||||
total: number
|
||||
expire: number
|
||||
}
|
||||
option?: IProfileOption
|
||||
home?: string
|
||||
}
|
||||
type IProfileItem = import('@/services/bindings').PrfItem_Serialize
|
||||
|
||||
interface IProfileOption {
|
||||
user_agent?: string
|
||||
with_proxy?: boolean
|
||||
self_proxy?: boolean
|
||||
update_interval?: number
|
||||
timeout_seconds?: number
|
||||
danger_accept_invalid_certs?: boolean
|
||||
allow_auto_update?: boolean
|
||||
merge?: string
|
||||
script?: string
|
||||
rules?: string
|
||||
proxies?: string
|
||||
groups?: string
|
||||
}
|
||||
type IProfileOption = import('@/services/bindings').PrfOption_Deserialize
|
||||
|
||||
interface IProfilesConfig {
|
||||
current?: string
|
||||
items?: IProfileItem[]
|
||||
}
|
||||
type IProfilesConfig = Partial<
|
||||
import('@/services/bindings').IProfiles_Serialize
|
||||
>
|
||||
|
||||
interface IVergeTestItem {
|
||||
uid: string
|
||||
name?: string
|
||||
icon?: string
|
||||
url: string
|
||||
}
|
||||
interface IAddress {
|
||||
V4?: {
|
||||
ip: string
|
||||
broadcast?: string
|
||||
netmask?: string
|
||||
}
|
||||
V6?: {
|
||||
ip: string
|
||||
broadcast?: string
|
||||
netmask?: string
|
||||
}
|
||||
}
|
||||
interface INetworkInterface {
|
||||
name: string
|
||||
addr: IAddress[]
|
||||
mac_addr?: string
|
||||
index: number
|
||||
}
|
||||
type IVergeTestItem = import('@/services/bindings').IVergeTestItem
|
||||
type IAddress = import('@/services/bindings').InterfaceAddress
|
||||
type INetworkInterface = import('@/services/bindings').NetworkInterfaceView
|
||||
|
||||
interface ISeqProfileConfig {
|
||||
prepend: []
|
||||
@@ -851,119 +784,24 @@ interface IProxyConfig
|
||||
| 'sudoku'
|
||||
}
|
||||
|
||||
interface IVergeConfig {
|
||||
app_log_level?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | string
|
||||
app_log_max_size?: number // KB
|
||||
app_log_max_count?: number
|
||||
language?: string
|
||||
tray_event?:
|
||||
| 'main_window'
|
||||
| 'tray_menu'
|
||||
| 'system_proxy'
|
||||
| 'tun_mode'
|
||||
| string
|
||||
env_type?: 'bash' | 'cmd' | 'powershell' | 'fish' | string
|
||||
startup_script?: string
|
||||
start_page?: string
|
||||
clash_core?: string
|
||||
theme_mode?: 'light' | 'dark' | 'system'
|
||||
traffic_graph?: boolean
|
||||
enable_memory_usage?: boolean
|
||||
enable_group_icon?: boolean
|
||||
pause_render_traffic_stats_on_blur?: boolean
|
||||
menu_icon?: 'monochrome' | 'colorful' | 'disable'
|
||||
menu_order?: string[]
|
||||
proxy_group_tools_position?: 'left' | 'right'
|
||||
notice_position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
|
||||
collapse_navbar?: boolean
|
||||
tray_icon?: 'monochrome' | 'colorful'
|
||||
common_tray_icon?: boolean
|
||||
sysproxy_tray_icon?: boolean
|
||||
tun_tray_icon?: boolean
|
||||
enable_tray_speed?: boolean
|
||||
tray_proxy_groups_display_mode?: 'default' | 'inline' | 'disable'
|
||||
tray_inline_outbound_modes?: boolean
|
||||
enable_tun_mode?: boolean
|
||||
enable_auto_light_weight_mode?: boolean
|
||||
auto_light_weight_minutes?: number
|
||||
enable_auto_launch?: boolean
|
||||
enable_silent_start?: boolean
|
||||
enable_system_proxy?: boolean
|
||||
enable_global_hotkey?: boolean
|
||||
enable_dns_settings?: boolean
|
||||
profile_dns_settings?: Record<string, { enabled: boolean }>
|
||||
proxy_auto_config?: boolean
|
||||
pac_file_content?: string
|
||||
proxy_host?: string
|
||||
enable_random_port?: boolean
|
||||
verge_mixed_port?: number
|
||||
verge_socks_port?: number
|
||||
verge_redir_port?: number
|
||||
verge_tproxy_port?: number
|
||||
verge_port?: number
|
||||
verge_redir_enabled?: boolean
|
||||
verge_tproxy_enabled?: boolean
|
||||
verge_socks_enabled?: boolean
|
||||
verge_http_enabled?: boolean
|
||||
enable_proxy_guard?: boolean
|
||||
enable_bypass_check?: boolean
|
||||
use_default_bypass?: boolean
|
||||
proxy_guard_duration?: number
|
||||
system_proxy_bypass?: string
|
||||
web_ui_list?: string[]
|
||||
hotkeys?: string[]
|
||||
theme_setting?: {
|
||||
primary_color?: string
|
||||
secondary_color?: string
|
||||
primary_text?: string
|
||||
secondary_text?: string
|
||||
info_color?: string
|
||||
error_color?: string
|
||||
warning_color?: string
|
||||
success_color?: string
|
||||
font_family?: string
|
||||
css_injection?: string
|
||||
background_image?: string
|
||||
background_blend_mode?: string
|
||||
background_opacity?: number
|
||||
}
|
||||
auto_close_connection?: boolean
|
||||
auto_check_update?: boolean
|
||||
default_latency_test?: string
|
||||
default_latency_timeout?: number
|
||||
enable_auto_delay_detection?: boolean
|
||||
auto_delay_detection_interval_minutes?: number
|
||||
enable_builtin_enhanced?: boolean
|
||||
auto_log_clean?: 0 | 1 | 2 | 3 | 4
|
||||
enable_auto_backup_schedule?: boolean
|
||||
auto_backup_interval_hours?: number
|
||||
auto_backup_on_change?: boolean
|
||||
proxy_layout_column?: number
|
||||
test_list?: IVergeTestItem[]
|
||||
webdav_url?: string
|
||||
webdav_username?: string
|
||||
webdav_password?: string
|
||||
home_cards?: Record<string, boolean>
|
||||
enable_hover_jump_navigator?: boolean
|
||||
hover_jump_navigator_delay?: number
|
||||
enable_external_controller?: boolean
|
||||
type IVergeConfig = Omit<
|
||||
Partial<import('@/services/bindings').IVerge_Serialize>,
|
||||
'theme_setting'
|
||||
> & {
|
||||
theme_setting?:
|
||||
| (import('@/services/bindings').IVergeTheme & {
|
||||
background_image?: string
|
||||
background_blend_mode?: string
|
||||
background_opacity?: number
|
||||
})
|
||||
| null
|
||||
}
|
||||
|
||||
interface IWebDavFile {
|
||||
type IWebDavFile = import('@/services/bindings').WebDavFile & {
|
||||
filename: string
|
||||
href: string
|
||||
last_modified: string
|
||||
content_length: number
|
||||
content_type: string
|
||||
tag: string
|
||||
}
|
||||
|
||||
interface ILocalBackupFile {
|
||||
filename: string
|
||||
path: string
|
||||
last_modified: string
|
||||
content_length: number
|
||||
}
|
||||
type ILocalBackupFile = import('@/services/bindings').LocalBackupFile
|
||||
|
||||
interface IWebDavConfig {
|
||||
url: string
|
||||
|
||||
+12
-83
@@ -1,86 +1,15 @@
|
||||
export interface ProxyViewV1 {
|
||||
schemaVersion: 1
|
||||
orderSource: 'runtime' | 'fallback'
|
||||
providerState: 'ready' | 'unavailable'
|
||||
global: ProxyGroupView | null
|
||||
direct: string | null
|
||||
groups: ProxyGroupView[]
|
||||
records: Record<string, ProxyNodeView>
|
||||
standalone: string[]
|
||||
providers: ProxyProviderView[]
|
||||
}
|
||||
|
||||
interface ProxyCapabilities {
|
||||
udp: boolean
|
||||
xudp: boolean
|
||||
tfo: boolean
|
||||
mptcp: boolean
|
||||
smux: boolean
|
||||
}
|
||||
|
||||
interface DelayHistory {
|
||||
time: string
|
||||
delay: number
|
||||
}
|
||||
|
||||
export interface ProxyGroupView extends ProxyCapabilities {
|
||||
name: string
|
||||
type: string
|
||||
alive: boolean
|
||||
now?: string
|
||||
fixed?: string
|
||||
hidden?: boolean
|
||||
icon?: string
|
||||
testUrl?: string
|
||||
history: DelayHistory[]
|
||||
extra?: Record<string, { alive: boolean; history: DelayHistory[] }>
|
||||
members: ProxyMemberRef[]
|
||||
}
|
||||
|
||||
export interface ProxyNodeView extends ProxyCapabilities {
|
||||
recordId: string
|
||||
name: string
|
||||
type: string
|
||||
alive: boolean
|
||||
history: DelayHistory[]
|
||||
extra?: Record<string, { alive: boolean; history: DelayHistory[] }>
|
||||
id?: string
|
||||
hidden?: boolean
|
||||
icon?: string
|
||||
testUrl?: string
|
||||
source:
|
||||
| { kind: 'core'; proxyName: string }
|
||||
| { kind: 'provider'; providerName: string; proxyName: string }
|
||||
}
|
||||
|
||||
type ProxyMemberUnresolvedReason =
|
||||
| 'missing'
|
||||
| 'ambiguous'
|
||||
| 'provider-unavailable'
|
||||
|
||||
export type ProxyMemberRef =
|
||||
| { kind: 'group'; name: string }
|
||||
| { kind: 'node'; name: string; recordId: string }
|
||||
| {
|
||||
kind: 'unresolved'
|
||||
name: string
|
||||
reason: ProxyMemberUnresolvedReason
|
||||
}
|
||||
|
||||
interface ProxyProviderView {
|
||||
name: string
|
||||
vehicleType: 'HTTP' | 'File' | 'Inline'
|
||||
updatedAt?: string
|
||||
subscriptionInfo?: ProxySubscriptionInfo
|
||||
proxyRecordIds: string[]
|
||||
}
|
||||
|
||||
interface ProxySubscriptionInfo {
|
||||
upload: number
|
||||
download: number
|
||||
total: number
|
||||
expire: number
|
||||
}
|
||||
import type {
|
||||
ProxyViewV1_Serialize as ProxyViewV1,
|
||||
ProxyGroupView_Serialize as ProxyGroupView,
|
||||
ProxyNodeView_Serialize as ProxyNodeView,
|
||||
ProxyMemberRef,
|
||||
} from '@/services/bindings'
|
||||
export type {
|
||||
ProxyViewV1_Serialize as ProxyViewV1,
|
||||
ProxyGroupView_Serialize as ProxyGroupView,
|
||||
ProxyNodeView_Serialize as ProxyNodeView,
|
||||
ProxyMemberRef,
|
||||
} from '@/services/bindings'
|
||||
|
||||
export type ResolvedProxyMember =
|
||||
| {
|
||||
|
||||
Reference in New Issue
Block a user