search feature added
This commit is contained in:
@@ -136,6 +136,10 @@ impl JellyfinClient {
|
|||||||
req = req.query(&[("IncludeItemTypes", types)]);
|
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}"))?;
|
let resp = req.send().map_err(|e| format!("request failed: {e}"))?;
|
||||||
|
|
||||||
if !resp.status().is_success() {
|
if !resp.status().is_success() {
|
||||||
@@ -155,21 +159,29 @@ impl JellyfinClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What kind of library list we want.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct LibraryQuery {
|
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 {
|
impl LibraryQuery {
|
||||||
pub fn movies() -> Self {
|
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 {
|
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 {
|
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,
|
Pass,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum SearchScope {
|
||||||
|
All,
|
||||||
|
Library {
|
||||||
|
title: String,
|
||||||
|
query: core::jellyfin::LibraryQuery, // includes item types
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SearchState {
|
||||||
|
scope: SearchScope,
|
||||||
|
input: String,
|
||||||
|
}
|
||||||
|
|
||||||
struct App {
|
struct App {
|
||||||
should_exit: bool,
|
should_exit: bool,
|
||||||
status: String,
|
status: String,
|
||||||
@@ -58,6 +72,8 @@ struct App {
|
|||||||
login_pass: String,
|
login_pass: String,
|
||||||
login_focus: LoginFocus,
|
login_focus: LoginFocus,
|
||||||
login_error: Option<String>,
|
login_error: Option<String>,
|
||||||
|
|
||||||
|
search: Option<SearchState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for App {
|
impl Default for App {
|
||||||
@@ -104,6 +120,7 @@ impl Default for App {
|
|||||||
login_pass: String::new(),
|
login_pass: String::new(),
|
||||||
login_focus: LoginFocus::User,
|
login_focus: LoginFocus::User,
|
||||||
login_error: None,
|
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 {
|
match code {
|
||||||
KeyCode::Char('q') => app.should_exit = true,
|
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
|
// Bottom Left
|
||||||
|
let status_text = if let Some(s) = &app.search {
|
||||||
|
format!("/{}", s.input)
|
||||||
|
} else {
|
||||||
|
app.status.clone()
|
||||||
|
};
|
||||||
|
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(app.status.as_str())
|
Paragraph::new(status_text)
|
||||||
.block(Block::bordered().title("Status").merge_borders(MergeStrategy::Exact)),
|
.block(Block::bordered().title("Status").merge_borders(MergeStrategy::Exact)),
|
||||||
panes.left_output,
|
panes.left_output,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user