feat: settings popup and uploads in the transfers pane

Press 'o' for a settings popup: edit the download folder, add/remove
share paths (applied live via a rescan and persisted to config.toml),
and force a re-index with 'r' for files changed on disk. The state
machine is unit-tested; feedback shows the shared file/folder counts.

The downloads pane is now Downloads/Uploads: uploads being served are
listed after downloads with an up-arrow marker, live progress, and
status; 'x' cancels an in-progress upload. Client::shared_counts exposes
the index size for the status line.
This commit is contained in:
michel
2026-07-20 20:56:39 +02:00
parent 4936d4dd2a
commit 724796c1eb
10 changed files with 533 additions and 19 deletions
+47 -5
View File
@@ -13,23 +13,25 @@ use ratatui::{
},
};
use soulseek_rs::DownloadStatus;
use soulseek_rs::types::{UploadInfo, UploadStatus};
pub fn render_downloads_pane(
frame: &mut Frame,
area: Rect,
downloads: &[DownloadEntry],
uploads: &[UploadInfo],
table_state: &mut TableState,
focused: bool,
) {
if downloads.is_empty() {
if downloads.is_empty() && uploads.is_empty() {
let empty_block = Block::default()
.borders(Borders::ALL)
.border_style(border_style(focused))
.border_type(border_type(focused))
.title("[3] Downloads");
.title("[3] Downloads/Uploads");
let paragraph = Paragraph::new(
"No downloads. Select files from Results and press Enter.",
"No transfers. Select files from Results and press Enter.",
)
.block(empty_block);
frame.render_widget(paragraph, area);
@@ -45,7 +47,7 @@ pub fn render_downloads_pane(
])
.height(1);
let rows: Vec<Row> = downloads
let mut rows: Vec<Row> = downloads
.iter()
.map(|download_entry| {
let download = &download_entry.download;
@@ -112,6 +114,46 @@ pub fn render_downloads_pane(
})
.collect();
rows.extend(uploads.iter().map(|upload| {
let (status_icon, status_style) = match &upload.status {
UploadStatus::InProgress => ("⧗", warning_style()),
UploadStatus::Completed => ("✓", success_style()),
UploadStatus::Cancelled => ("✗", inactive_style()),
UploadStatus::Failed(_) => ("✗", error_style()),
};
let progress_text = match &upload.status {
UploadStatus::InProgress => {
let percent = if upload.size > 0 {
(upload.bytes_sent as f64 / upload.size as f64 * 100.0)
as u8
} else {
0
};
format!(
"{}/{} ({}%)",
format_bytes(upload.bytes_sent),
format_bytes(upload.size),
percent
)
}
UploadStatus::Completed => format_bytes(upload.size),
UploadStatus::Cancelled => "Cancelled".to_string(),
UploadStatus::Failed(_) => "Failed".to_string(),
};
let basename = upload
.filename
.rsplit(['\\', '/'])
.next()
.unwrap_or(&upload.filename);
Row::new(vec![
Cell::from(format!("↑ {status_icon}")).style(status_style),
Cell::from(basename.to_string()),
Cell::from(upload.username.clone()),
Cell::from(progress_text),
Cell::from(String::new()),
])
}));
let widths = [
ratatui::layout::Constraint::Length(8),
ratatui::layout::Constraint::Fill(2),
@@ -130,7 +172,7 @@ pub fn render_downloads_pane(
.borders(Borders::ALL)
.border_style(border_style(focused))
.border_type(border_type(focused))
.title("[3] Downloads"),
.title("[3] Downloads/Uploads"),
);
frame.render_stateful_widget(table, area, table_state);