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:
parent
75c644555b
commit
4327294e3c
@ -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 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user