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,
|
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,
|
||||||
|
|||||||
Reference in New Issue
Block a user