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,
start_index: usize,
total: usize,
page_size: usize,
loading: bool,
},
Login,
}
@@ -106,7 +108,6 @@ impl Default for App {
}
}
impl App {
fn current_view(&self) -> &View {
self.view_stack.last().unwrap()
@@ -281,6 +282,8 @@ fn handle_key(app: &mut App, code: KeyCode) {
selected: 0,
start_index: 0,
total: page.total,
page_size: PAGE_SIZE,
loading: false,
});
app.status = "Loaded".to_string();
}
@@ -309,10 +312,25 @@ fn handle_key(app: &mut App, code: KeyCode) {
else { app.selected -= 1; }
app.status = format!("Selected: {}", app.items[app.selected]);
}
View::Library { items, selected, .. } => {
View::Library { items, selected, total, .. } => {
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 {
// wrap or stop—choose behavior:
// stop at end:
// do nothing
// OR wrap:
// *selected = 0;
*selected = 0;
}
} else {
*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) {
// Only meaningful if the current view is Library
let (query, next_start, total) = match app.view_stack.last() {
Some(View::Library { query, items, total, start_index, .. }) => {
// pull info + mark loading
let (query, next_start, total, page_size) = match app.view_stack.last_mut() {
Some(View::Library { query, items, total, start_index, loading, page_size, .. }) => {
if *loading { return; }
*loading = true;
let loaded = items.len();
let next_start = start_index + loaded;
(query.clone(), next_start, *total)
let next_start = *start_index + loaded;
(query.clone(), next_start, *total, *page_size)
}
_ => 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 {
Some(c) => c.clone(),
None => {
app.status = "No config loaded".to_string();
return;
}
None => { app.status = "No config loaded".to_string(); finish_loading(app); return; }
};
let user_id = match cfg.user_id.as_deref() {
Some(u) => u,
None => {
app.status = "Missing user_id in config (login again)".to_string();
return;
}
None => { app.status = "Missing user_id in config (login again)".to_string(); finish_loading(app); return; }
};
let client = match core::jellyfin::JellyfinClient::from_config(&cfg) {
Ok(c) => c,
Err(e) => {
app.status = e;
return;
}
Err(e) => { app.status = e; finish_loading(app); 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,
Err(e) => {
app.status = format!("Fetch failed: {e}");
return;
}
Err(e) => { app.status = format!("Fetch failed: {e}"); finish_loading(app); return; }
};
// Append the new items into the current library view
if let Some(View::Library { items, total, .. }) = app.view_stack.last_mut() {
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(
Paragraph::new(app.status.as_str())
.block(Block::bordered().title("Status").merge_borders(MergeStrategy::Exact)),
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(
Paragraph::new(app.config_status.as_str())
Paragraph::new(info)
.block(Block::bordered().title("Info").merge_borders(MergeStrategy::Exact)),
panes.right_output,
);
@@ -475,7 +523,6 @@ fn compute_window(len: usize, selected: usize, viewport: usize) -> (usize, usize
(start, end)
}
fn render_categories(frame: &mut Frame, app: &App, left: Rect, right: Rect) {
// how many rows can we show inside the bordered block?
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(
frame: &mut Frame,
title: &str,