Merge branch 'dev' into dev

This commit is contained in:
Junkai W.
2025-08-30 02:37:07 +08:00
committed by GitHub
Unverified
48 changed files with 3476 additions and 1475 deletions

View File

@@ -294,6 +294,15 @@ jobs:
sudo apt-get update
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
- name: Install x86 OpenSSL (macOS only)
if: matrix.target == 'x86_64-apple-darwin'
run: |
arch -x86_64 brew install openssl@3
echo "OPENSSL_DIR=$(brew --prefix openssl@3)" >> $GITHUB_ENV
echo "OPENSSL_INCLUDE_DIR=$(brew --prefix openssl@3)/include" >> $GITHUB_ENV
echo "OPENSSL_LIB_DIR=$(brew --prefix openssl@3)/lib" >> $GITHUB_ENV
echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
- name: Install Node
uses: actions/setup-node@v4
with:

View File

@@ -176,6 +176,15 @@ jobs:
sudo apt-get update
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
- name: Install x86 OpenSSL (macOS only)
if: matrix.target == 'x86_64-apple-darwin'
run: |
arch -x86_64 brew install openssl@3
echo "OPENSSL_DIR=$(brew --prefix openssl@3)" >> $GITHUB_ENV
echo "OPENSSL_INCLUDE_DIR=$(brew --prefix openssl@3)/include" >> $GITHUB_ENV
echo "OPENSSL_LIB_DIR=$(brew --prefix openssl@3)/lib" >> $GITHUB_ENV
echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
- uses: pnpm/action-setup@v4
name: Install pnpm
with:

View File

@@ -187,6 +187,15 @@ jobs:
sudo apt-get update
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
- name: Install x86 OpenSSL (macOS only)
if: matrix.target == 'x86_64-apple-darwin'
run: |
arch -x86_64 brew install openssl@3
echo "OPENSSL_DIR=$(brew --prefix openssl@3)" >> $GITHUB_ENV
echo "OPENSSL_INCLUDE_DIR=$(brew --prefix openssl@3)/include" >> $GITHUB_ENV
echo "OPENSSL_LIB_DIR=$(brew --prefix openssl@3)/lib" >> $GITHUB_ENV
echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
- name: Install Node
uses: actions/setup-node@v4
with:

View File

@@ -3,7 +3,10 @@
### 🏆 重大改进
- **应用响应速度提升**:采用全新异步处理架构,大幅提升应用响应速度和稳定性
- **文件操作性能提升**:优化文件读写性能,减少应用等待时间
### ✨ 新增功能
- **Mihomo(Meta) 内核升级至 v1.19.13**
### 🚀 性能优化
@@ -11,6 +14,8 @@
- 改进服务管理响应性,减少系统服务操作等待时间
- 提升文件和配置处理性能
- 优化任务管理和日志记录效率
- 优化异步内存管理,减少内存占用并提升多任务处理效率
- 优化启动阶段初始化性能
### 🐞 修复问题

View File

@@ -38,7 +38,7 @@
"@mui/icons-material": "^7.3.1",
"@mui/lab": "7.0.0-beta.16",
"@mui/material": "^7.3.1",
"@mui/x-data-grid": "^8.10.2",
"@mui/x-data-grid": "^8.11.0",
"@tauri-apps/api": "2.8.0",
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
"@tauri-apps/plugin-dialog": "^2.3.3",
@@ -51,7 +51,7 @@
"ahooks": "^3.9.4",
"axios": "^1.11.0",
"cli-color": "^2.0.4",
"dayjs": "1.11.13",
"dayjs": "1.11.16",
"foxact": "^0.2.49",
"glob": "^11.0.3",
"i18next": "^25.4.2",
@@ -65,7 +65,7 @@
"react-dom": "19.1.1",
"react-error-boundary": "6.0.0",
"react-hook-form": "^7.62.0",
"react-i18next": "15.7.2",
"react-i18next": "15.7.3",
"react-markdown": "10.1.0",
"react-monaco-editor": "0.59.0",
"react-router-dom": "7.8.2",
@@ -82,10 +82,10 @@
"@tauri-apps/cli": "2.8.3",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/react": "19.1.11",
"@types/react-dom": "19.1.8",
"@types/react": "19.1.12",
"@types/react-dom": "19.1.9",
"@vitejs/plugin-legacy": "^7.2.1",
"@vitejs/plugin-react": "5.0.1",
"@vitejs/plugin-react": "5.0.2",
"adm-zip": "^0.5.16",
"commander": "^14.0.0",
"cross-env": "^10.0.0",

2104
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -38,5 +38,5 @@
}
],
"postUpdateOptions": ["pnpmDedupe"],
"ignoreDeps": ["serde_yaml", "sysinfo", "zip", "criterion"]
"ignoreDeps": ["criterion"]
}

