diff --git a/Cargo.toml b/Cargo.toml index 57737a7..df58fbd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ derivative = "2.2.0" indoc = "1.0.8" log = "0.4.17" log4rs = { version = "1.2.0", features = ["file_appender"] } +regex = "1.7.1" reqwest = { version = "0.11.13", features = ["json"] } serde_yaml = "0.9.16" serde_json = "1.0.91" diff --git a/README.md b/README.md index 1711fff..95930d2 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ tautulli: - [x] View your library, downloads, collections, or calendar - [x] View details of a specific movie including description, history, downloaded file info, or the credits - [x] View details of any collection and the movies in them -- [x] Search your library for specific movies +- [x] Search your library or collections - [ ] Add movies to Radarr - [ ] Search your collections - [ ] Manage your quality profiles diff --git a/src/app/key_binding.rs b/src/app/key_binding.rs index 19b6624..376d027 100644 --- a/src/app/key_binding.rs +++ b/src/app/key_binding.rs @@ -15,6 +15,7 @@ generate_keybindings! { right, backspace, search, + filter, submit, quit, esc @@ -50,6 +51,10 @@ pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings { key: Key::Char('s'), desc: "Search", }, + filter: KeyBinding { + key: Key::Char('f'), + desc: "Filter", + }, submit: KeyBinding { key: Key::Enter, desc: "Select", diff --git a/src/app/radarr.rs b/src/app/radarr.rs index c8edb65..03a1fb9 100644 --- a/src/app/radarr.rs +++ b/src/app/radarr.rs @@ -30,6 +30,7 @@ pub struct RadarrData { pub main_tabs: TabState, pub movie_info_tabs: TabState, pub search: String, + pub filter: String, pub is_searching: bool, } @@ -38,6 +39,12 @@ impl RadarrData { self.collection_movies = StatefulTable::default(); } + pub fn reset_search(&mut self) { + self.is_searching = false; + self.search = String::default(); + self.filter = String::default(); + } + pub fn reset_movie_info_tabs(&mut self) { self.file_details = String::default(); self.audio_details = String::default(); @@ -73,23 +80,25 @@ impl Default for RadarrData { collections: StatefulTable::default(), collection_movies: StatefulTable::default(), search: String::default(), + filter: String::default(), is_searching: false, main_tabs: TabState::new(vec![ TabRoute { title: "Library".to_owned(), route: ActiveRadarrBlock::Movies.into(), - help: "<↑↓> scroll table | search | movie details | ←→ change tab " + help: "<↑↓> scroll | search | filter | details | ←→ change tab " .to_owned(), }, TabRoute { title: "Downloads".to_owned(), route: ActiveRadarrBlock::Downloads.into(), - help: "<↑↓> scroll table | ←→ change tab ".to_owned(), + help: "<↑↓> scroll | ←→ change tab ".to_owned(), }, TabRoute { title: "Collections".to_owned(), route: ActiveRadarrBlock::Collections.into(), - help: "<↑↓> scroll table | collection details | ←→ change tab ".to_owned(), + help: "<↑↓> scroll | search | filter | details | ←→ change tab " + .to_owned(), }, ]), movie_info_tabs: TabState::new(vec![ @@ -101,7 +110,7 @@ impl Default for RadarrData { TabRoute { title: "History".to_owned(), route: ActiveRadarrBlock::MovieHistory.into(), - help: "<↑↓> scroll table | ←→ change tab | close ".to_owned(), + help: "<↑↓> scroll | ←→ change tab | close ".to_owned(), }, TabRoute { title: "File".to_owned(), @@ -111,12 +120,12 @@ impl Default for RadarrData { TabRoute { title: "Cast".to_owned(), route: ActiveRadarrBlock::Cast.into(), - help: "<↑↓> scroll table | ←→ change tab | close ".to_owned(), + help: "<↑↓> scroll | ←→ change tab | close ".to_owned(), }, TabRoute { title: "Crew".to_owned(), route: ActiveRadarrBlock::Crew.into(), - help: "<↑↓> scroll table | ←→ change tab | close ".to_owned(), + help: "<↑↓> scroll | ←→ change tab | close ".to_owned(), }, ]), } @@ -132,11 +141,14 @@ pub enum ActiveRadarrBlock { Cast, Crew, FileInfo, + FilterCollections, + FilterMovies, Movies, MovieDetails, MovieHistory, Downloads, SearchMovie, + SearchCollection, SortOptions, ViewMovieOverview, } diff --git a/src/handlers/radarr_handlers/mod.rs b/src/handlers/radarr_handlers/mod.rs index 71340e8..2a4eede 100644 --- a/src/handlers/radarr_handlers/mod.rs +++ b/src/handlers/radarr_handlers/mod.rs @@ -1,9 +1,13 @@ +use regex::Regex; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::radarr::ActiveRadarrBlock; use crate::handlers::radarr_handlers::collection_details_handler::CollectionDetailsHandler; use crate::handlers::radarr_handlers::movie_details_handler::MovieDetailsHandler; use crate::handlers::{handle_clear_errors, KeyEventHandler}; +use crate::models::radarr_models::Movie; use crate::models::Scrollable; +use crate::utils::strip_non_alphanumeric_characters; use crate::{App, Key}; mod collection_details_handler; @@ -50,11 +54,39 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { fn handle_scroll_up(&mut self) { match self.active_radarr_block { - ActiveRadarrBlock::Collections => self.app.data.radarr_data.collections.scroll_up(), + ActiveRadarrBlock::Collections => { + if !self.app.data.radarr_data.filter.is_empty() { + self + .app + .data + .radarr_data + .collections + .scroll_up_with_filter(|&collection| { + strip_non_alphanumeric_characters(&collection.title) + .starts_with(&self.app.data.radarr_data.filter) + }); + } else { + self.app.data.radarr_data.collections.scroll_up() + } + } ActiveRadarrBlock::CollectionDetails => { self.app.data.radarr_data.collection_movies.scroll_up() } - ActiveRadarrBlock::Movies => self.app.data.radarr_data.movies.scroll_up(), + ActiveRadarrBlock::Movies => { + if !self.app.data.radarr_data.filter.is_empty() { + self + .app + .data + .radarr_data + .movies + .scroll_up_with_filter(|&movie| { + strip_non_alphanumeric_characters(&movie.title) + .starts_with(&self.app.data.radarr_data.filter) + }); + } else { + self.app.data.radarr_data.movies.scroll_up() + } + } ActiveRadarrBlock::Downloads => self.app.data.radarr_data.downloads.scroll_up(), _ => (), } @@ -62,11 +94,39 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { fn handle_scroll_down(&mut self) { match self.active_radarr_block { - ActiveRadarrBlock::Collections => self.app.data.radarr_data.collections.scroll_down(), + ActiveRadarrBlock::Collections => { + if !self.app.data.radarr_data.filter.is_empty() { + self + .app + .data + .radarr_data + .collections + .scroll_down_with_filter(|&collection| { + strip_non_alphanumeric_characters(&collection.title) + .starts_with(&self.app.data.radarr_data.filter) + }); + } else { + self.app.data.radarr_data.collections.scroll_down() + } + } ActiveRadarrBlock::CollectionDetails => { self.app.data.radarr_data.collection_movies.scroll_down() } - ActiveRadarrBlock::Movies => self.app.data.radarr_data.movies.scroll_down(), + ActiveRadarrBlock::Movies => { + if !self.app.data.radarr_data.filter.is_empty() { + self + .app + .data + .radarr_data + .movies + .scroll_down_with_filter(|&movie| { + strip_non_alphanumeric_characters(&movie.title) + .starts_with(&self.app.data.radarr_data.filter) + }); + } else { + self.app.data.radarr_data.movies.scroll_down() + } + } ActiveRadarrBlock::Downloads => self.app.data.radarr_data.downloads.scroll_down(), _ => (), } @@ -131,7 +191,9 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { .movies .items .iter() - .position(|movie| movie.title.to_lowercase() == search_string); + .position(|movie| { + strip_non_alphanumeric_characters(&movie.title).starts_with(&search_string) + }); self.app.data.radarr_data.is_searching = false; self.app.data.radarr_data.movies.select_index(movie_index); @@ -140,41 +202,155 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { self.app.pop_navigation_stack(); } } + ActiveRadarrBlock::SearchCollection => { + let search_string = self + .app + .data + .radarr_data + .search + .drain(..) + .collect::() + .to_lowercase(); + let collection_index = + self + .app + .data + .radarr_data + .collections + .items + .iter() + .position(|collection| { + strip_non_alphanumeric_characters(&collection.title).starts_with(&search_string) + }); + + self.app.data.radarr_data.is_searching = false; + self + .app + .data + .radarr_data + .collections + .select_index(collection_index); + + if collection_index.is_some() { + self.app.pop_navigation_stack(); + } + } + ActiveRadarrBlock::FilterMovies => { + self.app.data.radarr_data.filter = + strip_non_alphanumeric_characters(&self.app.data.radarr_data.filter); + let filter_string = &self.app.data.radarr_data.filter; + let filter_matches = self + .app + .data + .radarr_data + .movies + .items + .iter() + .filter(|&movie| { + strip_non_alphanumeric_characters(&movie.title).starts_with(filter_string) + }) + .count(); + + self.app.data.radarr_data.is_searching = false; + + if filter_matches > 0 { + self.app.pop_navigation_stack(); + } + } + ActiveRadarrBlock::FilterCollections => { + self.app.data.radarr_data.filter = + strip_non_alphanumeric_characters(&self.app.data.radarr_data.filter); + let filter_string = &self.app.data.radarr_data.filter; + let filter_matches = self + .app + .data + .radarr_data + .collections + .items + .iter() + .filter(|&collection| { + strip_non_alphanumeric_characters(&collection.title).starts_with(filter_string) + }) + .count(); + + self.app.data.radarr_data.is_searching = false; + + if filter_matches > 0 { + self.app.pop_navigation_stack(); + } + } _ => (), } } fn handle_esc(&mut self) { match self.active_radarr_block { - ActiveRadarrBlock::SearchMovie => { + ActiveRadarrBlock::SearchMovie + | ActiveRadarrBlock::SearchCollection + | ActiveRadarrBlock::FilterMovies + | ActiveRadarrBlock::FilterCollections => { self.app.pop_navigation_stack(); - self.app.data.radarr_data.is_searching = false; - self.app.data.radarr_data.search = String::default(); + self.app.data.radarr_data.reset_search(); + } + _ => { + self.app.data.radarr_data.reset_search(); + handle_clear_errors(self.app); } - _ => handle_clear_errors(self.app), } } fn handle_char_key_event(&mut self) { let key = self.key; match *self.active_radarr_block { - ActiveRadarrBlock::Movies => match key { + ActiveRadarrBlock::Movies => match self.key { _ if *key == DEFAULT_KEYBINDINGS.search.key => { self .app .push_navigation_stack(ActiveRadarrBlock::SearchMovie.into()); self.app.data.radarr_data.is_searching = true; } + _ if *key == DEFAULT_KEYBINDINGS.filter.key => { + self + .app + .push_navigation_stack(ActiveRadarrBlock::FilterMovies.into()); + self.app.data.radarr_data.is_searching = true; + } _ => (), }, - ActiveRadarrBlock::SearchMovie => match key { + ActiveRadarrBlock::Collections => match self.key { + _ if *key == DEFAULT_KEYBINDINGS.search.key => { + self + .app + .push_navigation_stack(ActiveRadarrBlock::SearchCollection.into()); + self.app.data.radarr_data.is_searching = true; + } + _ if *key == DEFAULT_KEYBINDINGS.filter.key => { + self + .app + .push_navigation_stack(ActiveRadarrBlock::FilterCollections.into()); + self.app.data.radarr_data.is_searching = true; + } + _ => (), + }, + ActiveRadarrBlock::SearchMovie | ActiveRadarrBlock::SearchCollection => match self.key { _ if *key == DEFAULT_KEYBINDINGS.backspace.key => { self.app.data.radarr_data.search.pop(); } - Key::Char(character) => self.app.data.radarr_data.search.push(*character), + Key::Char(character) => { + self.app.data.radarr_data.search.push(*character); + } _ => (), }, - _ => {} + ActiveRadarrBlock::FilterMovies | ActiveRadarrBlock::FilterCollections => match self.key { + _ if *key == DEFAULT_KEYBINDINGS.backspace.key => { + self.app.data.radarr_data.filter.pop(); + } + Key::Char(character) => { + self.app.data.radarr_data.filter.push(*character); + } + _ => (), + }, + _ => (), } } } diff --git a/src/models/mod.rs b/src/models/mod.rs index 5aded92..b752e36 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -40,7 +40,7 @@ impl Default for StatefulTable { } } -impl StatefulTable { +impl StatefulTable { pub fn set_items(&mut self, items: Vec) { let items_len = items.len(); self.items = items; @@ -69,6 +69,55 @@ impl StatefulTable { pub fn select_index(&mut self, index: Option) { self.state.select(index); } + + pub fn scroll_up_with_filter(&mut self, filter: F) + where + F: FnMut(&&T) -> bool, + { + let filtered_list: Vec<&T> = self.items.iter().filter(filter).collect(); + + let element_position = filtered_list + .iter() + .position(|&item| item == self.current_selection()) + .unwrap(); + + if element_position == 0 { + let selected_index = self + .items + .iter() + .position(|item| item == filtered_list[filtered_list.len()]); + self.select_index(selected_index); + } else { + let selected_index = self + .items + .iter() + .position(|item| item == filtered_list[element_position - 1]); + self.select_index(selected_index) + } + } + + pub fn scroll_down_with_filter(&mut self, filter: F) + where + F: FnMut(&&T) -> bool, + { + let filtered_list: Vec<&T> = self.items.iter().filter(filter).collect(); + + let element_position = filtered_list + .iter() + .position(|&item| item == self.current_selection()) + .unwrap(); + + if element_position + 1 > filtered_list.len() { + let selected_index = self.items.iter().position(|item| item == filtered_list[0]); + self.select_index(selected_index); + } else { + let selected_index = self + .items + .iter() + .position(|item| item == filtered_list[element_position + 1]); + self.select_index(selected_index) + } + } } impl Scrollable for StatefulTable { diff --git a/src/models/radarr_models.rs b/src/models/radarr_models.rs index 444bf46..61c56fc 100644 --- a/src/models/radarr_models.rs +++ b/src/models/radarr_models.rs @@ -19,7 +19,7 @@ pub struct SystemStatus { pub start_time: DateTime, } -#[derive(Derivative, Deserialize, Debug, Clone)] +#[derive(Derivative, Deserialize, Debug, Clone, PartialEq, Eq)] #[derivative(Default)] #[serde(rename_all = "camelCase")] pub struct Movie { @@ -48,7 +48,7 @@ pub struct Movie { pub collection: Option, } -#[derive(Derivative, Deserialize, Debug, Clone)] +#[derive(Derivative, Deserialize, Debug, Clone, PartialEq, Eq)] #[derivative(Default)] #[serde(rename_all = "camelCase")] pub struct CollectionMovie { @@ -62,7 +62,7 @@ pub struct CollectionMovie { pub ratings: RatingsList, } -#[derive(Deserialize, Derivative, Clone, Debug)] +#[derive(Deserialize, Derivative, Clone, Debug, PartialEq, Eq)] #[derivative(Default)] #[serde(rename_all = "camelCase")] pub struct Collection { @@ -75,7 +75,7 @@ pub struct Collection { pub movies: Option>, } -#[derive(Deserialize, Derivative, Debug, Clone)] +#[derive(Deserialize, Derivative, Debug, Clone, PartialEq, Eq)] #[derivative(Default)] #[serde(rename_all = "camelCase")] pub struct MovieFile { @@ -85,7 +85,7 @@ pub struct MovieFile { pub media_info: MediaInfo, } -#[derive(Deserialize, Derivative, Debug, Clone)] +#[derive(Deserialize, Derivative, Debug, Clone, PartialEq, Eq)] #[derivative(Default)] #[serde(rename_all = "camelCase")] pub struct MediaInfo { @@ -109,7 +109,7 @@ pub struct MediaInfo { pub scan_type: String, } -#[derive(Default, Deserialize, Debug, Clone)] +#[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct RatingsList { pub imdb: Option, @@ -117,7 +117,7 @@ pub struct RatingsList { pub rotten_tomatoes: Option, } -#[derive(Derivative, Deserialize, Debug, Clone)] +#[derive(Derivative, Deserialize, Debug, Clone, PartialEq, Eq)] #[derivative(Default)] pub struct Rating { #[derivative(Default(value = "Number::from(0)"))] @@ -189,7 +189,7 @@ pub enum CreditType { Crew, } -#[derive(Deserialize, Clone, Debug)] +#[derive(Deserialize, Clone, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct Credit { pub person_name: String, diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 26206ea..9376fa5 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,3 +1,6 @@ +use std::iter::Map; +use std::slice::Iter; + use tui::backend::Backend; use tui::layout::{Alignment, Constraint, Rect}; use tui::text::{Span, Spans, Text}; @@ -198,16 +201,18 @@ pub struct TableProps<'a, T> { pub constraints: Vec, } -fn draw_table<'a, B, T, F>( +fn draw_table<'a, B, T, F, S>( f: &mut Frame<'_, B>, content_area: Rect, block: Block, table_props: TableProps<'a, T>, row_mapper: F, + filter_fn: S, is_loading: bool, ) where B: Backend, F: Fn(&T) -> Row<'a>, + S: FnMut(&&T) -> bool, { let TableProps { content, @@ -216,7 +221,7 @@ fn draw_table<'a, B, T, F>( } = table_props; if !content.items.is_empty() { - let rows = content.items.iter().map(row_mapper); + let rows = content.items.iter().filter(filter_fn).map(row_mapper); let headers = Row::new(table_headers) .style(style_default_bold()) diff --git a/src/ui/radarr_ui/collection_details_ui.rs b/src/ui/radarr_ui/collection_details_ui.rs index 903af43..5b91f89 100644 --- a/src/ui/radarr_ui/collection_details_ui.rs +++ b/src/ui/radarr_ui/collection_details_ui.rs @@ -150,6 +150,7 @@ fn draw_collection_details(f: &mut Frame<'_, B>, app: &mut App, cont ]) .style(style_primary()) }, + |_| true, app.is_loading, ); } diff --git a/src/ui/radarr_ui/mod.rs b/src/ui/radarr_ui/mod.rs index 3ae162c..74fcdd7 100644 --- a/src/ui/radarr_ui/mod.rs +++ b/src/ui/radarr_ui/mod.rs @@ -2,6 +2,7 @@ use std::iter; use std::ops::Sub; use chrono::{Duration, Utc}; +use regex::Regex; use tui::backend::Backend; use tui::layout::{Alignment, Constraint, Rect}; use tui::style::{Color, Style}; @@ -12,14 +13,15 @@ use tui::Frame; use crate::app::radarr::{ActiveRadarrBlock, RadarrData}; use crate::app::App; use crate::logos::RADARR_LOGO; -use crate::models::radarr_models::{DiskSpace, DownloadRecord, Movie}; +use crate::models::radarr_models::{Collection, DiskSpace, DownloadRecord, Movie}; use crate::models::Route; use crate::ui::radarr_ui::collection_details_ui::draw_collection_details_popup; use crate::ui::radarr_ui::movie_details_ui::draw_movie_info; use crate::ui::utils::{ - borderless_block, horizontal_chunks, layout_block_top_border, line_gauge_with_label, - line_gauge_with_title, style_bold, style_default, style_failure, style_primary, style_success, - style_warning, title_block, vertical_chunks_with_margin, + borderless_block, horizontal_chunks, layout_block, layout_block_top_border, + line_gauge_with_label, line_gauge_with_title, show_cursor, style_bold, style_default, + style_failure, style_primary, style_success, style_warning, title_block, + vertical_chunks_with_margin, }; use crate::ui::{ draw_large_popup_over, draw_popup_over, draw_table, draw_tabs, loading, TableProps, @@ -35,9 +37,20 @@ pub(super) fn draw_radarr_ui(f: &mut Frame<'_, B>, app: &mut App, ar if let Route::Radarr(active_radarr_block) = app.get_current_route().clone() { match active_radarr_block { ActiveRadarrBlock::Movies => draw_library(f, app, content_rect), - ActiveRadarrBlock::SearchMovie => { + ActiveRadarrBlock::SearchMovie | ActiveRadarrBlock::FilterMovies => { draw_popup_over(f, app, content_rect, draw_library, draw_search_box, 30, 10) } + ActiveRadarrBlock::SearchCollection | ActiveRadarrBlock::FilterCollections => { + draw_popup_over( + f, + app, + content_rect, + draw_collections, + draw_search_box, + 30, + 10, + ) + } ActiveRadarrBlock::Downloads => draw_downloads(f, app, content_rect), ActiveRadarrBlock::Collections => draw_collections(f, app, content_rect), ActiveRadarrBlock::MovieDetails @@ -71,6 +84,16 @@ pub(super) fn draw_radarr_context_row(f: &mut Frame<'_, B>, app: &Ap fn draw_library(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let quality_profile_map = &app.data.radarr_data.quality_profile_map; let downloads_vec = &app.data.radarr_data.downloads.items; + let filter_fn: Box bool> = if !app.data.radarr_data.filter.is_empty() { + Box::new(|&movie| { + Regex::new(r"[^a-zA-Z0-9\s]") + .unwrap() + .replace_all(&movie.title.to_lowercase(), "") + .starts_with(&app.data.radarr_data.filter) + }) + } else { + Box::new(|_| true) + }; draw_table( f, @@ -118,6 +141,7 @@ fn draw_library(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { ]) .style(determine_row_style(downloads_vec, movie)) }, + filter_fn, app.is_loading, ); } @@ -125,23 +149,36 @@ fn draw_library(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { fn draw_search_box(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let chunks = vertical_chunks_with_margin(vec![Constraint::Length(3)], area, 1); if !app.data.radarr_data.is_searching { - let input = Paragraph::new("Movie not found!") + let error_msg = match app.get_current_route() { + Route::Radarr(active_radarr_block) => match active_radarr_block { + ActiveRadarrBlock::SearchMovie => "Movie not found!", + ActiveRadarrBlock::SearchCollection => "Collection not found!", + ActiveRadarrBlock::FilterMovies => "No movies found matching filter!", + ActiveRadarrBlock::FilterCollections => "No collections found matching filter!", + _ => "", + }, + _ => "", + }; + + let input = Paragraph::new(error_msg) .style(style_failure()) - .block(title_block("Search")); - f.set_cursor( - chunks[0].x + app.data.radarr_data.search.len() as u16 + 1, - chunks[0].y + 1, - ); + .block(layout_block()); f.render_widget(input, chunks[0]); } else { + let block_title = match app.get_current_route() { + Route::Radarr(active_radarr_block) => match active_radarr_block { + ActiveRadarrBlock::SearchMovie | ActiveRadarrBlock::SearchCollection => "Search", + ActiveRadarrBlock::FilterMovies | ActiveRadarrBlock::FilterCollections => "Filter", + _ => "", + }, + _ => "", + }; + let input = Paragraph::new(app.data.radarr_data.search.as_ref()) .style(style_default()) - .block(title_block("Search")); - f.set_cursor( - chunks[0].x + app.data.radarr_data.search.len() as u16 + 1, - chunks[0].y + 1, - ); + .block(title_block(block_title)); + show_cursor(f, chunks[0], &app.data.radarr_data.search); f.render_widget(input, chunks[0]); } @@ -238,12 +275,23 @@ fn draw_downloads(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { ]) .style(style_primary()) }, + |_| true, app.is_loading, ); } fn draw_collections(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let quality_profile_map = &app.data.radarr_data.quality_profile_map; + let filter_fn: Box bool> = if !app.data.radarr_data.filter.is_empty() { + Box::new(|&collection| { + Regex::new(r"[^a-zA-Z0-9\s]") + .unwrap() + .replace_all(&collection.title.to_lowercase(), "") + .starts_with(&app.data.radarr_data.filter) + }) + } else { + Box::new(|_| true) + }; draw_table( f, area, @@ -276,6 +324,7 @@ fn draw_collections(f: &mut Frame<'_, B>, app: &mut App, area: Rect) ]) .style(style_primary()) }, + filter_fn, app.is_loading, ); } diff --git a/src/ui/radarr_ui/movie_details_ui.rs b/src/ui/radarr_ui/movie_details_ui.rs index a141d61..5e45606 100644 --- a/src/ui/radarr_ui/movie_details_ui.rs +++ b/src/ui/radarr_ui/movie_details_ui.rs @@ -196,6 +196,7 @@ fn draw_movie_history( ]) .style(style_success()) }, + |_| true, app.is_loading, ); } @@ -228,6 +229,7 @@ fn draw_movie_cast( ]) .style(style_success()) }, + |_| true, app.is_loading, ) } @@ -262,6 +264,7 @@ fn draw_movie_crew( ]) .style(style_success()) }, + |_| true, app.is_loading, ); } diff --git a/src/ui/utils.rs b/src/ui/utils.rs index e5c93a4..8b9e457 100644 --- a/src/ui/utils.rs +++ b/src/ui/utils.rs @@ -1,8 +1,9 @@ +use tui::backend::Backend; use tui::layout::{Constraint, Direction, Layout, Rect}; use tui::style::{Color, Modifier, Style}; -use tui::symbols; use tui::text::{Span, Spans}; use tui::widgets::{Block, Borders, LineGauge}; +use tui::{symbols, Frame}; pub fn horizontal_chunks(constraints: Vec, size: Rect) -> Vec { Layout::default() @@ -204,3 +205,7 @@ pub fn line_gauge_with_label(title: &str, ratio: f64) -> LineGauge { .ratio(ratio) .label(Spans::from(format!("{}: {:.0}%", title, ratio * 100.0))) } + +pub fn show_cursor(f: &mut Frame<'_, B>, area: Rect, string: &String) { + f.set_cursor(area.x + string.len() as u16 + 1, area.y + 1); +} diff --git a/src/utils.rs b/src/utils.rs index a5d1f1c..a482eef 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,6 +2,7 @@ use log::LevelFilter; use log4rs::append::file::FileAppender; use log4rs::config::{Appender, Root}; use log4rs::encode::pattern::PatternEncoder; +use regex::Regex; pub fn init_logging_config() -> log4rs::Config { let file_path = "/tmp/managarr.log"; @@ -30,3 +31,10 @@ pub fn convert_runtime(runtime: u64) -> (u64, u64) { (hours, minutes) } + +pub fn strip_non_alphanumeric_characters(input: &str) -> String { + Regex::new(r"[^a-zA-Z0-9\s]") + .unwrap() + .replace_all(&input.to_lowercase(), "") + .to_string() +}