feat: mark browsed files with their download status
Queueing a file or folder from a user's browse tree left no trace in the window it was queued from. A row whose download is in the transfer list now carries the same status glyph the Downloads pane uses — ⋯ queued, ↯ active, ⏸ paused, ✓ done — and an active row shows its percent beside the size. Folders mark through their files: queue a folder and watch the ✓ spread down it.
This commit is contained in:
@@ -419,6 +419,7 @@ impl MainTui {
|
||||
&self.state.browse,
|
||||
&mut self.state.browse_table_state,
|
||||
self.spinner_state,
|
||||
&self.state.downloads,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::models::{BrowseState, BrowseStatus, BrowseTabs};
|
||||
use crate::models::{BrowseState, BrowseStatus, BrowseTabs, DownloadEntry};
|
||||
use crate::ui::{
|
||||
HIGHLIGHT_SYMBOL, body_style, dimmed_style, error_style, filter_title,
|
||||
format_bytes, get_spinner_char, highlight_style, page_window, pane_block,
|
||||
primary_style, render_page, row_highlight_style, warning_style,
|
||||
HIGHLIGHT_SYMBOL, body_style, dimmed_style, download_status_glyph,
|
||||
error_style, filter_title, format_bytes, get_spinner_char, highlight_style,
|
||||
page_window, pane_block, primary_style, render_page, row_highlight_style,
|
||||
warning_style,
|
||||
};
|
||||
use ratatui::{
|
||||
Frame,
|
||||
@@ -11,15 +12,21 @@ use ratatui::{
|
||||
text::{Line, Span},
|
||||
widgets::{Cell, HighlightSpacing, Paragraph, Row, Table, TableState},
|
||||
};
|
||||
use soulseek_rs::DownloadStatus;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// `downloads` is the window's transfer list; a file row whose download is
|
||||
/// there shows its status glyph, the way the results pane marks its own.
|
||||
pub fn render_browse_pane(
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
tabs: &BrowseTabs,
|
||||
table_state: &mut TableState,
|
||||
spinner_state: usize,
|
||||
downloads: &[DownloadEntry],
|
||||
) {
|
||||
let Some(active) = tabs.active_tab() else {
|
||||
return;
|
||||
@@ -36,7 +43,14 @@ pub fn render_browse_pane(
|
||||
area
|
||||
};
|
||||
|
||||
render_browse_one(frame, tree_area, active, table_state, spinner_state);
|
||||
render_browse_one(
|
||||
frame,
|
||||
tree_area,
|
||||
active,
|
||||
table_state,
|
||||
spinner_state,
|
||||
downloads,
|
||||
);
|
||||
}
|
||||
|
||||
fn render_browse_tabs(frame: &mut Frame, area: Rect, tabs: &BrowseTabs) {
|
||||
@@ -60,6 +74,7 @@ fn render_browse_one(
|
||||
browse: &BrowseState,
|
||||
table_state: &mut TableState,
|
||||
spinner_state: usize,
|
||||
downloads: &[DownloadEntry],
|
||||
) {
|
||||
let title = match browse.status {
|
||||
BrowseStatus::Loaded => {
|
||||
@@ -128,6 +143,15 @@ fn render_browse_one(
|
||||
}
|
||||
BrowseStatus::Loaded => {
|
||||
let all = browse.rows();
|
||||
// Queued files carry the download's full share path, so a row
|
||||
// matches a transfer of this user by path alone.
|
||||
let status_of: HashMap<&str, &DownloadStatus> = downloads
|
||||
.iter()
|
||||
.filter(|entry| entry.download.username == browse.username)
|
||||
.map(|entry| {
|
||||
(entry.download.filename.as_str(), &entry.download.status)
|
||||
})
|
||||
.collect();
|
||||
table_state.select(Some(browse.selected_row));
|
||||
let window = page_window(table_state, all.len(), area, 0);
|
||||
let start = window.start;
|
||||
@@ -146,13 +170,39 @@ fn render_browse_one(
|
||||
Cell::from(String::new()),
|
||||
)
|
||||
} else {
|
||||
let status = status_of.get(row.path.as_str()).copied();
|
||||
// The two spaces before a file's name become the
|
||||
// transfer's glyph while its download lives in the
|
||||
// list: ⋯ queued, ↯ active, ⏸ paused, ✓ done.
|
||||
let marker = status.map_or_else(
|
||||
|| Span::raw(" ".to_string()),
|
||||
|status| {
|
||||
let (glyph, style) =
|
||||
download_status_glyph(status);
|
||||
Span::styled(format!("{glyph} "), style)
|
||||
},
|
||||
);
|
||||
let plain_size =
|
||||
row.size.map(format_bytes).unwrap_or_default();
|
||||
let size_text = match status {
|
||||
Some(DownloadStatus::InProgress {
|
||||
bytes_downloaded,
|
||||
total_bytes,
|
||||
..
|
||||
}) if *total_bytes > 0 => {
|
||||
let percent =
|
||||
bytes_downloaded * 100 / total_bytes;
|
||||
format!("{plain_size} {percent}%")
|
||||
}
|
||||
_ => plain_size,
|
||||
};
|
||||
(
|
||||
Cell::from(format!("{indent} {}", row.name))
|
||||
.style(body_style()),
|
||||
Cell::from(
|
||||
row.size.map(format_bytes).unwrap_or_default(),
|
||||
)
|
||||
.style(warning_style()),
|
||||
Cell::from(Line::from(vec![
|
||||
Span::raw(indent),
|
||||
marker,
|
||||
Span::styled(row.name.clone(), body_style()),
|
||||
])),
|
||||
Cell::from(size_text).style(warning_style()),
|
||||
)
|
||||
};
|
||||
Row::new(vec![label, size])
|
||||
@@ -170,3 +220,110 @@ fn render_browse_one(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::render_browse_pane;
|
||||
use crate::models::{BrowseState, BrowseTabs, DownloadEntry};
|
||||
use crate::ui::{GLYPH_ACTIVE, GLYPH_DONE};
|
||||
use ratatui::{Terminal, backend::TestBackend, widgets::TableState};
|
||||
use soulseek_rs::types::{Download, DownloadMetadata, DownloadStatus};
|
||||
use soulseek_rs::{SharedDirectory, SharedFileEntry};
|
||||
use std::sync::mpsc;
|
||||
|
||||
fn share() -> SharedDirectory {
|
||||
SharedDirectory {
|
||||
name: "share".to_string(),
|
||||
files: ["active.mp3", "done.mp3", "plain.mp3"]
|
||||
.iter()
|
||||
.map(|name| SharedFileEntry {
|
||||
name: (*name).to_string(),
|
||||
size: 4096,
|
||||
attributes: Vec::new(),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn download(filename: &str, status: DownloadStatus) -> DownloadEntry {
|
||||
let (sender, _receiver) = mpsc::channel();
|
||||
DownloadEntry {
|
||||
download: Download {
|
||||
username: "bob".to_string(),
|
||||
// Queued files carry the full share path, as in the tree.
|
||||
filename: format!("share\\{filename}"),
|
||||
token: 1,
|
||||
size: 4096,
|
||||
download_directory: "/tmp".to_string(),
|
||||
status,
|
||||
sender,
|
||||
queue_position: None,
|
||||
metadata: DownloadMetadata::default(),
|
||||
},
|
||||
receiver: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn screen_with(downloads: &[DownloadEntry]) -> String {
|
||||
let mut browse = BrowseState::loading("bob".to_string());
|
||||
browse.load(&[share()]);
|
||||
let mut tabs = BrowseTabs::default();
|
||||
tabs.tabs.push(browse);
|
||||
let mut state = TableState::default();
|
||||
let mut terminal =
|
||||
Terminal::new(TestBackend::new(60, 8)).expect("backend");
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
render_browse_pane(
|
||||
frame,
|
||||
frame.area(),
|
||||
&tabs,
|
||||
&mut state,
|
||||
0,
|
||||
downloads,
|
||||
);
|
||||
})
|
||||
.expect("draw");
|
||||
terminal.backend().to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_downloading_file_carries_its_transfer_glyph() {
|
||||
let screen = screen_with(&[
|
||||
download(
|
||||
"active.mp3",
|
||||
DownloadStatus::InProgress {
|
||||
bytes_downloaded: 2048,
|
||||
total_bytes: 4096,
|
||||
speed_bytes_per_sec: 1.0,
|
||||
},
|
||||
),
|
||||
download("done.mp3", DownloadStatus::Completed),
|
||||
]);
|
||||
assert!(
|
||||
screen.contains(&format!("{GLYPH_ACTIVE} active.mp3")),
|
||||
"{screen}"
|
||||
);
|
||||
assert!(
|
||||
screen.contains(&format!("{GLYPH_DONE} done.mp3")),
|
||||
"{screen}"
|
||||
);
|
||||
assert!(
|
||||
screen.contains("50%"),
|
||||
"the size cell tells the progress: {screen}"
|
||||
);
|
||||
assert!(
|
||||
screen.contains(" plain.mp3"),
|
||||
"an unqueued row keeps the two plain spaces: {screen}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn another_user_s_transfers_mark_nothing() {
|
||||
let mut theirs = download("active.mp3", DownloadStatus::Completed);
|
||||
theirs.download.username = "carol".to_string();
|
||||
let screen = screen_with(&[theirs]);
|
||||
assert!(!screen.contains(GLYPH_ACTIVE), "{screen}");
|
||||
assert!(!screen.contains(GLYPH_DONE), "{screen}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user