861
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -25,10 +25,10 @@ dunce = "1.0.5"
log4rs = "1.3.0"
nanoid = "0.4"
chrono = "0.4.41"
sysinfo = { version = "=0.35.2", features = ["network", "system"] }
sysinfo = { version = "0.37.0", features = ["network", "system"] }
boa_engine = "0.20.0"
serde_json = "1.0.143"
serde_yaml = "0.9.34"
serde_yaml_ng = "0.10.0"
once_cell = "1.21.3"
port_scanner = "0.1.5"
delay_timer = "0.11.6"
@@ -41,7 +41,7 @@ tokio = { version = "1.47.1", features = [
"sync",
] }
serde = { version = "1.0.219", features = ["derive"] }
reqwest = { version = "0.12.23", features = ["json", "rustls-tls", "cookies"] }
reqwest = { version = "0.12.23", features = ["json", "cookies"] }
regex = "1.11.2"
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
tauri = { version = "2.8.4", features = [
@@ -60,8 +60,8 @@ tauri-plugin-clipboard-manager = "2.3.0"
tauri-plugin-deep-link = "2.4.2"
tauri-plugin-devtools = "2.0.1"
tauri-plugin-window-state = "2.4.0"
zip = "=4.2.0"
reqwest_dav = "0.2.1"
zip = "4.5.0"
reqwest_dav = "0.2.2"
aes-gcm = { version = "0.10.3", features = ["std"] }
base64 = "0.22.1"
getrandom = "0.3.3"
@@ -73,12 +73,16 @@ hmac = "0.12.1"
sha2 = "0.10.9"
hex = "0.4.3"
scopeguard = "1.2.0"
kode-bridge = "0.2.1-rc1"
kode-bridge = "0.2.1-rc2"
dashmap = "6.1.0"
tauri-plugin-notification = "2.3.1"
console-subscriber = { version = "0.4.1", optional = true }
tokio-stream = "0.1.17"
isahc = { version = "1.7.2", features = ["parking_lot"] }
isahc = { version = "1.7.2", default-features = false, features = [
"text-decoding",
"parking_lot",
] }
[target.'cfg(windows)'.dependencies]
runas = "=1.2.0"

View File

@@ -209,7 +209,7 @@ pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
#[tauri::command]
pub fn notify_ui_ready() -> CmdResult<()> {
log::info!(target: "app", "前端UI已准备就绪");
crate::utils::resolve::mark_ui_ready();
crate::utils::resolve::ui::mark_ui_ready();
Ok(())
}
@@ -218,7 +218,7 @@ pub fn notify_ui_ready() -> CmdResult<()> {
pub fn update_ui_stage(stage: String) -> CmdResult<()> {
log::info!(target: "app", "UI加载阶段更新: {stage}");
use crate::utils::resolve::UiReadyStage;
use crate::utils::resolve::ui::UiReadyStage;
let stage_enum = match stage.as_str() {
"NotStarted" => UiReadyStage::NotStarted,
@@ -232,7 +232,7 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> {
}
};
crate::utils::resolve::update_ui_ready_stage(stage_enum);
crate::utils::resolve::ui::update_ui_ready_stage(stage_enum);
Ok(())
}
@@ -240,6 +240,6 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> {
#[tauri::command]
pub fn reset_ui_ready_state() -> CmdResult<()> {
log::info!(target: "app", "重置UI就绪状态");
crate::utils::resolve::reset_ui_ready();
crate::utils::resolve::ui::reset_ui_ready();
Ok(())
}

View File

@@ -12,7 +12,7 @@ use crate::{
utils::logging::Type,
wrap_err,
};
use serde_yaml::Mapping;
use serde_yaml_ng::Mapping;
use std::time::Duration;
const CONFIG_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
@@ -143,7 +143,7 @@ pub async fn test_delay(url: String) -> CmdResult<u32> {
#[tauri::command]
pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
use crate::utils::dirs;
use serde_yaml;
use serde_yaml_ng;
use tokio::fs;
// 获取DNS配置文件路径
@@ -152,7 +152,7 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
.join("dns_config.yaml");
// 保存DNS配置到文件
let yaml_str = serde_yaml::to_string(&dns_config).map_err(|e| e.to_string())?;
let yaml_str = serde_yaml_ng::to_string(&dns_config).map_err(|e| e.to_string())?;
fs::write(&dns_path, yaml_str)
.await
.map_err(|e| e.to_string())?;
@@ -187,15 +187,16 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
})?;
// 解析DNS配置
let patch_config = serde_yaml::from_str::<serde_yaml::Mapping>(&dns_yaml).map_err(|e| {
logging!(error, Type::Config, "Failed to parse DNS config: {e}");
e.to_string()
})?;
let patch_config =
serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml).map_err(|e| {
logging!(error, Type::Config, "Failed to parse DNS config: {e}");
e.to_string()
})?;
logging!(info, Type::Config, "Applying DNS config from file");
// 创建包含DNS配置的patch
let mut patch = serde_yaml::Mapping::new();
let mut patch = serde_yaml_ng::Mapping::new();
patch.insert("dns".into(), patch_config.into());
// 应用DNS配置到运行时配置

View File

@@ -3,7 +3,7 @@ use crate::core::{async_proxy_query::AsyncProxyQuery, EventDrivenProxyManager};
use crate::process::AsyncHandler;
use crate::wrap_err;
use network_interface::NetworkInterface;
use serde_yaml::Mapping;
use serde_yaml_ng::Mapping;
/// get the system proxy
#[tauri::command]

View File

@@ -381,7 +381,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
match file_read_result {
Ok(Ok(content)) => {
let yaml_parse_result = AsyncHandler::spawn_blocking(move || {
serde_yaml::from_str::<serde_yaml::Value>(&content)
serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&content)
})
.await;

View File

@@ -1,7 +1,7 @@
use super::CmdResult;
use crate::{config::*, wrap_err};
use anyhow::Context;
use serde_yaml::Mapping;
use serde_yaml_ng::Mapping;
use std::collections::HashMap;
/// 获取运行时配置
@@ -19,7 +19,7 @@ pub async fn get_runtime_yaml() -> CmdResult<String> {
wrap_err!(config
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
.and_then(
|config| serde_yaml::to_string(config).context("failed to convert config to yaml")
|config| serde_yaml_ng::to_string(config).context("failed to convert config to yaml")
))
}

View File

@@ -3,7 +3,7 @@ use crate::utils::dirs::{ipc_path, path_to_str};
use crate::utils::{dirs, help};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_yaml::{Mapping, Value};
use serde_yaml_ng::{Mapping, Value};
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
str::FromStr,

View File

@@ -3,7 +3,6 @@ use crate::{
config::{profiles_append_item_safe, PrfItem},
core::{handle, CoreManager},
enhance, logging,
process::AsyncHandler,
utils::{dirs, help, logging::Type},
};
use anyhow::{anyhow, Result};
@@ -123,10 +122,8 @@ impl Config {
// 在单独的任务中发送通知
if let Some((msg_type, msg_content)) = validation_result {
AsyncHandler::spawn(move || async move {
sleep(Duration::from_secs(2)).await;
handle::Handle::notice_message(msg_type, &msg_content);
});
sleep(Duration::from_secs(2)).await;
handle::Handle::notice_message(msg_type, &msg_content);
}
Ok(())

View File

@@ -5,7 +5,7 @@ use crate::utils::{
};
use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use serde_yaml::Mapping;
use serde_yaml_ng::Mapping;
use std::{fs, time::Duration};
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
@@ -355,7 +355,7 @@ impl PrfItem {
let data = data.trim_start_matches('\u{feff}');
// check the data whether the valid yaml format
let yaml = serde_yaml::from_str::<Mapping>(data)
let yaml = serde_yaml_ng::from_str::<Mapping>(data)
.context("the remote profile data is invalid yaml")?;
if !yaml.contains_key("proxies") && !yaml.contains_key("proxy-providers") {

View File

@@ -6,7 +6,7 @@ use crate::{
};
use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use serde_yaml::Mapping;
use serde_yaml_ng::Mapping;
use std::collections::HashSet;
use tokio::fs;

View File

@@ -1,6 +1,6 @@
use crate::enhance::field::use_keys;
use serde::{Deserialize, Serialize};
use serde_yaml::{Mapping, Value};
use serde_yaml_ng::{Mapping, Value};
use std::collections::HashMap;
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct IRuntime {

View File

@@ -264,14 +264,14 @@ pub fn create_backup() -> Result<(String, PathBuf), Error> {
zip.write_all(fs::read(dirs::clash_path()?)?.as_slice())?;
let mut verge_config: serde_json::Value =
serde_yaml::from_str(&fs::read_to_string(dirs::verge_path()?)?)?;
serde_yaml_ng::from_str(&fs::read_to_string(dirs::verge_path()?)?)?;
if let Some(obj) = verge_config.as_object_mut() {
obj.remove("webdav_username");
obj.remove("webdav_password");
obj.remove("webdav_url");
}
zip.start_file(dirs::VERGE_CONFIG, options)?;
zip.write_all(serde_yaml::to_string(&verge_config)?.as_bytes())?;
zip.write_all(serde_yaml_ng::to_string(&verge_config)?.as_bytes())?;
zip.start_file(dirs::PROFILE_YAML, options)?;
zip.write_all(fs::read(dirs::profiles_path()?)?.as_slice())?;

View File

@@ -317,7 +317,7 @@ impl CoreManager {
};
// 对YAML文件尝试解析只检查语法正确性
logging!(info, Type::Config, true, "进行YAML语法检查");
match serde_yaml::from_str::<serde_yaml::Value>(&content) {
match serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&content) {
Ok(_) => {
logging!(info, Type::Config, true, "YAML语法检查通过");
Ok((true, String::new()))

View File

@@ -60,8 +60,6 @@ impl Timer {
return Ok(());
}
logging!(info, Type::Timer, true, "Initializing timer...");
// Initialize timer tasks
if let Err(e) = self.refresh().await {
// Reset initialization flag on error

View File

@@ -13,7 +13,7 @@ use crate::{
logging,
module::lightweight::is_in_lightweight_mode,
singleton_lazy,
utils::{dirs::find_target_icons, i18n::t, resolve::VERSION},
utils::{dirs::find_target_icons, i18n::t},
Type,
};
@@ -189,7 +189,11 @@ impl Default for Tray {
singleton_lazy!(Tray, TRAY, Tray::default);
impl Tray {
pub fn init(&self) -> Result<()> {
pub async fn init(&self) -> Result<()> {
let app_handle = handle::Handle::global()
.app_handle()
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for tray initialization"))?;
self.create_tray_from_handle(&app_handle).await?;
Ok(())
}
@@ -409,14 +413,6 @@ impl Tray {
}
};
let version = match VERSION.get() {
Some(v) => v,
None => {
log::warn!(target: "app", "更新托盘提示失败: 版本信息不存在");
return Ok(());
}
};
let verge = Config::verge().await.latest_ref().clone();
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
@@ -447,6 +443,7 @@ impl Tray {
let tun_text = t("TUN").await;
let profile_text = t("Profile").await;
let version = env!("CARGO_PKG_VERSION");
if let Some(tray) = app_handle.tray_by_id("main") {
let _ = tray.set_tooltip(Some(&format!(
"Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}",
@@ -473,10 +470,6 @@ impl Tray {
Ok(())
}
/// 取消订阅 traffic 数据
#[cfg(target_os = "macos")]
pub fn unsubscribe_traffic(&self) {}
pub async fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> {
log::info!(target: "app", "正在从AppHandle创建系统托盘");
@@ -599,6 +592,7 @@ async fn create_tray_menu(
let unknown_version = String::from("unknown");
let version = VERSION.get().unwrap_or(&unknown_version);
let hotkeys = Config::verge()
.await
.latest_ref()

View File

@@ -3,7 +3,7 @@ use crate::{
config::PrfItem,
utils::{dirs, help},
};
use serde_yaml::Mapping;
use serde_yaml_ng::Mapping;
use std::fs;
#[derive(Debug, Clone)]

View File

@@ -1,4 +1,4 @@
use serde_yaml::{Mapping, Value};
use serde_yaml_ng::{Mapping, Value};
use std::collections::HashSet;
pub const HANDLE_FIELDS: [&str; 12] = [

View File

@@ -1,5 +1,5 @@
use super::use_lowercase;
use serde_yaml::{self, Mapping, Value};
use serde_yaml_ng::{self, Mapping, Value};
fn deep_merge(a: &mut Value, b: &Value) {
match (a, b) {
@@ -54,10 +54,10 @@ fn test_merge() -> anyhow::Result<()> {
script1: test
";
let merge = serde_yaml::from_str::<Mapping>(merge)?;
let config = serde_yaml::from_str::<Mapping>(config)?;
let merge = serde_yaml_ng::from_str::<Mapping>(merge)?;
let config = serde_yaml_ng::from_str::<Mapping>(config)?;
let _ = serde_yaml::to_string(&use_merge(merge, config))?;
let _ = serde_yaml_ng::to_string(&use_merge(merge, config))?;
Ok(())
}

View File

@@ -7,7 +7,7 @@ mod tun;
use self::{chain::*, field::*, merge::*, script::*, seq::*, tun::*};
use crate::{config::Config, utils::tmpl};
use serde_yaml::Mapping;
use serde_yaml_ng::Mapping;
use std::collections::{HashMap, HashSet};
type ResultLog = Vec<(String, String)>;
@@ -386,7 +386,9 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
if dns_path.exists() {
if let Ok(dns_yaml) = fs::read_to_string(&dns_path) {
if let Ok(dns_config) = serde_yaml::from_str::<serde_yaml::Mapping>(&dns_yaml) {
if let Ok(dns_config) =
serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml)
{
// 处理hosts配置
if let Some(hosts_value) = dns_config.get("hosts") {
if hosts_value.is_mapping() {

View File

@@ -1,6 +1,6 @@
use super::use_lowercase;
use anyhow::{Error, Result};
use serde_yaml::Mapping;
use serde_yaml_ng::Mapping;
pub fn use_script(
script: String,
@@ -149,11 +149,11 @@ fn test_script() {
enable: false
";
let config = serde_yaml::from_str(config).expect("Failed to parse test config YAML");
let config = serde_yaml_ng::from_str(config).expect("Failed to parse test config YAML");
let (config, results) = use_script(script.into(), config, "".to_string())
.expect("Script execution should succeed in test");
let _ = serde_yaml::to_string(&config).expect("Failed to serialize config to YAML");
let _ = serde_yaml_ng::to_string(&config).expect("Failed to serialize config to YAML");
let yaml_config_size = std::mem::size_of_val(&config);
let box_yaml_config_size = std::mem::size_of_val(&Box::new(config));
assert!(box_yaml_config_size < yaml_config_size);

View File

@@ -1,5 +1,5 @@
use serde::{Deserialize, Serialize};
use serde_yaml::{Mapping, Sequence, Value};
use serde_yaml_ng::{Mapping, Sequence, Value};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SeqMap {
@@ -86,7 +86,7 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
mod tests {
use super::*;
#[allow(unused_imports)]
use serde_yaml::Value;
use serde_yaml_ng::Value;
#[test]
#[allow(clippy::unwrap_used)]
@@ -110,7 +110,7 @@ proxy-groups:
- "proxy1"
"#;
let mut config: Mapping =
serde_yaml::from_str(config_str).expect("Failed to parse test config YAML");
serde_yaml_ng::from_str(config_str).expect("Failed to parse test config YAML");
let seq = SeqMap {
prepend: Sequence::new(),

View File

@@ -1,4 +1,4 @@
use serde_yaml::{Mapping, Value};
use serde_yaml_ng::{Mapping, Value};
#[cfg(target_os = "macos")]
use crate::process::AsyncHandler;
@@ -63,8 +63,8 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
#[cfg(target_os = "macos")]
{
AsyncHandler::spawn(move || async move {
crate::utils::resolve::restore_public_dns().await;
crate::utils::resolve::set_public_dns("223.6.6.6".to_string()).await;
crate::utils::resolve::dns::restore_public_dns().await;
crate::utils::resolve::dns::set_public_dns("223.6.6.6".to_string()).await;
});
}
}
@@ -75,7 +75,7 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
// TUN未启用时仅恢复系统DNS不修改配置文件中的DNS设置
#[cfg(target_os = "macos")]
AsyncHandler::spawn(move || async move {
crate::utils::resolve::restore_public_dns().await;
crate::utils::resolve::dns::restore_public_dns().await;
});
}

View File

@@ -6,7 +6,7 @@ use crate::{
process::AsyncHandler,
utils::{logging::Type, resolve},
};
use serde_yaml::{Mapping, Value};
use serde_yaml_ng::{Mapping, Value};
/// Restart the Clash core
pub async fn restart_clash_core() {

View File

@@ -6,7 +6,7 @@ use crate::{
utils::logging::Type,
};
use anyhow::Result;
use serde_yaml::Mapping;
use serde_yaml_ng::Mapping;
/// Patch Clash configuration
pub async fn patch_clash(patch: Mapping) -> Result<()> {

View File

@@ -166,7 +166,7 @@ async fn clean_async() -> bool {
let dns_task = async {
match timeout(
Duration::from_millis(1000),
crate::utils::resolve::restore_public_dns(),
crate::utils::resolve::dns::restore_public_dns(),
)
.await
{

View File

@@ -1,3 +1,6 @@
#![allow(non_snake_case)]
#![recursion_limit = "512"]
mod cmd;
pub mod config;
mod core;
@@ -11,9 +14,13 @@ mod utils;
#[cfg(target_os = "macos")]
use crate::utils::window_manager::WindowManager;
use crate::{
core::handle,
core::hotkey,
process::AsyncHandler,
utils::{resolve, resolve::resolve_scheme, server},
utils::{
resolve::{self, scheme::resolve_scheme},
server,
},
};
use config::Config;
use parking_lot::Mutex;
@@ -28,15 +35,13 @@ use utils::logging::Type;
/// Application initialization helper functions
mod app_init {
use crate::core::handle;
use super::*;
/// Initialize singleton monitoring for other instances
pub fn init_singleton_check() {
AsyncHandler::spawn(move || async move {
logging!(info, Type::Setup, true, "开始检查单例实例...");
match timeout(Duration::from_secs(3), server::check_singleton()).await {
match timeout(Duration::from_millis(500), server::check_singleton()).await {
Ok(result) => {
if result.is_err() {
logging!(info, Type::Setup, true, "检测到已有应用实例运行");
@@ -133,47 +138,6 @@ mod app_init {
Ok(())
}
/// Initialize core components asynchronously
pub fn init_core_async(app_handle: &AppHandle) {
let app_handle = app_handle.clone();
AsyncHandler::spawn(move || async move {
logging!(info, Type::Setup, true, "异步执行应用设置...");
match timeout(
Duration::from_secs(30),
resolve::resolve_setup_async(&app_handle),
)
.await
{
Ok(_) => {
logging!(info, Type::Setup, true, "应用设置成功完成");
}
Err(_) => {
logging!(
error,
Type::Setup,
true,
"应用设置超时(30秒),继续执行后续流程"
);
}
}
});
}
/// Initialize core components synchronously
pub async fn init_core_sync(app_handle: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
logging!(info, Type::Setup, true, "初始化核心句柄...");
core::handle::Handle::global().init(app_handle.clone());
logging!(info, Type::Setup, true, "初始化配置...");
utils::init::init_config().await?;
logging!(info, Type::Setup, true, "初始化资源...");
utils::init::init_resources().await?;
logging!(info, Type::Setup, true, "核心组件初始化完成");
Ok(())
}
/// Generate all command handlers for the application
pub fn generate_handlers(
) -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
@@ -310,11 +274,8 @@ pub fn run() {
// Setup singleton check
app_init::init_singleton_check();
// Initialize network manager
utils::network::NetworkManager::new().init();
// Initialize portable flag
let _ = utils::dirs::init_portable_flag();
// We don't need init_portable_flag here anymore due to the init_config will do the things
// let _ = utils::dirs::init_portable_flag();
// Set Linux environment variable
#[cfg(target_os = "linux")]
@@ -373,23 +334,11 @@ pub fn run() {
let app_handle = app.handle().clone();
// Initialize core components asynchronously
app_init::init_core_async(&app_handle);
logging!(info, Type::Setup, true, "执行主要设置操作...");
// Initialize core components synchronously
AsyncHandler::spawn(move || async move {
if let Err(e) = app_init::init_core_sync(&app_handle).await {
logging!(
error,
Type::Setup,
true,
"Failed to initialize core components: {}",
e
);
}
});
logging!(info, Type::Setup, true, "异步执行应用设置...");
resolve::resolve_setup_sync(app_handle);
resolve::resolve_setup_async();
logging!(info, Type::Setup, true, "初始化完成,继续执行");
Ok(())

View File

@@ -78,7 +78,7 @@ pub async fn run_once_auto_lightweight() {
}
}
pub async fn auto_lightweight_mode_init() {
pub async fn auto_lightweight_mode_init() -> Result<()> {
if let Some(app_handle) = handle::Handle::global().app_handle() {
// Check if state is available before accessing it
if app_handle.try_state::<Mutex<LightWeightState>>().is_none() {
@@ -88,7 +88,7 @@ pub async fn auto_lightweight_mode_init() {
true,
"LightWeightState 尚未初始化,跳过自动轻量模式初始化"
);
return;
return Err(anyhow::anyhow!("LightWeightState has not been initialized"));
}
let is_silent_start =
@@ -114,9 +114,12 @@ pub async fn auto_lightweight_mode_init() {
// 确保托盘状态更新
if let Err(e) = Tray::global().update_part().await {
log::warn!("Failed to update tray: {e}");
return Err(e);
}
}
}
Ok(())
}
// 检查是否处于轻量模式
@@ -214,7 +217,7 @@ pub async fn exit_lightweight_mode() {
handle::Handle::global().set_activation_policy_regular();
// 重置UI就绪状态
crate::utils::resolve::reset_ui_ready();
crate::utils::resolve::ui::reset_ui_ready();
// 更新托盘显示
let _tray = crate::core::tray::Tray::global();

View File

@@ -50,9 +50,9 @@ impl AsyncHandler {
where
F: ?Sized,
{
const TRACE_MINI_SIZE: usize = 4;
const TRACE_SPECIAL_SIZE: [usize; 3] = [0, 4, 24];
let size = std::mem::size_of_val(f);
if size <= TRACE_MINI_SIZE {
if TRACE_SPECIAL_SIZE.contains(&size) {
return;
}

View File

@@ -27,46 +27,56 @@ impl ProxyRequestCache {
pub async fn get_or_fetch<F, Fut>(&self, key: String, ttl: Duration, fetch_fn: F) -> Arc<Value>
where
F: Fn() -> Fut,
Fut: std::future::Future<Output = Value>,
F: Fn() -> Fut + Send + 'static,
Fut: std::future::Future<Output = Value> + Send + 'static,
{
let now = Instant::now();
let key_cloned = key.clone();
let cell = self
.map
.entry(key)
.or_insert_with(|| Arc::new(OnceCell::new()))
.clone();
loop {
let now = Instant::now();
let key_cloned = key.clone();
if let Some(entry) = cell.get() {
if entry.expires_at > now {
return Arc::clone(&entry.value);
}
}
// Get or create the cell
let cell = self
.map
.entry(key_cloned.clone())
.or_insert_with(|| Arc::new(OnceCell::new()))
.clone();
if let Some(entry) = cell.get() {
if entry.expires_at <= now {
// Check if we have a valid cached entry
if let Some(entry) = cell.get() {
if entry.expires_at > now {
return Arc::clone(&entry.value);
}
// Entry is expired, remove it
self.map
.remove_if(&key_cloned, |_, v| Arc::ptr_eq(v, &cell));
let new_cell = Arc::new(OnceCell::new());
self.map.insert(key_cloned.clone(), Arc::clone(&new_cell));
return Box::pin(self.get_or_fetch(key_cloned, ttl, fetch_fn)).await;
continue; // Retry with fresh cell
}
// Try to set a new value
let value = fetch_fn().await;
let entry = CacheEntry {
value: Arc::new(value),
expires_at: Instant::now() + ttl,
};
match cell.set(entry) {
Ok(_) => {
// Successfully set the value, it must exist now
if let Some(set_entry) = cell.get() {
return Arc::clone(&set_entry.value);
}
}
Err(_) => {
if let Some(existing_entry) = cell.get() {
if existing_entry.expires_at > Instant::now() {
return Arc::clone(&existing_entry.value);
}
self.map
.remove_if(&key_cloned, |_, v| Arc::ptr_eq(v, &cell));
}
}
}
}
let value = fetch_fn().await;
let entry = CacheEntry {
value: Arc::new(value),
expires_at: Instant::now() + ttl,
};
let _ = cell.set(entry);
// Safe to unwrap here as we just set the value
Arc::clone(
&cell
.get()
.unwrap_or_else(|| unreachable!("Cell value should exist after set"))
.value,
)
}
}

View File

@@ -2,7 +2,7 @@ use crate::{enhance::seq::SeqMap, logging, utils::logging::Type};
use anyhow::{anyhow, bail, Context, Result};
use nanoid::nanoid;
use serde::{de::DeserializeOwned, Serialize};
use serde_yaml::Mapping;
use serde_yaml_ng::Mapping;
use std::{path::PathBuf, str::FromStr};
/// read data from yaml as struct T
@@ -13,7 +13,7 @@ pub async fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
let yaml_str = tokio::fs::read_to_string(path).await?;
Ok(serde_yaml::from_str::<T>(&yaml_str)?)
Ok(serde_yaml_ng::from_str::<T>(&yaml_str)?)
}
/// read mapping from yaml
@@ -27,7 +27,7 @@ pub async fn read_mapping(path: &PathBuf) -> Result<Mapping> {
.with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
// YAML语法检查
match serde_yaml::from_str::<serde_yaml::Value>(&yaml_str) {
match serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&yaml_str) {
Ok(mut val) => {
val.apply_merge()
.with_context(|| format!("failed to apply merge \"{}\"", path.display()))?;
@@ -66,7 +66,7 @@ pub async fn save_yaml<T: Serialize + Sync>(
data: &T,
prefix: Option<&str>,
) -> Result<()> {
let data_str = serde_yaml::to_string(data)?;
let data_str = serde_yaml_ng::to_string(data)?;
let yaml_str = match prefix {
Some(prefix) => format!("{prefix}\n\n{data_str}"),

View File

@@ -142,10 +142,10 @@ pub async fn delete_log() -> Result<()> {
/// 初始化DNS配置文件
async fn init_dns_config() -> Result<()> {
use serde_yaml::Value;
use serde_yaml_ng::Value;
// 创建DNS子配置
let dns_config = serde_yaml::Mapping::from_iter([
let dns_config = serde_yaml_ng::Mapping::from_iter([
("enable".into(), Value::Bool(true)),
("listen".into(), Value::String(":53".into())),
("enhanced-mode".into(), Value::String("fake-ip".into())),
@@ -197,7 +197,7 @@ async fn init_dns_config() -> Result<()> {
("fallback".into(), Value::Sequence(vec![])),
(
"nameserver-policy".into(),
Value::Mapping(serde_yaml::Mapping::new()),
Value::Mapping(serde_yaml_ng::Mapping::new()),
),
(
"proxy-server-nameserver".into(),
@@ -211,7 +211,7 @@ async fn init_dns_config() -> Result<()> {
("direct-nameserver-follow-policy".into(), Value::Bool(false)),
(
"fallback-filter".into(),
Value::Mapping(serde_yaml::Mapping::from_iter([
Value::Mapping(serde_yaml_ng::Mapping::from_iter([
("geoip".into(), Value::Bool(true)),
("geoip-code".into(), Value::String("CN".into())),
(
@@ -234,9 +234,12 @@ async fn init_dns_config() -> Result<()> {
]);
// 获取默认DNS和host配置
let default_dns_config = serde_yaml::Mapping::from_iter([
let default_dns_config = serde_yaml_ng::Mapping::from_iter([
("dns".into(), Value::Mapping(dns_config)),
("hosts".into(), Value::Mapping(serde_yaml::Mapping::new())),
(
"hosts".into(),
Value::Mapping(serde_yaml_ng::Mapping::new()),
),
]);
// 检查DNS配置文件是否存在

View File

@@ -1,737 +0,0 @@
use crate::{
config::{Config, PrfItem},
core::*,
logging, logging_error,
module::lightweight::{self, auto_lightweight_mode_init},
process::AsyncHandler,
utils::{init, logging::Type, server},
wrap_err,
};
use anyhow::{bail, Result};
use once_cell::sync::OnceCell;
use parking_lot::{Mutex, RwLock};
use percent_encoding::percent_decode_str;
use scopeguard;
use std::time::{Duration, Instant};
use tauri::{AppHandle, Manager};
use tauri::Url;
//#[cfg(not(target_os = "linux"))]
// use window_shadows::set_shadow;
pub static VERSION: OnceCell<String> = OnceCell::new();
// 定义默认窗口尺寸常量
const DEFAULT_WIDTH: u32 = 940;
const DEFAULT_HEIGHT: u32 = 700;
// 添加全局UI准备就绪标志
static UI_READY: OnceCell<RwLock<bool>> = OnceCell::new();
// 窗口创建锁,防止并发创建窗口
static WINDOW_CREATING: OnceCell<Mutex<(bool, Instant)>> = OnceCell::new();
// UI就绪阶段状态枚举
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum UiReadyStage {
NotStarted,
Loading,
DomReady,
ResourcesLoaded,
Ready,
}
// UI就绪详细状态
#[derive(Debug)]
struct UiReadyState {
stage: RwLock<UiReadyStage>,
}
impl Default for UiReadyState {
fn default() -> Self {
Self {
stage: RwLock::new(UiReadyStage::NotStarted),
}
}
}
// 获取UI就绪状态细节
static UI_READY_STATE: OnceCell<UiReadyState> = OnceCell::new();
fn get_window_creating_lock() -> &'static Mutex<(bool, Instant)> {
WINDOW_CREATING.get_or_init(|| Mutex::new((false, Instant::now())))
}
fn get_ui_ready() -> &'static RwLock<bool> {
UI_READY.get_or_init(|| RwLock::new(false))
}
fn get_ui_ready_state() -> &'static UiReadyState {
UI_READY_STATE.get_or_init(UiReadyState::default)
}
// 更新UI准备阶段
pub fn update_ui_ready_stage(stage: UiReadyStage) {
let state = get_ui_ready_state();
let mut stage_lock = state.stage.write();
*stage_lock = stage;
// 如果是最终阶段标记UI完全就绪
if stage == UiReadyStage::Ready {
mark_ui_ready();
}
}
// 标记UI已准备就绪
pub fn mark_ui_ready() {
let mut ready = get_ui_ready().write();
*ready = true;
logging!(info, Type::Window, true, "UI已标记为完全就绪");
}
// 重置UI就绪状态
pub fn reset_ui_ready() {
{
let mut ready = get_ui_ready().write();
*ready = false;
}
{
let state = get_ui_ready_state();
let mut stage = state.stage.write();
*stage = UiReadyStage::NotStarted;
}
logging!(info, Type::Window, true, "UI就绪状态已重置");
}
/// 异步方式处理启动后的额外任务
pub async fn resolve_setup_async(app_handle: &AppHandle) {
let start_time = std::time::Instant::now();
logging!(
info,
Type::Setup,
true,
"开始执行异步设置任务... 线程ID: {:?}",
std::thread::current().id()
);
if VERSION.get().is_none() {
let version = app_handle.package_info().version.to_string();
VERSION.get_or_init(|| {
logging!(info, Type::Setup, true, "初始化版本信息: {}", version);
version.clone()
});
}
logging_error!(Type::Setup, true, init::init_scheme());
logging_error!(Type::Setup, true, init::startup_script().await);
logging!(
info,
Type::Config,
true,
"开始初始化配置... 线程ID: {:?}",
std::thread::current().id()
);
logging_error!(Type::Config, true, Config::init_config().await);
logging!(info, Type::Config, true, "配置初始化完成");
logging!(trace, Type::Core, true, "启动核心管理器...");
logging_error!(Type::Core, true, CoreManager::global().init().await);
log::trace!(target: "app", "启动内嵌服务器...");
server::embed_server();
logging!(trace, Type::Core, true, "启动 IPC 监控服务...");
logging_error!(Type::Tray, true, tray::Tray::global().init());
if let Some(app_handle) = handle::Handle::global().app_handle() {
logging!(info, Type::Tray, true, "创建系统托盘...");
let result = tray::Tray::global()
.create_tray_from_handle(&app_handle)
.await;
if result.is_ok() {
logging!(info, Type::Tray, true, "系统托盘创建成功");
} else if let Err(e) = result {
logging!(error, Type::Tray, true, "系统托盘创建失败: {}", e);
}
} else {
logging!(
error,
Type::Tray,
true,
"无法创建系统托盘: app_handle不存在"
);
}
// 更新系统代理
logging_error!(
Type::System,
true,
sysopt::Sysopt::global().update_sysproxy().await
);
logging_error!(
Type::System,
true,
sysopt::Sysopt::global().init_guard_sysproxy()
);
// 创建窗口
let is_silent_start =
{ Config::verge().await.latest_ref().enable_silent_start }.unwrap_or(false);
#[cfg(target_os = "macos")]
{
if is_silent_start {
handle::Handle::global().set_activation_policy_accessory();
}
}
create_window(!is_silent_start).await;
// 初始化定时器
logging_error!(Type::System, true, timer::Timer::global().init().await);
// 自动进入轻量模式
auto_lightweight_mode_init().await;
logging_error!(Type::Tray, true, tray::Tray::global().update_part().await);
logging!(trace, Type::System, true, "初始化热键...");
logging_error!(Type::System, true, hotkey::Hotkey::global().init().await);
let elapsed = start_time.elapsed();
logging!(
info,
Type::Setup,
true,
"异步设置任务完成,耗时: {:?}",
elapsed
);
// 如果初始化时间过长,记录警告
if elapsed.as_secs() > 10 {
logging!(
warn,
Type::Setup,
true,
"异步设置任务耗时较长({:?})",
elapsed
);
}
}
/// reset system proxy (异步)
pub async fn resolve_reset_async() {
#[cfg(target_os = "macos")]
logging!(info, Type::Tray, true, "Unsubscribing from traffic updates");
#[cfg(target_os = "macos")]
tray::Tray::global().unsubscribe_traffic();
logging_error!(
Type::System,
true,
sysopt::Sysopt::global().reset_sysproxy().await
);
logging_error!(Type::Core, true, CoreManager::global().stop_core().await);
#[cfg(target_os = "macos")]
{
logging!(info, Type::System, true, "Restoring system DNS settings");
restore_public_dns().await;
}
}
/// Create the main window
pub async fn create_window(is_show: bool) -> bool {
logging!(
info,
Type::Window,
true,
"开始创建/显示主窗口, is_show={}",
is_show
);
if !is_show {
logging!(info, Type::Window, true, "静默模式启动时不创建窗口");
lightweight::set_lightweight_mode(true).await;
handle::Handle::notify_startup_completed();
return false;
}
if let Some(app_handle) = handle::Handle::global().app_handle() {
if let Some(window) = app_handle.get_webview_window("main") {
logging!(info, Type::Window, true, "主窗口已存在,将显示现有窗口");
if is_show {
if window.is_minimized().unwrap_or(false) {
logging!(info, Type::Window, true, "窗口已最小化,正在取消最小化");
let _ = window.unminimize();
}
let _ = window.show();
let _ = window.set_focus();
#[cfg(target_os = "macos")]
handle::Handle::global().set_activation_policy_regular();
}
return true;
}
}
let creating_lock = get_window_creating_lock();
let mut creating = creating_lock.lock();
let (is_creating, last_time) = *creating;
let elapsed = last_time.elapsed();
if is_creating && elapsed < Duration::from_secs(2) {
logging!(
info,
Type::Window,
true,
"窗口创建请求被忽略,因为最近创建过 ({:?}ms)",
elapsed.as_millis()
);
return false;
}
*creating = (true, Instant::now());
// ScopeGuard 确保创建状态重置,防止 webview 卡死
let _guard = scopeguard::guard(creating, |mut creating_guard| {
*creating_guard = (false, Instant::now());
logging!(debug, Type::Window, true, "[ScopeGuard] 窗口创建状态已重置");
});
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
logging!(
error,
Type::Window,
true,
"无法获取app_handle窗口创建失败"
);
return false;
}
};
match tauri::WebviewWindowBuilder::new(
&app_handle,
"main", /* the unique window label */
tauri::WebviewUrl::App("index.html".into()),
)
.title("Clash Verge")
.center()
.decorations(true)
.fullscreen(false)
.inner_size(DEFAULT_WIDTH as f64, DEFAULT_HEIGHT as f64)
.min_inner_size(520.0, 520.0)
.visible(true) // 立即显示窗口,避免用户等待
.initialization_script(
r#"
console.log('[Tauri] 窗口初始化脚本开始执行');
function createLoadingOverlay() {
if (document.getElementById('initial-loading-overlay')) {
console.log('[Tauri] 加载指示器已存在');
return;
}
console.log('[Tauri] 创建加载指示器');
const loadingDiv = document.createElement('div');
loadingDiv.id = 'initial-loading-overlay';
loadingDiv.innerHTML = `
<div style="
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: var(--bg-color, #f5f5f5); color: var(--text-color, #333);
display: flex; flex-direction: column; align-items: center;
justify-content: center; z-index: 9999;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
transition: opacity 0.3s ease;
">
<div style="margin-bottom: 20px;">
<div style="
width: 40px; height: 40px; border: 3px solid #e3e3e3;
border-top: 3px solid #3498db; border-radius: 50%;
animation: spin 1s linear infinite;
"></div>
</div>
<div style="font-size: 14px; opacity: 0.7;">Loading Clash Verge...</div>
</div>
<style>
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@media (prefers-color-scheme: dark) {
:root { --bg-color: #1a1a1a; --text-color: #ffffff; }
}
</style>
`;
if (document.body) {
document.body.appendChild(loadingDiv);
} else {
document.addEventListener('DOMContentLoaded', () => {
if (document.body && !document.getElementById('initial-loading-overlay')) {
document.body.appendChild(loadingDiv);
}
});
}
}
createLoadingOverlay();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createLoadingOverlay);
} else {
createLoadingOverlay();
}
console.log('[Tauri] 窗口初始化脚本执行完成');
"#,
)
.build()
{
Ok(newly_created_window) => {
logging!(debug, Type::Window, true, "主窗口实例创建成功");
update_ui_ready_stage(UiReadyStage::NotStarted);
AsyncHandler::spawn(move || async move {
handle::Handle::global().mark_startup_completed();
logging!(
debug,
Type::Window,
true,
"异步窗口任务开始 (启动已标记完成)"
);
// 先运行轻量模式检测
lightweight::run_once_auto_lightweight().await;
// 发送启动完成事件,触发前端开始加载
logging!(
debug,
Type::Window,
true,
"发送 verge://startup-completed 事件"
);
handle::Handle::notify_startup_completed();
if is_show {
let window_clone = newly_created_window.clone();
// 立即显示窗口
let _ = window_clone.show();
let _ = window_clone.set_focus();
logging!(info, Type::Window, true, "窗口已立即显示");
#[cfg(target_os = "macos")]
handle::Handle::global().set_activation_policy_regular();
let timeout_seconds = if crate::module::lightweight::is_in_lightweight_mode() {
3
} else {
8
};
logging!(
info,
Type::Window,
true,
"开始监控UI加载状态 (最多{}秒)...",
timeout_seconds
);
// 异步监控UI状态使用try_read避免死锁
AsyncHandler::spawn(move || async move {
logging!(
debug,
Type::Window,
true,
"启动UI状态监控线程超时{}秒",
timeout_seconds
);
let ui_ready_checker = || async {
let (mut check_count, mut consecutive_failures) = (0, 0);
loop {
let is_ready = get_ui_ready()
.try_read()
.map(|guard| *guard)
.unwrap_or_else(|| {
consecutive_failures += 1;
if consecutive_failures > 50 {
logging!(
warn,
Type::Window,
true,
"UI状态监控连续{}次无法获取读锁,可能存在死锁",
consecutive_failures
);
consecutive_failures = 0;
}
false
});
if is_ready {
logging!(
debug,
Type::Window,
true,
"UI状态监控检测到就绪信号退出监控"
);
return;
}
consecutive_failures = 0;
tokio::time::sleep(Duration::from_millis(100)).await;
check_count += 1;
if check_count % 20 == 0 {
logging!(
debug,
Type::Window,
true,
"UI加载状态检查... ({}秒)",
check_count / 10
);
}
}
};
let wait_result = tokio::time::timeout(
Duration::from_secs(timeout_seconds),
ui_ready_checker(),
)
.await;
match wait_result {
Ok(_) => {
logging!(info, Type::Window, true, "UI已完全加载就绪");
handle::Handle::global()
.get_window()
.map(|window| window.eval(r"
const overlay = document.getElementById('initial-loading-overlay');
if (overlay) {
overlay.style.opacity = '0';
setTimeout(() => overlay.remove(), 300);
}
"));
}
Err(_) => {
logging!(
warn,
Type::Window,
true,
"UI加载监控超时({}秒),但窗口已正常显示",
timeout_seconds
);
get_ui_ready()
.try_write()
.map(|mut guard| {
*guard = true;
logging!(
info,
Type::Window,
true,
"超时后成功设置UI就绪状态"
);
})
.unwrap_or_else(|| {
logging!(
error,
Type::Window,
true,
"超时后无法获取UI状态写锁可能存在严重死锁"
);
});
}
}
});
logging!(info, Type::Window, true, "窗口显示流程完成");
} else {
logging!(
debug,
Type::Window,
true,
"is_show为false窗口保持隐藏状态"
);
}
});
true
}
Err(e) => {
logging!(error, Type::Window, true, "主窗口构建失败: {}", e);
false
}
}
}
pub async fn resolve_scheme(param: String) -> Result<()> {
log::info!(target:"app", "received deep link: {param}");
let param_str = if param.starts_with("[") && param.len() > 4 {
param
.get(2..param.len() - 2)
.ok_or_else(|| anyhow::anyhow!("Invalid string slice boundaries"))?
} else {
param.as_str()
};
// 解析 URL
let link_parsed = match Url::parse(param_str) {
Ok(url) => url,
Err(e) => {
bail!("failed to parse deep link: {:?}, param: {:?}", e, param);
}
};
if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" {
let name = link_parsed
.query_pairs()
.find(|(key, _)| key == "name")
.map(|(_, value)| value.into_owned());
let url_param = if let Some(query) = link_parsed.query() {
let prefix = "url=";
if let Some(pos) = query.find(prefix) {
let raw_url = &query[pos + prefix.len()..];
Some(percent_decode_str(raw_url).decode_utf8_lossy().to_string())
} else {
None
}
} else {
None
};
match url_param {
Some(url) => {
log::info!(target:"app", "decoded subscription url: {url}");
create_window(false).await;
match PrfItem::from_url(url.as_ref(), name, None, None).await {
Ok(item) => {
let uid = match item.uid.clone() {
Some(uid) => uid,
None => {
logging!(error, Type::Config, true, "Profile item missing UID");
handle::Handle::notice_message(
"import_sub_url::error",
"Profile item missing UID".to_string(),
);
return Ok(());
}
};
let result = crate::config::profiles::profiles_append_item_safe(item).await;
let _ = wrap_err!(result);
handle::Handle::notice_message("import_sub_url::ok", uid);
}
Err(e) => {
handle::Handle::notice_message("import_sub_url::error", e.to_string());
}
}
}
None => bail!("failed to get profile url"),
}
}
Ok(())
}
#[cfg(target_os = "macos")]
pub async fn set_public_dns(dns_server: String) {
use crate::{core::handle, utils::dirs};
use tauri_plugin_shell::ShellExt;
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
log::error!(target: "app", "app_handle not available for DNS configuration");
return;
}
};
log::info!(target: "app", "try to set system dns");
let resource_dir = match dirs::app_resources_dir() {
Ok(dir) => dir,
Err(e) => {
log::error!(target: "app", "Failed to get resource directory: {}", e);
return;
}
};
let script = resource_dir.join("set_dns.sh");
if !script.exists() {
log::error!(target: "app", "set_dns.sh not found");
return;
}
let script = script.to_string_lossy().into_owned();
match app_handle
.shell()
.command("bash")
.args([script, dns_server])
.current_dir(resource_dir)
.status()
.await
{
Ok(status) => {
if status.success() {
log::info!(target: "app", "set system dns successfully");
} else {
let code = status.code().unwrap_or(-1);
log::error!(target: "app", "set system dns failed: {code}");
}
}
Err(err) => {
log::error!(target: "app", "set system dns failed: {err}");
}
}
}
#[cfg(target_os = "macos")]
pub async fn restore_public_dns() {
use crate::{core::handle, utils::dirs};
use tauri_plugin_shell::ShellExt;
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
log::error!(target: "app", "app_handle not available for DNS restoration");
return;
}
};
log::info!(target: "app", "try to unset system dns");
let resource_dir = match dirs::app_resources_dir() {
Ok(dir) => dir,
Err(e) => {
log::error!(target: "app", "Failed to get resource directory: {}", e);
return;
}
};
let script = resource_dir.join("unset_dns.sh");
if !script.exists() {
log::error!(target: "app", "unset_dns.sh not found");
return;
}
let script = script.to_string_lossy().into_owned();
match app_handle
.shell()
.command("bash")
.args([script])
.current_dir(resource_dir)
.status()
.await
{
Ok(status) => {
if status.success() {
log::info!(target: "app", "unset system dns successfully");
} else {
let code = status.code().unwrap_or(-1);
log::error!(target: "app", "unset system dns failed: {code}");
}
}
Err(err) => {
log::error!(target: "app", "unset system dns failed: {err}");
}
}
}

View File

@@ -0,0 +1,94 @@
#[cfg(target_os = "macos")]
pub async fn set_public_dns(dns_server: String) {
use crate::{core::handle, utils::dirs};
use tauri_plugin_shell::ShellExt;
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
log::error!(target: "app", "app_handle not available for DNS configuration");
return;
}
};
log::info!(target: "app", "try to set system dns");
let resource_dir = match dirs::app_resources_dir() {
Ok(dir) => dir,
Err(e) => {
log::error!(target: "app", "Failed to get resource directory: {}", e);
return;
}
};
let script = resource_dir.join("set_dns.sh");
if !script.exists() {
log::error!(target: "app", "set_dns.sh not found");
return;
}
let script = script.to_string_lossy().into_owned();
match app_handle
.shell()
.command("bash")
.args([script, dns_server])
.current_dir(resource_dir)
.status()
.await
{
Ok(status) => {
if status.success() {
log::info!(target: "app", "set system dns successfully");
} else {
let code = status.code().unwrap_or(-1);
log::error!(target: "app", "set system dns failed: {code}");
}
}
Err(err) => {
log::error!(target: "app", "set system dns failed: {err}");
}
}
}
#[cfg(target_os = "macos")]
pub async fn restore_public_dns() {
use crate::{core::handle, utils::dirs};
use tauri_plugin_shell::ShellExt;
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
log::error!(target: "app", "app_handle not available for DNS restoration");
return;
}
};
log::info!(target: "app", "try to unset system dns");
let resource_dir = match dirs::app_resources_dir() {
Ok(dir) => dir,
Err(e) => {
log::error!(target: "app", "Failed to get resource directory: {}", e);
return;
}
};
let script = resource_dir.join("unset_dns.sh");
if !script.exists() {
log::error!(target: "app", "unset_dns.sh not found");
return;
}
let script = script.to_string_lossy().into_owned();
match app_handle
.shell()
.command("bash")
.args([script])
.current_dir(resource_dir)
.status()
.await
{
Ok(status) => {
if status.success() {
log::info!(target: "app", "unset system dns successfully");
} else {
let code = status.code().unwrap_or(-1);
log::error!(target: "app", "unset system dns failed: {code}");
}
}
Err(err) => {
log::error!(target: "app", "unset system dns failed: {err}");
}
}
}

View File

@@ -0,0 +1,213 @@
use tauri::AppHandle;
use crate::{
config::Config,
core::{handle, hotkey::Hotkey, sysopt, tray::Tray, CoreManager, Timer},
logging, logging_error,
module::lightweight::auto_lightweight_mode_init,
process::AsyncHandler,
utils::{init, logging::Type, network::NetworkManager, resolve::window::create_window, server},
};
pub mod dns;
pub mod scheme;
pub mod ui;
pub mod window;
pub mod window_script;
pub fn resolve_setup_sync(app_handle: AppHandle) {
init_handle(app_handle);
init_scheme();
init_embed_server();
NetworkManager::new().init();
}
pub fn resolve_setup_async() {
let start_time = std::time::Instant::now();
logging!(
info,
Type::Setup,
true,
"开始执行异步设置任务... 线程ID: {:?}",
std::thread::current().id()
);
AsyncHandler::spawn(|| async {
init_resources().await;
init_work_config().await;
init_startup_script().await;
init_timer().await;
init_hotkey().await;
init_auto_lightweight_mode().await;
init_verge_config().await;
init_core_manager().await;
init_system_proxy().await;
// 在单独的 spawn 中运行 sync 函数,避免 Send 问题
AsyncHandler::spawn_blocking(|| {
init_system_proxy_guard();
});
init_window().await;
init_tray().await;
refresh_tray_menu().await
});
let elapsed = start_time.elapsed();
logging!(
info,
Type::Setup,
true,
"异步设置任务完成,耗时: {:?}",
elapsed
);
if elapsed.as_secs() > 10 {
logging!(
warn,
Type::Setup,
true,
"异步设置任务耗时较长({:?})",
elapsed
);
}
}
// 其它辅助函数不变
pub async fn resolve_reset_async() {
logging!(info, Type::Tray, true, "Resetting system proxy");
logging_error!(
Type::System,
true,
sysopt::Sysopt::global().reset_sysproxy().await
);
logging!(info, Type::Core, true, "Stopping core service");
logging_error!(Type::Core, true, CoreManager::global().stop_core().await);
#[cfg(target_os = "macos")]
{
use dns::restore_public_dns;
logging!(info, Type::System, true, "Restoring system DNS settings");
restore_public_dns().await;
}
}
pub fn init_handle(app_handle: AppHandle) {
logging!(info, Type::Setup, true, "Initializing app handle...");
handle::Handle::global().init(app_handle);
}
pub(super) fn init_scheme() {
logging!(info, Type::Setup, true, "Initializing custom URL scheme");
logging_error!(Type::Setup, true, init::init_scheme());
}
pub(super) fn init_embed_server() {
logging!(info, Type::Setup, true, "Initializing embedded server...");
server::embed_server();
}
pub(super) async fn init_resources() {
logging!(info, Type::Setup, true, "Initializing resources...");
logging_error!(Type::Setup, true, init::init_resources().await);
}
pub(super) async fn init_startup_script() {
logging!(info, Type::Setup, true, "Initializing startup script");
logging_error!(Type::Setup, true, init::startup_script().await);
}
pub(super) async fn init_timer() {
logging!(info, Type::Setup, true, "Initializing timer...");
logging_error!(Type::Setup, true, Timer::global().init().await);
}
pub(super) async fn init_hotkey() {
logging!(info, Type::Setup, true, "Initializing hotkey...");
logging_error!(Type::Setup, true, Hotkey::global().init().await);
}
pub(super) async fn init_auto_lightweight_mode() {
logging!(
info,
Type::Setup,
true,
"Initializing auto lightweight mode..."
);
logging_error!(Type::Setup, true, auto_lightweight_mode_init().await);
}
pub async fn init_work_config() {
logging!(
info,
Type::Setup,
true,
"Initializing work configuration..."
);
logging_error!(Type::Setup, true, init::init_config().await);
}
pub(super) async fn init_tray() {
logging!(info, Type::Setup, true, "Initializing system tray...");
logging_error!(Type::Setup, true, Tray::global().init().await);
}
pub(super) async fn init_verge_config() {
logging!(
info,
Type::Setup,
true,
"Initializing verge configuration..."
);
logging_error!(Type::Setup, true, Config::init_config().await);
}
pub(super) async fn init_core_manager() {
logging!(info, Type::Setup, true, "Initializing core manager...");
logging_error!(Type::Setup, true, CoreManager::global().init().await);
}
pub(super) async fn init_system_proxy() {
logging!(info, Type::Setup, true, "Initializing system proxy...");
logging_error!(
Type::Setup,
true,
sysopt::Sysopt::global().update_sysproxy().await
);
}
pub(super) fn init_system_proxy_guard() {
logging!(
info,
Type::Setup,
true,
"Initializing system proxy guard..."
);
logging_error!(
Type::Setup,
true,
sysopt::Sysopt::global().init_guard_sysproxy()
);
}
pub(super) async fn refresh_tray_menu() {
logging!(info, Type::Setup, true, "Refreshing tray menu...");
logging_error!(Type::Setup, true, Tray::global().update_part().await);
}
pub(super) async fn init_window() {
let is_silent_start =
{ Config::verge().await.latest_ref().enable_silent_start }.unwrap_or(false);
#[cfg(target_os = "macos")]
{
if is_silent_start {
use crate::core::handle::Handle;
Handle::global().set_activation_policy_accessory();
}
}
create_window(!is_silent_start).await;
}

View File

@@ -0,0 +1,75 @@
use anyhow::{bail, Result};
use percent_encoding::percent_decode_str;
use tauri::Url;
use crate::{config::PrfItem, core::handle, logging, utils::logging::Type, wrap_err};
pub async fn resolve_scheme(param: String) -> Result<()> {
log::info!(target:"app", "received deep link: {param}");
let param_str = if param.starts_with("[") && param.len() > 4 {
param
.get(2..param.len() - 2)
.ok_or_else(|| anyhow::anyhow!("Invalid string slice boundaries"))?
} else {
param.as_str()
};
// 解析 URL
let link_parsed = match Url::parse(param_str) {
Ok(url) => url,
Err(e) => {
bail!("failed to parse deep link: {:?}, param: {:?}", e, param);
}
};
if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" {
let name = link_parsed
.query_pairs()
.find(|(key, _)| key == "name")
.map(|(_, value)| value.into_owned());
let url_param = if let Some(query) = link_parsed.query() {
let prefix = "url=";
if let Some(pos) = query.find(prefix) {
let raw_url = &query[pos + prefix.len()..];
Some(percent_decode_str(raw_url).decode_utf8_lossy().to_string())
} else {
None
}
} else {
None
};
match url_param {
Some(url) => {
log::info!(target:"app", "decoded subscription url: {url}");
// create_window(false).await;
match PrfItem::from_url(url.as_ref(), name, None, None).await {
Ok(item) => {
let uid = match item.uid.clone() {
Some(uid) => uid,
None => {
logging!(error, Type::Config, true, "Profile item missing UID");
handle::Handle::notice_message(
"import_sub_url::error",
"Profile item missing UID".to_string(),
);
return Ok(());
}
};
let result = crate::config::profiles::profiles_append_item_safe(item).await;
let _ = wrap_err!(result);
handle::Handle::notice_message("import_sub_url::ok", uid);
}
Err(e) => {
handle::Handle::notice_message("import_sub_url::error", e.to_string());
}
}
}
None => bail!("failed to get profile url"),
}
}
Ok(())
}

View File

@@ -0,0 +1,106 @@
use once_cell::sync::OnceCell;
use parking_lot::RwLock;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use tokio::sync::Notify;
use crate::{logging, utils::logging::Type};
// 使用 AtomicBool 替代 RwLock<bool>,性能更好且无锁
static UI_READY: OnceCell<AtomicBool> = OnceCell::new();
// 获取UI就绪状态细节
static UI_READY_STATE: OnceCell<UiReadyState> = OnceCell::new();
// 添加通知机制,用于事件驱动的 UI 就绪检测
static UI_READY_NOTIFY: OnceCell<Arc<Notify>> = OnceCell::new();
// UI就绪阶段状态枚举
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum UiReadyStage {
NotStarted,
Loading,
DomReady,
ResourcesLoaded,
Ready,
}
// UI就绪详细状态
#[derive(Debug)]
struct UiReadyState {
stage: RwLock<UiReadyStage>,
}
impl Default for UiReadyState {
fn default() -> Self {
Self {
stage: RwLock::new(UiReadyStage::NotStarted),
}
}
}
pub(super) fn get_ui_ready() -> &'static AtomicBool {
UI_READY.get_or_init(|| AtomicBool::new(false))
}
fn get_ui_ready_state() -> &'static UiReadyState {
UI_READY_STATE.get_or_init(UiReadyState::default)
}
fn get_ui_ready_notify() -> &'static Arc<Notify> {
UI_READY_NOTIFY.get_or_init(|| Arc::new(Notify::new()))
}
/// 等待 UI 就绪的异步函数,使用事件驱动而非轮询
pub async fn wait_for_ui_ready(timeout_seconds: u64) -> bool {
// 首先检查是否已经就绪
if get_ui_ready().load(Ordering::Acquire) {
return true;
}
// 使用 tokio::select! 同时等待通知和超时
tokio::select! {
_ = get_ui_ready_notify().notified() => {
// 收到通知后再次确认状态(防止虚假通知)
get_ui_ready().load(Ordering::Acquire)
}
_ = tokio::time::sleep(std::time::Duration::from_secs(timeout_seconds)) => {
// 超时,返回当前状态
get_ui_ready().load(Ordering::Acquire)
}
}
}
// 更新UI准备阶段
pub fn update_ui_ready_stage(stage: UiReadyStage) {
let state = get_ui_ready_state();
let mut stage_lock = state.stage.write();
*stage_lock = stage;
// 如果是最终阶段标记UI完全就绪
if stage == UiReadyStage::Ready {
mark_ui_ready();
}
}
// 标记UI已准备就绪
pub fn mark_ui_ready() {
get_ui_ready().store(true, Ordering::Release);
logging!(info, Type::Window, true, "UI已标记为完全就绪");
// 通知所有等待的任务
get_ui_ready_notify().notify_waiters();
}
// 重置UI就绪状态
pub fn reset_ui_ready() {
get_ui_ready().store(false, Ordering::Release);
{
let state = get_ui_ready_state();
let mut stage = state.stage.write();
*stage = UiReadyStage::NotStarted;
}
logging!(info, Type::Window, true, "UI就绪状态已重置");
// 注意:这里不需要通知,因为重置后状态变为 false
}

View File

@@ -0,0 +1,298 @@
use std::time::{Duration, Instant};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use tauri::{Manager, WebviewWindow};
use crate::{
core::handle,
logging,
module::lightweight,
process::AsyncHandler,
utils::{
logging::Type,
resolve::{
ui::{get_ui_ready, update_ui_ready_stage, wait_for_ui_ready, UiReadyStage},
window_script::{INITIAL_LOADING_OVERLAY, WINDOW_INITIAL_SCRIPT},
},
},
};
// 定义默认窗口尺寸常量
const DEFAULT_WIDTH: f64 = 940.0;
const DEFAULT_HEIGHT: f64 = 700.0;
const MINIMAL_WIDTH: f64 = 520.0;
const MINIMAL_HEIGHT: f64 = 520.0;
// 窗口创建锁,防止并发创建窗口
static WINDOW_CREATING: OnceCell<Mutex<(bool, Instant)>> = OnceCell::new();
fn get_window_creating_lock() -> &'static Mutex<(bool, Instant)> {
WINDOW_CREATING.get_or_init(|| Mutex::new((false, Instant::now())))
}
/// 检查是否已存在窗口,如果存在则显示并返回 true
fn check_existing_window(is_show: bool) -> Option<bool> {
if let Some(app_handle) = handle::Handle::global().app_handle() {
if let Some(window) = app_handle.get_webview_window("main") {
logging!(info, Type::Window, true, "主窗口已存在,将显示现有窗口");
if is_show {
if window.is_minimized().unwrap_or(false) {
logging!(info, Type::Window, true, "窗口已最小化,正在取消最小化");
let _ = window.unminimize();
}
let _ = window.show();
let _ = window.set_focus();
#[cfg(target_os = "macos")]
handle::Handle::global().set_activation_policy_regular();
}
return Some(true);
}
}
None
}
/// 获取窗口创建锁,防止并发创建
fn acquire_window_creation_lock() -> Result<(), bool> {
let creating_lock = get_window_creating_lock();
let mut creating = creating_lock.lock();
let (is_creating, last_time) = *creating;
let elapsed = last_time.elapsed();
if is_creating && elapsed < Duration::from_secs(2) {
logging!(
info,
Type::Window,
true,
"窗口创建请求被忽略,因为最近创建过 ({:?}ms)",
elapsed.as_millis()
);
return Err(false);
}
*creating = (true, Instant::now());
Ok(())
}
/// 重置窗口创建锁
fn reset_window_creation_lock() {
let creating_lock = get_window_creating_lock();
let mut creating = creating_lock.lock();
*creating = (false, Instant::now());
logging!(debug, Type::Window, true, "窗口创建状态已重置");
}
/// 构建新的 WebView 窗口
fn build_new_window() -> Result<WebviewWindow, String> {
let app_handle = handle::Handle::global().app_handle().ok_or_else(|| {
logging!(
error,
Type::Window,
true,
"无法获取app_handle窗口创建失败"
);
"无法获取app_handle".to_string()
})?;
tauri::WebviewWindowBuilder::new(
&app_handle,
"main", /* the unique window label */
tauri::WebviewUrl::App("index.html".into()),
)
.title("Clash Verge")
.center()
.decorations(true)
.fullscreen(false)
.inner_size(DEFAULT_WIDTH, DEFAULT_HEIGHT)
.min_inner_size(MINIMAL_WIDTH, MINIMAL_HEIGHT)
.visible(true) // 立即显示窗口,避免用户等待
.initialization_script(WINDOW_INITIAL_SCRIPT)
.build()
.map_err(|e| {
logging!(error, Type::Window, true, "主窗口构建失败: {}", e);
e.to_string()
})
}
/// 窗口创建后的初始设置
async fn setup_window_post_creation() {
update_ui_ready_stage(UiReadyStage::NotStarted);
handle::Handle::global().mark_startup_completed();
logging!(
debug,
Type::Window,
true,
"异步窗口任务开始 (启动已标记完成)"
);
// 先运行轻量模式检测
lightweight::run_once_auto_lightweight().await;
// 发送启动完成事件,触发前端开始加载
logging!(
debug,
Type::Window,
true,
"发送 verge://startup-completed 事件"
);
handle::Handle::notify_startup_completed();
}
/// 通过窗口标签处理窗口显示逻辑(减少任务大小的优化版本)
fn handle_window_display_by_label(window_label: String, is_show: bool) {
if !is_show {
logging!(
debug,
Type::Window,
true,
"is_show为false窗口保持隐藏状态"
);
return;
}
// 通过标签重新获取窗口实例
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
logging!(error, Type::Window, true, "无法获取app handle");
return;
}
};
let window = match app_handle.get_webview_window(&window_label) {
Some(window) => window,
None => {
logging!(
error,
Type::Window,
true,
"无法通过标签获取窗口: {}",
window_label
);
return;
}
};
// 立即显示窗口
let _ = window.show();
let _ = window.set_focus();
logging!(info, Type::Window, true, "窗口已立即显示");
#[cfg(target_os = "macos")]
handle::Handle::global().set_activation_policy_regular();
let timeout_seconds = if crate::module::lightweight::is_in_lightweight_mode() {
3
} else {
8
};
logging!(
info,
Type::Window,
true,
"开始监控UI加载状态 (最多{}秒)...",
timeout_seconds
);
// 异步监控UI状态
AsyncHandler::spawn(move || async move {
monitor_ui_loading(timeout_seconds).await;
});
logging!(info, Type::Window, true, "窗口显示流程完成");
}
/// 监控 UI 加载状态 - 优雅的事件驱动版本
async fn monitor_ui_loading(timeout_seconds: u64) {
logging!(
debug,
Type::Window,
true,
"启动UI状态监控使用事件驱动方式超时{}秒",
timeout_seconds
);
// 使用事件驱动的方式等待 UI 就绪,完全消除轮询
let ui_ready = wait_for_ui_ready(timeout_seconds).await;
if ui_ready {
logging!(info, Type::Window, true, "UI已完全加载就绪事件驱动");
handle::Handle::global()
.get_window()
.map(|window| window.eval(INITIAL_LOADING_OVERLAY));
} else {
logging!(
warn,
Type::Window,
true,
"UI加载监控超时({}秒),但窗口已正常显示",
timeout_seconds
);
// 超时后手动设置 UI 就绪状态(保持向后兼容性)
get_ui_ready().store(true, std::sync::atomic::Ordering::Release);
logging!(info, Type::Window, true, "超时后成功设置UI就绪状态");
}
}
pub async fn create_window(is_show: bool) -> bool {
logging!(
info,
Type::Window,
true,
"开始创建/显示主窗口, is_show={}",
is_show
);
if !is_show {
lightweight::set_lightweight_mode(true).await;
handle::Handle::notify_startup_completed();
return false;
}
// 检查是否已存在窗口
if let Some(result) = check_existing_window(is_show) {
return result;
}
// 检查 app_handle 是否存在
if handle::Handle::global().app_handle().is_none() {
logging!(error, Type::Window, true, "No window found in app_handle");
return false;
}
// 获取窗口创建锁
if let Err(should_return) = acquire_window_creation_lock() {
return should_return;
}
// 构建新窗口
let newly_created_window = match build_new_window() {
Ok(window) => {
// 窗口创建成功,重置锁状态
reset_window_creation_lock();
window
}
Err(_) => {
// 窗口创建失败,重置锁状态
reset_window_creation_lock();
return false;
}
};
logging!(debug, Type::Window, true, "主窗口实例创建成功");
// 获取窗口标签,减少闭包捕获的大小
let window_label = newly_created_window.label().to_string();
// 异步处理窗口后续设置,只捕获必要的小数据
setup_window_post_creation().await;
handle_window_display_by_label(window_label, is_show);
true
}

View File

@@ -0,0 +1,71 @@
pub const WINDOW_INITIAL_SCRIPT: &str = r#"
console.log('[Tauri] 窗口初始化脚本开始执行');
function createLoadingOverlay() {
if (document.getElementById('initial-loading-overlay')) {
console.log('[Tauri] 加载指示器已存在');
return;
}
console.log('[Tauri] 创建加载指示器');
const loadingDiv = document.createElement('div');
loadingDiv.id = 'initial-loading-overlay';
loadingDiv.innerHTML = `
<div style="
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: var(--bg-color, #f5f5f5); color: var(--text-color, #333);
display: flex; flex-direction: column; align-items: center;
justify-content: center; z-index: 9999;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
transition: opacity 0.3s ease;
">
<div style="margin-bottom: 20px;">
<div style="
width: 40px; height: 40px; border: 3px solid #e3e3e3;
border-top: 3px solid #3498db; border-radius: 50%;
animation: spin 1s linear infinite;
"></div>
</div>
<div style="font-size: 14px; opacity: 0.7;">Loading Clash Verge...</div>
</div>
<style>
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@media (prefers-color-scheme: dark) {
:root { --bg-color: #1a1a1a; --text-color: #ffffff; }
}
</style>
`;
if (document.body) {
document.body.appendChild(loadingDiv);
} else {
document.addEventListener('DOMContentLoaded', () => {
if (document.body && !document.getElementById('initial-loading-overlay')) {
document.body.appendChild(loadingDiv);
}
});
}
}
createLoadingOverlay();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createLoadingOverlay);
} else {
createLoadingOverlay();
}
console.log('[Tauri] 窗口初始化脚本执行完成');
"#;
pub const INITIAL_LOADING_OVERLAY: &str = r"
const overlay = document.getElementById('initial-loading-overlay');
if (overlay) {
overlay.style.opacity = '0';
setTimeout(() => overlay.remove(), 300);
}
";

View File

@@ -44,9 +44,8 @@ pub async fn check_singleton() -> Result<()> {
pub fn embed_server() {
let port = IVerge::get_singleton_port();
AsyncHandler::spawn(move || async move {
AsyncHandler::spawn_blocking(move || async move {
let visible = warp::path!("commands" / "visible").and_then(|| async {
resolve::create_window(false).await;
Ok::<_, warp::Rejection>(warp::reply::with_status(
"ok".to_string(),
warp::http::StatusCode::OK,
@@ -85,7 +84,11 @@ pub fn embed_server() {
// Spawn async work in a fire-and-forget manner
let param = query.param.clone();
tokio::task::spawn_local(async move {
logging_error!(Type::Setup, true, resolve::resolve_scheme(param).await);
logging_error!(
Type::Setup,
true,
resolve::scheme::resolve_scheme(param).await
);
});
warp::reply::with_status("ok".to_string(), warp::http::StatusCode::OK)
});

View File

@@ -365,7 +365,7 @@ impl WindowManager {
use crate::utils::resolve;
// 使用 tokio runtime 阻塞调用 async 函数
AsyncHandler::block_on(resolve::create_window(true))
AsyncHandler::block_on(resolve::window::create_window(true))
}
/// 获取详细的窗口状态信息