Download Functionality Feature Complete
This commit is contained in:
185
src/main.rs
185
src/main.rs
@@ -1562,7 +1562,8 @@ fn ui(frame: &mut Frame, app: &App) {
|
||||
"{connected}\n\np/P: play/autoplay
|
||||
r/R: resume/autoplay
|
||||
d: download / cancel
|
||||
D: focus downloads\nq: quit\n/help: more help"
|
||||
q: quit
|
||||
/help: more help"
|
||||
);
|
||||
|
||||
frame.render_widget(
|
||||
@@ -2389,82 +2390,134 @@ fn mpv_progress(st: &MpvState) -> (u16, String) {
|
||||
}
|
||||
|
||||
fn render_downloads_panel(frame: &mut Frame, app: &App, area: Rect) {
|
||||
// Build rows
|
||||
let mut rows: Vec<ListItem> = Vec::new();
|
||||
|
||||
if let Some(dm) = &app.downloads {
|
||||
match dm.state.lock() {
|
||||
Ok(st) => {
|
||||
if st.queue.is_empty() {
|
||||
rows.push(ListItem::new("(empty)"));
|
||||
} else {
|
||||
// Fit to available height inside borders
|
||||
let max_rows = area.height.saturating_sub(2) as usize;
|
||||
|
||||
for (i, it) in st.queue.iter().enumerate().take(max_rows) {
|
||||
let status = match &it.status {
|
||||
DownloadStatus::Queued => "queued",
|
||||
DownloadStatus::Downloading => "downloading",
|
||||
DownloadStatus::Finalizing => "finalizing",
|
||||
DownloadStatus::Completed => "done",
|
||||
DownloadStatus::Cancelled => "cancelled",
|
||||
DownloadStatus::Failed(_) => "failed",
|
||||
};
|
||||
|
||||
// Always mark the active (front) item with ▶ like before
|
||||
let active_prefix = if i == 0 { "▶ " } else { " " };
|
||||
|
||||
// If downloading, show "NN% (ETA)" BEFORE the title
|
||||
let prog_prefix = if matches!(it.status, DownloadStatus::Downloading) {
|
||||
format_download_pct_eta(it.bytes_downloaded, it.bytes_total, it.added_at)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
rows.push(ListItem::new(format!(
|
||||
"{active_prefix}{status}: {prog_prefix}{}",
|
||||
it.title
|
||||
)));
|
||||
}
|
||||
|
||||
// NOTE: intentionally removed the footer "— {msg}" line
|
||||
// because you said it’s unnecessary.
|
||||
}
|
||||
}
|
||||
Err(_) => rows.push(ListItem::new("(downloads lock poisoned)")),
|
||||
}
|
||||
} else {
|
||||
rows.push(ListItem::new("(downloads disabled)"));
|
||||
}
|
||||
|
||||
// Create the list widget
|
||||
let title = if app.downloads_focus {
|
||||
"Downloads (focus)"
|
||||
} else {
|
||||
"Downloads"
|
||||
};
|
||||
|
||||
// rows available inside the border
|
||||
let viewport = area.height.saturating_sub(2) as usize;
|
||||
|
||||
let mut rows: Vec<ListItem> = Vec::new();
|
||||
let mut state = ListState::default();
|
||||
|
||||
// Helper: "basic" scrolling window (no centering)
|
||||
// Ensures selected is always visible within [start, start+viewport)
|
||||
fn basic_window(len: usize, selected: usize, viewport: usize) -> (usize, usize) {
|
||||
if len == 0 || viewport == 0 {
|
||||
return (0, 0);
|
||||
}
|
||||
let viewport = viewport.min(len);
|
||||
let mut start = 0usize;
|
||||
|
||||
// If selected is below the visible window, scroll down just enough
|
||||
if selected >= start + viewport {
|
||||
start = selected + 1 - viewport;
|
||||
}
|
||||
// If selected is above the visible window, scroll up just enough
|
||||
if selected < start {
|
||||
start = selected;
|
||||
}
|
||||
|
||||
// Clamp
|
||||
if start + viewport > len {
|
||||
start = len - viewport;
|
||||
}
|
||||
|
||||
(start, start + viewport)
|
||||
}
|
||||
|
||||
// Empty states
|
||||
if app.downloads.is_none() {
|
||||
rows.push(ListItem::new("(downloads disabled)"));
|
||||
state.select(None);
|
||||
|
||||
let list = List::new(rows)
|
||||
.block(Block::bordered().title(title).merge_borders(MergeStrategy::Exact))
|
||||
.highlight_symbol(">> ")
|
||||
.highlight_style(Style::default().add_modifier(Modifier::REVERSED));
|
||||
|
||||
frame.render_stateful_widget(list, area, &mut state);
|
||||
return;
|
||||
}
|
||||
|
||||
let dm = app.downloads.as_ref().unwrap();
|
||||
|
||||
match dm.state.lock() {
|
||||
Err(_) => {
|
||||
rows.push(ListItem::new("(downloads lock poisoned)"));
|
||||
state.select(None);
|
||||
}
|
||||
Ok(st) => {
|
||||
let len = st.queue.len();
|
||||
|
||||
if len == 0 {
|
||||
rows.push(ListItem::new("(empty)"));
|
||||
state.select(None);
|
||||
} else {
|
||||
// Clamp selection
|
||||
let selected = app.downloads_selected.min(len.saturating_sub(1));
|
||||
|
||||
// Basic scrolling window
|
||||
let (start, end) = basic_window(len, selected, viewport);
|
||||
|
||||
for i in start..end {
|
||||
let it = match st.queue.get(i) {
|
||||
Some(v) => v,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let status = match &it.status {
|
||||
DownloadStatus::Queued => "queued",
|
||||
DownloadStatus::Downloading => "downloading",
|
||||
DownloadStatus::Finalizing => "finalizing",
|
||||
DownloadStatus::Completed => "done",
|
||||
DownloadStatus::Cancelled => "cancelled",
|
||||
DownloadStatus::Failed(_) => "failed",
|
||||
};
|
||||
|
||||
let active_prefix = if i == 0 { "▶ " } else { " " };
|
||||
|
||||
// Prefer aria2-derived info if you have it; otherwise it will show --% (--:--)
|
||||
let prog_prefix = if matches!(it.status, DownloadStatus::Downloading) {
|
||||
// uses the fields you now populate from aria2 RPC
|
||||
// (bytes_downloaded, bytes_total, eta_secs)
|
||||
let pct_str = match it.bytes_total {
|
||||
Some(t) if t > 0 => {
|
||||
let pct = (it.bytes_downloaded as f64 / t as f64) * 100.0;
|
||||
format!("{:0.0}%", pct.clamp(0.0, 100.0))
|
||||
}
|
||||
_ => "--%".to_string(),
|
||||
};
|
||||
|
||||
let eta_str = match it.eta_secs {
|
||||
Some(s) => fmt_hms(s as f64),
|
||||
None => "--:--".to_string(),
|
||||
};
|
||||
|
||||
format!("{pct_str} ({eta_str}) ")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
rows.push(ListItem::new(format!(
|
||||
"{active_prefix}{status}: {prog_prefix}{}",
|
||||
it.title
|
||||
)));
|
||||
}
|
||||
|
||||
// selection must be relative to the window slice
|
||||
state.select(Some(selected - start));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let list = List::new(rows)
|
||||
.block(Block::bordered().title(title).merge_borders(MergeStrategy::Exact))
|
||||
.highlight_symbol(">> ")
|
||||
.highlight_style(Style::default().add_modifier(Modifier::REVERSED));
|
||||
|
||||
let mut state = ListState::default();
|
||||
|
||||
// Clamp selection so it never goes out of bounds
|
||||
let len = app
|
||||
.downloads
|
||||
.as_ref()
|
||||
.and_then(|dm| dm.state.lock().ok().map(|st| st.queue.len()))
|
||||
.unwrap_or(0);
|
||||
|
||||
if len == 0 {
|
||||
state.select(None);
|
||||
} else {
|
||||
let sel = app.downloads_selected.min(len.saturating_sub(1));
|
||||
state.select(Some(sel));
|
||||
}
|
||||
|
||||
frame.render_stateful_widget(list, area, &mut state);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user