diff --git a/src/app/radarr.rs b/src/app/radarr.rs index f2ac83c..a62b81c 100644 --- a/src/app/radarr.rs +++ b/src/app/radarr.rs @@ -45,6 +45,7 @@ pub struct RadarrData { pub sort_ascending: Option, pub prompt_confirm: bool, pub is_searching: bool, + pub is_filtering: bool, } impl RadarrData { @@ -61,6 +62,13 @@ impl RadarrData { self.add_searched_movies = StatefulTable::default(); } + pub fn reset_filter(&mut self) { + self.is_filtering = false; + self.filter = String::default(); + self.filtered_movies = StatefulTable::default(); + self.filtered_collections = StatefulTable::default(); + } + pub fn reset_movie_info_tabs(&mut self) { self.file_details = String::default(); self.audio_details = String::default(); @@ -115,6 +123,7 @@ impl Default for RadarrData { filter: String::default(), sort_ascending: None, is_searching: false, + is_filtering: false, prompt_confirm: false, main_tabs: TabState::new(vec![ TabRoute { @@ -241,6 +250,14 @@ pub const COLLECTION_DETAILS_BLOCKS: [ActiveRadarrBlock; 2] = [ ActiveRadarrBlock::CollectionDetails, ActiveRadarrBlock::ViewMovieOverview, ]; +pub const SEARCH_BLOCKS: [ActiveRadarrBlock; 2] = [ + ActiveRadarrBlock::SearchMovie, + ActiveRadarrBlock::SearchCollection, +]; +pub const FILTER_BLOCKS: [ActiveRadarrBlock; 2] = [ + ActiveRadarrBlock::FilterMovies, + ActiveRadarrBlock::FilterCollections, +]; impl ActiveRadarrBlock { pub fn next_add_prompt_block(&self) -> Self { @@ -439,6 +456,7 @@ pub mod radarr_test_utils { pub fn create_test_radarr_data() -> RadarrData { let mut radarr_data = RadarrData { is_searching: true, + is_filtering: true, search: "test search".to_owned(), filter: "test filter".to_owned(), file_details: "test file details".to_owned(), @@ -504,6 +522,16 @@ pub mod radarr_test_utils { }; } + #[macro_export] + macro_rules! assert_filter_reset { + ($radarr_data:expr) => { + assert!(!$radarr_data.is_filtering); + assert!($radarr_data.filter.is_empty()); + assert!($radarr_data.filtered_movies.items.is_empty()); + assert!($radarr_data.filtered_collections.items.is_empty()); + }; + } + #[macro_export] macro_rules! assert_movie_info_tabs_reset { ($radarr_data:expr) => { @@ -559,6 +587,15 @@ mod tests { assert_search_reset!(radarr_data); } + #[test] + fn test_reset_filter() { + let mut radarr_data = create_test_radarr_data(); + + radarr_data.reset_filter(); + + assert_filter_reset!(radarr_data); + } + #[test] fn test_reset_movie_info_tabs() { let mut radarr_data = create_test_radarr_data(); diff --git a/src/handlers/radarr_handlers/mod.rs b/src/handlers/radarr_handlers/mod.rs index afad449..f0c6e83 100644 --- a/src/handlers/radarr_handlers/mod.rs +++ b/src/handlers/radarr_handlers/mod.rs @@ -1,6 +1,7 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::radarr::{ - ActiveRadarrBlock, ADD_MOVIE_BLOCKS, COLLECTION_DETAILS_BLOCKS, MOVIE_DETAILS_BLOCKS, + ActiveRadarrBlock, ADD_MOVIE_BLOCKS, COLLECTION_DETAILS_BLOCKS, FILTER_BLOCKS, + MOVIE_DETAILS_BLOCKS, SEARCH_BLOCKS, }; use crate::handlers::radarr_handlers::add_movie_handler::AddMovieHandler; use crate::handlers::radarr_handlers::collection_details_handler::CollectionDetailsHandler; @@ -224,28 +225,61 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { .app .push_navigation_stack(ActiveRadarrBlock::CollectionDetails.into()), ActiveRadarrBlock::SearchMovie => { - let selected_index = self - .search_table(&self.app.data.radarr_data.movies.items.clone(), |movie| { - &movie.title - }); - self - .app - .data - .radarr_data - .movies - .select_index(selected_index); + if self.app.data.radarr_data.filtered_movies.items.is_empty() { + let selected_index = self + .search_table(&self.app.data.radarr_data.movies.items.clone(), |movie| { + &movie.title + }); + self + .app + .data + .radarr_data + .movies + .select_index(selected_index); + } else { + let selected_index = self.search_table( + &self.app.data.radarr_data.filtered_movies.items.clone(), + |movie| &movie.title, + ); + self + .app + .data + .radarr_data + .filtered_movies + .select_index(selected_index); + }; } ActiveRadarrBlock::SearchCollection => { - let selected_index = self.search_table( - &self.app.data.radarr_data.collections.items.clone(), - |collection| &collection.title, - ); - self + if self .app .data .radarr_data - .collections - .select_index(selected_index); + .filtered_collections + .items + .is_empty() + { + let selected_index = self.search_table( + &self.app.data.radarr_data.collections.items.clone(), + |collection| &collection.title, + ); + self + .app + .data + .radarr_data + .collections + .select_index(selected_index); + } else { + let selected_index = self.search_table( + &self.app.data.radarr_data.filtered_collections.items.clone(), + |collection| &collection.title, + ); + self + .app + .data + .radarr_data + .filtered_collections + .select_index(selected_index); + } } ActiveRadarrBlock::FilterMovies => { let filtered_movies = self @@ -318,10 +352,12 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { fn handle_esc(&mut self) { match self.active_radarr_block { - ActiveRadarrBlock::SearchMovie - | ActiveRadarrBlock::SearchCollection - | ActiveRadarrBlock::FilterMovies - | ActiveRadarrBlock::FilterCollections => { + _ if FILTER_BLOCKS.contains(self.active_radarr_block) => { + self.app.pop_navigation_stack(); + self.app.data.radarr_data.reset_filter(); + self.app.should_ignore_quit_key = false; + } + _ if SEARCH_BLOCKS.contains(self.active_radarr_block) => { self.app.pop_navigation_stack(); self.app.data.radarr_data.reset_search(); self.app.should_ignore_quit_key = false; @@ -336,6 +372,7 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { } _ => { self.app.data.radarr_data.reset_search(); + self.app.data.radarr_data.reset_filter(); handle_clear_errors(self.app); } } @@ -356,7 +393,7 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { self .app .push_navigation_stack(ActiveRadarrBlock::FilterMovies.into()); - self.app.data.radarr_data.is_searching = true; + self.app.data.radarr_data.is_filtering = true; self.app.should_ignore_quit_key = true; } _ if *key == DEFAULT_KEYBINDINGS.add.key => { @@ -392,7 +429,7 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { self .app .push_navigation_stack(ActiveRadarrBlock::FilterCollections.into()); - self.app.data.radarr_data.is_searching = true; + self.app.data.radarr_data.is_filtering = true; self.app.should_ignore_quit_key = true; } _ if *key == DEFAULT_KEYBINDINGS.refresh.key => { @@ -402,10 +439,10 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { } _ => (), }, - ActiveRadarrBlock::SearchMovie | ActiveRadarrBlock::SearchCollection => { + _ if SEARCH_BLOCKS.contains(self.active_radarr_block) => { handle_text_box_keys!(self, key, self.app.data.radarr_data.search) } - ActiveRadarrBlock::FilterMovies | ActiveRadarrBlock::FilterCollections => { + _ if FILTER_BLOCKS.contains(self.active_radarr_block) => { handle_text_box_keys!(self, key, self.app.data.radarr_data.filter) } _ => (), @@ -426,18 +463,18 @@ impl RadarrHandler<'_> { .drain(..) .collect::() .to_lowercase(); - let collection_index = rows.iter().position(|item| { + let search_index = rows.iter().position(|item| { strip_non_alphanumeric_characters(field_selection_fn(item)).contains(&search_string) }); self.app.data.radarr_data.is_searching = false; self.app.should_ignore_quit_key = false; - if collection_index.is_some() { + if search_index.is_some() { self.app.pop_navigation_stack(); } - collection_index + search_index } fn filter_table(&mut self, rows: &[T], field_selection_fn: F) -> Vec @@ -460,7 +497,7 @@ impl RadarrHandler<'_> { .cloned() .collect(); - self.app.data.radarr_data.is_searching = false; + self.app.data.radarr_data.is_filtering = false; self.app.should_ignore_quit_key = false; if !filter_matches.is_empty() { @@ -747,6 +784,29 @@ mod tests { ); } + #[test] + fn test_search_filtered_movies_submit() { + let mut app = App::default(); + app + .data + .radarr_data + .filtered_movies + .set_items(extended_stateful_iterable_vec!(Movie)); + app.data.radarr_data.search = "Test 2".to_owned(); + + RadarrHandler::with(&SUBMIT_KEY, &mut app, &ActiveRadarrBlock::SearchMovie).handle(); + + assert_str_eq!( + app + .data + .radarr_data + .filtered_movies + .current_selection() + .title, + "Test 2" + ); + } + #[test] fn test_search_collections_submit() { let mut app = App::default(); @@ -765,6 +825,29 @@ mod tests { ); } + #[test] + fn test_search_filtered_collections_submit() { + let mut app = App::default(); + app + .data + .radarr_data + .filtered_collections + .set_items(extended_stateful_iterable_vec!(Collection)); + app.data.radarr_data.search = "Test 2".to_owned(); + + RadarrHandler::with(&SUBMIT_KEY, &mut app, &ActiveRadarrBlock::SearchCollection).handle(); + + assert_str_eq!( + app + .data + .radarr_data + .filtered_collections + .current_selection() + .title, + "Test 2" + ); + } + #[test] fn test_filter_movies_submit() { let mut app = App::default(); @@ -892,7 +975,7 @@ mod tests { use rstest::rstest; use crate::app::radarr::radarr_test_utils::create_test_radarr_data; - use crate::assert_search_reset; + use crate::{assert_filter_reset, assert_search_reset}; use super::*; @@ -900,10 +983,8 @@ mod tests { #[rstest] #[case(ActiveRadarrBlock::Movies, ActiveRadarrBlock::SearchMovie)] - #[case(ActiveRadarrBlock::Movies, ActiveRadarrBlock::FilterMovies)] #[case(ActiveRadarrBlock::Collections, ActiveRadarrBlock::SearchCollection)] - #[case(ActiveRadarrBlock::Collections, ActiveRadarrBlock::FilterCollections)] - fn test_search_and_filter_blocks_esc( + fn test_search_blocks_esc( #[case] base_block: ActiveRadarrBlock, #[case] search_block: ActiveRadarrBlock, ) { @@ -920,6 +1001,26 @@ mod tests { assert_search_reset!(app.data.radarr_data); } + #[rstest] + #[case(ActiveRadarrBlock::Movies, ActiveRadarrBlock::FilterMovies)] + #[case(ActiveRadarrBlock::Collections, ActiveRadarrBlock::FilterCollections)] + fn test_filter_blocks_esc( + #[case] base_block: ActiveRadarrBlock, + #[case] filter_block: ActiveRadarrBlock, + ) { + let mut app = App::default(); + app.should_ignore_quit_key = true; + app.push_navigation_stack(base_block.into()); + app.push_navigation_stack(filter_block.into()); + app.data.radarr_data = create_test_radarr_data(); + + RadarrHandler::with(&ESC_KEY, &mut app, &filter_block).handle(); + + assert_eq!(app.get_current_route(), &base_block.into()); + assert!(!app.should_ignore_quit_key); + assert_filter_reset!(app.data.radarr_data); + } + #[rstest] #[case(ActiveRadarrBlock::Movies, ActiveRadarrBlock::DeleteMoviePrompt)] #[case(ActiveRadarrBlock::Movies, ActiveRadarrBlock::RefreshAllMoviesPrompt)] @@ -963,6 +1064,7 @@ mod tests { ); assert!(app.error.text.is_empty()); assert_search_reset!(app.data.radarr_data); + assert_filter_reset!(app.data.radarr_data); } } @@ -1012,7 +1114,7 @@ mod tests { .handle(); assert_eq!(app.get_current_route(), &expected_radarr_block.into()); - assert!(app.data.radarr_data.is_searching); + assert!(app.data.radarr_data.is_filtering); assert!(app.should_ignore_quit_key); } @@ -1207,7 +1309,7 @@ mod tests { assert_eq!(filter_matches.len(), 1); assert_str_eq!(filter_matches[0].title, "Test 2"); assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); - assert!(!app.data.radarr_data.is_searching); + assert!(!app.data.radarr_data.is_filtering); assert!(!app.should_ignore_quit_key); assert!(app.data.radarr_data.filter.is_empty()); } @@ -1221,7 +1323,7 @@ mod tests { .movies .set_items(extended_stateful_iterable_vec!(Movie)); app.data.radarr_data.filter = "Test 5".to_owned(); - app.data.radarr_data.is_searching = true; + app.data.radarr_data.is_filtering = true; app.should_ignore_quit_key = true; app.push_navigation_stack(ActiveRadarrBlock::FilterMovies.into()); diff --git a/src/ui/radarr_ui/mod.rs b/src/ui/radarr_ui/mod.rs index a9c12de..ff68f8e 100644 --- a/src/ui/radarr_ui/mod.rs +++ b/src/ui/radarr_ui/mod.rs @@ -10,7 +10,8 @@ use tui::widgets::{Cell, Paragraph, Row}; use tui::Frame; use crate::app::radarr::{ - ActiveRadarrBlock, RadarrData, ADD_MOVIE_BLOCKS, COLLECTION_DETAILS_BLOCKS, MOVIE_DETAILS_BLOCKS, + ActiveRadarrBlock, RadarrData, ADD_MOVIE_BLOCKS, COLLECTION_DETAILS_BLOCKS, FILTER_BLOCKS, + MOVIE_DETAILS_BLOCKS, SEARCH_BLOCKS, }; use crate::app::App; use crate::logos::RADARR_LOGO; @@ -41,20 +42,30 @@ 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() { match active_radarr_block { ActiveRadarrBlock::Movies => draw_library(f, app, content_rect), - ActiveRadarrBlock::SearchMovie | ActiveRadarrBlock::FilterMovies => { + ActiveRadarrBlock::SearchMovie => { 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::FilterMovies => { + draw_popup_over(f, app, content_rect, draw_library, draw_filter_box, 30, 10) } + ActiveRadarrBlock::SearchCollection => draw_popup_over( + f, + app, + content_rect, + draw_collections, + draw_search_box, + 30, + 10, + ), + ActiveRadarrBlock::FilterCollections => draw_popup_over( + f, + app, + content_rect, + draw_collections, + draw_filter_box, + 30, + 10, + ), ActiveRadarrBlock::Downloads => draw_downloads(f, app, content_rect), ActiveRadarrBlock::Collections => draw_collections(f, app, content_rect), _ if MOVIE_DETAILS_BLOCKS.contains(&active_radarr_block) => { @@ -121,7 +132,7 @@ 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 content = if !app.data.radarr_data.filtered_movies.items.is_empty() - && !app.data.radarr_data.is_searching + && !app.data.radarr_data.is_filtering { &mut app.data.radarr_data.filtered_movies } else { @@ -254,12 +265,49 @@ fn draw_delete_download_prompt(f: &mut Frame<'_, B>, app: &mut App, } 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); + let chunks = + vertical_chunks_with_margin(vec![Constraint::Length(3), Constraint::Min(0)], area, 1); if !app.data.radarr_data.is_searching { 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!", + _ => "", + }, + _ => "", + }; + + let input = Paragraph::new(error_msg) + .style(style_failure()) + .block(layout_block()); + + f.render_widget(input, chunks[0]); + } else { + let (block_title, block_content) = match app.get_current_route() { + Route::Radarr(active_radarr_block) => match active_radarr_block { + _ if SEARCH_BLOCKS.contains(active_radarr_block) => { + ("Search", app.data.radarr_data.search.as_str()) + } + _ => ("", ""), + }, + _ => ("", ""), + }; + + let input = Paragraph::new(block_content) + .style(style_default()) + .block(title_block_centered(block_title)); + show_cursor(f, chunks[0], block_content); + + f.render_widget(input, chunks[0]); + } +} + +fn draw_filter_box(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let chunks = + vertical_chunks_with_margin(vec![Constraint::Length(3), Constraint::Min(0)], area, 1); + if !app.data.radarr_data.is_filtering { + let error_msg = match app.get_current_route() { + Route::Radarr(active_radarr_block) => match active_radarr_block { ActiveRadarrBlock::FilterMovies => "No movies found matching filter!", ActiveRadarrBlock::FilterCollections => "No collections found matching filter!", _ => "", @@ -275,10 +323,7 @@ fn draw_search_box(f: &mut Frame<'_, B>, app: &mut App, area: Rect) } else { let (block_title, block_content) = match app.get_current_route() { Route::Radarr(active_radarr_block) => match active_radarr_block { - ActiveRadarrBlock::SearchMovie | ActiveRadarrBlock::SearchCollection => { - ("Search", app.data.radarr_data.search.as_str()) - } - ActiveRadarrBlock::FilterMovies | ActiveRadarrBlock::FilterCollections => { + _ if FILTER_BLOCKS.contains(active_radarr_block) => { ("Filter", app.data.radarr_data.filter.as_str()) } _ => ("", ""), @@ -397,7 +442,7 @@ fn draw_downloads(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { fn draw_collections(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let quality_profile_map = &app.data.radarr_data.quality_profile_map; let content = if !app.data.radarr_data.filtered_collections.items.is_empty() - && !app.data.radarr_data.is_searching + && !app.data.radarr_data.is_filtering { &mut app.data.radarr_data.filtered_collections } else {