improved scrolling and loading
This commit is contained in:
144
src/main.rs
144
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,
|
||||
|
||||
Reference in New Issue
Block a user