Compare commits

...

21 Commits

33 changed files with 699 additions and 346 deletions

View File

@@ -271,9 +271,23 @@ jobs:
- name: Rename - name: Rename
run: | run: |
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.exe' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.exe' $files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.nsis.zip' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.nsis.zip' foreach ($file in $files) {
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.nsis.zip.sig' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.nsis.zip.sig' $newName = $file.Name -replace "-setup\.exe$", "_fixed_webview2-setup.exe"
Rename-Item $file.FullName $newName
}
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip"
foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.nsis\.zip$", "_fixed_webview2-setup.nsis.zip"
Rename-Item $file.FullName $newName
}
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip.sig"
foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.nsis\.zip\.sig$", "_fixed_webview2-setup.nsis.zip.sig"
Rename-Item $file.FullName $newName
}
- name: Upload Release - name: Upload Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2

View File

@@ -1,7 +1,30 @@
## v2.1.0 发行代号:臻 ## v2.1.2
**发行代号:臻**
代号释义: 千锤百炼臻至善,集性能跃升、功能拓展、交互焕新于一体,彰显持续打磨、全方位优化的迭代精神。 代号释义: 千锤百炼臻至善,集性能跃升、功能拓展、交互焕新于一体,彰显持续打磨、全方位优化的迭代精神。
感谢 Tychristine 对社区群组管理做出的重大贡献!
2.1.2相对2.1.1(已下架不在提供)更新了:
- 无法更新和签名验证失败的问题(该死的CDN缓存)
- 设置菜单区分Verge基本设置和高级设置
- 增加v2 Updater的更多功能和权限
- 退出Verge后Tun代理状态仍保留的问题
2.1.1相对2.1.0(已下架不在提供)更新了:
- 检测所需的Clash Verge Service版本杀毒软件误报可能与此有关因为检测和安装新版本Service需管理员权限
- MacOS下支持彩色托盘图标和更好速率显示感谢Tunglies
- 文件类型判断不准导致脚本检测报错的问题
- 打开Win下的阴影(Win10因底层兼容性问题可能圆角和边框显示不太完美)
- 边框去白边
- 修复Linux下编译问题
- 修复热键无法关闭面板的问题
## v2.1.0 - 发行代号:臻
### 功能新增 ### 功能新增
- 新增窗口状态实时监控与自动保存功能 - 新增窗口状态实时监控与自动保存功能
@@ -22,12 +45,12 @@
- 重构代理列表渲染逻辑,提升布局计算效率 - 重构代理列表渲染逻辑,提升布局计算效率
- 优化代理数据更新机制采用乐观UI策略 - 优化代理数据更新机制采用乐观UI策略
- 改进虚拟列表渲染性能Virtuoso - 改进虚拟列表渲染性能Virtuoso
- 提升主窗口Clash模式切换速度 感谢Tunglies - 提升主窗口Clash模式切换速度感谢Tunglies
- 加速内核关闭流程并优化管理逻辑 - 加速内核关闭流程并优化管理逻辑
- 优化节点延迟刷新速率 - 优化节点延迟刷新速率
- 改进托盘网速显示更新逻辑 - 改进托盘网速显示更新逻辑
- 提升配置验证错误信息的可读性 - 提升配置验证错误信息的可读性
- 重构服务架构,优化代码组织结构 感谢Tunglies - 重构服务架构优化代码组织结构感谢Tunglies
- 优化内核启动时的配置验证流程 - 优化内核启动时的配置验证流程
### 问题修复 ### 问题修复

View File

