feat(tui): tabbed browsing — browse several users at once

Replace the single browse view with BrowseTabs (one tab per user),
mirroring the chat-room tabs. Browsing a new user (from search results
or a chat room's member list) opens or focuses their tab; Tab/Shift-Tab
switch between open users, 'w' closes the active tab, Esc hides the
popup while keeping the tabs. A tab bar shows the open users when more
than one is open, and each tab polls/loads independently. Re-browsing a
timed-out user retries. BrowseTabs is unit-tested.
This commit is contained in:
michel
2026-07-19 20:38:34 +02:00
parent 5a3bc6fcd5
commit 60896619d7
5 changed files with 246 additions and 49 deletions
+49 -7
View File
@@ -1,20 +1,62 @@
use crate::models::{BrowseState, BrowseStatus};
use crate::models::{BrowseState, BrowseStatus, BrowseTabs};
use crate::ui::{
HIGHLIGHT_SYMBOL, border_style, border_type, error_style, format_bytes,
get_spinner_char, highlight_style, primary_style,
HIGHLIGHT_SYMBOL, border_style, border_type, dimmed_style, error_style,
format_bytes, get_spinner_char, highlight_style, primary_style,
};
use ratatui::{
Frame,
layout::{Constraint, Rect},
text::Line,
layout::{Constraint, Layout, Rect},
style::Modifier,
text::{Line, Span},
widgets::{
Block, Borders, Cell, HighlightSpacing, Paragraph, Row, Table,
TableState,
},
};
/// Render the browse popup: a collapsible tree of a peer's shared files.
/// Render the browse popup: a tab bar of browsed users (when more than one is
/// open) above the active user's collapsible shared-file tree.
pub fn render_browse_pane(
frame: &mut Frame,
area: Rect,
tabs: &BrowseTabs,
table_state: &mut TableState,
spinner_state: usize,
) {
let Some(active) = tabs.active_tab() else {
return;
};
// With multiple users open, reserve the top row for a tab bar.
let tree_area = if tabs.tabs.len() > 1 {
let chunks =
Layout::vertical([Constraint::Length(1), Constraint::Fill(1)])
.split(area);
render_browse_tabs(frame, chunks[0], tabs);
chunks[1]
} else {
area
};
render_browse_one(frame, tree_area, active, table_state, spinner_state);
}
fn render_browse_tabs(frame: &mut Frame, area: Rect, tabs: &BrowseTabs) {
let mut spans: Vec<Span> = Vec::new();
for (i, tab) in tabs.tabs.iter().enumerate() {
let style = if i == tabs.active {
highlight_style().add_modifier(Modifier::BOLD)
} else {
dimmed_style()
};
spans.push(Span::styled(format!(" {} ", tab.username), style));
spans.push(Span::raw(" "));
}
frame.render_widget(Paragraph::new(Line::from(spans)), area);
}
/// Render one browsed user's shared-file tree.
fn render_browse_one(
frame: &mut Frame,
area: Rect,
browse: &BrowseState,
@@ -23,7 +65,7 @@ pub fn render_browse_pane(
) {
let title = match browse.status {
BrowseStatus::Loaded => format!(
" Browse {} — {} files, {} folders (Enter/d: download, Esc: close) ",
" Browse {} — {} files, {} folders (Enter/d: download, Tab: user, w: close, Esc: hide) ",
browse.username, browse.file_count, browse.folder_count
),
_ => format!(" Browse {} ", browse.username),