Compare commits

...

21 Commits

33 changed files with 699 additions and 346 deletions

View File

@@ -271,9 +271,23 @@ jobs:
- name: Rename
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'
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'
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'
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
foreach ($file in $files) {
$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
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策略
- 改进虚拟列表渲染性能Virtuoso
- 提升主窗口Clash模式切换速度 感谢Tunglies
- 提升主窗口Clash模式切换速度感谢Tunglies
- 加速内核关闭流程并优化管理逻辑
- 优化节点延迟刷新速率
- 改进托盘网速显示更新逻辑
- 提升配置验证错误信息的可读性
- 重构服务架构,优化代码组织结构 感谢Tunglies
- 重构服务架构优化代码组织结构感谢Tunglies
- 优化内核启动时的配置验证流程
### 问题修复

View File

@@ -1,6 +1,6 @@
{
"name": "clash-verge",
"version": "2.1.0",
"version": "2.1.2",
"license": "GPL-3.0-only",
"scripts": {
"dev": "cross-env RUST_BACKTRACE=1 tauri dev",
@@ -37,7 +37,7 @@
"@tauri-apps/plugin-notification": "^2.2.1",
"@tauri-apps/plugin-process": "^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",
"ahooks": "^3.8.4",
"axios": "^1.7.9",

10
pnpm-lock.yaml generated
View File

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

View File

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

2
src-tauri/Cargo.lock generated
View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "clash-verge"
version = "2.1.0"
version = "2.1.2"
description = "clash verge"
authors = ["zzzgydi", "wonfen", "MystiPanda"]
license = "GPL-3.0-only"
@@ -81,7 +81,7 @@ users = "0.11.0"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-autostart = "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"
#openssl

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

Binary file not shown.

View File

@@ -6,6 +6,13 @@
"permissions": [
"global-shortcut: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",
"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_guard = profiles.latest();
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 path = item.file.clone().ok_or("file field is null")?;
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()))?;
let file_path_str = file_path.to_string_lossy();
println!("[cmd配置save] 开始验证配置文件: {}", file_path_str);
let file_path_str = file_path.to_string_lossy().to_string();
println!("[cmd配置save] 开始验证配置文件: {}, 是否为merge文件: {}", file_path_str, is_merge_file);
// 验证配置文件
match CoreManager::global().validate_config_file(&file_path_str).await {
// 对于 merge 文件,只进行语法验证,不进行后续内核验证
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, _)) => {
println!("[cmd配置save] 验证成功");
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))?;
// 智能判断是否为脚本错误
// 智能判断错误类型
let is_script_error = file_path_str.ends_with(".js") ||
error_msg.contains("Script syntax error") ||
error_msg.contains("Script must contain a main function") ||
error_msg.contains("Failed to read script file");
error_msg.contains("Script syntax error") ||
error_msg.contains("Script must contain a main function") ||
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());
handle_script_validation_notice(&result, "脚本文件");
} else {
// 普通配置错误使用一般通知
println!("[cmd配置save] 其他类型验证失败,发送一般通知");
handle::Handle::notice_message("config_validate::error", &error_msg);
}
@@ -292,7 +334,7 @@ pub fn get_verge_config() -> CmdResult<IVergeResponse> {
#[tauri::command]
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]
@@ -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> {
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) => {
handle_script_validation_notice(&result, "脚本文件");
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::core::{clash_api, handle, service};
#[cfg(target_os = "macos")]
use crate::core::tray::Tray;
use crate::log_err;
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() {
let error_msg = format!("File not found: {}", config_path);
@@ -247,6 +248,12 @@ impl CoreManager {
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") {
true
@@ -273,6 +280,14 @@ impl CoreManager {
/// 检查文件是否为脚本文件
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) {
Ok(content) => content,
Err(err) => {
@@ -281,15 +296,52 @@ impl CoreManager {
}
};
// 检查文件前几行是否包含JavaScript特征
let first_lines = content.lines().take(5).collect::<String>();
Ok(first_lines.contains("function") ||
first_lines.contains("//") ||
first_lines.contains("/*") ||
first_lines.contains("import") ||
first_lines.contains("export") ||
first_lines.contains("const ") ||
first_lines.contains("let "))
// 3. 检查是否存在明显的YAML特征
let has_yaml_features = content.contains(": ") ||
content.contains("#") ||
content.contains("---") ||
content.lines().any(|line| line.trim().starts_with("- "));
// 4. 检查是否存在明显的JS特征
let has_js_features = content.contains("function ") ||
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,
Err(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));
}
};
@@ -394,85 +447,34 @@ impl CoreManager {
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::Path;
async fn create_test_script() -> Result<String> {
let temp_dir = std::env::temp_dir();
let script_path = temp_dir.join("test_script.js");
let script_content = r#"
// This is a test script
function main(config) {
console.log("Testing script");
return config;
/// 只进行文件语法检查,不进行完整验证
async fn validate_file_syntax(&self, config_path: &str) -> Result<(bool, String)> {
println!("[core配置语法检查] 开始检查文件: {}", config_path);
// 读取文件内容
let content = match std::fs::read_to_string(config_path) {
Ok(content) => content,
Err(err) => {
let error_msg = format!("Failed to read file: {}", err);
println!("[core配置语法检查] 无法读取文件: {}", error_msg);
return Ok((false, error_msg));
}
};
// 对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 来确保在正确的线程上执行
async_runtime::spawn_blocking(|| {
println!("Creating window in spawn_blocking");
log::info!(target: "app", "Creating window in spawn_blocking");
resolve::create_window();
println!("Toggle dashboard window visibility");
log::info!(target: "app", "Toggle dashboard window visibility");
// 检查窗口是否存在
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 ===");
@@ -117,7 +140,7 @@ impl Hotkey {
"clash_mode_global" => || feat::change_clash_mode("global".into()),
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
"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)),
_ => {
@@ -146,7 +169,23 @@ impl Hotkey {
// 直接执行函数,不做任何状态检查
println!("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
const SERVICE_URL: &str = "http://127.0.0.1:33211";
const REQUIRED_SERVICE_VERSION: &str = "1.0.1"; // 定义所需的服务版本号
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ResponseBody {
@@ -19,6 +20,12 @@ pub struct ResponseBody {
pub log_file: String,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct VersionResponse {
pub service: String,
pub version: String,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct JsonResponse {
pub code: u64,
@@ -26,6 +33,13 @@ pub struct JsonResponse {
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")]
pub async fn reinstall_service() -> Result<()> {
log::info!(target:"app", "reinstall service");
@@ -177,8 +191,43 @@ pub async fn check_service() -> Result<JsonResponse> {
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
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 = clash_core.unwrap_or("verge-mihomo".into());

View File

@@ -87,7 +87,7 @@ impl Tray {
{
match tray_event.as_str() {
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(),
"tun_mode" => feat::toggle_tun_mode(None),
"main_window" => resolve::create_window(),
_ => {}
}
@@ -102,7 +102,7 @@ impl Tray {
{
match tray_event.as_str() {
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(),
"tun_mode" => feat::toggle_tun_mode(None),
"main_window" => resolve::create_window(),
_ => {}
}
@@ -225,23 +225,27 @@ impl Tray {
#[cfg(target_os = "macos")]
{
let enable_tray_speed = Config::verge().latest().enable_tray_speed.unwrap_or(true);
let is_template =
crate::utils::help::is_monochrome_image_from_bytes(&icon_bytes).unwrap_or(false);
let icon_bytes = if enable_tray_speed {
let is_colorful = tray_icon == "colorful";
// 处理图标和速率
let final_icon_bytes = if enable_tray_speed {
let rate = rate.or_else(|| {
self.speed_rate
.lock()
.as_ref()
.and_then(|speed_rate| speed_rate.get_curent_rate())
});
// 使用新的方法渲染图标和速率
SpeedRate::add_speed_text(icon_bytes, rate)?
} else {
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"))]
@@ -590,7 +594,7 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
}
"open_window" => resolve::create_window(),
"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(),
"open_app_dir" => crate::log_err!(cmds::open_app_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 anyhow::Result;
use futures::Stream;
use image::{ImageBuffer, Rgba};
use image::{Rgba, GenericImageView, RgbaImage};
use imageproc::drawing::draw_text_mut;
use parking_lot::Mutex;
use rusttype::{Font, Scale};
@@ -14,7 +14,7 @@ use tokio_tungstenite::tungstenite::Message;
pub struct SpeedRate {
rate: Arc<Mutex<(Rate, Rate)>>,
last_update: Arc<Mutex<std::time::Instant>>,
base_image: Arc<Mutex<Option<(ImageBuffer<Rgba<u8>, Vec<u8>>, u32, u32)>>>, // 存储基础图像和尺寸
// 移除 base_image,不再缓存原始图像
}
impl SpeedRate {
@@ -22,7 +22,6 @@ impl SpeedRate {
Self {
rate: Arc::new(Mutex::new((Rate::default(), Rate::default()))),
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())
}
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 base_image = {
let tray = Self::global();
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 icon_image = image::load_from_memory(&icon_bytes)?;
let (icon_width, icon_height) = (icon_image.width(), icon_image.height());
let font =
Font::try_from_bytes(include_bytes!("../../../assets/fonts/FiraCode-Medium.ttf")).unwrap();
// 修改颜色和阴影参数
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;
// 判断是否为彩色图标
let is_colorful = !crate::utils::help::is_monochrome_image_from_bytes(&icon_bytes).unwrap_or(false);
// 用透明色清除文字区域
for x in text_area_start..image.width() as i32 {
for y in 0..image.height() as i32 {
image.put_pixel(x as u32, y as u32, Rgba([0, 0, 0, 0]));
// 增加文本宽度和间距
let text_width = 580; // 文本区域宽度
let total_width = icon_width + text_width;
// 创建新的透明画布
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_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;
// 计算垂直位置
let up_y = 0; // 上行速率紧贴顶部
let down_y = height as i32 - base_size as i32; // 下行速率紧贴底部
// 选择文本颜色
let (text_color, shadow_color) = if is_colorful {
// 彩色图标使用黑色文本和轻微白色阴影
(Rgba([255u8, 255u8, 255u8, 255u8]), Rgba([0u8, 0u8, 0u8, 160u8]))
} else {
// 单色图标使用白色文本和轻微黑色阴影
(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;
// 绘制上行速率(先画阴影,再画文字)
// 绘制上行速率
draw_text_mut(
&mut image,
&mut combined_image,
shadow_color,
text_start_x_up + shadow_offset,
up_text_x as i32 + shadow_offset,
up_y + shadow_offset,
scale,
&font,
&up_text,
);
draw_text_mut(
&mut image,
&mut combined_image,
text_color,
text_start_x_up,
up_text_x as i32,
up_y,
scale,
&font,
&up_text,
);
// 绘制下行速率(先画阴影,再画文字)
// 绘制下行速率
draw_text_mut(
&mut image,
&mut combined_image,
shadow_color,
text_start_x_down + shadow_offset,
down_text_x as i32 + shadow_offset,
down_y + shadow_offset,
scale,
&font,
&down_text,
);
draw_text_mut(
&mut image,
&mut combined_image,
text_color,
text_start_x_down,
down_text_x as i32,
down_y,
scale,
&font,
&down_text,
);
let mut bytes: Vec<u8> = Vec::new();
let mut cursor = Cursor::new(&mut bytes);
image.write_to(&mut cursor, image::ImageFormat::Png)?;
// 将结果转换为 PNG 数据
let mut bytes = Vec::new();
combined_image.write_to(&mut Cursor::new(&mut bytes), image::ImageFormat::Png)?;
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)]

View File

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

View File

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

View File

@@ -25,24 +25,23 @@
"devUrl": "http://localhost:3000/"
},
"productName": "Clash Verge",
"version": "2.1.0",
"version": "2.1.2",
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"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": {
"desktop": {
"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": {

View File

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

View File

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

View File

@@ -138,6 +138,9 @@ export const ConnectionTable = (props: Props) => {
const { metadata, rulePayload } = each;
const chains = [...each.chains].reverse().join(" / ");
const rule = rulePayload ? `${each.rule}(${rulePayload})` : each.rule;
const Destination = metadata.destinationIP
? `${metadata.destinationIP}:${metadata.destinationPort}`
: `${metadata.remoteDestination}:${metadata.destinationPort}`;
return {
id: each.id,
host: metadata.host
@@ -152,7 +155,7 @@ export const ConnectionTable = (props: Props) => {
process: truncateStr(metadata.process || metadata.processPath),
time: each.start,
source: `${metadata.sourceIP}:${metadata.sourcePort}`,
remoteDestination: `${metadata.remoteDestination}:${metadata.destinationPort}`,
remoteDestination: Destination,
type: `${metadata.type}(${metadata.network})`,
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 { useTranslation } from "react-i18next";
import { open } from "@tauri-apps/plugin-dialog";
import { Button, MenuItem, Select, Input, Typography } from "@mui/material";
import {
exitApp,
openAppDir,
openCoreDir,
openLogsDir,
openDevTools,
copyClashEnv,
} from "@/services/cmds";
import { check as checkUpdate } from "@tauri-apps/plugin-updater";
import { Button, MenuItem, Select, Input } from "@mui/material";
import { copyClashEnv } from "@/services/cmds";
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 { ThemeModeSwitch } from "./mods/theme-mode-switch";
@@ -49,7 +40,7 @@ const languageOptions = Object.entries(languages).map(([code, _]) => {
return { code, label: labels[code] };
});
const SettingVerge = ({ onError }: Props) => {
const SettingVergeBasic = ({ onError }: Props) => {
const { t } = useTranslation();
const { verge, patchVerge, mutateVerge } = useVerge();
@@ -60,7 +51,6 @@ const SettingVerge = ({ onError }: Props) => {
env_type,
startup_script,
start_page,
enable_lite_mode,
} = verge ?? {};
const configRef = useRef<DialogRef>(null);
const hotkeyRef = useRef<DialogRef>(null);
@@ -74,26 +64,13 @@ const SettingVerge = ({ onError }: Props) => {
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 () => {
await copyClashEnv();
Notice.success(t("Copy Success"), 1000);
}, []);
return (
<SettingList title={t("Verge Setting")}>
<SettingList title={t("Verge Basic Setting")}>
<ThemeViewer ref={themeRef} />
<ConfigViewer ref={configRef} />
<HotkeyViewer ref={hotkeyRef} />
@@ -261,62 +238,8 @@ const SettingVerge = ({ onError }: Props) => {
onClick={() => hotkeyRef.current?.open()}
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>
);
};
export default SettingVerge;
export default SettingVergeBasic;

View File

@@ -447,5 +447,7 @@
"File Not Found": "الملف غير موجود، تم التراجع عن التغييرات",
"Script File Error": "خطأ في ملف السكريبت، تم التراجع عن التغييرات",
"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 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",
"Verge Setting": "Verge Setting",
"Verge Basic Setting": "Verge Basic Setting",
"Verge Advanced Setting": "Verge Advanced Setting",
"Language": "Language",
"Theme Mode": "Theme Mode",
"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.",
"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.",
"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": "فایل یافت نشد، تغییرات برگشت داده شد",
"Script File Error": "خطای فایل اسکریپت، تغییرات برگشت داده شد",
"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",
"Script File Error": "Kesalahan file skrip, perubahan dibatalkan",
"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": "Файл не найден, изменения отменены",
"Script File Error": "Ошибка файла скрипта, изменения отменены",
"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": "Файл табылмады, үзгәрешләр кире кайтарылды",
"Script File Error": "Скрипт файлы хатасы, үзгәрешләр кире кайтарылды",
"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": "隐藏代理组",
"Group Name Required": "代理组名称不能为空",
"Group Name Already Exists": "代理组名称已存在",
"Extend Config": "扩展配置",
"Extend Config": "扩展覆写配置",
"Extend Script": "扩展脚本",
"Global Merge": "全局扩展配置",
"Global Merge": "全局扩展覆写配置",
"Global Script": "全局扩展脚本",
"Type": "类型",
"Name": "名称",
@@ -444,5 +444,20 @@
"File Not Found": "文件丢失,变更已撤销",
"Script File Error": "脚本文件错误,变更已撤销",
"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":
Notice.error(`${t("File Not Found")} ${msg}`);
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":
Notice.success(`${t("Core Changed Successfully")}: ${msg}`);
break;
@@ -202,12 +229,14 @@ const Layout = () => {
}}
sx={[
({ palette }) => ({ bgcolor: palette.background.paper }),
{
borderRadius: "8px",
border: "2px solid var(--divider-color)",
width: "calc(100vw - 4px)",
height: "calc(100vh - 4px)",
},
OS === "linux"
? {
borderRadius: "8px",
border: "1px solid var(--divider-color)",
width: "calc(100vw - 0px)",
height: "calc(100vh - 0px)",
}
: {},
]}
>
<div className="layout__left">

View File

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