search feature added
This commit is contained in:
@@ -136,6 +136,10 @@ impl JellyfinClient {
|
||||
req = req.query(&[("IncludeItemTypes", types)]);
|
||||
}
|
||||
|
||||
if let Some(term) = query.search_term.as_deref() {
|
||||
req = req.query(&[("SearchTerm", term)]);
|
||||
}
|
||||
|
||||
let resp = req.send().map_err(|e| format!("request failed: {e}"))?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
@@ -155,21 +159,29 @@ impl JellyfinClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// What kind of library list we want.
|
||||
#[derive(Clone)]
|
||||
pub struct LibraryQuery {
|
||||
pub include_item_types: Option<String>, // e.g. Some("Movie".into())
|
||||
pub include_item_types: Option<String>,
|
||||
pub search_term: Option<String>,
|
||||
}
|
||||
|
||||
impl LibraryQuery {
|
||||
pub fn movies() -> Self {
|
||||
Self { include_item_types: Some("Movie".into()) }
|
||||
Self { include_item_types: Some("Movie".into()), search_term: None }
|
||||
}
|
||||
pub fn shows() -> Self {
|
||||
Self { include_item_types: Some("Series".into()) }
|
||||
Self { include_item_types: Some("Series".into()), search_term: None }
|
||||
}
|
||||
pub fn music() -> Self {
|
||||
Self { include_item_types: Some("Audio".into()) }
|
||||
Self { include_item_types: Some("Audio".into()), search_term: None }
|
||||
}
|
||||
pub fn all() -> Self {
|
||||
Self { include_item_types: None, search_term: None }
|
||||
}
|
||||
|
||||
pub fn with_search(mut self, term: String) -> Self {
|
||||
self.search_term = Some(term);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
95
src/main.rs
95
src/main.rs
@@ -43,6 +43,20 @@ enum LoginFocus {
|
||||
Pass,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum SearchScope {
|
||||
All,
|
||||
Library {
|
||||
title: String,
|
||||
query: core::jellyfin::LibraryQuery, // includes item types
|
||||
},
|
||||
}
|
||||
|
||||
struct SearchState {
|
||||
scope: SearchScope,
|
||||
input: String,
|
||||
}
|
||||
|
||||
struct App {
|
||||
should_exit: bool,
|
||||
status: String,
|
||||
@@ -58,6 +72,8 @@ struct App {
|
||||
login_pass: String,
|
||||
login_focus: LoginFocus,
|
||||
login_error: Option<String>,
|
||||
|
||||
search: Option<SearchState>,
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
@@ -104,6 +120,7 @@ impl Default for App {
|
||||
login_pass: String::new(),
|
||||
login_focus: LoginFocus::User,
|
||||
login_error: None,
|
||||
search: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,6 +245,63 @@ fn handle_key(app: &mut App, code: KeyCode) {
|
||||
}
|
||||
}
|
||||
|
||||
// If search mode is active, it captures input first
|
||||
if let Some(search) = &mut app.search {
|
||||
match code {
|
||||
KeyCode::Esc => { app.search = None; return; }
|
||||
KeyCode::Backspace => { search.input.pop(); return; }
|
||||
KeyCode::Enter => {
|
||||
let term = search.input.trim().to_string();
|
||||
if term.is_empty() {
|
||||
app.search = None;
|
||||
return;
|
||||
}
|
||||
|
||||
// build query based on scope
|
||||
let (title, query) = match &search.scope {
|
||||
SearchScope::All => (
|
||||
format!("Search: {}", term),
|
||||
core::jellyfin::LibraryQuery::all().with_search(term.clone()),
|
||||
),
|
||||
SearchScope::Library { title, query } => (
|
||||
format!("{}: {}", title, term),
|
||||
query.clone().with_search(term.clone()),
|
||||
),
|
||||
};
|
||||
|
||||
app.search = None;
|
||||
|
||||
// run search (same as your library fetch)
|
||||
let cfg = match &app.config { Some(c) => c.clone(), None => { app.status = "No config".into(); return; } };
|
||||
let user_id = match cfg.user_id.as_deref() { Some(u) => u, None => { app.status = "Missing user_id".into(); return; } };
|
||||
let client = match core::jellyfin::JellyfinClient::from_config(&cfg) { Ok(c) => c, Err(e) => { app.status = e; return; } };
|
||||
|
||||
app.status = format!("Searching… {}", term);
|
||||
|
||||
match client.list_items(user_id, query.clone(), 0, PAGE_SIZE) {
|
||||
Ok(page) => {
|
||||
app.view_stack.push(View::Library {
|
||||
title,
|
||||
query,
|
||||
items: page.items,
|
||||
selected: 0,
|
||||
start_index: 0,
|
||||
total: page.total,
|
||||
page_size: PAGE_SIZE,
|
||||
loading: false,
|
||||
});
|
||||
app.status = "Search loaded".into();
|
||||
}
|
||||
Err(e) => app.status = format!("Search failed: {e}"),
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
KeyCode::Char(c) => { search.input.push(c); return; }
|
||||
_ => return,
|
||||
}
|
||||
}
|
||||
|
||||
match code {
|
||||
KeyCode::Char('q') => app.should_exit = true,
|
||||
|
||||
@@ -371,6 +445,19 @@ fn handle_key(app: &mut App, code: KeyCode) {
|
||||
}
|
||||
}
|
||||
|
||||
KeyCode::Char('/') => {
|
||||
let scope = match app.current_view() {
|
||||
View::Categories => SearchScope::All,
|
||||
View::Library { title, query, .. } => SearchScope::Library {
|
||||
title: title.clone(),
|
||||
query: query.clone(),
|
||||
},
|
||||
_ => SearchScope::All,
|
||||
};
|
||||
|
||||
app.search = Some(SearchState { scope, input: String::new() });
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -481,8 +568,14 @@ fn ui(frame: &mut Frame, app: &App) {
|
||||
}
|
||||
|
||||
// Bottom Left
|
||||
let status_text = if let Some(s) = &app.search {
|
||||
format!("/{}", s.input)
|
||||
} else {
|
||||
app.status.clone()
|
||||
};
|
||||
|
||||
frame.render_widget(
|
||||
Paragraph::new(app.status.as_str())
|
||||
Paragraph::new(status_text)
|
||||
.block(Block::bordered().title("Status").merge_borders(MergeStrategy::Exact)),
|
||||
panes.left_output,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user