@@ -1,6 +1,6 @@
{ {
"name": "clash-verge", "name": "clash-verge",
"version": "2.1.0", "version": "2.1.2",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"scripts": { "scripts": {
"dev": "cross-env RUST_BACKTRACE=1 tauri dev", "dev": "cross-env RUST_BACKTRACE=1 tauri dev",
@@ -37,7 +37,7 @@
"@tauri-apps/plugin-notification": "^2.2.1", "@tauri-apps/plugin-notification": "^2.2.1",
"@tauri-apps/plugin-process": "^2.2.0", "@tauri-apps/plugin-process": "^2.2.0",
"@tauri-apps/plugin-shell": "2.2.0", "@tauri-apps/plugin-shell": "2.2.0",
"@tauri-apps/plugin-updater": "2.4.0", "@tauri-apps/plugin-updater": "2.3.0",
"@types/json-schema": "^7.0.15", "@types/json-schema": "^7.0.15",
"ahooks": "^3.8.4", "ahooks": "^3.8.4",
"axios": "^1.7.9", "axios": "^1.7.9",

10
pnpm-lock.yaml generated
View File

@@ -62,8 +62,8 @@ importers:
specifier: 2.2.0 specifier: 2.2.0
version: 2.2.0 version: 2.2.0
"@tauri-apps/plugin-updater": "@tauri-apps/plugin-updater":
specifier: 2.4.0 specifier: 2.3.0
version: 2.4.0 version: 2.3.0
"@types/json-schema": "@types/json-schema":
specifier: ^7.0.15 specifier: ^7.0.15
version: 7.0.15 version: 7.0.15
@@ -2291,10 +2291,10 @@ packages:
integrity: sha512-iC3Ic1hLmasoboG7BO+7p+AriSoqAwKrIk+Hpk+S/bjTQdXqbl2GbdclghI4gM32X0bls7xHzIFqhRdrlvJeaA==, integrity: sha512-iC3Ic1hLmasoboG7BO+7p+AriSoqAwKrIk+Hpk+S/bjTQdXqbl2GbdclghI4gM32X0bls7xHzIFqhRdrlvJeaA==,
} }
"@tauri-apps/plugin-updater@2.4.0": "@tauri-apps/plugin-updater@2.3.0":
resolution: resolution:
{ {
integrity: sha512-BkeKN2WObAjobf2G77HyW/DxAfI0In+VSqWGnw/0cVPlM+VmA7fw9dKUnSunryZOG7ys9y07tj7FQa1ABMXGZQ==, integrity: sha512-qdzyZEUN69FZQ/nRx51fBub10tT6wffJl3DLVo9q922Gvw8Wk++rZhoD9eethPlZYbog/7RGgT8JkrfLh5BKAg==,
} }
"@types/babel__core@7.20.5": "@types/babel__core@7.20.5":
@@ -6279,7 +6279,7 @@ snapshots:
dependencies: dependencies:
"@tauri-apps/api": 2.2.0 "@tauri-apps/api": 2.2.0
"@tauri-apps/plugin-updater@2.4.0": "@tauri-apps/plugin-updater@2.3.0":
dependencies: dependencies:
"@tauri-apps/api": 2.2.0 "@tauri-apps/api": 2.2.0

View File

@@ -60,35 +60,35 @@ async function resolveUpdater() {
const { name, browser_download_url } = asset; const { name, browser_download_url } = asset;
// win64 url // win64 url
if (name.endsWith("x64-setup.nsis.zip")) { if (name.endsWith("x64-setup.exe")) {
updateData.platforms.win64.url = browser_download_url; updateData.platforms.win64.url = browser_download_url;
updateData.platforms["windows-x86_64"].url = browser_download_url; updateData.platforms["windows-x86_64"].url = browser_download_url;
} }
// win64 signature // win64 signature
if (name.endsWith("x64-setup.nsis.zip.sig")) { if (name.endsWith("x64-setup.exe.sig")) {
const sig = await getSignature(browser_download_url); const sig = await getSignature(browser_download_url);
updateData.platforms.win64.signature = sig; updateData.platforms.win64.signature = sig;
updateData.platforms["windows-x86_64"].signature = sig; updateData.platforms["windows-x86_64"].signature = sig;
} }
// win32 url // win32 url
if (name.endsWith("x86-setup.nsis.zip")) { if (name.endsWith("x86-setup.exe")) {
updateData.platforms["windows-x86"].url = browser_download_url; updateData.platforms["windows-x86"].url = browser_download_url;
updateData.platforms["windows-i686"].url = browser_download_url; updateData.platforms["windows-i686"].url = browser_download_url;
} }
// win32 signature // win32 signature
if (name.endsWith("x86-setup.nsis.zip.sig")) { if (name.endsWith("x86-setup.exe.sig")) {
const sig = await getSignature(browser_download_url); const sig = await getSignature(browser_download_url);
updateData.platforms["windows-x86"].signature = sig; updateData.platforms["windows-x86"].signature = sig;
updateData.platforms["windows-i686"].signature = sig; updateData.platforms["windows-i686"].signature = sig;
} }
// win arm url // win arm url
if (name.endsWith("arm64-setup.nsis.zip")) { if (name.endsWith("arm64-setup.exe")) {
updateData.platforms["windows-aarch64"].url = browser_download_url; updateData.platforms["windows-aarch64"].url = browser_download_url;
} }
// win arm signature // win arm signature
if (name.endsWith("arm64-setup.nsis.zip.sig")) { if (name.endsWith("arm64-setup.exe.sig")) {
const sig = await getSignature(browser_download_url); const sig = await getSignature(browser_download_url);
updateData.platforms["windows-aarch64"].signature = sig; updateData.platforms["windows-aarch64"].signature = sig;
} }

2
src-tauri/Cargo.lock generated
View File

@@ -999,7 +999,7 @@ dependencies = [
[[package]] [[package]]
name = "clash-verge" name = "clash-verge"
version = "2.1.0" version = "2.1.2"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"anyhow", "anyhow",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "clash-verge" name = "clash-verge"
version = "2.1.0" version = "2.1.2"
description = "clash verge" description = "clash verge"
authors = ["zzzgydi", "wonfen", "MystiPanda"] authors = ["zzzgydi", "wonfen", "MystiPanda"]
license = "GPL-3.0-only" license = "GPL-3.0-only"
@@ -81,7 +81,7 @@ users = "0.11.0"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-autostart = "2.2.0" tauri-plugin-autostart = "2.2.0"
tauri-plugin-global-shortcut = "2.2.0" tauri-plugin-global-shortcut = "2.2.0"
tauri-plugin-updater = "2.4.0" tauri-plugin-updater = "2.3.0"
tauri-plugin-window-state = "2.2.1" tauri-plugin-window-state = "2.2.1"
#openssl #openssl

BIN
src-tauri/assets/fonts/SF-Pro.ttf Executable file

Binary file not shown.

View File

@@ -6,6 +6,13 @@
"permissions": [ "permissions": [
"global-shortcut:default", "global-shortcut:default",
"updater:default", "updater:default",
"dialog:default",
"dialog:allow-ask",
"dialog:allow-message",
"updater:default",
"updater:allow-check",
"updater:allow-download-and-install",
"process:allow-restart",
"deep-link:default", "deep-link:default",
"window-state:default", "window-state:default",
"window-state:default", "window-state:default",

View File

@@ -187,24 +187,57 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
} }
// 在异步操作前完成所有文件操作 // 在异步操作前完成所有文件操作
let (file_path, original_content) = { let (file_path, original_content, is_merge_file) = {
let profiles = Config::profiles(); let profiles = Config::profiles();
let profiles_guard = profiles.latest(); let profiles_guard = profiles.latest();
let item = wrap_err!(profiles_guard.get_item(&index))?; let item = wrap_err!(profiles_guard.get_item(&index))?;
// 确定是否为merge类型文件
let is_merge = item.itype.as_ref().map_or(false, |t| t == "merge");
let content = wrap_err!(item.read_file())?; let content = wrap_err!(item.read_file())?;
let path = item.file.clone().ok_or("file field is null")?; let path = item.file.clone().ok_or("file field is null")?;
let profiles_dir = wrap_err!(dirs::app_profiles_dir())?; let profiles_dir = wrap_err!(dirs::app_profiles_dir())?;
(profiles_dir.join(path), content) (profiles_dir.join(path), content, is_merge)
}; };
// 保存新的配置文件 // 保存新的配置文件
wrap_err!(fs::write(&file_path, file_data.clone().unwrap()))?; wrap_err!(fs::write(&file_path, file_data.clone().unwrap()))?;
let file_path_str = file_path.to_string_lossy(); let file_path_str = file_path.to_string_lossy().to_string();
println!("[cmd配置save] 开始验证配置文件: {}", file_path_str); println!("[cmd配置save] 开始验证配置文件: {}, 是否为merge文件: {}", file_path_str, is_merge_file);
// 验证配置文件 // 对于 merge 文件,只进行语法验证,不进行后续内核验证
match CoreManager::global().validate_config_file(&file_path_str).await { if is_merge_file {
println!("[cmd配置save] 检测到merge文件只进行语法验证");
match CoreManager::global().validate_config_file(&file_path_str, Some(true)).await {
Ok((true, _)) => {
println!("[cmd配置save] merge文件语法验证通过");
// 成功后尝试更新整体配置
if let Err(e) = CoreManager::global().update_config().await {
println!("[cmd配置save] 更新整体配置时发生错误: {}", e);
log::warn!(target: "app", "更新整体配置时发生错误: {}", e);
}
return Ok(());
}
Ok((false, error_msg)) => {
println!("[cmd配置save] merge文件语法验证失败: {}", error_msg);
// 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content))?;
// 发送合并文件专用错误通知
let result = (false, error_msg.clone());
handle_yaml_validation_notice(&result, "合并配置文件");
return Ok(());
}
Err(e) => {
println!("[cmd配置save] 验证过程发生错误: {}", e);
// 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content))?;
return Err(e.to_string());
}
}
}
// 非merge文件使用完整验证流程
match CoreManager::global().validate_config_file(&file_path_str, None).await {
Ok((true, _)) => { Ok((true, _)) => {
println!("[cmd配置save] 验证成功"); println!("[cmd配置save] 验证成功");
Ok(()) Ok(())
@@ -214,18 +247,27 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
// 恢复原始配置文件 // 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content))?; wrap_err!(fs::write(&file_path, original_content))?;
// 智能判断是否为脚本错误 // 智能判断错误类型
let is_script_error = file_path_str.ends_with(".js") || let is_script_error = file_path_str.ends_with(".js") ||
error_msg.contains("Script syntax error") || error_msg.contains("Script syntax error") ||
error_msg.contains("Script must contain a main function") || error_msg.contains("Script must contain a main function") ||
error_msg.contains("Failed to read script file"); error_msg.contains("Failed to read script file");
if is_script_error { if error_msg.contains("YAML syntax error") ||
error_msg.contains("Failed to read file:") ||
(!file_path_str.ends_with(".js") && !is_script_error) {
// 普通YAML错误使用YAML通知处理
println!("[cmd配置save] YAML配置文件验证失败发送通知");
let result = (false, error_msg.clone());
handle_yaml_validation_notice(&result, "YAML配置文件");
} else if is_script_error {
// 脚本错误使用专门的通知处理 // 脚本错误使用专门的通知处理
println!("[cmd配置save] 脚本文件验证失败,发送通知");
let result = (false, error_msg.clone()); let result = (false, error_msg.clone());
handle_script_validation_notice(&result, "脚本文件"); handle_script_validation_notice(&result, "脚本文件");
} else { } else {
// 普通配置错误使用一般通知 // 普通配置错误使用一般通知
println!("[cmd配置save] 其他类型验证失败,发送一般通知");
handle::Handle::notice_message("config_validate::error", &error_msg); handle::Handle::notice_message("config_validate::error", &error_msg);
} }
@@ -292,7 +334,7 @@ pub fn get_verge_config() -> CmdResult<IVergeResponse> {
#[tauri::command] #[tauri::command]
pub async fn patch_verge_config(payload: IVerge) -> CmdResult { pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
wrap_err!(feat::patch_verge(payload).await) wrap_err!(feat::patch_verge(payload, false).await)
} }
#[tauri::command] #[tauri::command]
@@ -593,7 +635,7 @@ pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str)
pub async fn validate_script_file(file_path: String) -> CmdResult<bool> { pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
log::info!(target: "app", "验证脚本文件: {}", file_path); log::info!(target: "app", "验证脚本文件: {}", file_path);
match CoreManager::global().validate_config_file(&file_path).await { match CoreManager::global().validate_config_file(&file_path, None).await {
Ok(result) => { Ok(result) => {
handle_script_validation_notice(&result, "脚本文件"); handle_script_validation_notice(&result, "脚本文件");
Ok(result.0) // 返回验证结果布尔值 Ok(result.0) // 返回验证结果布尔值
@@ -606,3 +648,51 @@ pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
} }
} }
} }
/// 处理YAML验证相关的所有消息通知
/// 统一通知接口,保持消息类型一致性
pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
if !result.0 {
let error_msg = &result.1;
println!("[通知] 处理{}验证错误: {}", file_type, error_msg);
// 检查是否为merge文件
let is_merge_file = file_type.contains("合并");
// 根据错误消息内容判断错误类型
let status = if error_msg.starts_with("File not found:") {
"config_validate::file_not_found"
} else if error_msg.starts_with("Failed to read file:") {
"config_validate::yaml_read_error"
} else if error_msg.starts_with("YAML syntax error:") {
if is_merge_file {
"config_validate::merge_syntax_error"
} else {
"config_validate::yaml_syntax_error"
}
} else if error_msg.contains("mapping values are not allowed") {
if is_merge_file {
"config_validate::merge_mapping_error"
} else {
"config_validate::yaml_mapping_error"
}
} else if error_msg.contains("did not find expected key") {
if is_merge_file {
"config_validate::merge_key_error"
} else {
"config_validate::yaml_key_error"
}
} else {
// 如果是其他类型错误,根据文件类型作为一般错误处理
if is_merge_file {
"config_validate::merge_error"
} else {
"config_validate::yaml_error"
}
};
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
println!("[通知] 发送通知: status={}, msg={}", status, error_msg);
handle::Handle::notice_message(status, error_msg);
}
}

View File

@@ -1,5 +1,6 @@
use crate::config::*; use crate::config::*;
use crate::core::{clash_api, handle, service}; use crate::core::{clash_api, handle, service};
#[cfg(target_os = "macos")]
use crate::core::tray::Tray; use crate::core::tray::Tray;
use crate::log_err; use crate::log_err;
use crate::utils::{dirs, help}; use crate::utils::{dirs, help};
@@ -239,7 +240,7 @@ impl CoreManager {
} }
/// 验证指定的配置文件 /// 验证指定的配置文件
pub async fn validate_config_file(&self, config_path: &str) -> Result<(bool, String)> { pub async fn validate_config_file(&self, config_path: &str, is_merge_file: Option<bool>) -> Result<(bool, String)> {
// 检查文件是否存在 // 检查文件是否存在
if !std::path::Path::new(config_path).exists() { if !std::path::Path::new(config_path).exists() {
let error_msg = format!("File not found: {}", config_path); let error_msg = format!("File not found: {}", config_path);
@@ -247,6 +248,12 @@ impl CoreManager {
return Ok((false, error_msg)); return Ok((false, error_msg));
} }
// 如果是合并文件且不是强制验证,执行语法检查但不进行完整验证
if is_merge_file.unwrap_or(false) {
println!("[core配置验证] 检测到Merge文件仅进行语法检查: {}", config_path);
return self.validate_file_syntax(config_path).await;
}
// 检查是否为脚本文件 // 检查是否为脚本文件
let is_script = if config_path.ends_with(".js") { let is_script = if config_path.ends_with(".js") {
true true
@@ -273,6 +280,14 @@ impl CoreManager {
/// 检查文件是否为脚本文件 /// 检查文件是否为脚本文件
fn is_script_file(&self, path: &str) -> Result<bool> { fn is_script_file(&self, path: &str) -> Result<bool> {
// 1. 先通过扩展名快速判断
if path.ends_with(".yaml") || path.ends_with(".yml") {
return Ok(false); // YAML文件不是脚本文件
} else if path.ends_with(".js") {
return Ok(true); // JS文件是脚本文件
}
// 2. 读取文件内容
let content = match std::fs::read_to_string(path) { let content = match std::fs::read_to_string(path) {
Ok(content) => content, Ok(content) => content,
Err(err) => { Err(err) => {
@@ -281,15 +296,52 @@ impl CoreManager {
} }
}; };
// 检查文件前几行是否包含JavaScript特征 // 3. 检查是否存在明显的YAML特征
let first_lines = content.lines().take(5).collect::<String>(); let has_yaml_features = content.contains(": ") ||
Ok(first_lines.contains("function") || content.contains("#") ||
first_lines.contains("//") || content.contains("---") ||
first_lines.contains("/*") || content.lines().any(|line| line.trim().starts_with("- "));
first_lines.contains("import") ||
first_lines.contains("export") || // 4. 检查是否存在明显的JS特征
first_lines.contains("const ") || let has_js_features = content.contains("function ") ||
first_lines.contains("let ")) content.contains("const ") ||
content.contains("let ") ||
content.contains("var ") ||
content.contains("//") ||
content.contains("/*") ||
content.contains("*/") ||
content.contains("export ") ||
content.contains("import ");
// 5. 决策逻辑
if has_yaml_features && !has_js_features {
// 只有YAML特征没有JS特征
return Ok(false);
} else if has_js_features && !has_yaml_features {
// 只有JS特征没有YAML特征
return Ok(true);
} else if has_yaml_features && has_js_features {
// 两种特征都有,需要更精细判断
// 优先检查是否有明确的JS结构特征
if content.contains("function main") ||
content.contains("module.exports") ||
content.contains("export default") {
return Ok(true);
}
// 检查冒号后是否有空格YAML的典型特征
let yaml_pattern_count = content.lines()
.filter(|line| line.contains(": "))
.count();
if yaml_pattern_count > 2 {
return Ok(false); // 多个键值对格式更可能是YAML
}
}
// 默认情况:无法确定时,假设为非脚本文件(更安全)
log::debug!(target: "app", "无法确定文件类型默认当作YAML处理: {}", path);
Ok(false)
} }
/// 验证脚本文件语法 /// 验证脚本文件语法
@@ -299,7 +351,8 @@ impl CoreManager {
Ok(content) => content, Ok(content) => content,
Err(err) => { Err(err) => {
let error_msg = format!("Failed to read script file: {}", err); let error_msg = format!("Failed to read script file: {}", err);
//handle::Handle::notice_message("config_validate::script_error", &error_msg); log::warn!(target: "app", "脚本语法错误: {}", err);
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
return Ok((false, error_msg)); return Ok((false, error_msg));
} }
}; };
@@ -394,85 +447,34 @@ impl CoreManager {
} }
} }
} }
}
#[cfg(test)] /// 只进行文件语法检查,不进行完整验证
mod tests { async fn validate_file_syntax(&self, config_path: &str) -> Result<(bool, String)> {
use super::*; println!("[core配置语法检查] 开始检查文件: {}", config_path);
use std::fs;
use std::path::Path; // 读取文件内容
let content = match std::fs::read_to_string(config_path) {
async fn create_test_script() -> Result<String> { Ok(content) => content,
let temp_dir = std::env::temp_dir(); Err(err) => {
let script_path = temp_dir.join("test_script.js"); let error_msg = format!("Failed to read file: {}", err);
let script_content = r#" println!("[core配置语法检查] 无法读取文件: {}", error_msg);
// This is a test script return Ok((false, error_msg));
function main(config) { }
console.log("Testing script"); };
return config;
// 对YAML文件尝试解析只检查语法正确性
println!("[core配置语法检查] 进行YAML语法检查");
match serde_yaml::from_str::<serde_yaml::Value>(&content) {
Ok(_) => {
println!("[core配置语法检查] YAML语法检查通过");
Ok((true, String::new()))
},
Err(err) => {
// 使用标准化的前缀,以便错误处理函数能正确识别
let error_msg = format!("YAML syntax error: {}", err);
println!("[core配置语法检查] YAML语法错误: {}", error_msg);
Ok((false, error_msg))
}
} }
"#;
fs::write(&script_path, script_content)?;
Ok(script_path.to_string_lossy().to_string())
} }
}
async fn create_invalid_script() -> Result<String> {
let temp_dir = std::env::temp_dir();
let script_path = temp_dir.join("invalid_script.js");
let script_content = r#"
// This is an invalid script
function main(config { // Missing closing parenthesis
console.log("Testing script");
return config;
}
"#;
fs::write(&script_path, script_content)?;
Ok(script_path.to_string_lossy().to_string())
}
async fn create_no_main_script() -> Result<String> {
let temp_dir = std::env::temp_dir();
let script_path = temp_dir.join("no_main_script.js");
let script_content = r#"
// This script has no main function
function helper(config) {
console.log("Testing script");
return config;
}
"#;
fs::write(&script_path, script_content)?;
Ok(script_path.to_string_lossy().to_string())
}
#[tokio::test]
async fn test_validate_script_file() -> Result<()> {
let core_manager = CoreManager::global();
// 测试有效脚本
let script_path = create_test_script().await?;
let result = core_manager.validate_config_file(&script_path).await?;
assert!(result.0, "有效脚本应该通过验证");
// 测试无效脚本
let invalid_script_path = create_invalid_script().await?;
let result = core_manager.validate_config_file(&invalid_script_path).await?;
assert!(!result.0, "无效脚本不应该通过验证");
assert!(result.1.contains("脚本语法错误"), "无效脚本应该返回语法错误");
// 测试缺少main函数的脚本
let no_main_script_path = create_no_main_script().await?;
let result = core_manager.validate_config_file(&no_main_script_path).await?;
assert!(!result.0, "缺少main函数的脚本不应该通过验证");
assert!(result.1.contains("缺少main函数"), "应该提示缺少main函数");
// 清理测试文件
let _ = fs::remove_file(script_path);
let _ = fs::remove_file(invalid_script_path);
let _ = fs::remove_file(no_main_script_path);
Ok(())
}
}

View File

@@ -104,9 +104,32 @@ impl Hotkey {
// 使用 spawn_blocking 来确保在正确的线程上执行 // 使用 spawn_blocking 来确保在正确的线程上执行
async_runtime::spawn_blocking(|| { async_runtime::spawn_blocking(|| {
println!("Creating window in spawn_blocking"); println!("Toggle dashboard window visibility");
log::info!(target: "app", "Creating window in spawn_blocking"); log::info!(target: "app", "Toggle dashboard window visibility");
resolve::create_window();
// 检查窗口是否存在
if let Some(window) = handle::Handle::global().get_window() {
// 如果窗口可见,则隐藏它
if window.is_visible().unwrap_or(false) {
println!("Window is visible, hiding it");
log::info!(target: "app", "Window is visible, hiding it");
let _ = window.hide();
} else {
// 如果窗口不可见,则显示它
println!("Window is hidden, showing it");
log::info!(target: "app", "Window is hidden, showing it");
if window.is_minimized().unwrap_or(false) {
let _ = window.unminimize();
}
let _ = window.show();
let _ = window.set_focus();
}
} else {
// 如果窗口不存在,创建一个新窗口
println!("Window does not exist, creating a new one");
log::info!(target: "app", "Window does not exist, creating a new one");
resolve::create_window();
}
}); });
println!("=== Hotkey Dashboard Window Operation End ==="); println!("=== Hotkey Dashboard Window Operation End ===");
@@ -117,7 +140,7 @@ impl Hotkey {
"clash_mode_global" => || feat::change_clash_mode("global".into()), "clash_mode_global" => || feat::change_clash_mode("global".into()),
"clash_mode_direct" => || feat::change_clash_mode("direct".into()), "clash_mode_direct" => || feat::change_clash_mode("direct".into()),
"toggle_system_proxy" => || feat::toggle_system_proxy(), "toggle_system_proxy" => || feat::toggle_system_proxy(),
"toggle_tun_mode" => || feat::toggle_tun_mode(), "toggle_tun_mode" => || feat::toggle_tun_mode(None),
"quit" => || feat::quit(Some(0)), "quit" => || feat::quit(Some(0)),
_ => { _ => {
@@ -146,7 +169,23 @@ impl Hotkey {
// 直接执行函数,不做任何状态检查 // 直接执行函数,不做任何状态检查
println!("Executing function directly"); println!("Executing function directly");
log::info!(target: "app", "Executing function directly"); log::info!(target: "app", "Executing function directly");
f();
// 获取轻量模式状态和全局热键状态
let is_lite_mode = Config::verge().latest().enable_lite_mode.unwrap_or(false);
let is_enable_global_hotkey = Config::verge().latest().enable_global_hotkey.unwrap_or(true);
// 在轻量模式下或配置了全局热键时,始终执行热键功能
if is_lite_mode || is_enable_global_hotkey {
f();
} else if let Some(window) = app_handle.get_webview_window("main") {
// 非轻量模式且未启用全局热键时,只在窗口可见且有焦点的情况下响应热键
let is_visible = window.is_visible().unwrap_or(false);
let is_focused = window.is_focused().unwrap_or(false);
if is_focused && is_visible {
f();
}
}
} }
} }
}); });

View File

@@ -10,6 +10,7 @@ use tokio::time::Duration;
// Windows only // Windows only
const SERVICE_URL: &str = "http://127.0.0.1:33211"; const SERVICE_URL: &str = "http://127.0.0.1:33211";
const REQUIRED_SERVICE_VERSION: &str = "1.0.1"; // 定义所需的服务版本号
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ResponseBody { pub struct ResponseBody {
@@ -19,6 +20,12 @@ pub struct ResponseBody {
pub log_file: String, pub log_file: String,
} }
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct VersionResponse {
pub service: String,
pub version: String,
}
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
pub struct JsonResponse { pub struct JsonResponse {
pub code: u64, pub code: u64,
@@ -26,6 +33,13 @@ pub struct JsonResponse {
pub data: Option<ResponseBody>, pub data: Option<ResponseBody>,
} }
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct VersionJsonResponse {
pub code: u64,
pub msg: String,
pub data: Option<VersionResponse>,
}
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub async fn reinstall_service() -> Result<()> { pub async fn reinstall_service() -> Result<()> {
log::info!(target:"app", "reinstall service"); log::info!(target:"app", "reinstall service");
@@ -177,8 +191,43 @@ pub async fn check_service() -> Result<JsonResponse> {
Ok(response) Ok(response)
} }
/// check the service version
pub async fn check_service_version() -> Result<String> {
let url = format!("{SERVICE_URL}/version");
let response = reqwest::ClientBuilder::new()
.no_proxy()
.timeout(Duration::from_secs(3))
.build()?
.get(url)
.send()
.await
.context("failed to connect to the Clash Verge Service")?
.json::<VersionJsonResponse>()
.await
.context("failed to parse the Clash Verge Service version response")?;
match response.data {
Some(data) => Ok(data.version),
None => bail!("service version not found in response"),
}
}
/// check if service needs to be reinstalled
pub async fn check_service_needs_reinstall() -> bool {
match check_service_version().await {
Ok(version) => version != REQUIRED_SERVICE_VERSION,
Err(_) => true, // 如果无法获取版本或服务未运行,也需要重新安装
}
}
/// start the clash by service /// start the clash by service
pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> { pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
// 检查服务版本,如果不匹配则重新安装
if check_service_needs_reinstall().await {
log::info!(target: "app", "service version mismatch, reinstalling");
reinstall_service().await?;
}
let clash_core = { Config::verge().latest().clash_core.clone() }; let clash_core = { Config::verge().latest().clash_core.clone() };
let clash_core = clash_core.unwrap_or("verge-mihomo".into()); let clash_core = clash_core.unwrap_or("verge-mihomo".into());

View File

@@ -87,7 +87,7 @@ impl Tray {
{ {
match tray_event.as_str() { match tray_event.as_str() {
"system_proxy" => feat::toggle_system_proxy(), "system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(), "tun_mode" => feat::toggle_tun_mode(None),
"main_window" => resolve::create_window(), "main_window" => resolve::create_window(),
_ => {} _ => {}
} }
@@ -102,7 +102,7 @@ impl Tray {
{ {
match tray_event.as_str() { match tray_event.as_str() {
"system_proxy" => feat::toggle_system_proxy(), "system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(), "tun_mode" => feat::toggle_tun_mode(None),
"main_window" => resolve::create_window(), "main_window" => resolve::create_window(),
_ => {} _ => {}
} }
@@ -225,23 +225,27 @@ impl Tray {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
let enable_tray_speed = Config::verge().latest().enable_tray_speed.unwrap_or(true); let enable_tray_speed = Config::verge().latest().enable_tray_speed.unwrap_or(true);
let is_template = let is_colorful = tray_icon == "colorful";
crate::utils::help::is_monochrome_image_from_bytes(&icon_bytes).unwrap_or(false);
// 处理图标和速率
let icon_bytes = if enable_tray_speed { let final_icon_bytes = if enable_tray_speed {
let rate = rate.or_else(|| { let rate = rate.or_else(|| {
self.speed_rate self.speed_rate
.lock() .lock()
.as_ref() .as_ref()
.and_then(|speed_rate| speed_rate.get_curent_rate()) .and_then(|speed_rate| speed_rate.get_curent_rate())
}); });
// 使用新的方法渲染图标和速率
SpeedRate::add_speed_text(icon_bytes, rate)? SpeedRate::add_speed_text(icon_bytes, rate)?
} else { } else {
icon_bytes icon_bytes
}; };
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?)); // 设置系统托盘图标
let _ = tray.set_icon_as_template(is_template); let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&final_icon_bytes)?));
// 只对单色图标使用 template 模式
let _ = tray.set_icon_as_template(!is_colorful);
} }
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
@@ -590,7 +594,7 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
} }
"open_window" => resolve::create_window(), "open_window" => resolve::create_window(),
"system_proxy" => feat::toggle_system_proxy(), "system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(), "tun_mode" => feat::toggle_tun_mode(None),
"copy_env" => feat::copy_clash_env(), "copy_env" => feat::copy_clash_env(),
"open_app_dir" => crate::log_err!(cmds::open_app_dir()), "open_app_dir" => crate::log_err!(cmds::open_app_dir()),
"open_core_dir" => crate::log_err!(cmds::open_core_dir()), "open_core_dir" => crate::log_err!(cmds::open_core_dir()),

View File

@@ -2,7 +2,7 @@ use crate::core::clash_api::{get_traffic_ws_url, Rate};
use crate::utils::help::format_bytes_speed; use crate::utils::help::format_bytes_speed;
use anyhow::Result; use anyhow::Result;
use futures::Stream; use futures::Stream;
use image::{ImageBuffer, Rgba}; use image::{Rgba, GenericImageView, RgbaImage};
use imageproc::drawing::draw_text_mut; use imageproc::drawing::draw_text_mut;
use parking_lot::Mutex; use parking_lot::Mutex;
use rusttype::{Font, Scale}; use rusttype::{Font, Scale};
@@ -14,7 +14,7 @@ use tokio_tungstenite::tungstenite::Message;
pub struct SpeedRate { pub struct SpeedRate {
rate: Arc<Mutex<(Rate, Rate)>>, rate: Arc<Mutex<(Rate, Rate)>>,
last_update: Arc<Mutex<std::time::Instant>>, last_update: Arc<Mutex<std::time::Instant>>,
base_image: Arc<Mutex<Option<(ImageBuffer<Rgba<u8>, Vec<u8>>, u32, u32)>>>, // 存储基础图像和尺寸 // 移除 base_image,不再缓存原始图像
} }
impl SpeedRate { impl SpeedRate {
@@ -22,7 +22,6 @@ impl SpeedRate {
Self { Self {
rate: Arc::new(Mutex::new((Rate::default(), Rate::default()))), rate: Arc::new(Mutex::new((Rate::default(), Rate::default()))),
last_update: Arc::new(Mutex::new(std::time::Instant::now())), last_update: Arc::new(Mutex::new(std::time::Instant::now())),
base_image: Arc::new(Mutex::new(None)),
} }
} }
@@ -67,125 +66,114 @@ impl SpeedRate {
Some(current.clone()) Some(current.clone())
} }
pub fn add_speed_text(icon: Vec<u8>, rate: Option<Rate>) -> Result<Vec<u8>> { // 分离图标加载和速率渲染
pub fn add_speed_text(icon_bytes: Vec<u8>, rate: Option<Rate>) -> Result<Vec<u8>> {
let rate = rate.unwrap_or(Rate { up: 0, down: 0 }); let rate = rate.unwrap_or(Rate { up: 0, down: 0 });
// 获取或创建基础图像 // 加载原始图标
let base_image = { let icon_image = image::load_from_memory(&icon_bytes)?;
let tray = Self::global(); let (icon_width, icon_height) = (icon_image.width(), icon_image.height());
let mut base = tray.base_image.lock();
if base.is_none() {
let img = image::load_from_memory(&icon)?;
let (width, height) = (img.width(), img.height());
let icon_text_gap = 10;
let max_text_width = 510.0;
let total_width = width as f32 + icon_text_gap as f32 + max_text_width;
let mut image = ImageBuffer::new(total_width.ceil() as u32, height);
image::imageops::replace(&mut image, &img, 0_i64, 0_i64);
*base = Some((image, width, height));
}
base.clone().unwrap()
};
let (mut image, width, height) = base_image;
let font = // 判断是否为彩色图标
Font::try_from_bytes(include_bytes!("../../../assets/fonts/FiraCode-Medium.ttf")).unwrap(); let is_colorful = !crate::utils::help::is_monochrome_image_from_bytes(&icon_bytes).unwrap_or(false);
// 修改颜色和阴影参数
let text_color = Rgba([255u8, 255u8, 255u8, 255u8]); // 纯白色
let shadow_color = Rgba([0u8, 0u8, 0u8, 120u8]); // 降低阴影不透明度
let base_size = height as f32 * 0.6; // 保持字体大小
let scale = Scale::uniform(base_size);
let up_text = format_bytes_speed(rate.up);
let down_text = format_bytes_speed(rate.down);
// 计算文本宽度
let up_width = font
.layout(&up_text, scale, rusttype::Point { x: 0.0, y: 0.0 })
.map(|g| g.position().x + g.unpositioned().h_metrics().advance_width)
.last()
.unwrap_or(0.0);
let down_width = font
.layout(&down_text, scale, rusttype::Point { x: 0.0, y: 0.0 })
.map(|g| g.position().x + g.unpositioned().h_metrics().advance_width)
.last()
.unwrap_or(0.0);
let icon_text_gap = 10;
let max_text_width: f32 = 510.0;
let text_area_start = width as i32 + icon_text_gap;
// 用透明色清除文字区域 // 增加文本宽度和间距
for x in text_area_start..image.width() as i32 { let text_width = 580; // 文本区域宽度
for y in 0..image.height() as i32 { let total_width = icon_width + text_width;
image.put_pixel(x as u32, y as u32, Rgba([0, 0, 0, 0]));
// 创建新的透明画布
let mut combined_image = RgbaImage::new(total_width, icon_height);
// 将原始图标绘制到新画布的左侧
for y in 0..icon_height {
for x in 0..icon_width {
let pixel = icon_image.get_pixel(x, y);
combined_image.put_pixel(x, y, pixel);
} }
} }
// 计算文字的起始x坐标使文字右对齐 // 选择文本颜色
let text_start_x_up = (width as f32 + icon_text_gap as f32 + max_text_width - up_width).max(width as f32 + icon_text_gap as f32) as i32; let (text_color, shadow_color) = if is_colorful {
let text_start_x_down = (width as f32 + icon_text_gap as f32 + max_text_width - down_width).max(width as f32 + icon_text_gap as f32) as i32; // 彩色图标使用黑色文本和轻微白色阴影
(Rgba([255u8, 255u8, 255u8, 255u8]), Rgba([0u8, 0u8, 0u8, 160u8]))
// 计算垂直位置 } else {
let up_y = 0; // 上行速率紧贴顶部 // 单色图标使用白色文本和轻微黑色阴影
let down_y = height as i32 - base_size as i32; // 下行速率紧贴底部 (Rgba([255u8, 255u8, 255u8, 255u8]), Rgba([0u8, 0u8, 0u8, 120u8]))
};
// 减小字体大小以适应文本区域
let font = Font::try_from_bytes(include_bytes!("../../../assets/fonts/SF-Pro.ttf")).unwrap();
let font_size = icon_height as f32 * 0.6; // 稍微减小字体
let scale = Scale::uniform(font_size);
// 使用更简洁的速率格式
let up_text = format_bytes_speed(rate.up);
let down_text = format_bytes_speed(rate.down);
// 计算文本位置,确保垂直间距合适
// 修改文本位置为居右显示
let up_text_width = imageproc::drawing::text_size(scale, &font, &up_text).0 as u32;
let down_text_width = imageproc::drawing::text_size(scale, &font, &down_text).0 as u32;
// 计算右对齐的文本位置
let up_text_x = total_width - up_text_width;
let down_text_x = total_width - down_text_width;
// 优化垂直位置,使速率显示的高度和上下间距正好等于图标大小
let text_height = font_size as i32;
let total_text_height = text_height * 2;
let up_y = (icon_height as i32 - total_text_height) / 2;
let down_y = up_y + text_height;
// 绘制速率文本(先阴影后文字)
let shadow_offset = 1; let shadow_offset = 1;
// 绘制上行速率(先画阴影,再画文字) // 绘制上行速率
draw_text_mut( draw_text_mut(
&mut image, &mut combined_image,
shadow_color, shadow_color,
text_start_x_up + shadow_offset, up_text_x as i32 + shadow_offset,
up_y + shadow_offset, up_y + shadow_offset,
scale, scale,
&font, &font,
&up_text, &up_text,
); );
draw_text_mut( draw_text_mut(
&mut image, &mut combined_image,
text_color, text_color,
text_start_x_up, up_text_x as i32,
up_y, up_y,
scale, scale,
&font, &font,
&up_text, &up_text,
); );
// 绘制下行速率(先画阴影,再画文字) // 绘制下行速率
draw_text_mut( draw_text_mut(
&mut image, &mut combined_image,
shadow_color, shadow_color,
text_start_x_down + shadow_offset, down_text_x as i32 + shadow_offset,
down_y + shadow_offset, down_y + shadow_offset,
scale, scale,
&font, &font,
&down_text, &down_text,
); );
draw_text_mut( draw_text_mut(
&mut image, &mut combined_image,
text_color, text_color,
text_start_x_down, down_text_x as i32,
down_y, down_y,
scale, scale,
&font, &font,
&down_text, &down_text,
); );
let mut bytes: Vec<u8> = Vec::new(); // 将结果转换为 PNG 数据
let mut cursor = Cursor::new(&mut bytes); let mut bytes = Vec::new();
image.write_to(&mut cursor, image::ImageFormat::Png)?; combined_image.write_to(&mut Cursor::new(&mut bytes), image::ImageFormat::Png)?;
Ok(bytes) Ok(bytes)
} }
pub fn global() -> &'static SpeedRate {
static INSTANCE: once_cell::sync::OnceCell<SpeedRate> = once_cell::sync::OnceCell::new();
INSTANCE.get_or_init(SpeedRate::new)
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@@ -151,7 +151,7 @@ pub fn toggle_system_proxy() {
match patch_verge(IVerge { match patch_verge(IVerge {
enable_system_proxy: Some(!enable), enable_system_proxy: Some(!enable),
..IVerge::default() ..IVerge::default()
}) }, false)
.await .await
{ {
Ok(_) => handle::Handle::refresh_verge(), Ok(_) => handle::Handle::refresh_verge(),
@@ -176,7 +176,7 @@ pub fn toggle_proxy_profile(profile_index: String) {
} }
// 切换tun模式 // 切换tun模式
pub fn toggle_tun_mode() { pub fn toggle_tun_mode(not_save_file: Option<bool>) {
let enable = Config::verge().data().enable_tun_mode; let enable = Config::verge().data().enable_tun_mode;
let enable = enable.unwrap_or(false); let enable = enable.unwrap_or(false);
@@ -184,7 +184,7 @@ pub fn toggle_tun_mode() {
match patch_verge(IVerge { match patch_verge(IVerge {
enable_tun_mode: Some(!enable), enable_tun_mode: Some(!enable),
..IVerge::default() ..IVerge::default()
}) }, not_save_file.unwrap_or(false))
.await .await
{ {
Ok(_) => handle::Handle::refresh_verge(), Ok(_) => handle::Handle::refresh_verge(),
@@ -196,6 +196,7 @@ pub fn toggle_tun_mode() {
pub fn quit(code: Option<i32>) { pub fn quit(code: Option<i32>) {
let app_handle = handle::Handle::global().app_handle().unwrap(); let app_handle = handle::Handle::global().app_handle().unwrap();
handle::Handle::global().set_is_exiting(); handle::Handle::global().set_is_exiting();
toggle_tun_mode(Some(true));
resolve::resolve_reset(); resolve::resolve_reset();
log_err!(handle::Handle::global().get_window().unwrap().close()); log_err!(handle::Handle::global().get_window().unwrap().close());
app_handle.exit(code.unwrap_or(0)); app_handle.exit(code.unwrap_or(0));
@@ -236,7 +237,7 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
/// 修改verge的订阅 /// 修改verge的订阅
/// 一般都是一个个的修改 /// 一般都是一个个的修改
pub async fn patch_verge(patch: IVerge) -> Result<()> { pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
Config::verge().draft().patch_config(patch.clone()); Config::verge().draft().patch_config(patch.clone());
let tun_mode = patch.enable_tun_mode; let tun_mode = patch.enable_tun_mode;
@@ -397,7 +398,9 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
match res { match res {
Ok(()) => { Ok(()) => {
Config::verge().apply(); Config::verge().apply();
Config::verge().data().save_file()?; if !not_save_file {
Config::verge().data().save_file()?;
}
Ok(()) Ok(())
} }
@@ -617,7 +620,8 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
webdav_username, webdav_username,
webdav_password, webdav_password,
..IVerge::default() ..IVerge::default()
}) },
false)
.await .await
); );
// 最后删除临时文件 // 最后删除临时文件

View File

@@ -161,7 +161,7 @@ pub fn create_window() {
.maximizable(true) .maximizable(true)
.additional_browser_args("--enable-features=msWebView2EnableDraggableRegions --disable-features=OverscrollHistoryNavigation,msExperimentalScrolling") .additional_browser_args("--enable-features=msWebView2EnableDraggableRegions --disable-features=OverscrollHistoryNavigation,msExperimentalScrolling")
.transparent(true) .transparent(true)
.shadow(false) .shadow(true)
.build(); .build();
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]

View File

@@ -25,24 +25,23 @@
"devUrl": "http://localhost:3000/" "devUrl": "http://localhost:3000/"
}, },
"productName": "Clash Verge", "productName": "Clash Verge",
"version": "2.1.0", "version": "2.1.2",
"identifier": "io.github.clash-verge-rev.clash-verge-rev", "identifier": "io.github.clash-verge-rev.clash-verge-rev",
"plugins": { "plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK",
"endpoints": [
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json"
],
"windows": {
"installMode": "basicUi"
}
},
"deep-link": { "deep-link": {
"desktop": { "desktop": {
"schemes": ["clash", "clash-verge"] "schemes": ["clash", "clash-verge"]
} }
},
"updater": {
"active": true,
"dialog": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK",
"endpoints": [
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json",
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/alpha/update-alpha-proxy.json",
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/alpha/update-alpha.json"
]
} }
}, },
"app": { "app": {

View File

@@ -111,7 +111,7 @@
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 2px; right: 1px;
bottom: 0px; bottom: 0px;
} }
} }

View File

@@ -51,8 +51,11 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
const chains = [...data.chains].reverse().join(" / "); const chains = [...data.chains].reverse().join(" / ");
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule; const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule;
const host = metadata.host const host = metadata.host
? `${metadata.host}:${metadata.remoteDestination}` ? `${metadata.host}:${metadata.destinationPort}`
: `${metadata.remoteDestination}:${metadata.destinationPort}`; : `${metadata.remoteDestination}:${metadata.destinationPort}`;
const Destination = metadata.destinationIP
? metadata.destinationIP
: metadata.remoteDestination;
const information = [ const information = [
{ label: t("Host"), value: host }, { label: t("Host"), value: host },
@@ -79,7 +82,7 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
label: t("Source"), label: t("Source"),
value: `${metadata.sourceIP}:${metadata.sourcePort}`, value: `${metadata.sourceIP}:${metadata.sourcePort}`,
}, },
{ label: t("Destination"), value: metadata.remoteDestination }, { label: t("Destination"), value: Destination },
{ label: t("DestinationPort"), value: `${metadata.destinationPort}` }, { label: t("DestinationPort"), value: `${metadata.destinationPort}` },
{ label: t("Type"), value: `${metadata.type}(${metadata.network})` }, { label: t("Type"), value: `${metadata.type}(${metadata.network})` },
]; ];

View File

@@ -138,6 +138,9 @@ export const ConnectionTable = (props: Props) => {
const { metadata, rulePayload } = each; const { metadata, rulePayload } = each;
const chains = [...each.chains].reverse().join(" / "); const chains = [...each.chains].reverse().join(" / ");
const rule = rulePayload ? `${each.rule}(${rulePayload})` : each.rule; const rule = rulePayload ? `${each.rule}(${rulePayload})` : each.rule;
const Destination = metadata.destinationIP
? `${metadata.destinationIP}:${metadata.destinationPort}`
: `${metadata.remoteDestination}:${metadata.destinationPort}`;
return { return {
id: each.id, id: each.id,
host: metadata.host host: metadata.host
@@ -152,7 +155,7 @@ export const ConnectionTable = (props: Props) => {
process: truncateStr(metadata.process || metadata.processPath), process: truncateStr(metadata.process || metadata.processPath),
time: each.start, time: each.start,
source: `${metadata.sourceIP}:${metadata.sourcePort}`, source: `${metadata.sourceIP}:${metadata.sourcePort}`,
remoteDestination: `${metadata.remoteDestination}:${metadata.destinationPort}`, remoteDestination: Destination,
type: `${metadata.type}(${metadata.network})`, type: `${metadata.type}(${metadata.network})`,
connectionData: each, connectionData: each,
}; };

View File

@@ -0,0 +1,121 @@
import { useRef } from "react";
import { useTranslation } from "react-i18next";
import { Typography } from "@mui/material";
import {
exitApp,
openAppDir,
openCoreDir,
openLogsDir,
openDevTools,
} from "@/services/cmds";
import { check as checkUpdate } from "@tauri-apps/plugin-updater";
import { useVerge } from "@/hooks/use-verge";
import { version } from "@root/package.json";
import { DialogRef, Notice } from "@/components/base";
import { SettingList, SettingItem } from "./mods/setting-comp";
import { ConfigViewer } from "./mods/config-viewer";
import { HotkeyViewer } from "./mods/hotkey-viewer";
import { MiscViewer } from "./mods/misc-viewer";
import { ThemeViewer } from "./mods/theme-viewer";
import { LayoutViewer } from "./mods/layout-viewer";
import { UpdateViewer } from "./mods/update-viewer";
import { BackupViewer } from "./mods/backup-viewer";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
interface Props {
onError?: (err: Error) => void;
}
const SettingVergeAdvanced = ({ onError }: Props) => {
const { t } = useTranslation();
const { verge, patchVerge, mutateVerge } = useVerge();
const configRef = useRef<DialogRef>(null);
const hotkeyRef = useRef<DialogRef>(null);
const miscRef = useRef<DialogRef>(null);
const themeRef = useRef<DialogRef>(null);
const layoutRef = useRef<DialogRef>(null);
const updateRef = useRef<DialogRef>(null);
const backupRef = useRef<DialogRef>(null);
const onCheckUpdate = async () => {
try {
const info = await checkUpdate();
if (!info?.available) {
Notice.success(t("Currently on the Latest Version"));
} else {
updateRef.current?.open();
}
} catch (err: any) {
Notice.error(err.message || err.toString());
}
};
return (
<SettingList title={t("Verge Advanced Setting")}>
<ThemeViewer ref={themeRef} />
<ConfigViewer ref={configRef} />
<HotkeyViewer ref={hotkeyRef} />
<MiscViewer ref={miscRef} />
<LayoutViewer ref={layoutRef} />
<UpdateViewer ref={updateRef} />
<BackupViewer ref={backupRef} />
<SettingItem
onClick={() => backupRef.current?.open()}
label={t("Backup Setting")}
extra={
<TooltipIcon
title={t("Backup Setting Info")}
sx={{ opacity: "0.7" }}
/>
}
/>
<SettingItem
onClick={() => configRef.current?.open()}
label={t("Runtime Config")}
/>
<SettingItem
onClick={openAppDir}
label={t("Open Conf Dir")}
extra={
<TooltipIcon
title={t("Open Conf Dir Info")}
sx={{ opacity: "0.7" }}
/>
}
/>
<SettingItem onClick={openCoreDir} label={t("Open Core Dir")} />
<SettingItem onClick={openLogsDir} label={t("Open Logs Dir")} />
<SettingItem onClick={onCheckUpdate} label={t("Check for Updates")} />
<SettingItem onClick={openDevTools} label={t("Open Dev Tools")} />
<SettingItem
label={t("Lite Mode")}
extra={
<TooltipIcon title={t("Lite Mode Info")} sx={{ opacity: "0.7" }} />
}
onClick={() => patchVerge({ enable_lite_mode: true })}
/>
<SettingItem
onClick={() => {
exitApp();
}}
label={t("Exit")}
/>
<SettingItem label={t("Verge Version")}>
<Typography sx={{ py: "7px", pr: 1 }}>v{version}</Typography>
</SettingItem>
</SettingList>
);
};
export default SettingVergeAdvanced;

View File

@@ -1,18 +1,9 @@
import { useCallback, useRef } from "react"; import { useCallback, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { open } from "@tauri-apps/plugin-dialog"; import { open } from "@tauri-apps/plugin-dialog";
import { Button, MenuItem, Select, Input, Typography } from "@mui/material"; import { Button, MenuItem, Select, Input } from "@mui/material";
import { import { copyClashEnv } from "@/services/cmds";
exitApp,
openAppDir,
openCoreDir,
openLogsDir,
openDevTools,
copyClashEnv,
} from "@/services/cmds";
import { check as checkUpdate } from "@tauri-apps/plugin-updater";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { version } from "@root/package.json";
import { DialogRef, Notice } from "@/components/base"; import { DialogRef, Notice } from "@/components/base";
import { SettingList, SettingItem } from "./mods/setting-comp"; import { SettingList, SettingItem } from "./mods/setting-comp";
import { ThemeModeSwitch } from "./mods/theme-mode-switch"; import { ThemeModeSwitch } from "./mods/theme-mode-switch";
@@ -49,7 +40,7 @@ const languageOptions = Object.entries(languages).map(([code, _]) => {
return { code, label: labels[code] }; return { code, label: labels[code] };
}); });
const SettingVerge = ({ onError }: Props) => { const SettingVergeBasic = ({ onError }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { verge, patchVerge, mutateVerge } = useVerge(); const { verge, patchVerge, mutateVerge } = useVerge();
@@ -60,7 +51,6 @@ const SettingVerge = ({ onError }: Props) => {
env_type, env_type,
startup_script, startup_script,
start_page, start_page,
enable_lite_mode,
} = verge ?? {}; } = verge ?? {};
const configRef = useRef<DialogRef>(null); const configRef = useRef<DialogRef>(null);
const hotkeyRef = useRef<DialogRef>(null); const hotkeyRef = useRef<DialogRef>(null);
@@ -74,26 +64,13 @@ const SettingVerge = ({ onError }: Props) => {
mutateVerge({ ...verge, ...patch }, false); mutateVerge({ ...verge, ...patch }, false);
}; };
const onCheckUpdate = async () => {
try {
const info = await checkUpdate();
if (!info?.available) {
Notice.success(t("Currently on the Latest Version"));
} else {
updateRef.current?.open();
}
} catch (err: any) {
Notice.error(err.message || err.toString());
}
};
const onCopyClashEnv = useCallback(async () => { const onCopyClashEnv = useCallback(async () => {
await copyClashEnv(); await copyClashEnv();
Notice.success(t("Copy Success"), 1000); Notice.success(t("Copy Success"), 1000);
}, []); }, []);
return ( return (
<SettingList title={t("Verge Setting")}> <SettingList title={t("Verge Basic Setting")}>
<ThemeViewer ref={themeRef} /> <ThemeViewer ref={themeRef} />
<ConfigViewer ref={configRef} /> <ConfigViewer ref={configRef} />
<HotkeyViewer ref={hotkeyRef} /> <HotkeyViewer ref={hotkeyRef} />
@@ -261,62 +238,8 @@ const SettingVerge = ({ onError }: Props) => {
onClick={() => hotkeyRef.current?.open()} onClick={() => hotkeyRef.current?.open()}
label={t("Hotkey Setting")} label={t("Hotkey Setting")}
/> />
<SettingItem
onClick={() => backupRef.current?.open()}
label={t("Backup Setting")}
extra={
<TooltipIcon
title={t("Backup Setting Info")}
sx={{ opacity: "0.7" }}
/>
}
/>
<SettingItem
onClick={() => configRef.current?.open()}
label={t("Runtime Config")}
/>
<SettingItem
onClick={openAppDir}
label={t("Open Conf Dir")}
extra={
<TooltipIcon
title={t("Open Conf Dir Info")}
sx={{ opacity: "0.7" }}
/>
}
/>
<SettingItem onClick={openCoreDir} label={t("Open Core Dir")} />
<SettingItem onClick={openLogsDir} label={t("Open Logs Dir")} />
<SettingItem onClick={onCheckUpdate} label={t("Check for Updates")} />
<SettingItem onClick={openDevTools} label={t("Open Dev Tools")} />
<SettingItem
label={t("Lite Mode")}
extra={
<TooltipIcon title={t("Lite Mode Info")} sx={{ opacity: "0.7" }} />
}
onClick={() => patchVerge({ enable_lite_mode: true })}
/>
<SettingItem
onClick={() => {
exitApp();
}}
label={t("Exit")}
/>
<SettingItem label={t("Verge Version")}>
<Typography sx={{ py: "7px", pr: 1 }}>v{version}</Typography>
</SettingItem>
</SettingList> </SettingList>
); );
}; };
export default SettingVerge; export default SettingVergeBasic;

View File

@@ -447,5 +447,7 @@
"File Not Found": "الملف غير موجود، تم التراجع عن التغييرات", "File Not Found": "الملف غير موجود، تم التراجع عن التغييرات",
"Script File Error": "خطأ في ملف السكريبت، تم التراجع عن التغييرات", "Script File Error": "خطأ في ملف السكريبت، تم التراجع عن التغييرات",
"Core Changed Successfully": "تم تغيير النواة بنجاح", "Core Changed Successfully": "تم تغيير النواة بنجاح",
"Failed to Change Core": "فشل تغيير النواة" "Failed to Change Core": "فشل تغيير النواة",
"Verge Basic Setting": "الإعدادات الأساسية Verge",
"Verge Advanced Setting": "الإعدادات الأساسية Verge"
} }

