improved scrolling and loading

This commit is contained in:
2026-02-14 01:28:46 -06:00
parent da6bda9772
commit fb86573c2c

View File

@@ -31,6 +31,8 @@ enum View {
selected: usize, selected: usize,
start_index: usize, start_index: usize,
total: usize, total: usize,
page_size: usize,
loading: bool,
}, },
Login, Login,
} }
@@ -106,7 +108,6 @@ impl Default for App {
} }
} }
impl App { impl App {
fn current_view(&self) -> &View { fn current_view(&self) -> &View {
self.view_stack.last().unwrap() self.view_stack.last().unwrap()
@@ -281,6 +282,8 @@ fn handle_key(app: &mut App, code: KeyCode) {
selected: 0, selected: 0,
start_index: 0, start_index: 0,
total: page.total, total: page.total,
page_size: PAGE_SIZE,
loading: false,
}); });
app.status = "Loaded".to_string(); app.status = "Loaded".to_string();
} }
@@ -309,10 +312,25 @@ fn handle_key(app: &mut App, code: KeyCode) {
else { app.selected -= 1; } else { app.selected -= 1; }
app.status = format!("Selected: {}", app.items[app.selected]); app.status = format!("Selected: {}", app.items[app.selected]);
} }
View::Library { items, selected, .. } => { View::Library { items, selected, total, .. } => {
if items.is_empty() { return; } if items.is_empty() { return; }
if *selected == 0 { *selected = items.len() - 1; }
else { *selected -= 1; } if *selected == 0 {
// wrap-up wants the real end
if items.len() < *total {
ensure_loaded_to_end(app);
}
if let Some(View::Library { items, selected, .. }) = app.view_stack.last_mut() {
if !items.is_empty() {
*selected = items.len() - 1;
}
}
} else {
*selected -= 1;
}
maybe_prefetch(app);
} }
_ => {} _ => {}
} }
@@ -341,15 +359,13 @@ fn handle_key(app: &mut App, code: KeyCode) {
} }
} }
} else { } else {
// wrap or stop—choose behavior: *selected = 0;
// stop at end:
// do nothing
// OR wrap:
// *selected = 0;
} }
} else { } else {
*selected += 1; *selected += 1;
} }
maybe_prefetch(app);
} }
_ => {} _ => {}
} }
@@ -361,66 +377,85 @@ fn handle_key(app: &mut App, code: KeyCode) {
fn load_next_library_page(app: &mut App) { fn load_next_library_page(app: &mut App) {
// Only meaningful if the current view is Library // pull info + mark loading
let (query, next_start, total) = match app.view_stack.last() { let (query, next_start, total, page_size) = match app.view_stack.last_mut() {
Some(View::Library { query, items, total, start_index, .. }) => { Some(View::Library { query, items, total, start_index, loading, page_size, .. }) => {
if *loading { return; }
*loading = true;
let loaded = items.len(); let loaded = items.len();
let next_start = start_index + loaded; let next_start = *start_index + loaded;
(query.clone(), next_start, *total) (query.clone(), next_start, *total, *page_size)
} }
_ => return, _ => return,
}; };
// If we already loaded everything, do nothing
let loaded = match app.view_stack.last() {
Some(View::Library { items, .. }) => items.len(),
_ => 0,
};
if loaded >= total {
return;
}
let cfg = match &app.config { let cfg = match &app.config {
Some(c) => c.clone(), Some(c) => c.clone(),
None => { None => { app.status = "No config loaded".to_string(); finish_loading(app); return; }
app.status = "No config loaded".to_string();
return;
}
}; };
let user_id = match cfg.user_id.as_deref() { let user_id = match cfg.user_id.as_deref() {
Some(u) => u, Some(u) => u,
None => { None => { app.status = "Missing user_id in config (login again)".to_string(); finish_loading(app); return; }
app.status = "Missing user_id in config (login again)".to_string();
return;
}
}; };
let client = match core::jellyfin::JellyfinClient::from_config(&cfg) { let client = match core::jellyfin::JellyfinClient::from_config(&cfg) {
Ok(c) => c, Ok(c) => c,
Err(e) => { Err(e) => { app.status = e; finish_loading(app); return; }
app.status = e;
return;
}
}; };
app.status = format!("Loading more... ({}/{})", loaded, total); let page = match client.list_items(user_id, query.clone(), next_start, page_size) {
let page = match client.list_items(user_id, query.clone(), next_start, PAGE_SIZE) {
Ok(p) => p, Ok(p) => p,
Err(e) => { Err(e) => { app.status = format!("Fetch failed: {e}"); finish_loading(app); return; }
app.status = format!("Fetch failed: {e}");
return;
}
}; };
// Append the new items into the current library view
if let Some(View::Library { items, total, .. }) = app.view_stack.last_mut() { if let Some(View::Library { items, total, .. }) = app.view_stack.last_mut() {
items.extend(page.items); items.extend(page.items);
*total = page.total; // keep total fresh in case server changes *total = page.total;
} }
app.status = "Loaded more".to_string(); finish_loading(app);
}
fn finish_loading(app: &mut App) {
if let Some(View::Library { loading, .. }) = app.view_stack.last_mut() {
*loading = false;
}
}
fn maybe_prefetch(app: &mut App) {
const PREFETCH_ROWS: usize = 0;
let should = match app.view_stack.last() {
Some(View::Library { items, selected, total, loading, .. }) => {
if *loading { false }
else if items.len() >= *total { false }
else {
let remaining_loaded = items.len().saturating_sub(*selected);
remaining_loaded <= PREFETCH_ROWS
}
}
_ => false,
};
if should {
load_next_library_page(app);
}
}
fn ensure_loaded_to_end(app: &mut App) {
loop {
let done = match app.view_stack.last() {
Some(View::Library { items, total, loading, .. }) => {
!*loading && items.len() >= *total
}
_ => true,
};
if done { break; }
load_next_library_page(app);
}
} }
@@ -445,14 +480,27 @@ fn ui(frame: &mut Frame, app: &App) {
} }
} }
// Bottom // Bottom Left
frame.render_widget( frame.render_widget(
Paragraph::new(app.status.as_str()) Paragraph::new(app.status.as_str())
.block(Block::bordered().title("Status").merge_borders(MergeStrategy::Exact)), .block(Block::bordered().title("Status").merge_borders(MergeStrategy::Exact)),
panes.left_output, panes.left_output,
); );
// Bottom Right
let info = match app.current_view() {
View::Library { items, total, loading, .. } => {
if *loading {
format!("loading… {}/{}", items.len(), total)
} else {
format!("{}/{}", items.len(), total)
}
}
_ => app.config_status.clone(),
};
frame.render_widget( frame.render_widget(
Paragraph::new(app.config_status.as_str()) Paragraph::new(info)
.block(Block::bordered().title("Info").merge_borders(MergeStrategy::Exact)), .block(Block::bordered().title("Info").merge_borders(MergeStrategy::Exact)),
panes.right_output, panes.right_output,
); );
@@ -475,7 +523,6 @@ fn compute_window(len: usize, selected: usize, viewport: usize) -> (usize, usize
(start, end) (start, end)
} }
fn render_categories(frame: &mut Frame, app: &App, left: Rect, right: Rect) { fn render_categories(frame: &mut Frame, app: &App, left: Rect, right: Rect) {
// how many rows can we show inside the bordered block? // how many rows can we show inside the bordered block?
let viewport = left.height.saturating_sub(2) as usize; let viewport = left.height.saturating_sub(2) as usize;
@@ -528,7 +575,6 @@ fn render_placeholder(frame: &mut Frame, name: &str, left: Rect, right: Rect) {
); );
} }
fn render_library( fn render_library(
frame: &mut Frame, frame: &mut Frame,
title: &str, title: &str,