diff --git a/src/proxy_view.rs b/src/proxy_view.rs index c2f1b84..b5d60bb 100644 --- a/src/proxy_view.rs +++ b/src/proxy_view.rs @@ -1,13 +1,15 @@ use iocraft::prelude::*; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::time::Duration; use crate::clash::ProxyItem; enum DataState { Init, - Loading, - Loaded(Vec), + Loaded { + groups: Vec, + all_proxies: HashMap, + }, Error(String), } @@ -38,36 +40,60 @@ pub fn ProxyView(mut hooks: Hooks) -> impl Into> { let test_delay_url = ctx.test_delay_url.clone(); let test_delay_timeout = ctx.test_delay_timeout; - let load = hooks.use_async_handler(move |_: ()| { + // Initial load: shows "Loading..." state + let load_init = hooks.use_async_handler(move |_: ()| { let client = client.clone(); async move { - state.set(DataState::Loading); - match client.get_proxies().await { - Ok(resp) => { - let mut groups: Vec = resp - .proxies - .into_values() - .filter(|p| p.is_group()) - .collect(); - groups.sort_by(|a, b| a.name.cmp(&b.name)); - state.set(DataState::Loaded(groups)); - } - Err(e) => { - state.set(DataState::Error(e.to_string())); + match client.get_proxies().await { + Ok(resp) => { + let all_proxies = resp.proxies; + let mut groups: Vec = all_proxies + .values() + .filter(|p| p.is_group()) + .cloned() + .collect(); + groups.sort_by(|a, b| a.name.cmp(&b.name)); + state.set(DataState::Loaded { groups, all_proxies }); + } + Err(e) => { + state.set(DataState::Error(e.to_string())); + } } } + }); + + // Background refresh: silently updates data without resetting UI + let client_refresh = ctx.client.clone(); + let load_refresh = hooks.use_async_handler(move |_: ()| { + let client = client_refresh.clone(); + async move { + match client.get_proxies().await { + Ok(resp) => { + let all_proxies = resp.proxies; + let mut groups: Vec = all_proxies + .values() + .filter(|p| p.is_group()) + .cloned() + .collect(); + groups.sort_by(|a, b| a.name.cmp(&b.name)); + state.set(DataState::Loaded { groups, all_proxies }); + } + Err(_) => { + // Silently ignore background refresh errors + } + } } }); if matches!(*state.read(), DataState::Init) { - load(()); + load_init(()); } - let load_refresh = load.clone(); + let refresh_timer = load_refresh.clone(); hooks.use_future(async move { loop { smol::Timer::after(Duration::from_secs(refresh_interval)).await; - load_refresh(()); + refresh_timer(()); } }); @@ -84,131 +110,131 @@ pub fn ProxyView(mut hooks: Hooks) -> impl Into> { }); let client_for_select = ctx.client.clone(); - let load_select = load.clone(); + let load_select = load_refresh.clone(); let client_for_delay = ctx.client.clone(); - let load_after_delay = load.clone(); + let load_after_delay = load_refresh.clone(); let testing_state = testing; hooks.use_terminal_events(move |event| { if let TerminalEvent::Key(key_event) = event { let state_read = state.read(); - if let DataState::Loaded(groups) = &*state_read { - let group_count = groups.len(); - if group_count == 0 { - return; + let DataState::Loaded { groups, .. } = &*state_read else { + return; + }; + let group_count = groups.len(); + if group_count == 0 { + return; + } + + let current_group = selected_group.get().min(group_count - 1); + let current_members = groups[current_group] + .all + .as_ref() + .map(|m| m.len()) + .unwrap_or(0); + + // Test delay: single member + if focus_pane.get() == FocusPane::Members + && kb.test_delay.matches(&key_event) + && current_members > 0 + { + let members = groups[current_group].all.as_deref().unwrap_or(&[]); + let member_name = members[selected_member.get().min(current_members - 1)].clone(); + let client = client_for_delay.clone(); + let load = load_after_delay.clone(); + let mut ts = testing_state; + let url = test_delay_url.clone(); + let timeout = test_delay_timeout; + ts.write().insert(member_name.clone()); + smol::spawn(async move { + let _ = client.test_delay(&member_name, timeout, &url).await; + smol::Timer::after(Duration::from_millis(300)).await; + ts.write().remove(&member_name); + load(()); + }) + .detach(); + return; + } + + // Test delay: all members in current group + if focus_pane.get() == FocusPane::Members + && kb.test_delay_all.matches(&key_event) + && current_members > 0 + { + let members = groups[current_group].all.as_deref().unwrap_or(&[]); + let client = client_for_delay.clone(); + let load = load_after_delay.clone(); + let mut ts = testing_state; + let url = test_delay_url.clone(); + let timeout = test_delay_timeout; + for name in members.iter() { + ts.write().insert(name.clone()); } - - let current_group = selected_group.get().min(group_count - 1); - let current_members = groups[current_group] - .all - .as_ref() - .map(|m| m.len()) - .unwrap_or(0); - - // Test delay: single member - if focus_pane.get() == FocusPane::Members - && kb.test_delay.matches(&key_event) - && current_members > 0 - { - let members = groups[current_group].all.as_deref().unwrap_or(&[]); - let member_name = members[selected_member.get().min(current_members - 1)].clone(); - let client = client_for_delay.clone(); - let load = load_after_delay.clone(); - let mut ts = testing_state; - let url = test_delay_url.clone(); - let timeout = test_delay_timeout; - ts.write().insert(member_name.clone()); - smol::spawn(async move { - let _ = client.test_delay(&member_name, timeout, &url).await; - smol::Timer::after(Duration::from_millis(300)).await; - ts.write().remove(&member_name); - load(()); - }) - .detach(); - return; - } - - // Test delay: all members in current group - if focus_pane.get() == FocusPane::Members - && kb.test_delay_all.matches(&key_event) - && current_members > 0 - { - let members = groups[current_group].all.as_deref().unwrap_or(&[]); - let client = client_for_delay.clone(); - let load = load_after_delay.clone(); - let mut ts = testing_state; - let url = test_delay_url.clone(); - let timeout = test_delay_timeout; - for name in members.iter() { - ts.write().insert(name.clone()); + let names: Vec = members.to_vec(); + smol::spawn(async move { + let mut tasks = Vec::new(); + for name in &names { + let c = client.clone(); + let n = name.clone(); + let u = url.clone(); + let mut ts2 = ts; + tasks.push(smol::spawn(async move { + let _ = c.test_delay(&n, timeout, &u).await; + ts2.write().remove(&n); + })); } - let names: Vec = members.to_vec(); - smol::spawn(async move { - // Test all concurrently - let mut tasks = Vec::new(); - for name in &names { - let c = client.clone(); - let n = name.clone(); - let u = url.clone(); - let mut ts2 = ts; - tasks.push(smol::spawn(async move { - let _ = c.test_delay(&n, timeout, &u).await; - ts2.write().remove(&n); - })); - } - for t in tasks { - let _ = t.await; - } - load(()); - }) - .detach(); - return; - } + for t in tasks { + let _ = t.await; + } + load(()); + }) + .detach(); + return; + } - match focus_pane.get() { - FocusPane::Groups => { - if kb.up.matches(&key_event) { - if current_group > 0 { - selected_group.set(current_group - 1); - selected_member.set(0); - } - } else if kb.down.matches(&key_event) { - if current_group + 1 < group_count { - selected_group.set(current_group + 1); - selected_member.set(0); - } - } else if (kb.right.matches(&key_event) || kb.select.matches(&key_event)) - && current_members > 0 + match focus_pane.get() { + FocusPane::Groups => { + if kb.up.matches(&key_event) { + if current_group > 0 { + selected_group.set(current_group - 1); + selected_member.set(0); + } + } else if kb.down.matches(&key_event) { + if current_group + 1 < group_count { + selected_group.set(current_group + 1); + selected_member.set(0); + } + } else if (kb.right.matches(&key_event) || kb.select.matches(&key_event)) + && current_members > 0 + { + focus_pane.set(FocusPane::Members); + } + } + FocusPane::Members => { + if kb.up.matches(&key_event) { + if selected_member.get() > 0 { + selected_member.set(selected_member.get() - 1); + } + } else if kb.down.matches(&key_event) { + if selected_member.get() + 1 < current_members { + selected_member.set(selected_member.get() + 1); + } + } else if kb.left.matches(&key_event) { + focus_pane.set(FocusPane::Groups); + } else if kb.select.matches(&key_event) { + let group = &groups[current_group]; + if let Some(ref members) = group.all + && selected_member.get() < members.len() { - focus_pane.set(FocusPane::Members); - } - } - FocusPane::Members => { - if kb.up.matches(&key_event) { - if selected_member.get() > 0 { - selected_member.set(selected_member.get() - 1); - } - } else if kb.down.matches(&key_event) { - if selected_member.get() + 1 < current_members { - selected_member.set(selected_member.get() + 1); - } - } else if kb.left.matches(&key_event) { - focus_pane.set(FocusPane::Groups); - } else if kb.select.matches(&key_event) { - let group = &groups[current_group]; - if let Some(ref members) = group.all - && selected_member.get() < members.len() - { - let group_name = group.name.clone(); - let member_name = members[selected_member.get()].clone(); - let client = client_for_select.clone(); - let load_after = load_select.clone(); - smol::spawn(async move { - let _ = client.select_proxy(&group_name, &member_name).await; - smol::Timer::after(Duration::from_millis(200)).await; - load_after(()); - }) - .detach(); - } + let group_name = group.name.clone(); + let member_name = members[selected_member.get()].clone(); + let client = client_for_select.clone(); + let load_after = load_select.clone(); + smol::spawn(async move { + let _ = client.select_proxy(&group_name, &member_name).await; + smol::Timer::after(Duration::from_millis(200)).await; + load_after(()); + }) + .detach(); } } } @@ -225,7 +251,7 @@ pub fn ProxyView(mut hooks: Hooks) -> impl Into> { let state_read = state.read(); match &*state_read { - DataState::Init | DataState::Loading => { + DataState::Init => { element! { View( width: 100pct, @@ -249,7 +275,7 @@ pub fn ProxyView(mut hooks: Hooks) -> impl Into> { } } } - DataState::Loaded(groups) => { + DataState::Loaded { groups, all_proxies } => { let current_group_idx = selected_group.get().min(groups.len().saturating_sub(1)); let current_group = &groups[current_group_idx]; let members = current_group.all.as_deref().unwrap_or(&[]); @@ -344,13 +370,13 @@ pub fn ProxyView(mut hooks: Hooks) -> impl Into> { let is_testing = testing_read.contains(member.as_str()); + // Look up delay from the full proxies map (not just groups) let (delay_text, delay_color) = if is_testing { - (format!("{} testing", SPINNER_FRAMES[frame]), Color::Yellow) + (SPINNER_FRAMES[frame].to_string(), Color::Yellow) } else { - let delay = groups - .iter() - .find(|g| g.name == *member) - .and_then(|g| g.latest_delay()); + let delay = all_proxies + .get(member.as_str()) + .and_then(|p| p.latest_delay()); match delay { Some(0) => ("timeout".to_string(), Color::Red), Some(d) => (format!("{}ms", d), delay_color_for(d)), @@ -365,7 +391,11 @@ pub fn ProxyView(mut hooks: Hooks) -> impl Into> { padding_left: 1, ) { Text(content: format!("{}{} ", marker, member), color: if is_current { Color::Green } else if is_selected { Color::White } else { Color::Grey }) - Text(content: format!("({})", delay_text), color: delay_color) + #(if delay_text.is_empty() { + element! { Fragment }.into_any() + } else { + element! { Text(content: format!(" {} ", delay_text), color: delay_color) }.into_any() + }) #(if is_current { element! { Text(content: " <--", color: Color::Green) }.into_any() } else {