View File

@@ -277,7 +277,8 @@
"Open UWP tool": "Open UWP tool", "Open UWP tool": "Open UWP tool",
"Open UWP tool Info": "Since Windows 8, UWP apps (such as Microsoft Store) are restricted from directly accessing local host network services, and this tool can be used to bypass this restriction", "Open UWP tool Info": "Since Windows 8, UWP apps (such as Microsoft Store) are restricted from directly accessing local host network services, and this tool can be used to bypass this restriction",
"Update GeoData": "Update GeoData", "Update GeoData": "Update GeoData",
"Verge Setting": "Verge Setting", "Verge Basic Setting": "Verge Basic Setting",
"Verge Advanced Setting": "Verge Advanced Setting",
"Language": "Language", "Language": "Language",
"Theme Mode": "Theme Mode", "Theme Mode": "Theme Mode",
"theme.light": "Light", "theme.light": "Light",
@@ -445,5 +446,24 @@
"Config Validation Failed": "Subscription configuration validation failed. Please check the subscription configuration file; modifications have been rolled back.", "Config Validation Failed": "Subscription configuration validation failed. Please check the subscription configuration file; modifications have been rolled back.",
"Boot Config Validation Failed": "Boot subscription configuration validation failed. Started with the default configuration; please check the subscription configuration file.", "Boot Config Validation Failed": "Boot subscription configuration validation failed. Started with the default configuration; please check the subscription configuration file.",
"Core Change Config Validation Failed": "Configuration validation failed when switching the kernel. Started with the default configuration; please check the subscription configuration file.", "Core Change Config Validation Failed": "Configuration validation failed when switching the kernel. Started with the default configuration; please check the subscription configuration file.",
"Config Validation Process Terminated": "The validation process has been terminated." "Config Validation Process Terminated": "The validation process has been terminated.",
"Script Syntax Error": "Script syntax error, changes reverted",
"Script Missing Main": "Script error, changes reverted",
"File Not Found": "File missing, changes reverted",
"Script File Error": "Script file error, changes reverted",
"Core Changed Successfully": "Core changed successfully",
"Failed to Change Core": "Failed to change core",
"YAML Syntax Error": "YAML syntax error, changes reverted",
"YAML Read Error": "YAML read error, changes reverted",
"YAML Mapping Error": "YAML mapping error, changes reverted",
"YAML Key Error": "YAML key error, changes reverted",
"YAML Error": "YAML error, changes reverted",
"Merge File Syntax Error": "Merge file syntax error, changes reverted",
"Merge File Mapping Error": "Merge file mapping error, changes reverted",
"Merge File Key Error": "Merge file key error, changes reverted",
"Merge File Error": "Merge file error, changes reverted",
"Validate YAML File": "Validate YAML File",
"Validate Merge File": "Validate Merge File",
"Validation Success": "Validation Success",
"Validation Failed": "Validation Failed"
} }

