diff --git a/soulseek-rs/src/models/browse.rs b/soulseek-rs/src/models/browse.rs index e4726d2..28026bd 100644 --- a/soulseek-rs/src/models/browse.rs +++ b/soulseek-rs/src/models/browse.rs @@ -371,6 +371,20 @@ impl BrowseTabs { } } + /// Retry the active tab if it timed out: reset it to loading and return the + /// username so the caller can re-issue the request. Returns `None` if the + /// active tab isn't in a retryable (timed-out) state. + pub fn retry_active(&mut self) -> Option { + let tab = self.tabs.get_mut(self.active)?; + if tab.status == BrowseStatus::TimedOut { + let username = tab.username.clone(); + *tab = BrowseState::loading(username.clone()); + Some(username) + } else { + None + } + } + /// Close the active tab, returning `false` when no tabs remain. pub fn close_active(&mut self) -> bool { if self.active < self.tabs.len() { @@ -545,6 +559,20 @@ mod tests { assert_eq!(tabs.active_tab().unwrap().status, BrowseStatus::Loading); } + #[test] + fn browse_tabs_retry_active_only_when_timed_out() { + let mut tabs = BrowseTabs::new(); + tabs.open("alice"); + // Loading tab isn't retryable. + assert_eq!(tabs.retry_active(), None); + tabs.active_tab_mut().unwrap().status = BrowseStatus::TimedOut; + // Timed-out tab retries and resets to loading. + assert_eq!(tabs.retry_active().as_deref(), Some("alice")); + assert_eq!(tabs.active_tab().unwrap().status, BrowseStatus::Loading); + // Now loading again, no further retry. + assert_eq!(tabs.retry_active(), None); + } + #[test] fn browse_state_load_auto_expands_top_level() { let mut state = BrowseState::loading("bob".to_string()); diff --git a/soulseek-rs/src/ui/main_tui.rs b/soulseek-rs/src/ui/main_tui.rs index d982d01..0f63b46 100644 --- a/soulseek-rs/src/ui/main_tui.rs +++ b/soulseek-rs/src/ui/main_tui.rs @@ -388,6 +388,7 @@ impl MainTui { ("Enter", "open/download"), ("d", "download folder"), ("Tab", "switch user"), + ("r", "retry"), ("w", "close tab"), ("Esc", "hide"), ] @@ -631,6 +632,13 @@ impl MainTui { self.sync_browse_selection(); return; } + KeyCode::Char('r') => { + // Retry a timed-out browse. + if let Some(username) = self.state.browse.retry_active() { + let _ = self.client.browse_user(&username); + } + return; + } _ => {} } diff --git a/soulseek-rs/src/ui/panes/browse_pane.rs b/soulseek-rs/src/ui/panes/browse_pane.rs index 4230ca0..e963361 100644 --- a/soulseek-rs/src/ui/panes/browse_pane.rs +++ b/soulseek-rs/src/ui/panes/browse_pane.rs @@ -90,10 +90,18 @@ fn render_browse_one( frame.render_widget(Paragraph::new(text).block(block), area); } BrowseStatus::TimedOut => { - let text = Line::styled( - "Timed out waiting for the file list. Press b to retry.", - error_style(), - ); + let text = vec![ + Line::styled( + format!("Couldn't reach {}.", browse.username), + error_style(), + ), + Line::raw(""), + Line::raw( + "They may be offline, or their connection can't be \ + reached (both of you may be behind a router/firewall).", + ), + Line::raw("Press r to try again."), + ]; frame.render_widget(Paragraph::new(text).block(block), area); } BrowseStatus::Loaded => { diff --git a/soulseek-rs/src/ui/panes/download_info_pane.rs b/soulseek-rs/src/ui/panes/download_info_pane.rs index e3f4bc4..1f55cad 100644 --- a/soulseek-rs/src/ui/panes/download_info_pane.rs +++ b/soulseek-rs/src/ui/panes/download_info_pane.rs @@ -104,12 +104,12 @@ fn build_info_lines( } else { "all busy".to_string() }; - lines.push(label_value("Peer slots", &slot_text)); + lines.push(label_value("Free slots", &slot_text)); } if let Some(speed) = download.metadata.peer_upload_speed { lines.push(label_value( - "Peer upload speed", + "Upload speed", &format_speed(f64::from(speed)), )); }