Download Functionality Feature Complete
This commit is contained in:
141
src/main.rs
141
src/main.rs
@@ -1562,7 +1562,8 @@ fn ui(frame: &mut Frame, app: &App) {
|
|||||||
"{connected}\n\np/P: play/autoplay
|
"{connected}\n\np/P: play/autoplay
|
||||||
r/R: resume/autoplay
|
r/R: resume/autoplay
|
||||||
d: download / cancel
|
d: download / cancel
|
||||||
D: focus downloads\nq: quit\n/help: more help"
|
q: quit
|
||||||
|
/help: more help"
|
||||||
);
|
);
|
||||||
|
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
@@ -2389,19 +2390,84 @@ fn mpv_progress(st: &MpvState) -> (u16, String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_downloads_panel(frame: &mut Frame, app: &App, area: Rect) {
|
fn render_downloads_panel(frame: &mut Frame, app: &App, area: Rect) {
|
||||||
// Build rows
|
let title = if app.downloads_focus {
|
||||||
let mut rows: Vec<ListItem> = Vec::new();
|
"Downloads (focus)"
|
||||||
|
|
||||||
if let Some(dm) = &app.downloads {
|
|
||||||
match dm.state.lock() {
|
|
||||||
Ok(st) => {
|
|
||||||
if st.queue.is_empty() {
|
|
||||||
rows.push(ListItem::new("(empty)"));
|
|
||||||
} else {
|
} else {
|
||||||
// Fit to available height inside borders
|
"Downloads"
|
||||||
let max_rows = area.height.saturating_sub(2) as usize;
|
};
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
};
|
||||||
|
|
||||||
for (i, it) in st.queue.iter().enumerate().take(max_rows) {
|
|
||||||
let status = match &it.status {
|
let status = match &it.status {
|
||||||
DownloadStatus::Queued => "queued",
|
DownloadStatus::Queued => "queued",
|
||||||
DownloadStatus::Downloading => "downloading",
|
DownloadStatus::Downloading => "downloading",
|
||||||
@@ -2411,12 +2477,26 @@ fn render_downloads_panel(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
DownloadStatus::Failed(_) => "failed",
|
DownloadStatus::Failed(_) => "failed",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Always mark the active (front) item with ▶ like before
|
|
||||||
let active_prefix = if i == 0 { "▶ " } else { " " };
|
let active_prefix = if i == 0 { "▶ " } else { " " };
|
||||||
|
|
||||||
// If downloading, show "NN% (ETA)" BEFORE the title
|
// Prefer aria2-derived info if you have it; otherwise it will show --% (--:--)
|
||||||
let prog_prefix = if matches!(it.status, DownloadStatus::Downloading) {
|
let prog_prefix = if matches!(it.status, DownloadStatus::Downloading) {
|
||||||
format_download_pct_eta(it.bytes_downloaded, it.bytes_total, it.added_at)
|
// 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 {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
@@ -2427,44 +2507,17 @@ fn render_downloads_panel(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: intentionally removed the footer "— {msg}" line
|
// selection must be relative to the window slice
|
||||||
// because you said it’s unnecessary.
|
state.select(Some(selected - start));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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"
|
|
||||||
};
|
|
||||||
|
|
||||||
let list = List::new(rows)
|
let list = List::new(rows)
|
||||||
.block(Block::bordered().title(title).merge_borders(MergeStrategy::Exact))
|
.block(Block::bordered().title(title).merge_borders(MergeStrategy::Exact))
|
||||||
.highlight_symbol(">> ")
|
.highlight_symbol(">> ")
|
||||||
.highlight_style(Style::default().add_modifier(Modifier::REVERSED));
|
.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);
|
frame.render_stateful_widget(list, area, &mut state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user