View File

@@ -444,5 +444,7 @@
"File Not Found": "فایل یافت نشد، تغییرات برگشت داده شد", "File Not Found": "فایل یافت نشد، تغییرات برگشت داده شد",
"Script File Error": "خطای فایل اسکریپت، تغییرات برگشت داده شد", "Script File Error": "خطای فایل اسکریپت، تغییرات برگشت داده شد",
"Core Changed Successfully": "هسته با موفقیت تغییر کرد", "Core Changed Successfully": "هسته با موفقیت تغییر کرد",
"Failed to Change Core": "تغییر هسته ناموفق بود" "Failed to Change Core": "تغییر هسته ناموفق بود",
"Verge Basic Setting": "تنظیمات پایه Verge",
"Verge Advanced Setting": "تنظیمات پیشرفته Verge"
} }

View File

@@ -443,5 +443,7 @@
"File Not Found": "File tidak ditemukan, perubahan dibatalkan", "File Not Found": "File tidak ditemukan, perubahan dibatalkan",
"Script File Error": "Kesalahan file skrip, perubahan dibatalkan", "Script File Error": "Kesalahan file skrip, perubahan dibatalkan",
"Core Changed Successfully": "Inti berhasil diubah", "Core Changed Successfully": "Inti berhasil diubah",
"Failed to Change Core": "Gagal mengubah inti" "Failed to Change Core": "Gagal mengubah inti",
"Verge Basic Setting": "Pengaturan Dasar Verge",
"Verge Advanced Setting": "Pengaturan Lanjutan Verge"
} }

