loads all shows
This commit is contained in:
259
src/main.rs
259
src/main.rs
@@ -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 there’s 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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user