Fix delay display and prevent refresh from resetting UI

- Store full proxies map (not just groups) so delay lookups find
  individual proxy nodes correctly
- Display delay as plain number with color coding (green/yellow/red)
  instead of wrapped in parentheses
- Separate initial load from background refresh: periodic refresh
  silently updates data without flashing "Loading..." state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
yly 2026-05-10 15:52:06 +08:00
parent 75c644555b
commit 4327294e3c

View File

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