View File

@@ -444,5 +444,7 @@
"File Not Found": "Файл не найден, изменения отменены", "File Not Found": "Файл не найден, изменения отменены",
"Script File Error": "Ошибка файла скрипта, изменения отменены", "Script File Error": "Ошибка файла скрипта, изменения отменены",
"Core Changed Successfully": "Ядро успешно сменено", "Core Changed Successfully": "Ядро успешно сменено",
"Failed to Change Core": "Не удалось сменить ядро" "Failed to Change Core": "Не удалось сменить ядро",
"Verge Basic Setting": "Основные настройки Verge",
"Verge Advanced Setting": "Расширенные настройки Verge"
} }

View File

@@ -443,5 +443,7 @@
"File Not Found": "Файл табылмады, үзгәрешләр кире кайтарылды", "File Not Found": "Файл табылмады, үзгәрешләр кире кайтарылды",
"Script File Error": "Скрипт файлы хатасы, үзгәрешләр кире кайтарылды", "Script File Error": "Скрипт файлы хатасы, үзгәрешләр кире кайтарылды",
"Core Changed Successfully": "Ядро уңышлы алыштырылды", "Core Changed Successfully": "Ядро уңышлы алыштырылды",
"Failed to Change Core": "Ядро алыштыру уңышсыз булды" "Failed to Change Core": "Ядро алыштыру уңышсыз булды",
"Verge Basic Setting": "Verge Төп көйләүләр",
"Verge Advanced Setting": "Verge Киңәйтелгән көйләүләр"
} }

