mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-04-18 08:21:34 +08:00
Compare commits
7 Commits
afa29c4d2a
...
325de86e35
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
325de86e35 | ||
|
|
3aa39bff94 | ||
|
|
437fef1c30 | ||
|
|
ec82b69786 | ||
|
|
04ce3d1772 | ||
|
|
b8fbabae04 | ||
|
|
2c766e1ada |
8
.github/workflows/pr-ai-slop-review.lock.yml
generated
vendored
8
.github/workflows/pr-ai-slop-review.lock.yml
generated
vendored
@ -60,7 +60,7 @@ jobs:
|
|||||||
title: ${{ steps.sanitized.outputs.title }}
|
title: ${{ steps.sanitized.outputs.title }}
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Scripts
|
- name: Setup Scripts
|
||||||
uses: github/gh-aw-actions/setup@742ca9c12baa13667ac53db8eb95f48414f60792 # v0.65.7
|
uses: github/gh-aw-actions/setup@addd8a8bc8bad66050cec907c7bf182cca4d2e69 # v0.67.1
|
||||||
with:
|
with:
|
||||||
destination: ${{ runner.temp }}/gh-aw/actions
|
destination: ${{ runner.temp }}/gh-aw/actions
|
||||||
- name: Generate agentic run info
|
- name: Generate agentic run info
|
||||||
@ -271,7 +271,7 @@ jobs:
|
|||||||
output_types: ${{ steps.collect_output.outputs.output_types }}
|
output_types: ${{ steps.collect_output.outputs.output_types }}
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Scripts
|
- name: Setup Scripts
|
||||||
uses: github/gh-aw-actions/setup@742ca9c12baa13667ac53db8eb95f48414f60792 # v0.65.7
|
uses: github/gh-aw-actions/setup@addd8a8bc8bad66050cec907c7bf182cca4d2e69 # v0.67.1
|
||||||
with:
|
with:
|
||||||
destination: ${{ runner.temp }}/gh-aw/actions
|
destination: ${{ runner.temp }}/gh-aw/actions
|
||||||
- name: Set runtime paths
|
- name: Set runtime paths
|
||||||
@ -888,7 +888,7 @@ jobs:
|
|||||||
total_count: ${{ steps.missing_tool.outputs.total_count }}
|
total_count: ${{ steps.missing_tool.outputs.total_count }}
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Scripts
|
- name: Setup Scripts
|
||||||
uses: github/gh-aw-actions/setup@742ca9c12baa13667ac53db8eb95f48414f60792 # v0.65.7
|
uses: github/gh-aw-actions/setup@addd8a8bc8bad66050cec907c7bf182cca4d2e69 # v0.67.1
|
||||||
with:
|
with:
|
||||||
destination: ${{ runner.temp }}/gh-aw/actions
|
destination: ${{ runner.temp }}/gh-aw/actions
|
||||||
- name: Download agent output artifact
|
- name: Download agent output artifact
|
||||||
@ -999,7 +999,7 @@ jobs:
|
|||||||
process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }}
|
process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }}
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Scripts
|
- name: Setup Scripts
|
||||||
uses: github/gh-aw-actions/setup@742ca9c12baa13667ac53db8eb95f48414f60792 # v0.65.7
|
uses: github/gh-aw-actions/setup@addd8a8bc8bad66050cec907c7bf182cca4d2e69 # v0.67.1
|
||||||
with:
|
with:
|
||||||
destination: ${{ runner.temp }}/gh-aw/actions
|
destination: ${{ runner.temp }}/gh-aw/actions
|
||||||
- name: Download agent output artifact
|
- name: Download agent output artifact
|
||||||
|
|||||||
@ -54,7 +54,7 @@
|
|||||||
"@tauri-apps/plugin-http": "~2.5.7",
|
"@tauri-apps/plugin-http": "~2.5.7",
|
||||||
"@tauri-apps/plugin-process": "^2.3.1",
|
"@tauri-apps/plugin-process": "^2.3.1",
|
||||||
"@tauri-apps/plugin-shell": "2.3.5",
|
"@tauri-apps/plugin-shell": "2.3.5",
|
||||||
"@tauri-apps/plugin-updater": "2.10.0",
|
"@tauri-apps/plugin-updater": "2.10.1",
|
||||||
"ahooks": "^3.9.6",
|
"ahooks": "^3.9.6",
|
||||||
"cidr-block": "^2.3.0",
|
"cidr-block": "^2.3.0",
|
||||||
"dayjs": "1.11.20",
|
"dayjs": "1.11.20",
|
||||||
|
|||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@ -69,8 +69,8 @@ importers:
|
|||||||
specifier: 2.3.5
|
specifier: 2.3.5
|
||||||
version: 2.3.5
|
version: 2.3.5
|
||||||
'@tauri-apps/plugin-updater':
|
'@tauri-apps/plugin-updater':
|
||||||
specifier: 2.10.0
|
specifier: 2.10.1
|
||||||
version: 2.10.0
|
version: 2.10.1
|
||||||
ahooks:
|
ahooks:
|
||||||
specifier: ^3.9.6
|
specifier: ^3.9.6
|
||||||
version: 3.9.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 3.9.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
@ -1562,8 +1562,8 @@ packages:
|
|||||||
'@tauri-apps/plugin-shell@2.3.5':
|
'@tauri-apps/plugin-shell@2.3.5':
|
||||||
resolution: {integrity: sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==}
|
resolution: {integrity: sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==}
|
||||||
|
|
||||||
'@tauri-apps/plugin-updater@2.10.0':
|
'@tauri-apps/plugin-updater@2.10.1':
|
||||||
resolution: {integrity: sha512-ljN8jPlnT0aSn8ecYhuBib84alxfMx6Hc8vJSKMJyzGbTPFZAC44T2I1QNFZssgWKrAlofvJqCC6Rr472JWfkQ==}
|
resolution: {integrity: sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==}
|
||||||
|
|
||||||
'@tybys/wasm-util@0.10.1':
|
'@tybys/wasm-util@0.10.1':
|
||||||
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
||||||
@ -5059,7 +5059,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.10.1
|
'@tauri-apps/api': 2.10.1
|
||||||
|
|
||||||
'@tauri-apps/plugin-updater@2.10.0':
|
'@tauri-apps/plugin-updater@2.10.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.10.1
|
'@tauri-apps/api': 2.10.1
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::core::{autostart, handle};
|
use crate::core::autostart;
|
||||||
use crate::utils::resolve::ui::{self, UiReadyStage};
|
|
||||||
use crate::{cmd::StringifyErr as _, feat, utils::dirs};
|
use crate::{cmd::StringifyErr as _, feat, utils::dirs};
|
||||||
use clash_verge_logging::{Type, logging};
|
|
||||||
use smartstring::alias::String;
|
use smartstring::alias::String;
|
||||||
use tauri::{AppHandle, Manager as _};
|
use tauri::{AppHandle, Manager as _};
|
||||||
|
|
||||||
@ -109,22 +107,3 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
|
|||||||
pub async fn copy_icon_file(path: String, icon_info: feat::IconInfo) -> CmdResult<String> {
|
pub async fn copy_icon_file(path: String, icon_info: feat::IconInfo) -> CmdResult<String> {
|
||||||
feat::copy_icon_file(path, icon_info).await
|
feat::copy_icon_file(path, icon_info).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 通知UI已准备就绪
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn notify_ui_ready() {
|
|
||||||
logging!(info, Type::Cmd, "前端UI已准备就绪");
|
|
||||||
ui::mark_ui_ready();
|
|
||||||
|
|
||||||
handle::Handle::refresh_clash();
|
|
||||||
let delayed_refresh_delay = std::time::Duration::from_millis(1500);
|
|
||||||
tokio::time::sleep(delayed_refresh_delay).await;
|
|
||||||
handle::Handle::refresh_clash();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// UI加载阶段
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn update_ui_stage(stage: UiReadyStage) {
|
|
||||||
logging!(info, Type::Cmd, "UI加载阶段更新: {:?}", &stage);
|
|
||||||
ui::update_ui_ready_stage(stage);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ use crate::{
|
|||||||
core::{CoreManager, handle, tray},
|
core::{CoreManager, handle, tray},
|
||||||
feat::clean_async,
|
feat::clean_async,
|
||||||
process::AsyncHandler,
|
process::AsyncHandler,
|
||||||
utils::{self, resolve::reset_resolve_done},
|
utils,
|
||||||
};
|
};
|
||||||
use clash_verge_logging::{Type, logging};
|
use clash_verge_logging::{Type, logging};
|
||||||
use serde_yaml_ng::{Mapping, Value};
|
use serde_yaml_ng::{Mapping, Value};
|
||||||
@ -42,7 +42,6 @@ pub async fn restart_app() {
|
|||||||
if cleanup_result { 0 } else { 1 }
|
if cleanup_result { 0 } else { 1 }
|
||||||
);
|
);
|
||||||
|
|
||||||
reset_resolve_done();
|
|
||||||
let app_handle = handle::Handle::app_handle();
|
let app_handle = handle::Handle::app_handle();
|
||||||
app_handle.restart();
|
app_handle.restart();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -149,8 +149,6 @@ mod app_init {
|
|||||||
cmd::start_core,
|
cmd::start_core,
|
||||||
cmd::stop_core,
|
cmd::stop_core,
|
||||||
cmd::restart_core,
|
cmd::restart_core,
|
||||||
cmd::notify_ui_ready,
|
|
||||||
cmd::update_ui_stage,
|
|
||||||
cmd::get_running_mode,
|
cmd::get_running_mode,
|
||||||
cmd::get_auto_launch_status,
|
cmd::get_auto_launch_status,
|
||||||
cmd::entry_lightweight_mode,
|
cmd::entry_lightweight_mode,
|
||||||
@ -253,7 +251,6 @@ pub fn run() {
|
|||||||
resolve::resolve_setup_async();
|
resolve::resolve_setup_async();
|
||||||
resolve::resolve_setup_sync();
|
resolve::resolve_setup_sync();
|
||||||
resolve::init_signal();
|
resolve::init_signal();
|
||||||
resolve::resolve_done();
|
|
||||||
|
|
||||||
logging!(info, Type::Setup, "初始化已启动");
|
logging!(info, Type::Setup, "初始化已启动");
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@ -6,6 +6,7 @@ use crate::{
|
|||||||
config::Config,
|
config::Config,
|
||||||
core::{
|
core::{
|
||||||
CoreManager, Timer,
|
CoreManager, Timer,
|
||||||
|
handle::Handle,
|
||||||
hotkey::Hotkey,
|
hotkey::Hotkey,
|
||||||
logger::Logger,
|
logger::Logger,
|
||||||
service::{SERVICE_MANAGER, ServiceManager, is_service_ipc_path_exists},
|
service::{SERVICE_MANAGER, ServiceManager, is_service_ipc_path_exists},
|
||||||
@ -22,7 +23,6 @@ use clash_verge_signal;
|
|||||||
|
|
||||||
pub mod dns;
|
pub mod dns;
|
||||||
pub mod scheme;
|
pub mod scheme;
|
||||||
pub mod ui;
|
|
||||||
pub mod window;
|
pub mod window;
|
||||||
pub mod window_script;
|
pub mod window_script;
|
||||||
|
|
||||||
@ -62,14 +62,9 @@ pub fn resolve_setup_async() {
|
|||||||
init_system_proxy_guard().await;
|
init_system_proxy_guard().await;
|
||||||
});
|
});
|
||||||
|
|
||||||
let tray_init = async {
|
|
||||||
init_tray().await;
|
|
||||||
refresh_tray_menu().await;
|
|
||||||
};
|
|
||||||
|
|
||||||
let _ = futures::join!(
|
let _ = futures::join!(
|
||||||
core_init,
|
core_init,
|
||||||
tray_init,
|
init_tray(),
|
||||||
init_timer(),
|
init_timer(),
|
||||||
init_hotkey(),
|
init_hotkey(),
|
||||||
init_auto_lightweight_boot(),
|
init_auto_lightweight_boot(),
|
||||||
@ -77,7 +72,9 @@ pub fn resolve_setup_async() {
|
|||||||
init_silent_updater(),
|
init_silent_updater(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Handle::refresh_clash();
|
||||||
refresh_tray_menu().await;
|
refresh_tray_menu().await;
|
||||||
|
resolve_done();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,7 +216,3 @@ pub fn resolve_done() {
|
|||||||
pub fn is_resolve_done() -> bool {
|
pub fn is_resolve_done() -> bool {
|
||||||
RESOLVE_DONE.load(Ordering::Acquire)
|
RESOLVE_DONE.load(Ordering::Acquire)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset_resolve_done() {
|
|
||||||
RESOLVE_DONE.store(false, Ordering::Release);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,57 +0,0 @@
|
|||||||
use once_cell::sync::OnceCell;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::sync::{
|
|
||||||
Arc,
|
|
||||||
atomic::{AtomicBool, AtomicU8, Ordering},
|
|
||||||
};
|
|
||||||
use tokio::sync::Notify;
|
|
||||||
|
|
||||||
use clash_verge_logging::{Type, logging};
|
|
||||||
|
|
||||||
// 获取 UI 是否准备就绪的全局状态
|
|
||||||
static UI_READY: AtomicBool = AtomicBool::new(false);
|
|
||||||
// 获取UI就绪状态细节
|
|
||||||
static UI_READY_STATE: AtomicU8 = AtomicU8::new(0);
|
|
||||||
// 添加通知机制,用于事件驱动的 UI 就绪检测
|
|
||||||
static UI_READY_NOTIFY: OnceCell<Arc<Notify>> = OnceCell::new();
|
|
||||||
|
|
||||||
// UI就绪阶段状态枚举
|
|
||||||
#[repr(u8)]
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
|
||||||
pub enum UiReadyStage {
|
|
||||||
NotStarted = 0,
|
|
||||||
Loading,
|
|
||||||
DomReady,
|
|
||||||
ResourcesLoaded,
|
|
||||||
Ready,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_ui_ready() -> &'static AtomicBool {
|
|
||||||
&UI_READY
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_ui_ready_state() -> &'static AtomicU8 {
|
|
||||||
&UI_READY_STATE
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_ui_ready_notify() -> &'static Arc<Notify> {
|
|
||||||
UI_READY_NOTIFY.get_or_init(|| Arc::new(Notify::new()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新UI准备阶段
|
|
||||||
pub fn update_ui_ready_stage(stage: UiReadyStage) {
|
|
||||||
get_ui_ready_state().store(stage as u8, Ordering::Release);
|
|
||||||
// 如果是最终阶段,标记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, "UI已标记为完全就绪");
|
|
||||||
|
|
||||||
// 通知所有等待的任务
|
|
||||||
get_ui_ready_notify().notify_waiters();
|
|
||||||
}
|
|
||||||
@ -2,11 +2,7 @@ use dark_light::{Mode as SystemTheme, detect as detect_system_theme};
|
|||||||
use tauri::utils::config::Color;
|
use tauri::utils::config::Color;
|
||||||
use tauri::{Theme, WebviewWindow};
|
use tauri::{Theme, WebviewWindow};
|
||||||
|
|
||||||
use crate::{
|
use crate::{config::Config, core::handle, utils::resolve::window_script::build_window_initial_script};
|
||||||
config::Config,
|
|
||||||
core::handle,
|
|
||||||
utils::resolve::window_script::{INITIAL_LOADING_OVERLAY, build_window_initial_script},
|
|
||||||
};
|
|
||||||
use clash_verge_logging::{Type, logging_error};
|
use clash_verge_logging::{Type, logging_error};
|
||||||
|
|
||||||
const DARK_BACKGROUND_COLOR: Color = Color(46, 48, 61, 255); // #2E303D
|
const DARK_BACKGROUND_COLOR: Color = Color(46, 48, 61, 255); // #2E303D
|
||||||
@ -82,7 +78,6 @@ pub async fn build_new_window() -> Result<WebviewWindow, String> {
|
|||||||
match builder.build() {
|
match builder.build() {
|
||||||
Ok(window) => {
|
Ok(window) => {
|
||||||
logging_error!(Type::Window, window.set_background_color(Some(background_color)));
|
logging_error!(Type::Window, window.set_background_color(Some(background_color)));
|
||||||
logging_error!(Type::Window, window.eval(INITIAL_LOADING_OVERLAY));
|
|
||||||
Ok(window)
|
Ok(window)
|
||||||
}
|
}
|
||||||
Err(e) => Err(e.to_string()),
|
Err(e) => Err(e.to_string()),
|
||||||
|
|||||||
@ -91,11 +91,3 @@ pub const WINDOW_INITIAL_SCRIPT: &str = r##"
|
|||||||
|
|
||||||
console.log('[Tauri] 窗口初始化脚本执行完成');
|
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);
|
|
||||||
}
|
|
||||||
";
|
|
||||||
|
|||||||
@ -129,7 +129,7 @@ impl WindowManager {
|
|||||||
logging!(info, Type::Window, "窗口不存在,创建新窗口");
|
logging!(info, Type::Window, "窗口不存在,创建新窗口");
|
||||||
if Self::create_window(true).await {
|
if Self::create_window(true).await {
|
||||||
logging!(info, Type::Window, "窗口创建成功");
|
logging!(info, Type::Window, "窗口创建成功");
|
||||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||||
WindowOperationResult::Created
|
WindowOperationResult::Created
|
||||||
} else {
|
} else {
|
||||||
logging!(warn, Type::Window, "窗口创建失败");
|
logging!(warn, Type::Window, "窗口创建失败");
|
||||||
@ -285,31 +285,31 @@ impl WindowManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 创建新窗口,防抖避免重复调用
|
/// 创建新窗口,防抖避免重复调用
|
||||||
pub fn create_window(is_show: bool) -> Pin<Box<dyn Future<Output = bool> + Send>> {
|
/// 窗口创建后保持隐藏,由前端 index.html 在 overlay 渲染后调用 show,避免主题闪烁
|
||||||
|
pub fn create_window(should_create: bool) -> Pin<Box<dyn Future<Output = bool> + Send>> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
logging!(info, Type::Window, "开始创建/显示主窗口, is_show={}", is_show);
|
logging!(info, Type::Window, "开始创建主窗口, should_create={}", should_create);
|
||||||
|
|
||||||
if !is_show {
|
if !should_create {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let window = match build_new_window().await {
|
match build_new_window().await {
|
||||||
Ok(window) => {
|
Ok(_) => {
|
||||||
logging!(info, Type::Window, "新窗口创建成功");
|
logging!(info, Type::Window, "新窗口创建成功,等待前端渲染后显示");
|
||||||
window
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
handle::Handle::global().set_activation_policy_regular();
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
logging!(error, Type::Window, "新窗口创建失败: {}", e);
|
logging!(error, Type::Window, "新窗口创建失败: {}", e);
|
||||||
return false;
|
false
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// 直接激活刚创建的窗口,避免因防抖导致首次显示被跳过
|
|
||||||
if WindowOperationResult::Failed == Self::activate_window(&window) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -41,7 +41,7 @@ const MODE_META: Record<
|
|||||||
export const ClashModeCard = () => {
|
export const ClashModeCard = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { verge } = useVerge()
|
const { verge } = useVerge()
|
||||||
const { clashConfig, refreshClashConfig } = useAppData()
|
const { clashConfig, isCoreDataPending, refreshClashConfig } = useAppData()
|
||||||
|
|
||||||
// 支持的模式列表
|
// 支持的模式列表
|
||||||
const modeList = CLASH_MODES
|
const modeList = CLASH_MODES
|
||||||
@ -57,8 +57,11 @@ export const ClashModeCard = () => {
|
|||||||
if (currentModeKey) {
|
if (currentModeKey) {
|
||||||
return t(MODE_META[currentModeKey].description)
|
return t(MODE_META[currentModeKey].description)
|
||||||
}
|
}
|
||||||
|
if (isCoreDataPending) {
|
||||||
|
return '\u00A0'
|
||||||
|
}
|
||||||
return t('home.components.clashMode.errors.communication')
|
return t('home.components.clashMode.errors.communication')
|
||||||
}, [currentModeKey, t])
|
}, [currentModeKey, isCoreDataPending, t])
|
||||||
|
|
||||||
// 模式图标映射
|
// 模式图标映射
|
||||||
const modeIcons = useMemo(
|
const modeIcons = useMemo(
|
||||||
|
|||||||
@ -105,7 +105,8 @@ export const CurrentProxyCard = () => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const { proxies, clashConfig, refreshProxy, rules } = useAppData()
|
const { proxies, clashConfig, isCoreDataPending, refreshProxy, rules } =
|
||||||
|
useAppData()
|
||||||
const { verge } = useVerge()
|
const { verge } = useVerge()
|
||||||
const { current: currentProfile } = useProfiles()
|
const { current: currentProfile } = useProfiles()
|
||||||
const autoDelayEnabled = verge?.enable_auto_delay_detection ?? false
|
const autoDelayEnabled = verge?.enable_auto_delay_detection ?? false
|
||||||
@ -444,6 +445,12 @@ export const CurrentProxyCard = () => {
|
|||||||
[setState],
|
[setState],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (timeoutRef.current) clearTimeout(timeoutRef.current)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
// 处理代理组变更
|
// 处理代理组变更
|
||||||
const handleGroupChange = useCallback(
|
const handleGroupChange = useCallback(
|
||||||
(event: SelectChangeEvent<string>) => {
|
(event: SelectChangeEvent<string>) => {
|
||||||
@ -905,7 +912,9 @@ export const CurrentProxyCard = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{currentProxy ? (
|
{isCoreDataPending ? (
|
||||||
|
<Box sx={{ py: 4 }} />
|
||||||
|
) : currentProxy ? (
|
||||||
<Box>
|
<Box>
|
||||||
{/* 代理节点信息显示 */}
|
{/* 代理节点信息显示 */}
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@ -425,7 +425,7 @@ function useIPInfo() {
|
|||||||
queryKey: [IP_INFO_CACHE_KEY],
|
queryKey: [IP_INFO_CACHE_KEY],
|
||||||
queryFn: getIpInfo,
|
queryFn: getIpInfo,
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
gcTime: Infinity,
|
gcTime: 60 * 60 * 1000,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
retry: 1,
|
retry: 1,
|
||||||
|
|||||||
@ -168,6 +168,13 @@ export const EditorViewer = ({
|
|||||||
}
|
}
|
||||||
}, [open, syncMaximizedState])
|
}, [open, syncMaximizedState])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
editorRef.current?.dispose()
|
||||||
|
editorRef.current = null
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={open}
|
open={open}
|
||||||
|
|||||||
@ -34,11 +34,13 @@ import {
|
|||||||
requestIdleCallback,
|
requestIdleCallback,
|
||||||
} from 'foxact/request-idle-callback'
|
} from 'foxact/request-idle-callback'
|
||||||
import yaml from 'js-yaml'
|
import yaml from 'js-yaml'
|
||||||
|
import type { editor } from 'monaco-editor'
|
||||||
import {
|
import {
|
||||||
startTransition,
|
startTransition,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { Controller, useForm } from 'react-hook-form'
|
import { Controller, useForm } from 'react-hook-form'
|
||||||
@ -149,6 +151,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
[t],
|
[t],
|
||||||
)
|
)
|
||||||
const themeMode = useThemeMode()
|
const themeMode = useThemeMode()
|
||||||
|
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
|
||||||
const [prevData, setPrevData] = useState('')
|
const [prevData, setPrevData] = useState('')
|
||||||
const [currData, setCurrData] = useState('')
|
const [currData, setCurrData] = useState('')
|
||||||
const [visualization, setVisualization] = useState(true)
|
const [visualization, setVisualization] = useState(true)
|
||||||
@ -481,6 +484,13 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
getInterfaceNameList()
|
getInterfaceNameList()
|
||||||
}, [fetchContent, fetchProfile, getInterfaceNameList, open])
|
}, [fetchContent, fetchProfile, getInterfaceNameList, open])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
editorRef.current?.dispose()
|
||||||
|
editorRef.current = null
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const validateGroup = () => {
|
const validateGroup = () => {
|
||||||
const group = formIns.getValues()
|
const group = formIns.getValues()
|
||||||
if (group.name === '') {
|
if (group.name === '') {
|
||||||
@ -1105,6 +1115,9 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
language="yaml"
|
language="yaml"
|
||||||
value={currData}
|
value={currData}
|
||||||
theme={themeMode === 'light' ? 'light' : 'vs-dark'}
|
theme={themeMode === 'light' ? 'light' : 'vs-dark'}
|
||||||
|
onMount={(editorInstance) => {
|
||||||
|
editorRef.current = editorInstance
|
||||||
|
}}
|
||||||
options={{
|
options={{
|
||||||
tabSize: 2, // 根据语言类型设置缩进大小
|
tabSize: 2, // 根据语言类型设置缩进大小
|
||||||
minimap: {
|
minimap: {
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import {
|
|||||||
import { open } from '@tauri-apps/plugin-shell'
|
import { open } from '@tauri-apps/plugin-shell'
|
||||||
import { useLockFn } from 'ahooks'
|
import { useLockFn } from 'ahooks'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { useCallback, useEffect, useReducer, useState } from 'react'
|
import { useCallback, useEffect, useReducer, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { ConfirmViewer } from '@/components/profile/confirm-viewer'
|
import { ConfirmViewer } from '@/components/profile/confirm-viewer'
|
||||||
@ -96,7 +96,11 @@ export const ProfileItem = (props: Props) => {
|
|||||||
|
|
||||||
// 新增状态:是否显示下次更新时间
|
// 新增状态:是否显示下次更新时间
|
||||||
const [showNextUpdate, setShowNextUpdate] = useState(false)
|
const [showNextUpdate, setShowNextUpdate] = useState(false)
|
||||||
|
const showNextUpdateRef = useRef(false)
|
||||||
const [nextUpdateTime, setNextUpdateTime] = useState('')
|
const [nextUpdateTime, setNextUpdateTime] = useState('')
|
||||||
|
const refreshTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(
|
||||||
|
undefined,
|
||||||
|
)
|
||||||
|
|
||||||
const { uid, name = 'Profile', extra, updated = 0, option } = itemData
|
const { uid, name = 'Profile', extra, updated = 0, option } = itemData
|
||||||
|
|
||||||
@ -178,6 +182,10 @@ export const ProfileItem = (props: Props) => {
|
|||||||
setShowNextUpdate(!showNextUpdate)
|
setShowNextUpdate(!showNextUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
showNextUpdateRef.current = showNextUpdate
|
||||||
|
}, [showNextUpdate])
|
||||||
|
|
||||||
// 当组件加载或更新间隔变化时更新下次更新时间
|
// 当组件加载或更新间隔变化时更新下次更新时间
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showNextUpdate) {
|
if (showNextUpdate) {
|
||||||
@ -192,19 +200,18 @@ export const ProfileItem = (props: Props) => {
|
|||||||
|
|
||||||
// 订阅定时器更新事件
|
// 订阅定时器更新事件
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let refreshTimeout: number | undefined
|
|
||||||
// 处理定时器更新事件 - 这个事件专门用于通知定时器变更
|
// 处理定时器更新事件 - 这个事件专门用于通知定时器变更
|
||||||
const handleTimerUpdate = (event: Event) => {
|
const handleTimerUpdate = (event: Event) => {
|
||||||
const source = event as CustomEvent<string> & { payload?: string }
|
const source = event as CustomEvent<string> & { payload?: string }
|
||||||
const updatedUid = source.detail ?? source.payload
|
const updatedUid = source.detail ?? source.payload
|
||||||
|
|
||||||
// 只有当更新的是当前配置时才刷新显示
|
// 只有当更新的是当前配置时才刷新显示
|
||||||
if (updatedUid === itemData.uid && showNextUpdate) {
|
if (updatedUid === itemData.uid && showNextUpdateRef.current) {
|
||||||
debugLog(`收到定时器更新事件: uid=${updatedUid}`)
|
debugLog(`收到定时器更新事件: uid=${updatedUid}`)
|
||||||
if (refreshTimeout !== undefined) {
|
if (refreshTimeoutRef.current !== undefined) {
|
||||||
clearTimeout(refreshTimeout)
|
clearTimeout(refreshTimeoutRef.current)
|
||||||
}
|
}
|
||||||
refreshTimeout = window.setTimeout(() => {
|
refreshTimeoutRef.current = window.setTimeout(() => {
|
||||||
fetchNextUpdateTime(true)
|
fetchNextUpdateTime(true)
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
@ -214,13 +221,13 @@ export const ProfileItem = (props: Props) => {
|
|||||||
window.addEventListener('verge://timer-updated', handleTimerUpdate)
|
window.addEventListener('verge://timer-updated', handleTimerUpdate)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (refreshTimeout !== undefined) {
|
if (refreshTimeoutRef.current !== undefined) {
|
||||||
clearTimeout(refreshTimeout)
|
clearTimeout(refreshTimeoutRef.current)
|
||||||
}
|
}
|
||||||
// 清理事件监听
|
// 清理事件监听
|
||||||
window.removeEventListener('verge://timer-updated', handleTimerUpdate)
|
window.removeEventListener('verge://timer-updated', handleTimerUpdate)
|
||||||
}
|
}
|
||||||
}, [fetchNextUpdateTime, itemData.uid, showNextUpdate])
|
}, [fetchNextUpdateTime, itemData.uid])
|
||||||
|
|
||||||
// local file mode
|
// local file mode
|
||||||
// remote file mode
|
// remote file mode
|
||||||
|
|||||||
@ -27,11 +27,13 @@ import {
|
|||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { useLockFn } from 'ahooks'
|
import { useLockFn } from 'ahooks'
|
||||||
import yaml from 'js-yaml'
|
import yaml from 'js-yaml'
|
||||||
|
import type { editor } from 'monaco-editor'
|
||||||
import {
|
import {
|
||||||
startTransition,
|
startTransition,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -56,6 +58,7 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
const { profileUid, property, open, onClose, onSave } = props
|
const { profileUid, property, open, onClose, onSave } = props
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const themeMode = useThemeMode()
|
const themeMode = useThemeMode()
|
||||||
|
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
|
||||||
const [prevData, setPrevData] = useState('')
|
const [prevData, setPrevData] = useState('')
|
||||||
const [currData, setCurrData] = useState('')
|
const [currData, setCurrData] = useState('')
|
||||||
const [visualization, setVisualization] = useState(true)
|
const [visualization, setVisualization] = useState(true)
|
||||||
@ -343,6 +346,13 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
fetchProfile()
|
fetchProfile()
|
||||||
}, [fetchContent, fetchProfile, open])
|
}, [fetchContent, fetchProfile, open])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
editorRef.current?.dispose()
|
||||||
|
editorRef.current = null
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleSave = useLockFn(async () => {
|
const handleSave = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
await saveProfileFile(property, currData)
|
await saveProfileFile(property, currData)
|
||||||
@ -469,6 +479,9 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
language="yaml"
|
language="yaml"
|
||||||
value={currData}
|
value={currData}
|
||||||
theme={themeMode === 'light' ? 'light' : 'vs-dark'}
|
theme={themeMode === 'light' ? 'light' : 'vs-dark'}
|
||||||
|
onMount={(editorInstance) => {
|
||||||
|
editorRef.current = editorInstance
|
||||||
|
}}
|
||||||
options={{
|
options={{
|
||||||
tabSize: 2, // 根据语言类型设置缩进大小
|
tabSize: 2, // 根据语言类型设置缩进大小
|
||||||
minimap: {
|
minimap: {
|
||||||
|
|||||||
@ -29,11 +29,13 @@ import {
|
|||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { useLockFn } from 'ahooks'
|
import { useLockFn } from 'ahooks'
|
||||||
import yaml from 'js-yaml'
|
import yaml from 'js-yaml'
|
||||||
|
import type { editor } from 'monaco-editor'
|
||||||
import {
|
import {
|
||||||
startTransition,
|
startTransition,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -251,6 +253,8 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const themeMode = useThemeMode()
|
const themeMode = useThemeMode()
|
||||||
|
|
||||||
|
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
|
||||||
|
|
||||||
const [prevData, setPrevData] = useState('')
|
const [prevData, setPrevData] = useState('')
|
||||||
const [currData, setCurrData] = useState('')
|
const [currData, setCurrData] = useState('')
|
||||||
const [visualization, setVisualization] = useState(true)
|
const [visualization, setVisualization] = useState(true)
|
||||||
@ -536,6 +540,13 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
fetchProfile()
|
fetchProfile()
|
||||||
}, [fetchContent, fetchProfile, open])
|
}, [fetchContent, fetchProfile, open])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
editorRef.current?.dispose()
|
||||||
|
editorRef.current = null
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const validateRule = () => {
|
const validateRule = () => {
|
||||||
if ((ruleType.required ?? true) && !ruleContent) {
|
if ((ruleType.required ?? true) && !ruleContent) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -770,6 +781,9 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
language="yaml"
|
language="yaml"
|
||||||
value={currData}
|
value={currData}
|
||||||
theme={themeMode === 'light' ? 'light' : 'vs-dark'}
|
theme={themeMode === 'light' ? 'light' : 'vs-dark'}
|
||||||
|
onMount={(editorInstance) => {
|
||||||
|
editorRef.current = editorInstance
|
||||||
|
}}
|
||||||
options={{
|
options={{
|
||||||
tabSize: 2, // 根据语言类型设置缩进大小
|
tabSize: 2, // 根据语言类型设置缩进大小
|
||||||
minimap: {
|
minimap: {
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {
|
|||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import { useLockFn } from 'ahooks'
|
import { useLockFn } from 'ahooks'
|
||||||
import yaml from 'js-yaml'
|
import yaml from 'js-yaml'
|
||||||
|
import type { editor } from 'monaco-editor'
|
||||||
import type { Ref } from 'react'
|
import type { Ref } from 'react'
|
||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
@ -189,6 +190,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [visualization, setVisualization] = useState(true)
|
const [visualization, setVisualization] = useState(true)
|
||||||
const skipYamlSyncRef = useRef(false)
|
const skipYamlSyncRef = useRef(false)
|
||||||
|
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
|
||||||
const [values, setValues] = useState<{
|
const [values, setValues] = useState<{
|
||||||
enable: boolean
|
enable: boolean
|
||||||
listen: string
|
listen: string
|
||||||
@ -453,6 +455,13 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
}
|
}
|
||||||
}, [visualization])
|
}, [visualization])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
editorRef.current?.dispose()
|
||||||
|
editorRef.current = null
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const initDnsConfig = useCallback(async () => {
|
const initDnsConfig = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const dnsConfigExists = await invoke<boolean>(
|
const dnsConfigExists = await invoke<boolean>(
|
||||||
@ -1057,6 +1066,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
value={yamlContent}
|
value={yamlContent}
|
||||||
theme={themeMode === 'light' ? 'light' : 'vs-dark'}
|
theme={themeMode === 'light' ? 'light' : 'vs-dark'}
|
||||||
className="flex-grow"
|
className="flex-grow"
|
||||||
|
onMount={(editorInstance) => {
|
||||||
|
editorRef.current = editorInstance
|
||||||
|
}}
|
||||||
options={{
|
options={{
|
||||||
tabSize: 2,
|
tabSize: 2,
|
||||||
minimap: {
|
minimap: {
|
||||||
|
|||||||
@ -42,7 +42,7 @@ export const useIconCache = ({
|
|||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
gcTime: Infinity,
|
gcTime: 30 * 60 * 1000,
|
||||||
retry: 2,
|
retry: 2,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -93,7 +93,7 @@ export const useMihomoWsSubscription = <T>(
|
|||||||
subscriptionCacheKey ?? '$sub$__disabled__',
|
subscriptionCacheKey ?? '$sub$__disabled__',
|
||||||
]) ?? fallbackData,
|
]) ?? fallbackData,
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
gcTime: Infinity,
|
gcTime: 30_000,
|
||||||
enabled: subscriptionCacheKey !== null,
|
enabled: subscriptionCacheKey !== null,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -243,8 +243,11 @@ export const useMihomoWsSubscription = <T>(
|
|||||||
}, [subscriptionCacheKey])
|
}, [subscriptionCacheKey])
|
||||||
|
|
||||||
const refresh = useCallback(() => {
|
const refresh = useCallback(() => {
|
||||||
|
if (subscriptionCacheKey) {
|
||||||
|
queryClient.removeQueries({ queryKey: [subscriptionCacheKey] })
|
||||||
|
}
|
||||||
setDate(Date.now())
|
setDate(Date.now())
|
||||||
}, [setDate])
|
}, [queryClient, subscriptionCacheKey, setDate])
|
||||||
|
|
||||||
return { response, refresh, subscriptionCacheKey, wsRef }
|
return { response, refresh, subscriptionCacheKey, wsRef }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,27 +11,15 @@
|
|||||||
<title>Clash Verge</title>
|
<title>Clash Verge</title>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--initial-bg: #f5f5f5;
|
--bg-color: #f5f5f5;
|
||||||
--initial-text: #333;
|
--text-color: #333;
|
||||||
--initial-spinner-track: #e3e3e3;
|
|
||||||
--initial-spinner-top: #3498db;
|
|
||||||
--bg-color: var(--initial-bg);
|
|
||||||
--text-color: var(--initial-text);
|
|
||||||
--spinner-track: var(--initial-spinner-track);
|
|
||||||
--spinner-top: var(--initial-spinner-top);
|
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
--initial-bg: #2e303d;
|
--bg-color: #2e303d;
|
||||||
--initial-text: #ffffff;
|
--text-color: #ffffff;
|
||||||
--initial-spinner-track: #3a3a3a;
|
|
||||||
--initial-spinner-top: #0a84ff;
|
|
||||||
--bg-color: var(--initial-bg);
|
|
||||||
--text-color: var(--initial-text);
|
|
||||||
--spinner-track: var(--initial-spinner-track);
|
|
||||||
--spinner-top: var(--initial-spinner-top);
|
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -53,48 +41,28 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: var(--bg-color);
|
background: var(--bg-color);
|
||||||
color: var(--text-color);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
font-family:
|
transition: opacity 0.2s ease-out;
|
||||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#initial-loading-overlay[data-hidden='true'] {
|
#initial-loading-overlay[data-hidden='true'] {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.initial-spinner {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border: 3px solid var(--spinner-track);
|
|
||||||
border-top: 3px solid var(--spinner-top);
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: initial-spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes initial-spin {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="initial-loading-overlay">
|
<div id="initial-loading-overlay"></div>
|
||||||
<div class="initial-spinner"></div>
|
<script>
|
||||||
<div style="font-size: 14px; opacity: 0.7; margin-top: 20px">
|
if (window.__TAURI_INTERNALS__) {
|
||||||
Loading Clash Verge...
|
window.__TAURI_INTERNALS__
|
||||||
</div>
|
.invoke('plugin:window|show', { label: 'main' })
|
||||||
</div>
|
.catch(function () {});
|
||||||
|
window.__TAURI_INTERNALS__
|
||||||
|
.invoke('plugin:window|set_focus', { label: 'main' })
|
||||||
|
.catch(function () {});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="./main.tsx"></script>
|
<script type="module" src="./main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -44,7 +44,6 @@ import { useThemeMode } from '@/services/states'
|
|||||||
import getSystem from '@/utils/get-system'
|
import getSystem from '@/utils/get-system'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useAppInitialization,
|
|
||||||
useCustomTheme,
|
useCustomTheme,
|
||||||
useLayoutEvents,
|
useLayoutEvents,
|
||||||
useLoadingOverlay,
|
useLoadingOverlay,
|
||||||
@ -217,7 +216,6 @@ const Layout = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
useLoadingOverlay(themeReady)
|
useLoadingOverlay(themeReady)
|
||||||
useAppInitialization()
|
|
||||||
|
|
||||||
const handleNotice = useCallback(
|
const handleNotice = useCallback(
|
||||||
(payload: [string, string]) => {
|
(payload: [string, string]) => {
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
export { useAppInitialization } from './use-app-initialization'
|
|
||||||
export { useLayoutEvents } from './use-layout-events'
|
export { useLayoutEvents } from './use-layout-events'
|
||||||
export { useLoadingOverlay } from './use-loading-overlay'
|
export { useLoadingOverlay } from './use-loading-overlay'
|
||||||
export { useNavMenuOrder } from './use-nav-menu-order'
|
export { useNavMenuOrder } from './use-nav-menu-order'
|
||||||
|
|||||||
@ -1,112 +0,0 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core'
|
|
||||||
import { useEffect, useRef } from 'react'
|
|
||||||
|
|
||||||
import { hideInitialOverlay } from '../utils'
|
|
||||||
|
|
||||||
export const useAppInitialization = () => {
|
|
||||||
const initRef = useRef(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (initRef.current) return
|
|
||||||
initRef.current = true
|
|
||||||
|
|
||||||
let isInitialized = false
|
|
||||||
let isCancelled = false
|
|
||||||
const timers = new Set<number>()
|
|
||||||
|
|
||||||
const scheduleTimeout = (handler: () => void, delay: number) => {
|
|
||||||
if (isCancelled) return -1
|
|
||||||
const id = window.setTimeout(() => {
|
|
||||||
if (!isCancelled) {
|
|
||||||
handler()
|
|
||||||
}
|
|
||||||
timers.delete(id)
|
|
||||||
}, delay)
|
|
||||||
timers.add(id)
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
const notifyBackend = async (stage?: string) => {
|
|
||||||
if (isCancelled) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (stage) {
|
|
||||||
await invoke('update_ui_stage', { stage })
|
|
||||||
} else {
|
|
||||||
await invoke('notify_ui_ready')
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`[Initialization] Failed to notify backend:`, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeLoadingOverlay = () => {
|
|
||||||
hideInitialOverlay({ schedule: scheduleTimeout })
|
|
||||||
}
|
|
||||||
|
|
||||||
const performInitialization = async () => {
|
|
||||||
if (isCancelled || isInitialized) return
|
|
||||||
isInitialized = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
removeLoadingOverlay()
|
|
||||||
await notifyBackend('Loading')
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
const check = () => {
|
|
||||||
const root = document.getElementById('root')
|
|
||||||
if (root && root.children.length > 0) {
|
|
||||||
resolve()
|
|
||||||
} else {
|
|
||||||
scheduleTimeout(check, 50)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
check()
|
|
||||||
scheduleTimeout(resolve, 2000)
|
|
||||||
})
|
|
||||||
|
|
||||||
await notifyBackend('DomReady')
|
|
||||||
await new Promise((resolve) => requestAnimationFrame(resolve))
|
|
||||||
await notifyBackend('ResourcesLoaded')
|
|
||||||
await notifyBackend()
|
|
||||||
} catch (error) {
|
|
||||||
if (!isCancelled) {
|
|
||||||
console.error('[Initialization] Failed:', error)
|
|
||||||
removeLoadingOverlay()
|
|
||||||
notifyBackend().catch(console.error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkBackendReady = async () => {
|
|
||||||
try {
|
|
||||||
if (isCancelled) return
|
|
||||||
|
|
||||||
await invoke('update_ui_stage', { stage: 'Loading' })
|
|
||||||
performInitialization()
|
|
||||||
} catch {
|
|
||||||
scheduleTimeout(performInitialization, 1500)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scheduleTimeout(checkBackendReady, 100)
|
|
||||||
scheduleTimeout(() => {
|
|
||||||
if (!isInitialized) {
|
|
||||||
removeLoadingOverlay()
|
|
||||||
notifyBackend().catch(console.error)
|
|
||||||
}
|
|
||||||
}, 5000)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isCancelled = true
|
|
||||||
timers.forEach((id) => {
|
|
||||||
try {
|
|
||||||
window.clearTimeout(id)
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[Initialization] Failed to clear timer:', error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
timers.clear()
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
}
|
|
||||||
@ -309,20 +309,22 @@ export const useCustomTheme = () => {
|
|||||||
styleElement.innerHTML = effectiveInjectedCss + globalStyles
|
styleElement.innerHTML = effectiveInjectedCss + globalStyles
|
||||||
}
|
}
|
||||||
|
|
||||||
const { palette } = muiTheme
|
|
||||||
setTimeout(() => {
|
|
||||||
const dom = document.querySelector('#Gradient2')
|
|
||||||
if (dom) {
|
|
||||||
dom.innerHTML = `
|
|
||||||
<stop offset="0%" stop-color="${palette.primary.main}" />
|
|
||||||
<stop offset="80%" stop-color="${palette.primary.dark}" />
|
|
||||||
<stop offset="100%" stop-color="${palette.primary.dark}" />
|
|
||||||
`
|
|
||||||
}
|
|
||||||
}, 0)
|
|
||||||
|
|
||||||
return muiTheme
|
return muiTheme
|
||||||
}, [mode, theme_setting, userBackgroundImage, hasUserBackground])
|
}, [mode, theme_setting, userBackgroundImage, hasUserBackground])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const id = setTimeout(() => {
|
||||||
|
const dom = document.querySelector('#Gradient2')
|
||||||
|
if (dom) {
|
||||||
|
dom.innerHTML = `
|
||||||
|
<stop offset="0%" stop-color="${theme.palette.primary.main}" />
|
||||||
|
<stop offset="80%" stop-color="${theme.palette.primary.dark}" />
|
||||||
|
<stop offset="100%" stop-color="${theme.palette.primary.dark}" />
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
return () => clearTimeout(id)
|
||||||
|
}, [theme.palette.primary.main, theme.palette.primary.dark])
|
||||||
|
|
||||||
return { theme }
|
return { theme }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,46 +3,15 @@ import { useEffect, useRef } from 'react'
|
|||||||
import { hideInitialOverlay } from '../utils'
|
import { hideInitialOverlay } from '../utils'
|
||||||
|
|
||||||
export const useLoadingOverlay = (themeReady: boolean) => {
|
export const useLoadingOverlay = (themeReady: boolean) => {
|
||||||
const overlayRemovedRef = useRef(false)
|
const doneRef = useRef(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!themeReady || overlayRemovedRef.current) return
|
if (!themeReady || doneRef.current) return
|
||||||
|
doneRef.current = true
|
||||||
let removalTimer: number | undefined
|
|
||||||
let retryTimer: number | undefined
|
|
||||||
let attempts = 0
|
|
||||||
const maxAttempts = 50
|
|
||||||
let stopped = false
|
|
||||||
|
|
||||||
const tryRemoveOverlay = () => {
|
|
||||||
if (stopped || overlayRemovedRef.current) return
|
|
||||||
|
|
||||||
const { removed, removalTimer: timerId } = hideInitialOverlay({
|
|
||||||
assumeMissingAsRemoved: true,
|
|
||||||
})
|
|
||||||
if (typeof timerId === 'number') {
|
|
||||||
removalTimer = timerId
|
|
||||||
}
|
|
||||||
|
|
||||||
if (removed) {
|
|
||||||
overlayRemovedRef.current = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attempts < maxAttempts) {
|
|
||||||
attempts += 1
|
|
||||||
retryTimer = window.setTimeout(tryRemoveOverlay, 100)
|
|
||||||
} else {
|
|
||||||
console.warn('[Loading Overlay] Element not found')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tryRemoveOverlay()
|
|
||||||
|
|
||||||
|
const timer = hideInitialOverlay()
|
||||||
return () => {
|
return () => {
|
||||||
stopped = true
|
if (timer !== undefined) window.clearTimeout(timer)
|
||||||
if (typeof removalTimer === 'number') window.clearTimeout(removalTimer)
|
|
||||||
if (typeof retryTimer === 'number') window.clearTimeout(retryTimer)
|
|
||||||
}
|
}
|
||||||
}, [themeReady])
|
}, [themeReady])
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,45 +1,17 @@
|
|||||||
const OVERLAY_ID = 'initial-loading-overlay'
|
let removed = false
|
||||||
const REMOVE_DELAY = 300
|
|
||||||
|
|
||||||
let overlayRemoved = false
|
export const hideInitialOverlay = (): number | undefined => {
|
||||||
|
if (removed) return undefined
|
||||||
|
|
||||||
type HideOverlayOptions = {
|
const overlay = document.getElementById('initial-loading-overlay')
|
||||||
schedule?: (handler: () => void, delay: number) => number
|
|
||||||
assumeMissingAsRemoved?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
type HideOverlayResult = {
|
|
||||||
removed: boolean
|
|
||||||
removalTimer?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export const hideInitialOverlay = (
|
|
||||||
options: HideOverlayOptions = {},
|
|
||||||
): HideOverlayResult => {
|
|
||||||
if (overlayRemoved) {
|
|
||||||
return { removed: true }
|
|
||||||
}
|
|
||||||
|
|
||||||
const overlay = document.getElementById(OVERLAY_ID)
|
|
||||||
if (!overlay) {
|
if (!overlay) {
|
||||||
if (options.assumeMissingAsRemoved) {
|
removed = true
|
||||||
overlayRemoved = true
|
return undefined
|
||||||
return { removed: true }
|
|
||||||
}
|
|
||||||
return { removed: false }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
overlayRemoved = true
|
removed = true
|
||||||
overlay.dataset.hidden = 'true'
|
overlay.dataset.hidden = 'true'
|
||||||
|
|
||||||
const schedule = options.schedule ?? window.setTimeout
|
const timer = window.setTimeout(() => overlay.remove(), 200)
|
||||||
const removalTimer = schedule(() => {
|
return timer
|
||||||
try {
|
|
||||||
overlay.remove()
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[Loading Overlay] Removal failed:', error)
|
|
||||||
}
|
|
||||||
}, REMOVE_DELAY)
|
|
||||||
|
|
||||||
return { removed: true, removalTimer }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ export interface AppDataContextType {
|
|||||||
proxyProviders: Record<string, ProxyProvider>
|
proxyProviders: Record<string, ProxyProvider>
|
||||||
ruleProviders: Record<string, RuleProvider>
|
ruleProviders: Record<string, RuleProvider>
|
||||||
systemProxyAddress: string
|
systemProxyAddress: string
|
||||||
|
isCoreDataPending: boolean
|
||||||
|
|
||||||
refreshProxy: () => Promise<any>
|
refreshProxy: () => Promise<any>
|
||||||
refreshClashConfig: () => Promise<any>
|
refreshClashConfig: () => Promise<any>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { listen } from '@tauri-apps/api/event'
|
import { listen } from '@tauri-apps/api/event'
|
||||||
import React, { useCallback, useEffect, useMemo } from 'react'
|
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||||
import {
|
import {
|
||||||
getBaseConfig,
|
getBaseConfig,
|
||||||
getRuleProviders,
|
getRuleProviders,
|
||||||
@ -23,7 +23,7 @@ const TQ_MIHOMO = {
|
|||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
staleTime: 1500,
|
staleTime: 1500,
|
||||||
retry: 3,
|
retry: 3,
|
||||||
retryDelay: 2000,
|
retryDelay: (attempt: number) => Math.min(200 * 2 ** attempt, 3000),
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
const TQ_DEFAULTS = {
|
const TQ_DEFAULTS = {
|
||||||
@ -41,13 +41,21 @@ export const AppDataProvider = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { verge } = useVerge()
|
const { verge } = useVerge()
|
||||||
|
|
||||||
const { data: proxiesData, refetch: refreshProxy } = useQuery({
|
const {
|
||||||
|
data: proxiesData,
|
||||||
|
isPending: isProxiesPending,
|
||||||
|
refetch: refreshProxy,
|
||||||
|
} = useQuery({
|
||||||
queryKey: ['getProxies'],
|
queryKey: ['getProxies'],
|
||||||
queryFn: calcuProxies,
|
queryFn: calcuProxies,
|
||||||
...TQ_MIHOMO,
|
...TQ_MIHOMO,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: clashConfig, refetch: refreshClashConfig } = useQuery({
|
const {
|
||||||
|
data: clashConfig,
|
||||||
|
isPending: isClashConfigPending,
|
||||||
|
refetch: refreshClashConfig,
|
||||||
|
} = useQuery({
|
||||||
queryKey: ['getClashConfig'],
|
queryKey: ['getClashConfig'],
|
||||||
queryFn: getBaseConfig,
|
queryFn: getBaseConfig,
|
||||||
...TQ_MIHOMO,
|
...TQ_MIHOMO,
|
||||||
@ -71,106 +79,45 @@ export const AppDataProvider = ({
|
|||||||
...TQ_MIHOMO,
|
...TQ_MIHOMO,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const refreshProxyRef = useRef(refreshProxy)
|
||||||
|
const refreshRulesRef = useRef(refreshRules)
|
||||||
|
const refreshRuleProvidersRef = useRef(refreshRuleProviders)
|
||||||
|
useEffect(() => {
|
||||||
|
refreshProxyRef.current = refreshProxy
|
||||||
|
}, [refreshProxy])
|
||||||
|
useEffect(() => {
|
||||||
|
refreshRulesRef.current = refreshRules
|
||||||
|
}, [refreshRules])
|
||||||
|
useEffect(() => {
|
||||||
|
refreshRuleProvidersRef.current = refreshRuleProviders
|
||||||
|
}, [refreshRuleProviders])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let lastProfileId: string | null = null
|
let lastProfileId: string | null = null
|
||||||
let lastUpdateTime = 0
|
let lastUpdateTime = 0
|
||||||
const refreshThrottle = 800
|
const refreshThrottle = 800
|
||||||
|
|
||||||
let isUnmounted = false
|
|
||||||
const scheduledTimeouts = new Set<number>()
|
|
||||||
const cleanupFns: Array<() => void> = []
|
const cleanupFns: Array<() => void> = []
|
||||||
|
|
||||||
const registerCleanup = (fn: () => void) => {
|
|
||||||
if (isUnmounted) {
|
|
||||||
try {
|
|
||||||
fn()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[DataProvider] Immediate cleanup failed:', error)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cleanupFns.push(fn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const addWindowListener = (eventName: string, handler: EventListener) => {
|
|
||||||
// eslint-disable-next-line @eslint-react/web-api-no-leaked-event-listener
|
|
||||||
window.addEventListener(eventName, handler)
|
|
||||||
return () => window.removeEventListener(eventName, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
const scheduleTimeout = (
|
|
||||||
callback: () => void | Promise<void>,
|
|
||||||
delay: number,
|
|
||||||
) => {
|
|
||||||
if (isUnmounted) return -1
|
|
||||||
|
|
||||||
const timeoutId = window.setTimeout(() => {
|
|
||||||
scheduledTimeouts.delete(timeoutId)
|
|
||||||
if (!isUnmounted) {
|
|
||||||
void callback()
|
|
||||||
}
|
|
||||||
}, delay)
|
|
||||||
|
|
||||||
scheduledTimeouts.add(timeoutId)
|
|
||||||
return timeoutId
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearAllTimeouts = () => {
|
|
||||||
scheduledTimeouts.forEach((timeoutId) => clearTimeout(timeoutId))
|
|
||||||
scheduledTimeouts.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleProfileChanged = (event: { payload: string }) => {
|
const handleProfileChanged = (event: { payload: string }) => {
|
||||||
const newProfileId = event.payload
|
const newProfileId = event.payload
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
lastProfileId === newProfileId &&
|
lastProfileId === newProfileId &&
|
||||||
now - lastUpdateTime < refreshThrottle
|
now - lastUpdateTime < refreshThrottle
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
lastProfileId = newProfileId
|
lastProfileId = newProfileId
|
||||||
lastUpdateTime = now
|
lastUpdateTime = now
|
||||||
|
refreshRulesRef.current().catch(() => {})
|
||||||
scheduleTimeout(() => {
|
refreshRuleProvidersRef.current().catch(() => {})
|
||||||
refreshRules().catch((error) =>
|
|
||||||
console.warn('[DataProvider] Rules refresh failed:', error),
|
|
||||||
)
|
|
||||||
refreshRuleProviders().catch((error) =>
|
|
||||||
console.warn('[DataProvider] Rule providers refresh failed:', error),
|
|
||||||
)
|
|
||||||
}, 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRefreshClash = () => {
|
|
||||||
const now = Date.now()
|
|
||||||
if (now - lastUpdateTime <= refreshThrottle) return
|
|
||||||
|
|
||||||
lastUpdateTime = now
|
|
||||||
scheduleTimeout(async () => {
|
|
||||||
await Promise.all([
|
|
||||||
refreshProxy().catch((error) =>
|
|
||||||
console.error('[DataProvider] Proxy refresh failed:', error),
|
|
||||||
),
|
|
||||||
refreshClashConfig().catch((error) =>
|
|
||||||
console.error('[DataProvider] Clash config refresh failed:', error),
|
|
||||||
),
|
|
||||||
])
|
|
||||||
}, 200)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRefreshProxy = () => {
|
const handleRefreshProxy = () => {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
if (now - lastUpdateTime <= refreshThrottle) return
|
if (now - lastUpdateTime <= refreshThrottle) return
|
||||||
|
|
||||||
lastUpdateTime = now
|
lastUpdateTime = now
|
||||||
scheduleTimeout(() => {
|
refreshProxyRef.current().catch(() => {})
|
||||||
refreshProxy().catch((error) =>
|
|
||||||
console.warn('[DataProvider] Proxy refresh failed:', error),
|
|
||||||
)
|
|
||||||
}, 200)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const initializeListeners = async () => {
|
const initializeListeners = async () => {
|
||||||
@ -179,62 +126,34 @@ export const AppDataProvider = ({
|
|||||||
'profile-changed',
|
'profile-changed',
|
||||||
handleProfileChanged,
|
handleProfileChanged,
|
||||||
)
|
)
|
||||||
registerCleanup(unlistenProfile)
|
cleanupFns.push(unlistenProfile)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[AppDataProvider] 监听 Profile 事件失败:', error)
|
console.error('[AppDataProvider] 监听 Profile 事件失败:', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const unlistenClash = await listen(
|
|
||||||
'verge://refresh-clash-config',
|
|
||||||
handleRefreshClash,
|
|
||||||
)
|
|
||||||
const unlistenProxy = await listen(
|
const unlistenProxy = await listen(
|
||||||
'verge://refresh-proxy-config',
|
'verge://refresh-proxy-config',
|
||||||
handleRefreshProxy,
|
handleRefreshProxy,
|
||||||
)
|
)
|
||||||
|
cleanupFns.push(unlistenProxy)
|
||||||
registerCleanup(() => {
|
|
||||||
unlistenClash()
|
|
||||||
unlistenProxy()
|
|
||||||
})
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[AppDataProvider] 设置 Tauri 事件监听器失败:', error)
|
console.warn('[AppDataProvider] 设置 Tauri 事件监听器失败:', error)
|
||||||
|
|
||||||
const fallbackHandlers: Array<[string, EventListener]> = [
|
|
||||||
['verge://refresh-clash-config', handleRefreshClash],
|
|
||||||
['verge://refresh-proxy-config', handleRefreshProxy],
|
|
||||||
]
|
|
||||||
|
|
||||||
fallbackHandlers.forEach(([eventName, handler]) => {
|
|
||||||
registerCleanup(addWindowListener(eventName, handler))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void initializeListeners()
|
void initializeListeners()
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
isUnmounted = true
|
cleanupFns.forEach((fn) => {
|
||||||
clearAllTimeouts()
|
|
||||||
|
|
||||||
const errors: Error[] = []
|
|
||||||
cleanupFns.splice(0).forEach((fn) => {
|
|
||||||
try {
|
try {
|
||||||
fn()
|
fn()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errors.push(error instanceof Error ? error : new Error(String(error)))
|
console.error('[DataProvider] Cleanup error:', error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (errors.length > 0) {
|
|
||||||
console.error(
|
|
||||||
`[DataProvider] ${errors.length} errors during cleanup:`,
|
|
||||||
errors,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [refreshProxy, refreshClashConfig, refreshRules, refreshRuleProviders])
|
}, [])
|
||||||
|
|
||||||
const { data: sysproxy, refetch: refreshSysproxy } = useQuery({
|
const { data: sysproxy, refetch: refreshSysproxy } = useQuery({
|
||||||
queryKey: ['getSystemProxy'],
|
queryKey: ['getSystemProxy'],
|
||||||
@ -323,6 +242,9 @@ export const AppDataProvider = ({
|
|||||||
|
|
||||||
systemProxyAddress: calculateSystemProxyAddress(),
|
systemProxyAddress: calculateSystemProxyAddress(),
|
||||||
|
|
||||||
|
// core 数据加载状态
|
||||||
|
isCoreDataPending: isProxiesPending || isClashConfigPending,
|
||||||
|
|
||||||
// 刷新方法
|
// 刷新方法
|
||||||
refreshProxy,
|
refreshProxy,
|
||||||
refreshClashConfig,
|
refreshClashConfig,
|
||||||
@ -335,6 +257,8 @@ export const AppDataProvider = ({
|
|||||||
}, [
|
}, [
|
||||||
proxiesData,
|
proxiesData,
|
||||||
clashConfig,
|
clashConfig,
|
||||||
|
isProxiesPending,
|
||||||
|
isClashConfigPending,
|
||||||
rulesData,
|
rulesData,
|
||||||
sysproxy,
|
sysproxy,
|
||||||
runningMode,
|
runningMode,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user