loads all shows

This commit is contained in:
2026-02-14 00:51:30 -06:00
parent b5a0790cb6
commit da6bda9772
2 changed files with 411 additions and 77 deletions

View File

@@ -1,5 +1,7 @@
mod core;
const PAGE_SIZE: usize = 50;
use std::{
io,
time::{Duration, Instant},
@@ -22,7 +24,14 @@ use ratatui::backend::CrosstermBackend;
#[derive(Clone)]
enum View {
Categories,
Placeholder(String), // temporary for testing navigation
Library {
title: String,
query: core::jellyfin::LibraryQuery,
items: Vec<core::jellyfin::JfItem>,
selected: usize,
start_index: usize,
total: usize,
},
Login,
}
@@ -180,7 +189,7 @@ fn handle_key(app: &mut App, code: KeyCode) {
}
};
let client = core::jellyfin::JellyfinClient::from_config(&cfg);
let client = core::jellyfin::JellyfinClient::unauthenticated(&cfg.base_url);
match client.login_by_name(&app.login_user, &app.login_pass) {
Ok(res) => {
let mut new_cfg = cfg;
@@ -224,10 +233,61 @@ fn handle_key(app: &mut App, code: KeyCode) {
KeyCode::Enter => {
match app.current_view() {
View::Categories => {
let selected = app.items[app.selected].clone();
app.view_stack.push(View::Placeholder(selected.clone()));
app.status = format!("Entered {}", selected);
app.selected = 0;
let cat = app.items[app.selected].clone();
// Map your categories to Jellyfin queries (expand later)
let query = match cat.as_str() {
"Movies" => core::jellyfin::LibraryQuery::movies(),
"Shows" => core::jellyfin::LibraryQuery::shows(),
"Music" => core::jellyfin::LibraryQuery::music(),
_ => {
app.status = format!("Not wired yet: {}", cat);
return;
}
};
let cfg = match &app.config {
Some(c) => c.clone(),
None => {
app.status = "No config loaded".to_string();
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;
}
};
let client = match core::jellyfin::JellyfinClient::from_config(&cfg) {
Ok(c) => c,
Err(e) => {
app.status = e;
return;
}
};
app.status = format!("Fetching {}...", cat);
match client.list_items(user_id, query.clone(), 0, PAGE_SIZE) {
Ok(page) => {
app.view_stack.push(View::Library {
title: cat,
query,
items: page.items,
selected: 0,
start_index: 0,
total: page.total,
});
app.status = "Loaded".to_string();
}
Err(e) => {
app.status = format!("Fetch failed: {e}");
}
}
}
_ => {}
}
@@ -242,28 +302,128 @@ fn handle_key(app: &mut App, code: KeyCode) {
}
KeyCode::Up | KeyCode::Char('k') => {
if app.items.is_empty() {
return;
match app.view_stack.last_mut().unwrap() {
View::Categories => {
if app.items.is_empty() { return; }
if app.selected == 0 { app.selected = app.items.len() - 1; }
else { app.selected -= 1; }
app.status = format!("Selected: {}", app.items[app.selected]);
}
View::Library { items, selected, .. } => {
if items.is_empty() { return; }
if *selected == 0 { *selected = items.len() - 1; }
else { *selected -= 1; }
}
_ => {}
}
if app.selected == 0 {
app.selected = app.items.len() - 1;
} else {
app.selected -= 1;
}
app.status = format!("Selected: {}", app.items[app.selected]);
}
KeyCode::Down | KeyCode::Char('j') => {
if app.items.is_empty() {
return;
match app.view_stack.last_mut().unwrap() {
View::Categories => {
if app.items.is_empty() { return; }
app.selected = (app.selected + 1) % app.items.len();
app.status = format!("Selected: {}", app.items[app.selected]);
}
View::Library { items, selected, total, .. } => {
if items.is_empty() { return; }
// If we're at the end of what we have loaded…
if *selected + 1 >= items.len() {
// …and theres more available on the server, fetch next page.
if items.len() < *total {
load_next_library_page(app);
// After loading more, move down if possible.
if let Some(View::Library { items, selected, .. }) = app.view_stack.last_mut() {
if !items.is_empty() && *selected + 1 < items.len() {
*selected += 1;
}
}
} else {
// wrap or stop—choose behavior:
// stop at end:
// do nothing
// OR wrap:
// *selected = 0;
}
} else {
*selected += 1;
}
}
_ => {}
}
app.selected = (app.selected + 1) % app.items.len();
app.status = format!("Selected: {}", app.items[app.selected]);
}
_ => {}
}
}
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, .. }) => {
let loaded = items.len();
let next_start = start_index + loaded;
(query.clone(), next_start, *total)
}
_ => 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;
}
};
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;
}
};
let client = match core::jellyfin::JellyfinClient::from_config(&cfg) {
Ok(c) => c,
Err(e) => {
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) {
Ok(p) => p,
Err(e) => {
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() {
items.extend(page.items);
*total = page.total; // keep total fresh in case server changes
}
app.status = "Loaded more".to_string();
}
fn ui(frame: &mut Frame, app: &App) {
let area = frame.area();
let panes = layout(area);
@@ -277,8 +437,8 @@ fn ui(frame: &mut Frame, app: &App) {
View::Categories => {
render_categories(frame, app, panes.left_main, panes.right_main);
}
View::Placeholder(name) => {
render_placeholder(frame, name, panes.left_main, panes.right_main);
View::Library { title, items, selected, start_index, total, .. } => {
render_library(frame, title, *start_index, *total, items, *selected, panes.left_main, panes.right_main);
}
View::Login => {
render_login(frame, app, panes.left_main, panes.right_main);
@@ -368,6 +528,63 @@ fn render_placeholder(frame: &mut Frame, name: &str, left: Rect, right: Rect) {
);
}
fn render_library(
frame: &mut Frame,
title: &str,
start_index: usize,
total: usize,
items: &[core::jellyfin::JfItem],
selected: usize,
left: Rect,
right: Rect,
) {
let viewport = left.height.saturating_sub(2) as usize;
let len = items.len();
let selected = selected.min(len.saturating_sub(1));
let (start, end) = compute_window(len, selected, viewport);
let list_items: Vec<ListItem> = items[start..end]
.iter()
.map(|it| {
let year = it.production_year.map(|y| format!(" ({y})")).unwrap_or_default();
ListItem::new(format!("{}{}", it.name, year))
})
.collect();
let list = List::new(list_items)
.block(
Block::bordered()
.title(format!("{title} [{}/{}]", items.len(), total))
.merge_borders(MergeStrategy::Exact),
)
.highlight_symbol("> ")
.highlight_style(Style::default().add_modifier(Modifier::REVERSED));
let mut state = ListState::default();
state.select(if len > 0 { Some(selected - start) } else { None });
frame.render_stateful_widget(list, left, &mut state);
let detail = if len > 0 {
let it = &items[selected];
format!(
"Name: {}\nId: {}\nType: {:?}\nYear: {:?}",
it.name, it.id, it.item_type, it.production_year
)
} else {
"No items returned".to_string()
};
frame.render_widget(
Paragraph::new(detail)
.block(Block::bordered().title("Details").merge_borders(MergeStrategy::Exact)),
right,
);
}
struct Panes {
top: Rect,
left_main: Rect,
@@ -388,7 +605,7 @@ fn layout(area: Rect) -> Panes {
.areas(bottom);
let [left_main, right_main] =
Layout::horizontal([Fill(2), Fill(5)])
Layout::horizontal([Fill(3), Fill(5)])
.spacing(Spacing::Overlap(1))
.areas(main_area);