View File

@@ -135,9 +135,9 @@
"Hidden": "隐藏代理组", "Hidden": "隐藏代理组",
"Group Name Required": "代理组名称不能为空", "Group Name Required": "代理组名称不能为空",
"Group Name Already Exists": "代理组名称已存在", "Group Name Already Exists": "代理组名称已存在",
"Extend Config": "扩展配置", "Extend Config": "扩展覆写配置",
"Extend Script": "扩展脚本", "Extend Script": "扩展脚本",
"Global Merge": "全局扩展配置", "Global Merge": "全局扩展覆写配置",
"Global Script": "全局扩展脚本", "Global Script": "全局扩展脚本",
"Type": "类型", "Type": "类型",
"Name": "名称", "Name": "名称",
@@ -444,5 +444,20 @@
"File Not Found": "文件丢失,变更已撤销", "File Not Found": "文件丢失,变更已撤销",
"Script File Error": "脚本文件错误,变更已撤销", "Script File Error": "脚本文件错误,变更已撤销",
"Core Changed Successfully": "内核切换成功", "Core Changed Successfully": "内核切换成功",
"Failed to Change Core": "无法切换内核" "Failed to Change Core": "无法切换内核",
"YAML Syntax Error": "YAML语法错误变更已撤销",
"YAML Read Error": "YAML读取错误变更已撤销",
"YAML Mapping Error": "YAML映射错误变更已撤销",
"YAML Key Error": "YAML键错误变更已撤销",
"YAML Error": "YAML错误变更已撤销",
"Merge File Syntax Error": "覆写文件语法错误,变更已撤销",
"Merge File Mapping Error": "覆写文件映射错误,变更已撤销",
"Merge File Key Error": "覆写文件键错误,变更已撤销",
"Merge File Error": "覆写文件错误,变更已撤销",
"Validate YAML File": "验证YAML文件",
"Validate Merge File": "验证覆写文件",
"Validation Success": "验证成功",
"Validation Failed": "验证失败",
"Verge Basic Setting": "Verge 基础设置",
"Verge Advanced Setting": "Verge 高级设置"
} }

