diff --git a/src/main.rs b/src/main.rs index 024498a..b51a859 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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,