View File

@@ -84,6 +84,33 @@ const handleNoticeMessage = (
case "config_validate::file_not_found": case "config_validate::file_not_found":
Notice.error(`${t("File Not Found")} ${msg}`); Notice.error(`${t("File Not Found")} ${msg}`);
break; break;
case "config_validate::yaml_syntax_error":
Notice.error(`${t("YAML Syntax Error")} ${msg}`);
break;
case "config_validate::yaml_read_error":
Notice.error(`${t("YAML Read Error")} ${msg}`);
break;
case "config_validate::yaml_mapping_error":
Notice.error(`${t("YAML Mapping Error")} ${msg}`);
break;
case "config_validate::yaml_key_error":
Notice.error(`${t("YAML Key Error")} ${msg}`);
break;
case "config_validate::yaml_error":
Notice.error(`${t("YAML Error")} ${msg}`);
break;
case "config_validate::merge_syntax_error":
Notice.error(`${t("Merge File Syntax Error")} ${msg}`);
break;
case "config_validate::merge_mapping_error":
Notice.error(`${t("Merge File Mapping Error")} ${msg}`);
break;
case "config_validate::merge_key_error":
Notice.error(`${t("Merge File Key Error")} ${msg}`);
break;
case "config_validate::merge_error":
Notice.error(`${t("Merge File Error")} ${msg}`);
break;
case "config_core::change_success": case "config_core::change_success":
Notice.success(`${t("Core Changed Successfully")}: ${msg}`); Notice.success(`${t("Core Changed Successfully")}: ${msg}`);
break; break;
@@ -202,12 +229,14 @@ const Layout = () => {
}} }}
sx={[ sx={[
({ palette }) => ({ bgcolor: palette.background.paper }), ({ palette }) => ({ bgcolor: palette.background.paper }),
{ OS === "linux"
borderRadius: "8px", ? {
border: "2px solid var(--divider-color)", borderRadius: "8px",
width: "calc(100vw - 4px)", border: "1px solid var(--divider-color)",
height: "calc(100vh - 4px)", width: "calc(100vw - 0px)",
}, height: "calc(100vh - 0px)",
}
: {},
]} ]}
> >
<div className="layout__left"> <div className="layout__left">

View File

@@ -11,7 +11,8 @@ import { useTranslation } from "react-i18next";
import { BasePage, Notice } from "@/components/base"; import { BasePage, Notice } from "@/components/base";
import { GitHub, HelpOutlineRounded, Telegram } from "@mui/icons-material"; import { GitHub, HelpOutlineRounded, Telegram } from "@mui/icons-material";
import { openWebUrl } from "@/services/cmds"; import { openWebUrl } from "@/services/cmds";
import SettingVerge from "@/components/setting/setting-verge"; import SettingVergeBasic from "@/components/setting/setting-verge-basic";
import SettingVergeAdvanced from "@/components/setting/setting-verge-advanced";
import SettingClash from "@/components/setting/setting-clash"; import SettingClash from "@/components/setting/setting-clash";
import SettingSystem from "@/components/setting/setting-system"; import SettingSystem from "@/components/setting/setting-system";
import { useThemeMode } from "@/services/states"; import { useThemeMode } from "@/services/states";
@@ -95,10 +96,19 @@ const SettingPage = () => {
<Box <Box
sx={{ sx={{
borderRadius: 2, borderRadius: 2,
marginBottom: 1.5,
backgroundColor: isDark ? "#282a36" : "#ffffff", backgroundColor: isDark ? "#282a36" : "#ffffff",
}} }}
> >
<SettingVerge onError={onError} /> <SettingVergeBasic onError={onError} />
</Box>
<Box
sx={{
borderRadius: 2,
backgroundColor: isDark ? "#282a36" : "#ffffff",
}}
>
<SettingVergeAdvanced onError={onError} />
</Box> </Box>
</Grid> </Grid>
</Grid> </Grid>