From adda82f7f3b48e9825d5fdd209a857760e1a2865 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Sun, 11 Feb 2024 19:02:18 -0700 Subject: [PATCH] Refactored table filtering and searching so that they are now relative to the table being filtered/searched on. Also created two new widgets for error messages and popups to make life easier moving forward. Going to refactor table sorting into StatefulTable's as well so all tables can be searched, filtered, and sorted moving forwards. --- src/app/radarr/mod.rs | 25 +- src/app/radarr/radarr_tests.rs | 15 +- .../collections/collections_handler_tests.rs | 265 +++--- .../radarr_handlers/collections/mod.rs | 145 ++-- .../library/add_movie_handler.rs | 12 +- .../library/add_movie_handler_tests.rs | 34 +- .../library/library_handler_tests.rs | 252 +++--- src/handlers/radarr_handlers/library/mod.rs | 111 ++- src/handlers/radarr_handlers/mod.rs | 41 - .../radarr_handler_test_utils.rs | 2 - .../radarr_handlers/radarr_handler_tests.rs | 248 +----- src/models/mod.rs | 231 +++++- src/models/model_tests.rs | 762 +++++++++++++++++- src/models/servarr_data/radarr/modals.rs | 12 +- .../servarr_data/radarr/modals_tests.rs | 12 +- src/models/servarr_data/radarr/radarr_data.rs | 43 +- .../servarr_data/radarr/radarr_data_tests.rs | 27 +- .../servarr_data/radarr/radarr_test_utils.rs | 42 +- src/network/radarr_network.rs | 61 +- src/network/radarr_network_tests.rs | 12 +- src/ui/mod.rs | 35 - .../collections/collection_details_ui.rs | 7 +- .../collections/edit_collection_ui.rs | 46 +- src/ui/radarr_ui/collections/mod.rs | 33 +- src/ui/radarr_ui/library/add_movie_ui.rs | 31 +- src/ui/radarr_ui/library/edit_movie_ui.rs | 44 +- src/ui/radarr_ui/library/mod.rs | 33 +- src/ui/radarr_ui/system/system_details_ui.rs | 5 - src/ui/widgets/button.rs | 6 +- src/ui/widgets/checkbox.rs | 2 +- src/ui/widgets/error_message.rs | 36 + src/ui/widgets/input_box.rs | 8 +- src/ui/widgets/loading_block.rs | 6 +- src/ui/widgets/managarr_table.rs | 28 +- src/ui/widgets/mod.rs | 2 + src/ui/widgets/popup.rs | 34 + src/utils.rs | 8 - src/utils_tests.rs | 10 +- 38 files changed, 1561 insertions(+), 1165 deletions(-) create mode 100644 src/ui/widgets/error_message.rs create mode 100644 src/ui/widgets/popup.rs diff --git a/src/app/radarr/mod.rs b/src/app/radarr/mod.rs index 344bd58..1daa1af 100644 --- a/src/app/radarr/mod.rs +++ b/src/app/radarr/mod.rs @@ -182,23 +182,14 @@ impl<'a> App<'a> { } async fn populate_movie_collection_table(&mut self) { - let collection_movies = - if let Some(filtered_collections) = self.data.radarr_data.filtered_collections.as_ref() { - filtered_collections - .current_selection() - .clone() - .movies - .unwrap_or_default() - } else { - self - .data - .radarr_data - .collections - .current_selection() - .clone() - .movies - .unwrap_or_default() - }; + let collection_movies = self + .data + .radarr_data + .collections + .current_selection() + .clone() + .movies + .unwrap_or_default(); self .data .radarr_data diff --git a/src/app/radarr/radarr_tests.rs b/src/app/radarr/radarr_tests.rs index 95c06f8..8aa8431 100644 --- a/src/app/radarr/radarr_tests.rs +++ b/src/app/radarr/radarr_tests.rs @@ -8,7 +8,6 @@ mod tests { use crate::app::App; use crate::models::radarr_models::{Collection, CollectionMovie, Credit, Release}; use crate::models::servarr_data::radarr::modals::MovieDetailsModal; - use crate::models::StatefulTable; use crate::network::radarr_network::RadarrEvent; use crate::network::NetworkEvent; @@ -666,12 +665,14 @@ mod tests { #[tokio::test] async fn test_populate_movie_collection_table_filtered() { let mut app = App::default(); - let mut filtered_collections = StatefulTable::default(); - filtered_collections.set_items(vec![Collection { - movies: Some(vec![CollectionMovie::default()]), - ..Collection::default() - }]); - app.data.radarr_data.filtered_collections = Some(filtered_collections); + app + .data + .radarr_data + .collections + .set_filtered_items(vec![Collection { + movies: Some(vec![CollectionMovie::default()]), + ..Collection::default() + }]); app.populate_movie_collection_table().await; diff --git a/src/handlers/radarr_handlers/collections/collections_handler_tests.rs b/src/handlers/radarr_handlers/collections/collections_handler_tests.rs index b2b6ed7..bb192d5 100644 --- a/src/handlers/radarr_handlers/collections/collections_handler_tests.rs +++ b/src/handlers/radarr_handlers/collections/collections_handler_tests.rs @@ -19,7 +19,6 @@ mod tests { mod test_handle_scroll_up_and_down { use rstest::rstest; - use crate::models::StatefulTable; use crate::{simple_stateful_iterable_vec, test_iterable_scroll}; use super::*; @@ -34,55 +33,11 @@ mod tests { title, to_string ); - - #[rstest] - fn test_filtered_collections_scroll( - #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, - ) { - let mut app = App::default(); - let mut filtered_collections = StatefulTable::default(); - filtered_collections.set_items(simple_stateful_iterable_vec!( - Collection, - HorizontallyScrollableText - )); - app.data.radarr_data.filtered_collections = Some(filtered_collections); - - CollectionsHandler::with(&key, &mut app, &ActiveRadarrBlock::Collections, &None).handle(); - - assert_str_eq!( - app - .data - .radarr_data - .filtered_collections - .as_ref() - .unwrap() - .current_selection() - .title - .to_string(), - "Test 2" - ); - - CollectionsHandler::with(&key, &mut app, &ActiveRadarrBlock::Collections, &None).handle(); - - assert_str_eq!( - app - .data - .radarr_data - .filtered_collections - .as_ref() - .unwrap() - .current_selection() - .title - .to_string(), - "Test 1" - ); - } } mod test_handle_home_end { use pretty_assertions::assert_eq; - use crate::models::StatefulTable; use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end}; use super::*; @@ -98,63 +53,10 @@ mod tests { to_string ); - #[test] - fn test_filtered_collections_home_end() { - let mut app = App::default(); - let mut filtered_collections = StatefulTable::default(); - filtered_collections.set_items(extended_stateful_iterable_vec!( - Collection, - HorizontallyScrollableText - )); - app.data.radarr_data.filtered_collections = Some(filtered_collections); - - CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::Collections, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .filtered_collections - .as_ref() - .unwrap() - .current_selection() - .title - .to_string(), - "Test 3" - ); - - CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::Collections, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .filtered_collections - .as_ref() - .unwrap() - .current_selection() - .title - .to_string(), - "Test 1" - ); - } - #[test] fn test_collection_search_box_home_end_keys() { let mut app = App::default(); - app.data.radarr_data.search = Some("Test".into()); + app.data.radarr_data.collections.search = Some("Test".into()); CollectionsHandler::with( &DEFAULT_KEYBINDINGS.home.key, @@ -168,6 +70,7 @@ mod tests { *app .data .radarr_data + .collections .search .as_ref() .unwrap() @@ -188,6 +91,7 @@ mod tests { *app .data .radarr_data + .collections .search .as_ref() .unwrap() @@ -200,7 +104,7 @@ mod tests { #[test] fn test_collection_filter_box_home_end_keys() { let mut app = App::default(); - app.data.radarr_data.filter = Some("Test".into()); + app.data.radarr_data.collections.filter = Some("Test".into()); CollectionsHandler::with( &DEFAULT_KEYBINDINGS.home.key, @@ -214,6 +118,7 @@ mod tests { *app .data .radarr_data + .collections .filter .as_ref() .unwrap() @@ -234,6 +139,7 @@ mod tests { *app .data .radarr_data + .collections .filter .as_ref() .unwrap() @@ -326,7 +232,7 @@ mod tests { #[test] fn test_collection_search_box_left_right_keys() { let mut app = App::default(); - app.data.radarr_data.search = Some("Test".into()); + app.data.radarr_data.collections.search = Some("Test".into()); CollectionsHandler::with( &DEFAULT_KEYBINDINGS.left.key, @@ -340,6 +246,7 @@ mod tests { *app .data .radarr_data + .collections .search .as_ref() .unwrap() @@ -360,6 +267,7 @@ mod tests { *app .data .radarr_data + .collections .search .as_ref() .unwrap() @@ -372,7 +280,7 @@ mod tests { #[test] fn test_collection_filter_box_left_right_keys() { let mut app = App::default(); - app.data.radarr_data.filter = Some("Test".into()); + app.data.radarr_data.collections.filter = Some("Test".into()); CollectionsHandler::with( &DEFAULT_KEYBINDINGS.left.key, @@ -386,6 +294,7 @@ mod tests { *app .data .radarr_data + .collections .filter .as_ref() .unwrap() @@ -406,6 +315,7 @@ mod tests { *app .data .radarr_data + .collections .filter .as_ref() .unwrap() @@ -417,7 +327,6 @@ mod tests { } mod test_handle_submit { - use crate::models::StatefulTable; use pretty_assertions::assert_eq; use crate::network::radarr_network::RadarrEvent; @@ -457,7 +366,7 @@ mod tests { Collection, HorizontallyScrollableText )); - app.data.radarr_data.search = Some("Test 2".into()); + app.data.radarr_data.collections.search = Some("Test 2".into()); CollectionsHandler::with( &SUBMIT_KEY, @@ -496,7 +405,7 @@ mod tests { Collection, HorizontallyScrollableText )); - app.data.radarr_data.search = Some("Test 5".into()); + app.data.radarr_data.collections.search = Some("Test 5".into()); CollectionsHandler::with( &SUBMIT_KEY, @@ -527,13 +436,15 @@ mod tests { let mut app = App::default(); app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); app.push_navigation_stack(ActiveRadarrBlock::SearchCollection.into()); - let mut filtered_collections = StatefulTable::default(); - filtered_collections.set_items(extended_stateful_iterable_vec!( - Collection, - HorizontallyScrollableText - )); - app.data.radarr_data.filtered_collections = Some(filtered_collections); - app.data.radarr_data.search = Some("Test 2".into()); + app + .data + .radarr_data + .collections + .set_items(extended_stateful_iterable_vec!( + Collection, + HorizontallyScrollableText + )); + app.data.radarr_data.collections.search = Some("Test 2".into()); CollectionsHandler::with( &SUBMIT_KEY, @@ -547,9 +458,7 @@ mod tests { app .data .radarr_data - .filtered_collections - .as_ref() - .unwrap() + .collections .current_selection() .title .text, @@ -574,7 +483,7 @@ mod tests { Collection, HorizontallyScrollableText )); - app.data.radarr_data.filter = Some("Test".into()); + app.data.radarr_data.collections.filter = Some("Test".into()); CollectionsHandler::with( &SUBMIT_KEY, @@ -584,15 +493,16 @@ mod tests { ) .handle(); - assert!(app.data.radarr_data.filtered_collections.is_some()); + assert!(!app.should_ignore_quit_key); + assert!(app.data.radarr_data.collections.filtered_items.is_some()); assert_eq!( app .data .radarr_data - .filtered_collections + .collections + .filtered_items .as_ref() .unwrap() - .items .len(), 3 ); @@ -600,9 +510,7 @@ mod tests { app .data .radarr_data - .filtered_collections - .as_ref() - .unwrap() + .collections .current_selection() .title .text, @@ -627,7 +535,7 @@ mod tests { Collection, HorizontallyScrollableText )); - app.data.radarr_data.filter = Some("Test 5".into()); + app.data.radarr_data.collections.filter = Some("Test 5".into()); CollectionsHandler::with( &SUBMIT_KEY, @@ -637,7 +545,8 @@ mod tests { ) .handle(); - assert!(app.data.radarr_data.filtered_collections.is_none()); + assert!(!app.should_ignore_quit_key); + assert!(app.data.radarr_data.collections.filtered_items.is_none()); assert_eq!( app.get_current_route(), &ActiveRadarrBlock::FilterCollectionsError.into() @@ -695,9 +604,10 @@ mod tests { mod test_handle_esc { use pretty_assertions::assert_eq; + use ratatui::widgets::TableState; use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils::create_test_radarr_data; - use crate::{assert_filter_reset, assert_search_reset}; + use crate::models::StatefulTable; use super::*; @@ -716,6 +626,7 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); app.push_navigation_stack(active_radarr_block.into()); app.data.radarr_data = create_test_radarr_data(); + app.data.radarr_data.collections.search = Some("Test".into()); CollectionsHandler::with(&ESC_KEY, &mut app, &active_radarr_block, &None).handle(); @@ -724,7 +635,7 @@ mod tests { &ActiveRadarrBlock::Collections.into() ); assert!(!app.should_ignore_quit_key); - assert_search_reset!(app.data.radarr_data); + assert_eq!(app.data.radarr_data.collections.search, None); } #[rstest] @@ -740,6 +651,12 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); app.push_navigation_stack(active_radarr_block.into()); app.data.radarr_data = create_test_radarr_data(); + app.data.radarr_data.collections = StatefulTable { + filter: Some("Test".into()), + filtered_items: Some(Vec::new()), + filtered_state: Some(TableState::default()), + ..StatefulTable::default() + }; CollectionsHandler::with(&ESC_KEY, &mut app, &active_radarr_block, &None).handle(); @@ -748,7 +665,9 @@ mod tests { &ActiveRadarrBlock::Collections.into() ); assert!(!app.should_ignore_quit_key); - assert_filter_reset!(app.data.radarr_data); + assert_eq!(app.data.radarr_data.collections.filter, None); + assert_eq!(app.data.radarr_data.collections.filtered_items, None); + assert_eq!(app.data.radarr_data.collections.filtered_state, None); } #[test] @@ -780,6 +699,13 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); app.data.radarr_data = create_test_radarr_data(); + app.data.radarr_data.collections = StatefulTable { + search: Some("Test".into()), + filter: Some("Test".into()), + filtered_items: Some(Vec::new()), + filtered_state: Some(TableState::default()), + ..StatefulTable::default() + }; CollectionsHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::Collections, &None).handle(); @@ -788,8 +714,10 @@ mod tests { &ActiveRadarrBlock::Collections.into() ); assert!(app.error.text.is_empty()); - assert_search_reset!(app.data.radarr_data); - assert_filter_reset!(app.data.radarr_data); + assert_eq!(app.data.radarr_data.collections.search, None); + assert_eq!(app.data.radarr_data.collections.filter, None); + assert_eq!(app.data.radarr_data.collections.filtered_items, None); + assert_eq!(app.data.radarr_data.collections.filtered_state, None); } } @@ -805,6 +733,7 @@ mod tests { RadarrData, EDIT_COLLECTION_SELECTION_BLOCKS, }; + use crate::models::StatefulTable; use crate::{assert_refresh_key, test_edit_collection_key}; use super::*; @@ -825,9 +754,11 @@ mod tests { app.get_current_route(), &ActiveRadarrBlock::SearchCollection.into() ); - assert!(app.data.radarr_data.is_searching); assert!(app.should_ignore_quit_key); - assert!(app.data.radarr_data.search.is_some()); + assert_eq!( + app.data.radarr_data.collections.search, + Some(HorizontallyScrollableText::default()) + ); } #[test] @@ -846,9 +777,8 @@ mod tests { app.get_current_route(), &ActiveRadarrBlock::FilterCollections.into() ); - assert!(app.data.radarr_data.is_filtering); assert!(app.should_ignore_quit_key); - assert!(app.data.radarr_data.filter.is_some()); + assert!(app.data.radarr_data.collections.filter.is_some()); } #[test] @@ -857,6 +787,8 @@ mod tests { app.should_ignore_quit_key = true; app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); app.data.radarr_data = create_test_radarr_data(); + app.data.radarr_data.collections = StatefulTable::default(); + app.data.radarr_data.collections.filter = Some("Test".into()); CollectionsHandler::with( &DEFAULT_KEYBINDINGS.filter.key, @@ -870,10 +802,13 @@ mod tests { app.get_current_route(), &ActiveRadarrBlock::FilterCollections.into() ); - assert!(app.data.radarr_data.is_filtering); assert!(app.should_ignore_quit_key); - assert!(app.data.radarr_data.filter.is_some()); - assert!(app.data.radarr_data.filtered_collections.is_none()); + assert_eq!( + app.data.radarr_data.collections.filter, + Some(HorizontallyScrollableText::default()) + ); + assert!(app.data.radarr_data.collections.filtered_items.is_none()); + assert!(app.data.radarr_data.collections.filtered_state.is_none()); } #[test] @@ -911,7 +846,7 @@ mod tests { #[test] fn test_search_collections_box_backspace_key() { let mut app = App::default(); - app.data.radarr_data.search = Some("Test".into()); + app.data.radarr_data.collections.search = Some("Test".into()); CollectionsHandler::with( &DEFAULT_KEYBINDINGS.backspace.key, @@ -921,13 +856,24 @@ mod tests { ) .handle(); - assert_str_eq!(app.data.radarr_data.search.as_ref().unwrap().text, "Tes"); + assert_str_eq!( + app + .data + .radarr_data + .collections + .search + .as_ref() + .unwrap() + .text, + "Tes" + ); } #[test] fn test_filter_collections_box_backspace_key() { let mut app = App::default(); - app.data.radarr_data.filter = Some("Test".into()); + app.data.radarr_data.collections = StatefulTable::default(); + app.data.radarr_data.collections.filter = Some("Test".into()); CollectionsHandler::with( &DEFAULT_KEYBINDINGS.backspace.key, @@ -937,13 +883,23 @@ mod tests { ) .handle(); - assert_str_eq!(app.data.radarr_data.filter.as_ref().unwrap().text, "Tes"); + assert_str_eq!( + app + .data + .radarr_data + .collections + .filter + .as_ref() + .unwrap() + .text, + "Tes" + ); } #[test] fn test_search_collections_box_char_key() { let mut app = App::default(); - app.data.radarr_data.search = Some(HorizontallyScrollableText::default()); + app.data.radarr_data.collections.search = Some(HorizontallyScrollableText::default()); CollectionsHandler::with( &Key::Char('h'), @@ -953,13 +909,24 @@ mod tests { ) .handle(); - assert_str_eq!(app.data.radarr_data.search.as_ref().unwrap().text, "h"); + assert_str_eq!( + app + .data + .radarr_data + .collections + .search + .as_ref() + .unwrap() + .text, + "h" + ); } #[test] fn test_filter_collections_box_char_key() { let mut app = App::default(); - app.data.radarr_data.filter = Some(HorizontallyScrollableText::default()); + app.data.radarr_data.collections = StatefulTable::default(); + app.data.radarr_data.collections.filter = Some(HorizontallyScrollableText::default()); CollectionsHandler::with( &Key::Char('h'), @@ -969,7 +936,17 @@ mod tests { ) .handle(); - assert_str_eq!(app.data.radarr_data.filter.as_ref().unwrap().text, "h"); + assert_str_eq!( + app + .data + .radarr_data + .collections + .filter + .as_ref() + .unwrap() + .text, + "h" + ); } } diff --git a/src/handlers/radarr_handlers/collections/mod.rs b/src/handlers/radarr_handlers/collections/mod.rs index a9d598a..06c852c 100644 --- a/src/handlers/radarr_handlers/collections/mod.rs +++ b/src/handlers/radarr_handlers/collections/mod.rs @@ -8,10 +8,9 @@ use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, COLLECTIONS_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS, }; -use crate::models::{BlockSelectionState, HorizontallyScrollableText, Scrollable, StatefulTable}; +use crate::models::{BlockSelectionState, HorizontallyScrollableText, Scrollable}; use crate::network::radarr_network::RadarrEvent; -use crate::utils::strip_non_search_characters; -use crate::{filter_table, handle_text_box_keys, handle_text_box_left_right_keys, search_table}; +use crate::{handle_text_box_keys, handle_text_box_left_right_keys}; mod collection_details_handler; mod edit_collection_handler; @@ -68,38 +67,24 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<' fn handle_scroll_up(&mut self) { if self.active_radarr_block == &ActiveRadarrBlock::Collections { - if let Some(filtered_collections) = self.app.data.radarr_data.filtered_collections.as_mut() { - filtered_collections.scroll_up(); - } else { - self.app.data.radarr_data.collections.scroll_up() - } + self.app.data.radarr_data.collections.scroll_up() } } fn handle_scroll_down(&mut self) { if self.active_radarr_block == &ActiveRadarrBlock::Collections { - if let Some(filtered_collections) = self.app.data.radarr_data.filtered_collections.as_mut() { - filtered_collections.scroll_down(); - } else { - self.app.data.radarr_data.collections.scroll_down() - } + self.app.data.radarr_data.collections.scroll_down() } } fn handle_home(&mut self) { match self.active_radarr_block { - ActiveRadarrBlock::Collections => { - if let Some(filtered_collections) = self.app.data.radarr_data.filtered_collections.as_mut() - { - filtered_collections.scroll_to_top(); - } else { - self.app.data.radarr_data.collections.scroll_to_top() - } - } + ActiveRadarrBlock::Collections => self.app.data.radarr_data.collections.scroll_to_top(), ActiveRadarrBlock::SearchCollection => self .app .data .radarr_data + .collections .search .as_mut() .unwrap() @@ -108,6 +93,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<' .app .data .radarr_data + .collections .filter .as_mut() .unwrap() @@ -118,18 +104,12 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<' fn handle_end(&mut self) { match self.active_radarr_block { - ActiveRadarrBlock::Collections => { - if let Some(filtered_collections) = self.app.data.radarr_data.filtered_collections.as_mut() - { - filtered_collections.scroll_to_bottom(); - } else { - self.app.data.radarr_data.collections.scroll_to_bottom() - } - } + ActiveRadarrBlock::Collections => self.app.data.radarr_data.collections.scroll_to_bottom(), ActiveRadarrBlock::SearchCollection => self .app .data .radarr_data + .collections .search .as_mut() .unwrap() @@ -138,6 +118,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<' .app .data .radarr_data + .collections .filter .as_mut() .unwrap() @@ -156,14 +137,28 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<' handle_text_box_left_right_keys!( self, self.key, - self.app.data.radarr_data.search.as_mut().unwrap() + self + .app + .data + .radarr_data + .collections + .search + .as_mut() + .unwrap() ) } ActiveRadarrBlock::FilterCollections => { handle_text_box_left_right_keys!( self, self.key, - self.app.data.radarr_data.filter.as_mut().unwrap() + self + .app + .data + .radarr_data + .collections + .filter + .as_mut() + .unwrap() ) } _ => (), @@ -176,28 +171,42 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<' .app .push_navigation_stack(ActiveRadarrBlock::CollectionDetails.into()), ActiveRadarrBlock::SearchCollection => { - if self.app.data.radarr_data.filtered_collections.is_some() { - search_table!( - self.app, - filtered_collections, - ActiveRadarrBlock::SearchCollectionError, - true - ); - } else { - search_table!( - self.app, - collections, - ActiveRadarrBlock::SearchCollectionError - ); + self.app.pop_navigation_stack(); + self.app.should_ignore_quit_key = false; + + if self.app.data.radarr_data.collections.search.is_some() { + let has_match = self + .app + .data + .radarr_data + .collections + .apply_search(|collection| &collection.title.text); + + if !has_match { + self + .app + .push_navigation_stack(ActiveRadarrBlock::SearchCollectionError.into()); + } } } ActiveRadarrBlock::FilterCollections => { - filter_table!( - self.app, - collections, - filtered_collections, - ActiveRadarrBlock::FilterCollectionsError - ); + self.app.pop_navigation_stack(); + self.app.should_ignore_quit_key = false; + + if self.app.data.radarr_data.collections.filter.is_some() { + let has_matches = self + .app + .data + .radarr_data + .collections + .apply_filter(|collection| &collection.title.text); + + if !has_matches { + self + .app + .push_navigation_stack(ActiveRadarrBlock::FilterCollectionsError.into()); + } + } } ActiveRadarrBlock::UpdateAllCollectionsPrompt => { if self.app.data.radarr_data.prompt_confirm { @@ -214,12 +223,12 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<' match self.active_radarr_block { ActiveRadarrBlock::FilterCollections | ActiveRadarrBlock::FilterCollectionsError => { self.app.pop_navigation_stack(); - self.app.data.radarr_data.reset_filter(); + self.app.data.radarr_data.collections.reset_filter(); self.app.should_ignore_quit_key = false; } ActiveRadarrBlock::SearchCollection | ActiveRadarrBlock::SearchCollectionError => { self.app.pop_navigation_stack(); - self.app.data.radarr_data.reset_search(); + self.app.data.radarr_data.collections.reset_search(); self.app.should_ignore_quit_key = false; } ActiveRadarrBlock::UpdateAllCollectionsPrompt => { @@ -227,8 +236,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<' self.app.data.radarr_data.prompt_confirm = false; } _ => { - self.app.data.radarr_data.reset_search(); - self.app.data.radarr_data.reset_filter(); + self.app.data.radarr_data.collections.reset_search(); + self.app.data.radarr_data.collections.reset_filter(); handle_clear_errors(self.app); } } @@ -242,17 +251,17 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<' self .app .push_navigation_stack(ActiveRadarrBlock::SearchCollection.into()); - self.app.data.radarr_data.search = Some(HorizontallyScrollableText::default()); - self.app.data.radarr_data.is_searching = true; + self.app.data.radarr_data.collections.search = + Some(HorizontallyScrollableText::default()); self.app.should_ignore_quit_key = true; } _ if *key == DEFAULT_KEYBINDINGS.filter.key => { self .app .push_navigation_stack(ActiveRadarrBlock::FilterCollections.into()); - self.app.data.radarr_data.reset_filter(); - self.app.data.radarr_data.filter = Some(HorizontallyScrollableText::default()); - self.app.data.radarr_data.is_filtering = true; + self.app.data.radarr_data.collections.reset_filter(); + self.app.data.radarr_data.collections.filter = + Some(HorizontallyScrollableText::default()); self.app.should_ignore_quit_key = true; } _ if *key == DEFAULT_KEYBINDINGS.edit.key => { @@ -282,14 +291,28 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<' handle_text_box_keys!( self, key, - self.app.data.radarr_data.search.as_mut().unwrap() + self + .app + .data + .radarr_data + .collections + .search + .as_mut() + .unwrap() ) } ActiveRadarrBlock::FilterCollections => { handle_text_box_keys!( self, key, - self.app.data.radarr_data.filter.as_mut().unwrap() + self + .app + .data + .radarr_data + .collections + .filter + .as_mut() + .unwrap() ) } _ => (), diff --git a/src/handlers/radarr_handlers/library/add_movie_handler.rs b/src/handlers/radarr_handlers/library/add_movie_handler.rs index b9f4b92..ca85ddd 100644 --- a/src/handlers/radarr_handlers/library/add_movie_handler.rs +++ b/src/handlers/radarr_handlers/library/add_movie_handler.rs @@ -193,7 +193,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a, .app .data .radarr_data - .search + .add_movie_search .as_mut() .unwrap() .scroll_home(), @@ -260,7 +260,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a, .app .data .radarr_data - .search + .add_movie_search .as_mut() .unwrap() .reset_offset(), @@ -286,7 +286,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a, handle_text_box_left_right_keys!( self, self.key, - self.app.data.radarr_data.search.as_mut().unwrap() + self.app.data.radarr_data.add_movie_search.as_mut().unwrap() ) } ActiveRadarrBlock::AddMovieTagsInput => { @@ -314,7 +314,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a, .app .data .radarr_data - .search + .add_movie_search .as_mut() .unwrap() .text @@ -407,7 +407,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a, match self.active_radarr_block { ActiveRadarrBlock::AddMovieSearchInput => { self.app.pop_navigation_stack(); - self.app.data.radarr_data.reset_search(); + self.app.data.radarr_data.add_movie_search = None; self.app.should_ignore_quit_key = false; } ActiveRadarrBlock::AddMovieSearchResults | ActiveRadarrBlock::AddMovieEmptySearchResults => { @@ -440,7 +440,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a, handle_text_box_keys!( self, key, - self.app.data.radarr_data.search.as_mut().unwrap() + self.app.data.radarr_data.add_movie_search.as_mut().unwrap() ) } ActiveRadarrBlock::AddMovieTagsInput => { diff --git a/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs b/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs index 9ef7779..391f68b 100644 --- a/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs +++ b/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs @@ -632,7 +632,7 @@ mod tests { #[test] fn test_add_movie_search_input_home_end_keys() { let mut app = App::default(); - app.data.radarr_data.search = Some("Test".into()); + app.data.radarr_data.add_movie_search = Some("Test".into()); AddMovieHandler::with( &DEFAULT_KEYBINDINGS.home.key, @@ -646,7 +646,7 @@ mod tests { *app .data .radarr_data - .search + .add_movie_search .as_ref() .unwrap() .offset @@ -666,7 +666,7 @@ mod tests { *app .data .radarr_data - .search + .add_movie_search .as_ref() .unwrap() .offset @@ -749,7 +749,7 @@ mod tests { #[test] fn test_add_movie_search_input_left_right_keys() { let mut app = App::default(); - app.data.radarr_data.search = Some("Test".into()); + app.data.radarr_data.add_movie_search = Some("Test".into()); AddMovieHandler::with( &DEFAULT_KEYBINDINGS.left.key, @@ -763,7 +763,7 @@ mod tests { *app .data .radarr_data - .search + .add_movie_search .as_ref() .unwrap() .offset @@ -783,7 +783,7 @@ mod tests { *app .data .radarr_data - .search + .add_movie_search .as_ref() .unwrap() .offset @@ -863,7 +863,7 @@ mod tests { fn test_add_movie_search_input_submit() { let mut app = App::default(); app.should_ignore_quit_key = true; - app.data.radarr_data.search = Some("test".into()); + app.data.radarr_data.add_movie_search = Some("test".into()); AddMovieHandler::with( &SUBMIT_KEY, @@ -883,7 +883,7 @@ mod tests { #[test] fn test_add_movie_search_input_submit_noop_on_empty_search() { let mut app = App::default(); - app.data.radarr_data.search = Some(HorizontallyScrollableText::default()); + app.data.radarr_data.add_movie_search = Some(HorizontallyScrollableText::default()); app.push_navigation_stack(ActiveRadarrBlock::AddMovieSearchInput.into()); app.should_ignore_quit_key = true; @@ -1147,7 +1147,7 @@ mod tests { use crate::models::servarr_data::radarr::modals::AddMovieModal; use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils::create_test_radarr_data; use crate::models::StatefulTable; - use crate::{assert_search_reset, simple_stateful_iterable_vec}; + use crate::simple_stateful_iterable_vec; use super::*; @@ -1170,7 +1170,7 @@ mod tests { assert!(!app.should_ignore_quit_key); assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); - assert_search_reset!(app.data.radarr_data); + assert_eq!(app.data.radarr_data.add_movie_search, None); } #[test] @@ -1344,7 +1344,7 @@ mod tests { #[test] fn test_add_movie_search_input_backspace() { let mut app = App::default(); - app.data.radarr_data.search = Some("Test".into()); + app.data.radarr_data.add_movie_search = Some("Test".into()); AddMovieHandler::with( &DEFAULT_KEYBINDINGS.backspace.key, @@ -1354,7 +1354,10 @@ mod tests { ) .handle(); - assert_str_eq!(app.data.radarr_data.search.as_ref().unwrap().text, "Tes"); + assert_str_eq!( + app.data.radarr_data.add_movie_search.as_ref().unwrap().text, + "Tes" + ); } #[test] @@ -1389,7 +1392,7 @@ mod tests { #[test] fn test_add_movie_search_input_char_key() { let mut app = App::default(); - app.data.radarr_data.search = Some(HorizontallyScrollableText::default()); + app.data.radarr_data.add_movie_search = Some(HorizontallyScrollableText::default()); AddMovieHandler::with( &Key::Char('h'), @@ -1399,7 +1402,10 @@ mod tests { ) .handle(); - assert_str_eq!(app.data.radarr_data.search.as_ref().unwrap().text, "h"); + assert_str_eq!( + app.data.radarr_data.add_movie_search.as_ref().unwrap().text, + "h" + ); } #[test] diff --git a/src/handlers/radarr_handlers/library/library_handler_tests.rs b/src/handlers/radarr_handlers/library/library_handler_tests.rs index e02cc46..246612b 100644 --- a/src/handlers/radarr_handlers/library/library_handler_tests.rs +++ b/src/handlers/radarr_handlers/library/library_handler_tests.rs @@ -18,7 +18,6 @@ mod tests { use crate::test_handler_delegation; mod test_handle_scroll_up_and_down { - use crate::models::StatefulTable; use crate::{simple_stateful_iterable_vec, test_iterable_scroll}; use super::*; @@ -33,55 +32,11 @@ mod tests { title, to_string ); - - #[rstest] - fn test_filtered_movies_scroll( - #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, - ) { - let mut app = App::default(); - let mut filtered_movies = StatefulTable::default(); - filtered_movies.set_items(simple_stateful_iterable_vec!( - Movie, - HorizontallyScrollableText - )); - app.data.radarr_data.filtered_movies = Some(filtered_movies); - - LibraryHandler::with(&key, &mut app, &ActiveRadarrBlock::Movies, &None).handle(); - - assert_str_eq!( - app - .data - .radarr_data - .filtered_movies - .as_ref() - .unwrap() - .current_selection() - .title - .to_string(), - "Test 2" - ); - - LibraryHandler::with(&key, &mut app, &ActiveRadarrBlock::Movies, &None).handle(); - - assert_str_eq!( - app - .data - .radarr_data - .filtered_movies - .as_ref() - .unwrap() - .current_selection() - .title - .to_string(), - "Test 1" - ); - } } mod test_handle_home_end { use pretty_assertions::assert_eq; - use crate::models::StatefulTable; use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end}; use super::*; @@ -97,63 +52,10 @@ mod tests { to_string ); - #[test] - fn test_filtered_movies_home_end() { - let mut app = App::default(); - let mut filtered_movies = StatefulTable::default(); - filtered_movies.set_items(extended_stateful_iterable_vec!( - Movie, - HorizontallyScrollableText - )); - app.data.radarr_data.filtered_movies = Some(filtered_movies); - - LibraryHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::Movies, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .filtered_movies - .as_ref() - .unwrap() - .current_selection() - .title - .to_string(), - "Test 3" - ); - - LibraryHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::Movies, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .filtered_movies - .as_ref() - .unwrap() - .current_selection() - .title - .to_string(), - "Test 1" - ); - } - #[test] fn test_movie_search_box_home_end_keys() { let mut app = App::default(); - app.data.radarr_data.search = Some("Test".into()); + app.data.radarr_data.movies.search = Some("Test".into()); LibraryHandler::with( &DEFAULT_KEYBINDINGS.home.key, @@ -167,6 +69,7 @@ mod tests { *app .data .radarr_data + .movies .search .as_ref() .unwrap() @@ -187,6 +90,7 @@ mod tests { *app .data .radarr_data + .movies .search .as_ref() .unwrap() @@ -199,7 +103,7 @@ mod tests { #[test] fn test_movie_filter_box_home_end_keys() { let mut app = App::default(); - app.data.radarr_data.filter = Some("Test".into()); + app.data.radarr_data.movies.filter = Some("Test".into()); LibraryHandler::with( &DEFAULT_KEYBINDINGS.home.key, @@ -213,6 +117,7 @@ mod tests { *app .data .radarr_data + .movies .filter .as_ref() .unwrap() @@ -233,6 +138,7 @@ mod tests { *app .data .radarr_data + .movies .filter .as_ref() .unwrap() @@ -349,7 +255,7 @@ mod tests { #[test] fn test_movie_search_box_left_right_keys() { let mut app = App::default(); - app.data.radarr_data.search = Some("Test".into()); + app.data.radarr_data.movies.search = Some("Test".into()); LibraryHandler::with( &DEFAULT_KEYBINDINGS.left.key, @@ -363,6 +269,7 @@ mod tests { *app .data .radarr_data + .movies .search .as_ref() .unwrap() @@ -383,6 +290,7 @@ mod tests { *app .data .radarr_data + .movies .search .as_ref() .unwrap() @@ -395,7 +303,7 @@ mod tests { #[test] fn test_movie_filter_box_left_right_keys() { let mut app = App::default(); - app.data.radarr_data.filter = Some("Test".into()); + app.data.radarr_data.movies.filter = Some("Test".into()); LibraryHandler::with( &DEFAULT_KEYBINDINGS.left.key, @@ -409,6 +317,7 @@ mod tests { *app .data .radarr_data + .movies .filter .as_ref() .unwrap() @@ -429,6 +338,7 @@ mod tests { *app .data .radarr_data + .movies .filter .as_ref() .unwrap() @@ -443,7 +353,6 @@ mod tests { use pretty_assertions::assert_eq; use crate::extended_stateful_iterable_vec; - use crate::models::StatefulTable; use crate::network::radarr_network::RadarrEvent; use super::*; @@ -475,7 +384,7 @@ mod tests { Movie, HorizontallyScrollableText )); - app.data.radarr_data.search = Some("Test 2".into()); + app.data.radarr_data.movies.search = Some("Test 2".into()); LibraryHandler::with( &SUBMIT_KEY, @@ -505,7 +414,7 @@ mod tests { Movie, HorizontallyScrollableText )); - app.data.radarr_data.search = Some("Test 5".into()); + app.data.radarr_data.movies.search = Some("Test 5".into()); LibraryHandler::with( &SUBMIT_KEY, @@ -530,13 +439,15 @@ mod tests { let mut app = App::default(); app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); app.push_navigation_stack(ActiveRadarrBlock::SearchMovie.into()); - let mut filtered_movies = StatefulTable::default(); - filtered_movies.set_items(extended_stateful_iterable_vec!( - Movie, - HorizontallyScrollableText - )); - app.data.radarr_data.filtered_movies = Some(filtered_movies); - app.data.radarr_data.search = Some("Test 2".into()); + app + .data + .radarr_data + .movies + .set_filtered_items(extended_stateful_iterable_vec!( + Movie, + HorizontallyScrollableText + )); + app.data.radarr_data.movies.search = Some("Test 2".into()); LibraryHandler::with( &SUBMIT_KEY, @@ -547,15 +458,7 @@ mod tests { .handle(); assert_str_eq!( - app - .data - .radarr_data - .filtered_movies - .as_ref() - .unwrap() - .current_selection() - .title - .text, + app.data.radarr_data.movies.current_selection().title.text, "Test 2" ); assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); @@ -574,7 +477,7 @@ mod tests { Movie, HorizontallyScrollableText )); - app.data.radarr_data.filter = Some("Test".into()); + app.data.radarr_data.movies.filter = Some("Test".into()); LibraryHandler::with( &SUBMIT_KEY, @@ -584,28 +487,21 @@ mod tests { ) .handle(); - assert!(app.data.radarr_data.filtered_movies.is_some()); + assert!(app.data.radarr_data.movies.filtered_items.is_some()); + assert!(!app.should_ignore_quit_key); assert_eq!( app .data .radarr_data - .filtered_movies + .movies + .filtered_items .as_ref() .unwrap() - .items .len(), 3 ); assert_str_eq!( - app - .data - .radarr_data - .filtered_movies - .as_ref() - .unwrap() - .current_selection() - .title - .text, + app.data.radarr_data.movies.current_selection().title.text, "Test 1" ); assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); @@ -624,7 +520,7 @@ mod tests { Movie, HorizontallyScrollableText )); - app.data.radarr_data.filter = Some("Test 5".into()); + app.data.radarr_data.movies.filter = Some("Test 5".into()); LibraryHandler::with( &SUBMIT_KEY, @@ -634,7 +530,8 @@ mod tests { ) .handle(); - assert!(app.data.radarr_data.filtered_movies.is_none()); + assert!(!app.should_ignore_quit_key); + assert!(app.data.radarr_data.movies.filtered_items.is_none()); assert_eq!( app.get_current_route(), &ActiveRadarrBlock::FilterMoviesError.into() @@ -686,9 +583,10 @@ mod tests { mod test_handle_esc { use pretty_assertions::assert_eq; + use ratatui::widgets::TableState; use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils::create_test_radarr_data; - use crate::{assert_filter_reset, assert_search_reset}; + use crate::models::StatefulTable; use super::*; @@ -704,12 +602,13 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); app.push_navigation_stack(active_radarr_block.into()); app.data.radarr_data = create_test_radarr_data(); + app.data.radarr_data.movies.search = Some("Test".into()); LibraryHandler::with(&ESC_KEY, &mut app, &active_radarr_block, &None).handle(); assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); assert!(!app.should_ignore_quit_key); - assert_search_reset!(app.data.radarr_data); + assert_eq!(app.data.radarr_data.movies.search, None); } #[rstest] @@ -722,12 +621,20 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); app.push_navigation_stack(active_radarr_block.into()); app.data.radarr_data = create_test_radarr_data(); + app.data.radarr_data.movies = StatefulTable { + filter: Some("Test".into()), + filtered_items: Some(Vec::new()), + filtered_state: Some(TableState::default()), + ..StatefulTable::default() + }; LibraryHandler::with(&ESC_KEY, &mut app, &active_radarr_block, &None).handle(); assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); assert!(!app.should_ignore_quit_key); - assert_filter_reset!(app.data.radarr_data); + assert_eq!(app.data.radarr_data.movies.filter, None); + assert_eq!(app.data.radarr_data.movies.filtered_items, None); + assert_eq!(app.data.radarr_data.movies.filtered_state, None); } #[test] @@ -756,13 +663,22 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); app.data.radarr_data = create_test_radarr_data(); + app.data.radarr_data.movies = StatefulTable { + search: Some("Test".into()), + filter: Some("Test".into()), + filtered_items: Some(Vec::new()), + filtered_state: Some(TableState::default()), + ..StatefulTable::default() + }; LibraryHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::Movies, &None).handle(); assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); assert!(app.error.text.is_empty()); - assert_search_reset!(app.data.radarr_data); - assert_filter_reset!(app.data.radarr_data); + assert_eq!(app.data.radarr_data.movies.search, None); + assert_eq!(app.data.radarr_data.movies.filter, None); + assert_eq!(app.data.radarr_data.movies.filtered_items, None); + assert_eq!(app.data.radarr_data.movies.filtered_state, None); } } @@ -778,6 +694,7 @@ mod tests { RadarrData, EDIT_MOVIE_SELECTION_BLOCKS, }; + use crate::models::StatefulTable; use crate::{assert_refresh_key, test_edit_movie_key}; use super::*; @@ -798,14 +715,17 @@ mod tests { app.get_current_route(), &ActiveRadarrBlock::SearchMovie.into() ); - assert!(app.data.radarr_data.is_searching); assert!(app.should_ignore_quit_key); - assert!(app.data.radarr_data.search.is_some()); + assert_eq!( + app.data.radarr_data.movies.search, + Some(HorizontallyScrollableText::default()) + ); } #[test] fn test_filter_movies_key() { let mut app = App::default(); + app.data.radarr_data.movies = StatefulTable::default(); LibraryHandler::with( &DEFAULT_KEYBINDINGS.filter.key, @@ -819,9 +739,8 @@ mod tests { app.get_current_route(), &ActiveRadarrBlock::FilterMovies.into() ); - assert!(app.data.radarr_data.is_filtering); assert!(app.should_ignore_quit_key); - assert!(app.data.radarr_data.filter.is_some()); + assert!(app.data.radarr_data.movies.filter.is_some()); } #[test] @@ -830,6 +749,8 @@ mod tests { app.should_ignore_quit_key = true; app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); app.data.radarr_data = create_test_radarr_data(); + app.data.radarr_data.movies = StatefulTable::default(); + app.data.radarr_data.movies.filter = Some("Test".into()); LibraryHandler::with( &DEFAULT_KEYBINDINGS.filter.key, @@ -843,10 +764,13 @@ mod tests { app.get_current_route(), &ActiveRadarrBlock::FilterMovies.into() ); - assert!(app.data.radarr_data.is_filtering); assert!(app.should_ignore_quit_key); - assert!(app.data.radarr_data.filter.is_some()); - assert!(app.data.radarr_data.filtered_movies.is_none()); + assert_eq!( + app.data.radarr_data.movies.filter, + Some(HorizontallyScrollableText::default()) + ); + assert!(app.data.radarr_data.movies.filtered_items.is_none()); + assert!(app.data.radarr_data.movies.filtered_state.is_none()); } #[test] @@ -866,7 +790,7 @@ mod tests { &ActiveRadarrBlock::AddMovieSearchInput.into() ); assert!(app.should_ignore_quit_key); - assert!(app.data.radarr_data.search.is_some()); + assert!(app.data.radarr_data.add_movie_search.is_some()); } #[test] @@ -904,7 +828,7 @@ mod tests { #[test] fn test_search_movies_box_backspace_key() { let mut app = App::default(); - app.data.radarr_data.search = Some("Test".into()); + app.data.radarr_data.movies.search = Some("Test".into()); LibraryHandler::with( &DEFAULT_KEYBINDINGS.backspace.key, @@ -914,13 +838,17 @@ mod tests { ) .handle(); - assert_str_eq!(app.data.radarr_data.search.as_ref().unwrap().text, "Tes"); + assert_str_eq!( + app.data.radarr_data.movies.search.as_ref().unwrap().text, + "Tes" + ); } #[test] fn test_filter_movies_box_backspace_key() { let mut app = App::default(); - app.data.radarr_data.filter = Some("Test".into()); + app.data.radarr_data.movies = StatefulTable::default(); + app.data.radarr_data.movies.filter = Some("Test".into()); LibraryHandler::with( &DEFAULT_KEYBINDINGS.backspace.key, @@ -930,13 +858,16 @@ mod tests { ) .handle(); - assert_str_eq!(app.data.radarr_data.filter.as_ref().unwrap().text, "Tes"); + assert_str_eq!( + app.data.radarr_data.movies.filter.as_ref().unwrap().text, + "Tes" + ); } #[test] fn test_search_movies_box_char_key() { let mut app = App::default(); - app.data.radarr_data.search = Some(HorizontallyScrollableText::default()); + app.data.radarr_data.movies.search = Some(HorizontallyScrollableText::default()); LibraryHandler::with( &Key::Char('h'), @@ -946,13 +877,17 @@ mod tests { ) .handle(); - assert_str_eq!(app.data.radarr_data.search.as_ref().unwrap().text, "h"); + assert_str_eq!( + app.data.radarr_data.movies.search.as_ref().unwrap().text, + "h" + ); } #[test] fn test_filter_movies_box_char_key() { let mut app = App::default(); - app.data.radarr_data.filter = Some(HorizontallyScrollableText::default()); + app.data.radarr_data.movies = StatefulTable::default(); + app.data.radarr_data.movies.filter = Some(HorizontallyScrollableText::default()); LibraryHandler::with( &Key::Char('h'), @@ -962,7 +897,10 @@ mod tests { ) .handle(); - assert_str_eq!(app.data.radarr_data.filter.as_ref().unwrap().text, "h"); + assert_str_eq!( + app.data.radarr_data.movies.filter.as_ref().unwrap().text, + "h" + ); } } diff --git a/src/handlers/radarr_handlers/library/mod.rs b/src/handlers/radarr_handlers/library/mod.rs index b406763..1689287 100644 --- a/src/handlers/radarr_handlers/library/mod.rs +++ b/src/handlers/radarr_handlers/library/mod.rs @@ -11,10 +11,9 @@ use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, DELETE_MOVIE_SELECTION_BLOCKS, EDIT_MOVIE_SELECTION_BLOCKS, LIBRARY_BLOCKS, }; -use crate::models::{BlockSelectionState, HorizontallyScrollableText, Scrollable, StatefulTable}; +use crate::models::{BlockSelectionState, HorizontallyScrollableText, Scrollable}; use crate::network::radarr_network::RadarrEvent; -use crate::utils::strip_non_search_characters; -use crate::{filter_table, handle_text_box_keys, handle_text_box_left_right_keys, search_table}; +use crate::{handle_text_box_keys, handle_text_box_left_right_keys}; mod add_movie_handler; mod delete_movie_handler; @@ -81,38 +80,25 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, ' fn handle_scroll_up(&mut self) { if self.active_radarr_block == &ActiveRadarrBlock::Movies { - if let Some(filtered_movies) = self.app.data.radarr_data.filtered_movies.as_mut() { - filtered_movies.scroll_up(); - } else { - self.app.data.radarr_data.movies.scroll_up() - } + self.app.data.radarr_data.movies.scroll_up() } } fn handle_scroll_down(&mut self) { if self.active_radarr_block == &ActiveRadarrBlock::Movies { - if let Some(filtered_movies) = self.app.data.radarr_data.filtered_movies.as_mut() { - filtered_movies.scroll_down(); - } else { - self.app.data.radarr_data.movies.scroll_down() - } + self.app.data.radarr_data.movies.scroll_down() } } fn handle_home(&mut self) { match self.active_radarr_block { - ActiveRadarrBlock::Movies => { - if let Some(filtered_movies) = self.app.data.radarr_data.filtered_movies.as_mut() { - filtered_movies.scroll_to_top(); - } else { - self.app.data.radarr_data.movies.scroll_to_top() - } - } + ActiveRadarrBlock::Movies => self.app.data.radarr_data.movies.scroll_to_top(), ActiveRadarrBlock::SearchMovie => { self .app .data .radarr_data + .movies .search .as_mut() .unwrap() @@ -123,6 +109,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, ' .app .data .radarr_data + .movies .filter .as_mut() .unwrap() @@ -134,17 +121,12 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, ' fn handle_end(&mut self) { match self.active_radarr_block { - ActiveRadarrBlock::Movies => { - if let Some(filtered_movies) = self.app.data.radarr_data.filtered_movies.as_mut() { - filtered_movies.scroll_to_bottom(); - } else { - self.app.data.radarr_data.movies.scroll_to_bottom() - } - } + ActiveRadarrBlock::Movies => self.app.data.radarr_data.movies.scroll_to_bottom(), ActiveRadarrBlock::SearchMovie => self .app .data .radarr_data + .movies .search .as_mut() .unwrap() @@ -153,6 +135,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, ' .app .data .radarr_data + .movies .filter .as_mut() .unwrap() @@ -179,14 +162,14 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, ' handle_text_box_left_right_keys!( self, self.key, - self.app.data.radarr_data.search.as_mut().unwrap() + self.app.data.radarr_data.movies.search.as_mut().unwrap() ) } ActiveRadarrBlock::FilterMovies => { handle_text_box_left_right_keys!( self, self.key, - self.app.data.radarr_data.filter.as_mut().unwrap() + self.app.data.radarr_data.movies.filter.as_mut().unwrap() ) } _ => (), @@ -199,24 +182,42 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, ' .app .push_navigation_stack(ActiveRadarrBlock::MovieDetails.into()), ActiveRadarrBlock::SearchMovie => { - if self.app.data.radarr_data.filtered_movies.is_some() { - search_table!( - self.app, - filtered_movies, - ActiveRadarrBlock::SearchMovieError, - true - ); - } else { - search_table!(self.app, movies, ActiveRadarrBlock::SearchMovieError); + self.app.pop_navigation_stack(); + self.app.should_ignore_quit_key = false; + + if self.app.data.radarr_data.movies.search.is_some() { + let has_match = self + .app + .data + .radarr_data + .movies + .apply_search(|movie| &movie.title.text); + + if !has_match { + self + .app + .push_navigation_stack(ActiveRadarrBlock::SearchMovieError.into()); + } } } ActiveRadarrBlock::FilterMovies => { - filter_table!( - self.app, - movies, - filtered_movies, - ActiveRadarrBlock::FilterMoviesError - ); + self.app.pop_navigation_stack(); + self.app.should_ignore_quit_key = false; + + if self.app.data.radarr_data.movies.filter.is_some() { + let has_matches = self + .app + .data + .radarr_data + .movies + .apply_filter(|movie| &movie.title.text); + + if !has_matches { + self + .app + .push_navigation_stack(ActiveRadarrBlock::FilterMoviesError.into()); + } + } } ActiveRadarrBlock::UpdateAllMoviesPrompt => { if self.app.data.radarr_data.prompt_confirm { @@ -233,12 +234,12 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, ' match self.active_radarr_block { ActiveRadarrBlock::FilterMovies | ActiveRadarrBlock::FilterMoviesError => { self.app.pop_navigation_stack(); - self.app.data.radarr_data.reset_filter(); + self.app.data.radarr_data.movies.reset_filter(); self.app.should_ignore_quit_key = false; } ActiveRadarrBlock::SearchMovie | ActiveRadarrBlock::SearchMovieError => { self.app.pop_navigation_stack(); - self.app.data.radarr_data.reset_search(); + self.app.data.radarr_data.movies.reset_search(); self.app.should_ignore_quit_key = false; } ActiveRadarrBlock::UpdateAllMoviesPrompt => { @@ -246,8 +247,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, ' self.app.data.radarr_data.prompt_confirm = false; } _ => { - self.app.data.radarr_data.reset_search(); - self.app.data.radarr_data.reset_filter(); + self.app.data.radarr_data.movies.reset_search(); + self.app.data.radarr_data.movies.reset_filter(); handle_clear_errors(self.app); } } @@ -261,17 +262,15 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, ' self .app .push_navigation_stack(ActiveRadarrBlock::SearchMovie.into()); - self.app.data.radarr_data.search = Some(HorizontallyScrollableText::default()); - self.app.data.radarr_data.is_searching = true; + self.app.data.radarr_data.movies.search = Some(HorizontallyScrollableText::default()); self.app.should_ignore_quit_key = true; } _ if *key == DEFAULT_KEYBINDINGS.filter.key => { self .app .push_navigation_stack(ActiveRadarrBlock::FilterMovies.into()); - self.app.data.radarr_data.reset_filter(); - self.app.data.radarr_data.filter = Some(HorizontallyScrollableText::default()); - self.app.data.radarr_data.is_filtering = true; + self.app.data.radarr_data.movies.reset_filter(); + self.app.data.radarr_data.movies.filter = Some(HorizontallyScrollableText::default()); self.app.should_ignore_quit_key = true; } _ if *key == DEFAULT_KEYBINDINGS.edit.key => { @@ -290,7 +289,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, ' self .app .push_navigation_stack(ActiveRadarrBlock::AddMovieSearchInput.into()); - self.app.data.radarr_data.search = Some(HorizontallyScrollableText::default()); + self.app.data.radarr_data.add_movie_search = Some(HorizontallyScrollableText::default()); self.app.should_ignore_quit_key = true; } _ if *key == DEFAULT_KEYBINDINGS.update.key => { @@ -307,14 +306,14 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, ' handle_text_box_keys!( self, key, - self.app.data.radarr_data.search.as_mut().unwrap() + self.app.data.radarr_data.movies.search.as_mut().unwrap() ) } ActiveRadarrBlock::FilterMovies => { handle_text_box_keys!( self, key, - self.app.data.radarr_data.filter.as_mut().unwrap() + self.app.data.radarr_data.movies.filter.as_mut().unwrap() ) } _ => (), diff --git a/src/handlers/radarr_handlers/mod.rs b/src/handlers/radarr_handlers/mod.rs index 71d7e1d..19d05ce 100644 --- a/src/handlers/radarr_handlers/mod.rs +++ b/src/handlers/radarr_handlers/mod.rs @@ -179,44 +179,3 @@ macro_rules! search_table { } }; } - -#[macro_export] -macro_rules! filter_table { - ($app:expr, $source_table_ref:ident, $filter_table_ref:ident, $error_block:expr) => { - let empty_filter = match $app.data.radarr_data.filter.as_ref() { - Some(filter) if filter.text.is_empty() => true, - _ => false, - }; - let filter_matches = match $app.data.radarr_data.filter.as_ref() { - Some(filter) if !filter.text.is_empty() => { - let scrubbed_filter = strip_non_search_characters(&filter.text.clone()); - - $app - .data - .radarr_data - .$source_table_ref - .items - .iter() - .filter(|item| strip_non_search_characters(&item.title.text).contains(&scrubbed_filter)) - .cloned() - .collect() - } - _ => Vec::new(), - }; - - $app.data.radarr_data.filter = None; - $app.data.radarr_data.is_filtering = false; - $app.should_ignore_quit_key = false; - - if filter_matches.is_empty() && !empty_filter { - $app.pop_and_push_navigation_stack($error_block.into()); - } else if empty_filter { - $app.pop_navigation_stack(); - } else { - $app.pop_navigation_stack(); - let mut filter_table = StatefulTable::default(); - filter_table.set_items(filter_matches); - $app.data.radarr_data.$filter_table_ref = Some(filter_table); - } - }; -} diff --git a/src/handlers/radarr_handlers/radarr_handler_test_utils.rs b/src/handlers/radarr_handlers/radarr_handler_test_utils.rs index a71a3ae..88d6795 100644 --- a/src/handlers/radarr_handlers/radarr_handler_test_utils.rs +++ b/src/handlers/radarr_handlers/radarr_handler_test_utils.rs @@ -11,7 +11,6 @@ mod utils { (1111, "Any".to_owned()), ]), tags_map: BiMap::from_iter([(1, "test".to_owned())]), - filtered_movies: None, ..create_test_radarr_data() }; radarr_data.movies.set_items(vec![Movie { @@ -126,7 +125,6 @@ mod utils { (2222, "HD - 1080p".to_owned()), (1111, "Any".to_owned()), ]), - filtered_collections: None, ..create_test_radarr_data() }; radarr_data.collections.set_items(vec![Collection { diff --git a/src/handlers/radarr_handlers/radarr_handler_tests.rs b/src/handlers/radarr_handlers/radarr_handler_tests.rs index c500356..430c133 100644 --- a/src/handlers/radarr_handlers/radarr_handler_tests.rs +++ b/src/handlers/radarr_handlers/radarr_handler_tests.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod tests { - use pretty_assertions::{assert_eq, assert_str_eq}; + use pretty_assertions::assert_eq; use rstest::rstest; use strum::IntoEnumIterator; @@ -8,249 +8,8 @@ mod tests { use crate::app::App; use crate::handlers::radarr_handlers::{handle_change_tab_left_right_keys, RadarrHandler}; use crate::handlers::KeyEventHandler; - use crate::models::radarr_models::Movie; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; - use crate::models::{HorizontallyScrollableText, StatefulTable}; - use crate::utils::strip_non_search_characters; - use crate::{ - extended_stateful_iterable_vec, filter_table, search_table, test_handler_delegation, - }; - - #[test] - fn test_search_table_macro() { - let mut app = App::default(); - app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); - app.push_navigation_stack(ActiveRadarrBlock::SearchMovie.into()); - app - .data - .radarr_data - .movies - .set_items(extended_stateful_iterable_vec!( - Movie, - HorizontallyScrollableText - )); - app.data.radarr_data.search = Some("Test 2".into()); - app.data.radarr_data.is_searching = true; - app.should_ignore_quit_key = true; - - search_table!(app, movies, ActiveRadarrBlock::SearchMovieError); - - assert_str_eq!( - app.data.radarr_data.movies.current_selection().title.text, - "Test 2" - ); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); - assert!(!app.data.radarr_data.is_searching); - assert!(!app.should_ignore_quit_key); - assert!(app.data.radarr_data.search.is_none()); - } - - #[test] - fn test_search_table_macro_empty_search() { - let mut app = App::default(); - app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); - app.push_navigation_stack(ActiveRadarrBlock::SearchMovie.into()); - app - .data - .radarr_data - .movies - .set_items(extended_stateful_iterable_vec!( - Movie, - HorizontallyScrollableText - )); - app.data.radarr_data.search = Some("".into()); - app.data.radarr_data.is_searching = true; - app.should_ignore_quit_key = true; - - search_table!(app, movies, ActiveRadarrBlock::SearchMovieError); - - assert_str_eq!( - app.data.radarr_data.movies.current_selection().title.text, - "Test 1" - ); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); - assert!(!app.data.radarr_data.is_searching); - assert!(!app.should_ignore_quit_key); - assert!(app.data.radarr_data.search.is_none()); - } - - #[test] - fn test_search_table_macro_error_on_no_search_hits() { - let mut app = App::default(); - app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); - app.push_navigation_stack(ActiveRadarrBlock::SearchMovie.into()); - app - .data - .radarr_data - .movies - .set_items(extended_stateful_iterable_vec!( - Movie, - HorizontallyScrollableText - )); - app.data.radarr_data.search = Some("Test 5".into()); - app.data.radarr_data.is_searching = true; - app.should_ignore_quit_key = true; - - search_table!(app, movies, ActiveRadarrBlock::SearchMovieError); - - assert_str_eq!( - app.data.radarr_data.movies.current_selection().title.text, - "Test 1" - ); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::SearchMovieError.into() - ); - assert!(!app.data.radarr_data.is_searching); - assert!(!app.should_ignore_quit_key); - assert!(app.data.radarr_data.search.is_none()); - } - - #[test] - fn test_filter_table_macro() { - let mut app = App::default(); - app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); - app.push_navigation_stack(ActiveRadarrBlock::FilterMovies.into()); - app - .data - .radarr_data - .movies - .set_items(extended_stateful_iterable_vec!( - Movie, - HorizontallyScrollableText - )); - app.data.radarr_data.filter = Some("Test 2".into()); - app.data.radarr_data.is_filtering = true; - app.should_ignore_quit_key = true; - - filter_table!( - app, - movies, - filtered_movies, - ActiveRadarrBlock::FilterMoviesError - ); - - assert!(app.data.radarr_data.filtered_movies.is_some()); - assert_eq!( - app - .data - .radarr_data - .filtered_movies - .as_ref() - .unwrap() - .items - .len(), - 1 - ); - assert_str_eq!( - app.data.radarr_data.filtered_movies.as_ref().unwrap().items[0] - .title - .text, - "Test 2" - ); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); - assert!(!app.data.radarr_data.is_filtering); - assert!(!app.should_ignore_quit_key); - assert!(app.data.radarr_data.filter.is_none()); - } - - #[test] - fn test_filter_table_macro_reset_and_pop_on_empty_filter() { - let mut app = App::default(); - app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); - app.push_navigation_stack(ActiveRadarrBlock::FilterMovies.into()); - app - .data - .radarr_data - .movies - .set_items(extended_stateful_iterable_vec!( - Movie, - HorizontallyScrollableText - )); - app.data.radarr_data.filter = Some("".into()); - app.data.radarr_data.is_filtering = true; - app.should_ignore_quit_key = true; - - filter_table!( - app, - movies, - filtered_movies, - ActiveRadarrBlock::FilterMoviesError - ); - - assert!(app.data.radarr_data.filtered_movies.is_none()); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); - assert!(!app.data.radarr_data.is_filtering); - assert!(!app.should_ignore_quit_key); - assert!(app.data.radarr_data.filter.is_none()); - } - - #[test] - fn test_filter_table_error_on_no_filter_matches() { - let mut app = App::default(); - app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); - app.push_navigation_stack(ActiveRadarrBlock::FilterMovies.into()); - app - .data - .radarr_data - .movies - .set_items(extended_stateful_iterable_vec!( - Movie, - HorizontallyScrollableText - )); - app.data.radarr_data.filter = Some("Test 5".into()); - app.data.radarr_data.is_filtering = true; - app.should_ignore_quit_key = true; - - filter_table!( - app, - movies, - filtered_movies, - ActiveRadarrBlock::FilterMoviesError - ); - - assert!(app.data.radarr_data.filtered_movies.is_none()); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::FilterMoviesError.into() - ); - assert!(!app.data.radarr_data.is_filtering); - assert!(!app.should_ignore_quit_key); - assert!(app.data.radarr_data.filter.is_none()); - } - - #[test] - fn test_filter_table_macro_error_on_none_filter() { - let mut app = App::default(); - app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); - app.push_navigation_stack(ActiveRadarrBlock::FilterMovies.into()); - app - .data - .radarr_data - .movies - .set_items(extended_stateful_iterable_vec!( - Movie, - HorizontallyScrollableText - )); - app.data.radarr_data.is_filtering = true; - app.should_ignore_quit_key = true; - - filter_table!( - app, - movies, - filtered_movies, - ActiveRadarrBlock::FilterMoviesError - ); - - assert!(app.data.radarr_data.filtered_movies.is_none()); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::FilterMoviesError.into() - ); - assert!(!app.data.radarr_data.is_filtering); - assert!(!app.should_ignore_quit_key); - assert!(app.data.radarr_data.filter.is_none()); - } + use crate::test_handler_delegation; #[rstest] #[case(0, ActiveRadarrBlock::System, ActiveRadarrBlock::Downloads)] @@ -375,9 +134,6 @@ mod tests { #[rstest] fn test_delegates_indexers_blocks_to_indexers_handler( - // Add these once implemented: - // ActiveRadarrBlock::AddIndexer, - // ActiveRadarrBlock::EditIndexer, #[values( ActiveRadarrBlock::DeleteIndexerPrompt, ActiveRadarrBlock::Indexers, diff --git a/src/models/mod.rs b/src/models/mod.rs index 0ec09d5..811554d 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -3,47 +3,51 @@ use std::fmt::{Debug, Display, Formatter}; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use ratatui::widgets::{ListState, TableState}; +use regex::Regex; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use serde_json::Number; pub mod radarr_models; pub mod servarr_data; -#[cfg(test)] -#[path = "model_tests.rs"] -mod model_tests; - -// Allowing dead code for now since we'll eventually be implementing additional Servarr support and we'll need it then -#[allow(dead_code)] -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Route { - Radarr(ActiveRadarrBlock, Option), - Sonarr, - Readarr, - Lidarr, - Whisparr, - Bazarr, - Prowlarr, - Tautulli, -} - -pub trait Scrollable { - fn scroll_down(&mut self); - fn scroll_up(&mut self); - fn scroll_to_top(&mut self); - fn scroll_to_bottom(&mut self); -} - macro_rules! stateful_iterable { ($name:ident, $state:ty) => { #[derive(Default)] pub struct $name { pub state: $state, pub items: Vec, + pub filter: Option, + pub search: Option, + pub filtered_items: Option>, + pub filtered_state: Option<$state>, } impl Scrollable for $name { fn scroll_down(&mut self) { + if let Some(filtered_items) = self.filtered_items.as_ref() { + if filtered_items.is_empty() { + return; + } + + let selected_row = match self.filtered_state.as_ref().unwrap().selected() { + Some(i) => { + if i >= filtered_items.len() - 1 { + 0 + } else { + i + 1 + } + } + None => 0, + }; + + self + .filtered_state + .as_mut() + .unwrap() + .select(Some(selected_row)); + return; + } + if self.items.is_empty() { return; } @@ -63,6 +67,30 @@ macro_rules! stateful_iterable { } fn scroll_up(&mut self) { + if let Some(filtered_items) = self.filtered_items.as_ref() { + if filtered_items.is_empty() { + return; + } + + let selected_row = match self.filtered_state.as_ref().unwrap().selected() { + Some(i) => { + if i == 0 { + filtered_items.len() - 1 + } else { + i - 1 + } + } + None => 0, + }; + + self + .filtered_state + .as_mut() + .unwrap() + .select(Some(selected_row)); + return; + } + if self.items.is_empty() { return; } @@ -82,6 +110,15 @@ macro_rules! stateful_iterable { } fn scroll_to_top(&mut self) { + if let Some(filtered_items) = self.filtered_items.as_ref() { + if filtered_items.is_empty() { + return; + } + + self.filtered_state.as_mut().unwrap().select(Some(0)); + return; + } + if self.items.is_empty() { return; } @@ -90,6 +127,19 @@ macro_rules! stateful_iterable { } fn scroll_to_bottom(&mut self) { + if let Some(filtered_items) = self.filtered_items.as_ref() { + if filtered_items.is_empty() { + return; + } + + self + .filtered_state + .as_mut() + .unwrap() + .select(Some(filtered_items.len() - 1)); + return; + } + if self.items.is_empty() { return; } @@ -98,6 +148,7 @@ macro_rules! stateful_iterable { } } + #[allow(dead_code)] impl $name where T: Clone + PartialEq + Eq + Debug, @@ -119,25 +170,128 @@ macro_rules! stateful_iterable { } } + pub fn set_filtered_items(&mut self, filtered_items: Vec) { + self.filtered_items = Some(filtered_items); + let mut filtered_state: $state = Default::default(); + filtered_state.select(Some(0)); + self.filtered_state = Some(filtered_state); + } + + pub fn select_index(&mut self, index: Option) { + if let Some(filtered_state) = &mut self.filtered_state { + filtered_state.select(index); + } else { + self.state.select(index); + } + } + pub fn current_selection(&self) -> &T { - &self.items[self.state.selected().unwrap_or(0)] + if let Some(filtered_items) = &self.filtered_items { + &filtered_items[self + .filtered_state + .as_ref() + .unwrap() + .selected() + .unwrap_or(0)] + } else { + &self.items[self.state.selected().unwrap_or(0)] + } + } + + pub fn apply_filter(&mut self, filter_field: fn(&T) -> &str) -> bool { + let filter_matches = match self.filter { + Some(ref filter) if !filter.text.is_empty() => { + let scrubbed_filter = strip_non_search_characters(&filter.text.clone()); + + self + .items + .iter() + .filter(|item| { + strip_non_search_characters(filter_field(&item)).contains(&scrubbed_filter) + }) + .cloned() + .collect() + } + _ => Vec::new(), + }; + + self.filter = None; + + if filter_matches.is_empty() { + return false; + } + + self.set_filtered_items(filter_matches); + return true; + } + + pub fn reset_filter(&mut self) { + self.filter = None; + self.filtered_items = None; + self.filtered_state = None; + } + + pub fn apply_search(&mut self, search_field: fn(&T) -> &str) -> bool { + let search_index = if let Some(search) = self.search.as_ref() { + let search_string = search.text.clone().to_lowercase(); + + self + .filtered_items + .as_ref() + .unwrap_or(&self.items) + .iter() + .position(|item| { + strip_non_search_characters(search_field(&item)).contains(&search_string) + }) + } else { + None + }; + + self.search = None; + + if search_index.is_none() { + return false; + } + + self.select_index(search_index); + return true; + } + + pub fn reset_search(&mut self) { + self.search = None; } } }; } +#[cfg(test)] +#[path = "model_tests.rs"] +mod model_tests; + +// Allowing dead code for now since we'll eventually be implementing additional Servarr support, and we'll need it then +#[allow(dead_code)] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Route { + Radarr(ActiveRadarrBlock, Option), + Sonarr, + Readarr, + Lidarr, + Whisparr, + Bazarr, + Prowlarr, + Tautulli, +} + +pub trait Scrollable { + fn scroll_down(&mut self); + fn scroll_up(&mut self); + fn scroll_to_top(&mut self); + fn scroll_to_bottom(&mut self); +} + stateful_iterable!(StatefulList, ListState); stateful_iterable!(StatefulTable, TableState); -impl StatefulTable -where - T: Clone + PartialEq + Eq + Debug, -{ - pub fn select_index(&mut self, index: Option) { - self.state.select(index); - } -} - #[derive(Default)] pub struct ScrollableText { pub items: Vec, @@ -429,3 +583,10 @@ where "Unable to convert Number to i64: {num:?}" ))) } + +pub fn strip_non_search_characters(input: &str) -> String { + Regex::new(r"[^a-zA-Z0-9.,/'\-:\s]") + .unwrap() + .replace_all(&input.to_lowercase(), "") + .to_string() +} diff --git a/src/models/model_tests.rs b/src/models/model_tests.rs index 77538a5..dcaaf2d 100644 --- a/src/models/model_tests.rs +++ b/src/models/model_tests.rs @@ -3,14 +3,15 @@ mod tests { use std::cell::RefCell; use pretty_assertions::{assert_eq, assert_str_eq}; + use ratatui::widgets::{ListState, TableState}; use serde::de::value::Error as ValueError; use serde::de::value::F64Deserializer; use serde::de::value::I64Deserializer; use serde::de::IntoDeserializer; use serde_json::to_string; - use crate::models::from_i64; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; + use crate::models::{from_i64, strip_non_search_characters}; use crate::models::{ BlockSelectionState, HorizontallyScrollableText, Scrollable, ScrollableText, StatefulList, StatefulTable, TabRoute, TabState, @@ -46,6 +47,59 @@ mod tests { stateful_table.scroll_to_bottom(); } + #[test] + fn test_stateful_table_filtered_scrolling_on_empty_table_performs_no_op() { + let mut filtered_stateful_table: StatefulTable = StatefulTable { + filtered_items: Some(Vec::new()), + filtered_state: Some(TableState::default()), + ..StatefulTable::default() + }; + + assert_eq!( + filtered_stateful_table + .filtered_state + .as_ref() + .unwrap() + .selected(), + None + ); + + filtered_stateful_table.scroll_up(); + + assert_eq!( + filtered_stateful_table + .filtered_state + .as_ref() + .unwrap() + .selected(), + None + ); + + filtered_stateful_table.scroll_down(); + + assert_eq!( + filtered_stateful_table + .filtered_state + .as_ref() + .unwrap() + .selected(), + None + ); + + filtered_stateful_table.scroll_to_top(); + + assert_eq!( + filtered_stateful_table + .filtered_state + .as_ref() + .unwrap() + .selected(), + None + ); + + filtered_stateful_table.scroll_to_bottom(); + } + #[test] fn test_stateful_table_scroll() { let mut stateful_table = create_test_stateful_table(); @@ -77,6 +131,86 @@ mod tests { assert_eq!(stateful_table.state.selected(), Some(0)); } + #[test] + fn test_stateful_table_filtered_items_scroll() { + let mut filtered_stateful_table = create_test_filtered_stateful_table(); + + assert_eq!( + filtered_stateful_table + .filtered_state + .as_ref() + .unwrap() + .selected(), + Some(0) + ); + + filtered_stateful_table.scroll_down(); + + assert_eq!( + filtered_stateful_table + .filtered_state + .as_ref() + .unwrap() + .selected(), + Some(1) + ); + + filtered_stateful_table.scroll_down(); + + assert_eq!( + filtered_stateful_table + .filtered_state + .as_ref() + .unwrap() + .selected(), + Some(0) + ); + + filtered_stateful_table.scroll_up(); + + assert_eq!( + filtered_stateful_table + .filtered_state + .as_ref() + .unwrap() + .selected(), + Some(1) + ); + + filtered_stateful_table.scroll_up(); + + assert_eq!( + filtered_stateful_table + .filtered_state + .as_ref() + .unwrap() + .selected(), + Some(0) + ); + + filtered_stateful_table.scroll_to_bottom(); + + assert_eq!( + filtered_stateful_table + .filtered_state + .as_ref() + .unwrap() + .selected(), + Some(1) + ); + + filtered_stateful_table.scroll_to_top(); + + assert_eq!( + filtered_stateful_table + .filtered_state + .as_ref() + .unwrap() + .selected(), + Some(0) + ); + } + #[test] fn test_stateful_table_set_items() { let items_vec = vec!["Test 1", "Test 2", "Test 3"]; @@ -97,6 +231,27 @@ mod tests { assert_eq!(stateful_table.state.selected(), Some(2)); } + #[test] + fn test_stateful_table_set_filtered_items() { + let filtered_items_vec = vec!["Test 1", "Test 2", "Test 3"]; + let mut filtered_stateful_table: StatefulTable<&str> = StatefulTable::default(); + + filtered_stateful_table.set_filtered_items(filtered_items_vec.clone()); + + assert_eq!( + filtered_stateful_table + .filtered_state + .as_ref() + .unwrap() + .selected(), + Some(0) + ); + assert_eq!( + filtered_stateful_table.filtered_items, + Some(filtered_items_vec.clone()) + ); + } + #[test] fn test_stateful_table_current_selection() { let mut stateful_table = create_test_stateful_table(); @@ -108,6 +263,27 @@ mod tests { assert_str_eq!(stateful_table.current_selection(), &stateful_table.items[1]); } + #[test] + fn test_filtered_stateful_table_current_selection() { + let mut filtered_stateful_table = create_test_filtered_stateful_table(); + + assert_str_eq!( + filtered_stateful_table.current_selection(), + &filtered_stateful_table.filtered_items.as_ref().unwrap()[0] + ); + + filtered_stateful_table + .filtered_state + .as_mut() + .unwrap() + .select(Some(1)); + + assert_str_eq!( + filtered_stateful_table.current_selection(), + &filtered_stateful_table.filtered_items.as_ref().unwrap()[1] + ); + } + #[test] fn test_stateful_table_select_index() { let mut stateful_table = create_test_stateful_table(); @@ -123,6 +299,42 @@ mod tests { assert_eq!(stateful_table.state.selected(), None); } + #[test] + fn test_filtered_stateful_table_select_index() { + let mut filtered_stateful_table = create_test_filtered_stateful_table(); + + assert_eq!( + filtered_stateful_table + .filtered_state + .as_ref() + .unwrap() + .selected(), + Some(0) + ); + + filtered_stateful_table.select_index(Some(1)); + + assert_eq!( + filtered_stateful_table + .filtered_state + .as_ref() + .unwrap() + .selected(), + Some(1) + ); + + filtered_stateful_table.select_index(None); + + assert_eq!( + filtered_stateful_table + .filtered_state + .as_ref() + .unwrap() + .selected(), + None + ); + } + #[test] fn test_stateful_table_scroll_up() { let mut stateful_table = create_test_stateful_table(); @@ -139,7 +351,150 @@ mod tests { } #[test] - fn test_stateful_list_scrolling_on_empty_table_performs_no_op() { + fn test_filtered_stateful_table_scroll_up() { + let mut filtered_stateful_table = create_test_filtered_stateful_table(); + + assert_eq!( + filtered_stateful_table + .filtered_state + .as_ref() + .unwrap() + .selected(), + Some(0) + ); + + filtered_stateful_table.scroll_up(); + + assert_eq!( + filtered_stateful_table + .filtered_state + .as_ref() + .unwrap() + .selected(), + Some(1) + ); + + filtered_stateful_table.scroll_up(); + + assert_eq!( + filtered_stateful_table + .filtered_state + .as_ref() + .unwrap() + .selected(), + Some(0) + ); + } + + #[test] + fn test_stateful_table_apply_filter() { + let mut stateful_table: StatefulTable<&str> = StatefulTable::default(); + stateful_table.set_items(vec!["this", "is", "a", "test"]); + stateful_table.filter = Some("i".into()); + let expected_items = vec!["this", "is"]; + let mut expected_state = TableState::default(); + expected_state.select(Some(0)); + + let has_matches = stateful_table.apply_filter(|&item| item); + + assert_eq!(stateful_table.filter, None); + assert_eq!(stateful_table.filtered_items, Some(expected_items)); + assert_eq!(stateful_table.filtered_state, Some(expected_state)); + assert!(has_matches); + } + + #[test] + fn test_stateful_table_apply_filter_no_matches() { + let mut stateful_table: StatefulTable<&str> = StatefulTable::default(); + stateful_table.set_items(vec!["this", "is", "a", "test"]); + stateful_table.filter = Some("z".into()); + + let has_matches = stateful_table.apply_filter(|&item| item); + + assert_eq!(stateful_table.filter, None); + assert_eq!(stateful_table.filtered_items, None); + assert_eq!(stateful_table.filtered_state, None); + assert!(!has_matches); + } + + #[test] + fn test_stateful_table_reset_filter() { + let mut stateful_table = create_test_filtered_stateful_table(); + stateful_table.reset_filter(); + + assert_eq!(stateful_table.filter, None); + assert_eq!(stateful_table.filtered_items, None); + assert_eq!(stateful_table.filtered_state, None); + } + + #[test] + fn test_stateful_table_apply_search() { + let mut stateful_table: StatefulTable<&str> = StatefulTable::default(); + stateful_table.set_items(vec!["this", "is", "a", "test"]); + stateful_table.search = Some("test".into()); + let mut expected_state = TableState::default(); + expected_state.select(Some(3)); + + let has_match = stateful_table.apply_search(|&item| item); + + assert_eq!(stateful_table.search, None); + assert_eq!(stateful_table.state, expected_state); + assert!(has_match); + } + + #[test] + fn test_stateful_table_apply_search_no_match() { + let mut stateful_table: StatefulTable<&str> = StatefulTable::default(); + stateful_table.set_items(vec!["this", "is", "a", "test"]); + stateful_table.search = Some("shi-mon-a!".into()); + + let has_match = stateful_table.apply_search(|&item| item); + + assert_eq!(stateful_table.search, None); + assert!(!has_match); + } + + #[test] + fn test_filtered_stateful_table_apply_search() { + let mut stateful_table: StatefulTable<&str> = StatefulTable::default(); + stateful_table.set_filtered_items(vec!["this", "is", "a", "test"]); + stateful_table.search = Some("test".into()); + let mut expected_state = TableState::default(); + expected_state.select(Some(3)); + + let has_match = stateful_table.apply_search(|&item| item); + + assert_eq!(stateful_table.search, None); + assert_eq!(stateful_table.filtered_state, Some(expected_state)); + assert!(has_match); + } + + #[test] + fn test_filtered_stateful_table_apply_search_no_match() { + let mut stateful_table: StatefulTable<&str> = StatefulTable::default(); + stateful_table.set_filtered_items(vec!["this", "is", "a", "test"]); + stateful_table.search = Some("shi-mon-a!".into()); + let mut expected_state = TableState::default(); + expected_state.select(Some(0)); + + let has_match = stateful_table.apply_search(|&item| item); + + assert_eq!(stateful_table.search, None); + assert_eq!(stateful_table.filtered_state, Some(expected_state)); + assert!(!has_match); + } + + #[test] + fn test_stateful_table_reset_search() { + let mut stateful_table = create_test_stateful_table(); + stateful_table.search = Some("test".into()); + stateful_table.reset_search(); + + assert_eq!(stateful_table.search, None); + } + + #[test] + fn test_stateful_list_scrolling_on_empty_list_performs_no_op() { let mut stateful_list: StatefulList = StatefulList::default(); assert_eq!(stateful_list.state.selected(), None); @@ -159,6 +514,59 @@ mod tests { stateful_list.scroll_to_bottom(); } + #[test] + fn test_filtered_stateful_list_scrolling_on_empty_list_performs_no_op() { + let mut filtered_stateful_list: StatefulList = StatefulList { + filtered_items: Some(Vec::new()), + filtered_state: Some(ListState::default()), + ..StatefulList::default() + }; + + assert_eq!( + filtered_stateful_list + .filtered_state + .as_ref() + .unwrap() + .selected(), + None + ); + + filtered_stateful_list.scroll_up(); + + assert_eq!( + filtered_stateful_list + .filtered_state + .as_ref() + .unwrap() + .selected(), + None + ); + + filtered_stateful_list.scroll_down(); + + assert_eq!( + filtered_stateful_list + .filtered_state + .as_ref() + .unwrap() + .selected(), + None + ); + + filtered_stateful_list.scroll_to_top(); + + assert_eq!( + filtered_stateful_list + .filtered_state + .as_ref() + .unwrap() + .selected(), + None + ); + + filtered_stateful_list.scroll_to_bottom(); + } + #[test] fn test_stateful_list_scroll() { let mut stateful_list = create_test_stateful_list(); @@ -190,6 +598,86 @@ mod tests { assert_eq!(stateful_list.state.selected(), Some(0)); } + #[test] + fn test_filtered_stateful_list_scroll() { + let mut filtered_stateful_list = create_test_filtered_stateful_list(); + + assert_eq!( + filtered_stateful_list + .filtered_state + .as_ref() + .unwrap() + .selected(), + Some(0) + ); + + filtered_stateful_list.scroll_down(); + + assert_eq!( + filtered_stateful_list + .filtered_state + .as_ref() + .unwrap() + .selected(), + Some(1) + ); + + filtered_stateful_list.scroll_down(); + + assert_eq!( + filtered_stateful_list + .filtered_state + .as_ref() + .unwrap() + .selected(), + Some(0) + ); + + filtered_stateful_list.scroll_up(); + + assert_eq!( + filtered_stateful_list + .filtered_state + .as_ref() + .unwrap() + .selected(), + Some(1) + ); + + filtered_stateful_list.scroll_up(); + + assert_eq!( + filtered_stateful_list + .filtered_state + .as_ref() + .unwrap() + .selected(), + Some(0) + ); + + filtered_stateful_list.scroll_to_bottom(); + + assert_eq!( + filtered_stateful_list + .filtered_state + .as_ref() + .unwrap() + .selected(), + Some(1) + ); + + filtered_stateful_list.scroll_to_top(); + + assert_eq!( + filtered_stateful_list + .filtered_state + .as_ref() + .unwrap() + .selected(), + Some(0) + ); + } + #[test] fn test_stateful_list_set_items() { let items_vec = vec!["Test 1", "Test 2", "Test 3"]; @@ -210,6 +698,27 @@ mod tests { assert_eq!(stateful_list.state.selected(), Some(2)); } + #[test] + fn test_stateful_list_set_filtered_items() { + let filtered_items_vec = vec!["Test 1", "Test 2", "Test 3"]; + let mut filtered_stateful_list: StatefulList<&str> = StatefulList::default(); + + filtered_stateful_list.set_filtered_items(filtered_items_vec.clone()); + + assert_eq!( + filtered_stateful_list + .filtered_state + .as_ref() + .unwrap() + .selected(), + Some(0) + ); + assert_eq!( + filtered_stateful_list.filtered_items, + Some(filtered_items_vec.clone()) + ); + } + #[test] fn test_stateful_list_current_selection() { let mut stateful_list = create_test_stateful_list(); @@ -221,19 +730,234 @@ mod tests { assert_str_eq!(stateful_list.current_selection(), &stateful_list.items[1]); } + #[test] + fn test_filtered_stateful_list_current_selection() { + let mut filtered_stateful_list = create_test_filtered_stateful_list(); + + assert_str_eq!( + filtered_stateful_list.current_selection(), + &filtered_stateful_list.filtered_items.as_ref().unwrap()[0] + ); + + filtered_stateful_list + .filtered_state + .as_mut() + .unwrap() + .select(Some(1)); + + assert_str_eq!( + filtered_stateful_list.current_selection(), + &filtered_stateful_list.filtered_items.as_ref().unwrap()[1] + ); + } + + #[test] + fn test_stateful_list_select_index() { + let mut stateful_list = create_test_stateful_list(); + + assert_eq!(stateful_list.state.selected(), Some(0)); + + stateful_list.select_index(Some(1)); + + assert_eq!(stateful_list.state.selected(), Some(1)); + + stateful_list.select_index(None); + + assert_eq!(stateful_list.state.selected(), None); + } + + #[test] + fn test_filtered_stateful_list_select_index() { + let mut filtered_stateful_list = create_test_filtered_stateful_list(); + + assert_eq!( + filtered_stateful_list + .filtered_state + .as_ref() + .unwrap() + .selected(), + Some(0) + ); + + filtered_stateful_list.select_index(Some(1)); + + assert_eq!( + filtered_stateful_list + .filtered_state + .as_ref() + .unwrap() + .selected(), + Some(1) + ); + + filtered_stateful_list.select_index(None); + + assert_eq!( + filtered_stateful_list + .filtered_state + .as_ref() + .unwrap() + .selected(), + None + ); + } + #[test] fn test_stateful_list_scroll_up() { - let mut stateful_table = create_test_stateful_table(); + let mut stateful_list = create_test_stateful_list(); - assert_eq!(stateful_table.state.selected(), Some(0)); + assert_eq!(stateful_list.state.selected(), Some(0)); - stateful_table.scroll_up(); + stateful_list.scroll_up(); - assert_eq!(stateful_table.state.selected(), Some(1)); + assert_eq!(stateful_list.state.selected(), Some(1)); - stateful_table.scroll_up(); + stateful_list.scroll_up(); - assert_eq!(stateful_table.state.selected(), Some(0)); + assert_eq!(stateful_list.state.selected(), Some(0)); + } + + #[test] + fn test_filtered_stateful_list_scroll_up() { + let mut filtered_stateful_list = create_test_filtered_stateful_list(); + + assert_eq!( + filtered_stateful_list + .filtered_state + .as_ref() + .unwrap() + .selected(), + Some(0) + ); + + filtered_stateful_list.scroll_up(); + + assert_eq!( + filtered_stateful_list + .filtered_state + .as_ref() + .unwrap() + .selected(), + Some(1) + ); + + filtered_stateful_list.scroll_up(); + + assert_eq!( + filtered_stateful_list + .filtered_state + .as_ref() + .unwrap() + .selected(), + Some(0) + ); + } + + #[test] + fn test_stateful_list_apply_filter() { + let mut stateful_list: StatefulList<&str> = StatefulList::default(); + stateful_list.set_items(vec!["this", "is", "a", "test"]); + stateful_list.filter = Some("i".into()); + let expected_items = vec!["this", "is"]; + let mut expected_state = ListState::default(); + expected_state.select(Some(0)); + + let has_matches = stateful_list.apply_filter(|&item| item); + + assert_eq!(stateful_list.filter, None); + assert_eq!(stateful_list.filtered_items, Some(expected_items)); + assert_eq!(stateful_list.filtered_state, Some(expected_state)); + assert!(has_matches); + } + + #[test] + fn test_stateful_list_apply_filter_no_matches() { + let mut stateful_list: StatefulList<&str> = StatefulList::default(); + stateful_list.set_items(vec!["this", "is", "a", "test"]); + stateful_list.filter = Some("z".into()); + + let has_matches = stateful_list.apply_filter(|&item| item); + + assert_eq!(stateful_list.filter, None); + assert_eq!(stateful_list.filtered_items, None); + assert_eq!(stateful_list.filtered_state, None); + assert!(!has_matches); + } + + #[test] + fn test_stateful_list_reset_filter() { + let mut stateful_list = create_test_filtered_stateful_list(); + stateful_list.reset_filter(); + + assert_eq!(stateful_list.filter, None); + assert_eq!(stateful_list.filtered_items, None); + assert_eq!(stateful_list.filtered_state, None); + } + + #[test] + fn test_stateful_list_apply_search() { + let mut stateful_list: StatefulList<&str> = StatefulList::default(); + stateful_list.set_items(vec!["this", "is", "a", "test"]); + stateful_list.search = Some("test".into()); + let mut expected_state = ListState::default(); + expected_state.select(Some(3)); + + let has_match = stateful_list.apply_search(|&item| item); + + assert_eq!(stateful_list.search, None); + assert_eq!(stateful_list.state, expected_state); + assert!(has_match); + } + + #[test] + fn test_stateful_list_apply_search_no_match() { + let mut stateful_list: StatefulList<&str> = StatefulList::default(); + stateful_list.set_items(vec!["this", "is", "a", "test"]); + stateful_list.search = Some("shi-mon-a!".into()); + + let has_match = stateful_list.apply_search(|&item| item); + + assert_eq!(stateful_list.search, None); + assert!(!has_match); + } + + #[test] + fn test_filtered_stateful_list_apply_search() { + let mut stateful_list: StatefulList<&str> = StatefulList::default(); + stateful_list.set_filtered_items(vec!["this", "is", "a", "test"]); + stateful_list.search = Some("test".into()); + let mut expected_state = ListState::default(); + expected_state.select(Some(3)); + + let has_match = stateful_list.apply_search(|&item| item); + + assert_eq!(stateful_list.search, None); + assert_eq!(stateful_list.filtered_state, Some(expected_state)); + assert!(has_match); + } + + #[test] + fn test_filtered_stateful_list_apply_search_no_match() { + let mut stateful_list: StatefulList<&str> = StatefulList::default(); + stateful_list.set_filtered_items(vec!["this", "is", "a", "test"]); + stateful_list.search = Some("shi-mon-a!".into()); + let mut expected_state = ListState::default(); + expected_state.select(Some(0)); + + let has_match = stateful_list.apply_search(|&item| item); + + assert_eq!(stateful_list.search, None); + assert_eq!(stateful_list.filtered_state, Some(expected_state)); + assert!(!has_match); + } + + #[test] + fn test_stateful_list_reset_search() { + let mut stateful_list = create_test_stateful_list(); + stateful_list.search = Some("test".into()); + stateful_list.reset_search(); + + assert_eq!(stateful_list.search, None); } #[test] @@ -771,10 +1495,32 @@ mod tests { stateful_table } + fn create_test_filtered_stateful_table() -> StatefulTable<&'static str> { + let mut stateful_table = StatefulTable::default(); + stateful_table.set_filtered_items(vec!["Test 1", "Test 2"]); + + stateful_table + } + fn create_test_stateful_list() -> StatefulList<&'static str> { let mut stateful_list = StatefulList::default(); stateful_list.set_items(vec!["Test 1", "Test 2"]); stateful_list } + + fn create_test_filtered_stateful_list() -> StatefulList<&'static str> { + let mut stateful_list = StatefulList::default(); + stateful_list.set_filtered_items(vec!["Test 1", "Test 2"]); + + stateful_list + } + + #[test] + fn test_strip_non_alphanumeric_characters() { + assert_eq!( + strip_non_search_characters("Te$t S7r!ng::'~-@_`,(.)/*}^&%#+="), + "tet s7rng::'-,./".to_owned() + ) + } } diff --git a/src/models/servarr_data/radarr/modals.rs b/src/models/servarr_data/radarr/modals.rs index a093d84..fb44826 100644 --- a/src/models/servarr_data/radarr/modals.rs +++ b/src/models/servarr_data/radarr/modals.rs @@ -133,11 +133,7 @@ impl From<&RadarrData<'_>> for EditMovieModal { minimum_availability, quality_profile_id, .. - } = if let Some(filtered_movies) = radarr_data.filtered_movies.as_ref() { - filtered_movies.current_selection() - } else { - radarr_data.movies.current_selection() - }; + } = radarr_data.movies.current_selection(); edit_movie_modal .minimum_availability_list @@ -249,11 +245,7 @@ impl From<&RadarrData<'_>> for EditCollectionModal { minimum_availability, quality_profile_id, .. - } = if let Some(filtered_collections) = radarr_data.filtered_collections.as_ref() { - filtered_collections.current_selection() - } else { - radarr_data.collections.current_selection() - }; + } = radarr_data.collections.current_selection(); edit_collection_modal.path = root_folder_path.clone().unwrap_or_default().into(); edit_collection_modal.monitored = Some(*monitored); diff --git a/src/models/servarr_data/radarr/modals_tests.rs b/src/models/servarr_data/radarr/modals_tests.rs index 26861aa..03dc309 100644 --- a/src/models/servarr_data/radarr/modals_tests.rs +++ b/src/models/servarr_data/radarr/modals_tests.rs @@ -117,7 +117,7 @@ mod test { (1111, "Any".to_owned()), ]), tags_map: BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]), - filtered_movies: None, + movies: StatefulTable::default(), ..create_test_radarr_data() }; let movie = Movie { @@ -130,9 +130,7 @@ mod test { }; if test_filtered_movies { - let mut filtered_movies = StatefulTable::default(); - filtered_movies.set_items(vec![movie]); - radarr_data.filtered_movies = Some(filtered_movies); + radarr_data.movies.set_filtered_items(vec![movie]); } else { radarr_data.movies.set_items(vec![movie]); } @@ -209,7 +207,7 @@ mod test { (2222, "HD - 1080p".to_owned()), (1111, "Any".to_owned()), ]), - filtered_collections: None, + collections: StatefulTable::default(), ..create_test_radarr_data() }; let collection = Collection { @@ -222,9 +220,7 @@ mod test { }; if test_filtered_collections { - let mut filtered_collections = StatefulTable::default(); - filtered_collections.set_items(vec![collection]); - radarr_data.filtered_collections = Some(filtered_collections); + radarr_data.collections.set_filtered_items(vec![collection]); } else { radarr_data.collections.set_items(vec![collection]); } diff --git a/src/models/servarr_data/radarr/radarr_data.rs b/src/models/servarr_data/radarr/radarr_data.rs index f33f56b..592d249 100644 --- a/src/models/servarr_data/radarr/radarr_data.rs +++ b/src/models/servarr_data/radarr/radarr_data.rs @@ -50,16 +50,13 @@ pub struct RadarrData<'a> { pub updates: ScrollableText, pub main_tabs: TabState, pub movie_info_tabs: TabState, - pub search: Option, - pub filter: Option, + pub add_movie_search: Option, pub add_movie_modal: Option, pub add_searched_movies: Option>, pub edit_movie_modal: Option, pub edit_collection_modal: Option, pub edit_indexer_modal: Option, pub edit_root_folder: Option, - pub filtered_collections: Option>, - pub filtered_movies: Option>, pub indexer_settings: Option, pub indexer_test_all_results: Option>, pub movie_details_modal: Option, @@ -67,8 +64,6 @@ pub struct RadarrData<'a> { pub prompt_confirm_action: Option, pub delete_movie_files: bool, pub add_list_exclusion: bool, - pub is_searching: bool, - pub is_filtering: bool, } impl<'a> RadarrData<'a> { @@ -77,22 +72,6 @@ impl<'a> RadarrData<'a> { self.add_list_exclusion = false; } - pub fn reset_search(&mut self) { - self.is_searching = false; - self.search = None; - self.filter = None; - self.filtered_movies = None; - self.filtered_collections = None; - self.add_searched_movies = None; - } - - pub fn reset_filter(&mut self) { - self.is_filtering = false; - self.filter = None; - self.filtered_movies = None; - self.filtered_collections = None; - } - pub fn reset_movie_info_tabs(&mut self) { self.movie_details_modal = None; self.movie_info_tabs.index = 0; @@ -119,21 +98,16 @@ impl<'a> Default for RadarrData<'a> { tasks: StatefulTable::default(), queued_events: StatefulTable::default(), updates: ScrollableText::default(), - search: None, - filter: None, + add_movie_search: None, add_movie_modal: None, add_searched_movies: None, edit_movie_modal: None, edit_collection_modal: None, edit_indexer_modal: None, edit_root_folder: None, - filtered_collections: None, - filtered_movies: None, indexer_settings: None, indexer_test_all_results: None, movie_details_modal: None, - is_searching: false, - is_filtering: false, prompt_confirm: false, prompt_confirm_action: None, delete_movie_files: false, @@ -505,16 +479,3 @@ impl From<(ActiveRadarrBlock, Option)> for Route { Route::Radarr(value.0, value.1) } } - -#[allow(dead_code)] // Returning to this work tomorrow -pub struct EditIndexerSettings { - pub allow_hardcoded_subs: bool, - pub availability_delay: HorizontallyScrollableText, - pub id: HorizontallyScrollableText, - pub maximum_size: HorizontallyScrollableText, - pub minimum_age: HorizontallyScrollableText, - pub prefer_indexer_flags: bool, - pub retention: HorizontallyScrollableText, - pub rss_sync_interval: HorizontallyScrollableText, - pub whitelisted_hardcoded_subs: HorizontallyScrollableText, -} diff --git a/src/models/servarr_data/radarr/radarr_data_tests.rs b/src/models/servarr_data/radarr/radarr_data_tests.rs index 5269296..6f33cde 100644 --- a/src/models/servarr_data/radarr/radarr_data_tests.rs +++ b/src/models/servarr_data/radarr/radarr_data_tests.rs @@ -16,8 +16,8 @@ mod tests { use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData}; use crate::models::Route; + use crate::assert_movie_info_tabs_reset; use crate::models::BlockSelectionState; - use crate::{assert_filter_reset, assert_movie_info_tabs_reset, assert_search_reset}; #[test] fn test_from_tuple_to_route_with_context() { @@ -43,24 +43,6 @@ mod tests { assert!(!radarr_data.add_list_exclusion); } - #[test] - fn test_reset_search() { - let mut radarr_data = utils::create_test_radarr_data(); - - radarr_data.reset_search(); - - assert_search_reset!(radarr_data); - } - - #[test] - fn test_reset_filter() { - let mut radarr_data = utils::create_test_radarr_data(); - - radarr_data.reset_filter(); - - assert_filter_reset!(radarr_data); - } - #[test] fn test_reset_movie_info_tabs() { let mut radarr_data = utils::create_test_radarr_data(); @@ -91,21 +73,16 @@ mod tests { assert!(radarr_data.tasks.items.is_empty()); assert!(radarr_data.queued_events.items.is_empty()); assert!(radarr_data.updates.get_text().is_empty()); - assert!(radarr_data.search.is_none()); - assert!(radarr_data.filter.is_none()); + assert!(radarr_data.add_movie_search.is_none()); assert!(radarr_data.add_movie_modal.is_none()); assert!(radarr_data.add_searched_movies.is_none()); assert!(radarr_data.edit_movie_modal.is_none()); assert!(radarr_data.edit_collection_modal.is_none()); assert!(radarr_data.edit_root_folder.is_none()); assert!(radarr_data.edit_indexer_modal.is_none()); - assert!(radarr_data.filtered_collections.is_none()); - assert!(radarr_data.filtered_movies.is_none()); assert!(radarr_data.indexer_settings.is_none()); assert!(radarr_data.indexer_test_all_results.is_none()); assert!(radarr_data.movie_details_modal.is_none()); - assert!(!radarr_data.is_searching); - assert!(!radarr_data.is_filtering); assert!(radarr_data.prompt_confirm_action.is_none()); assert!(!radarr_data.prompt_confirm); assert!(!radarr_data.delete_movie_files); diff --git a/src/models/servarr_data/radarr/radarr_test_utils.rs b/src/models/servarr_data/radarr/radarr_test_utils.rs index 585bd28..c555308 100644 --- a/src/models/servarr_data/radarr/radarr_test_utils.rs +++ b/src/models/servarr_data/radarr/radarr_test_utils.rs @@ -1,8 +1,7 @@ #[cfg(test)] pub mod utils { use crate::models::radarr_models::{ - AddMovieSearchResult, Collection, CollectionMovie, Credit, Movie, MovieHistoryItem, Release, - ReleaseField, + AddMovieSearchResult, CollectionMovie, Credit, MovieHistoryItem, Release, ReleaseField, }; use crate::models::servarr_data::radarr::modals::MovieDetailsModal; use crate::models::servarr_data::radarr::radarr_data::RadarrData; @@ -31,30 +30,15 @@ pub mod utils { movie_details_modal.sort_ascending = Some(true); let mut radarr_data = RadarrData { - is_searching: true, - is_filtering: true, delete_movie_files: true, add_list_exclusion: true, - search: Some("test search".into()), - filter: Some("test filter".into()), + add_movie_search: Some("test search".into()), edit_root_folder: Some("test path".into()), movie_details_modal: Some(movie_details_modal), - filtered_movies: Some(StatefulTable::default()), - filtered_collections: Some(StatefulTable::default()), add_searched_movies: Some(StatefulTable::default()), ..RadarrData::default() }; radarr_data.movie_info_tabs.index = 1; - radarr_data - .filtered_movies - .as_mut() - .unwrap() - .set_items(vec![Movie::default()]); - radarr_data - .filtered_collections - .as_mut() - .unwrap() - .set_items(vec![Collection::default()]); radarr_data .add_searched_movies .as_mut() @@ -70,28 +54,6 @@ pub mod utils { radarr_data } - #[macro_export] - macro_rules! assert_search_reset { - ($radarr_data:expr) => { - assert!(!$radarr_data.is_searching); - assert!($radarr_data.search.is_none()); - assert!($radarr_data.filter.is_none()); - assert!($radarr_data.filtered_movies.is_none()); - assert!($radarr_data.filtered_collections.is_none()); - assert!($radarr_data.add_searched_movies.is_none()); - }; - } - - #[macro_export] - macro_rules! assert_filter_reset { - ($radarr_data:expr) => { - assert!(!$radarr_data.is_filtering); - assert!($radarr_data.filter.is_none()); - assert!($radarr_data.filtered_movies.is_none()); - assert!($radarr_data.filtered_collections.is_none()); - }; - } - #[macro_export] macro_rules! assert_movie_info_tabs_reset { ($radarr_data:expr) => { diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 9cef674..224cae3 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -1433,7 +1433,7 @@ impl<'a, 'b> Network<'a, 'b> { .await .data .radarr_data - .search + .add_movie_search .clone() .ok_or(anyhow!("Encountered a race condition")); @@ -1711,65 +1711,22 @@ impl<'a, 'b> Network<'a, 'b> { async fn extract_movie_id(&mut self) -> (i64, i64) { let app = self.app.lock().await; - if app.data.radarr_data.filtered_movies.is_some() { - ( - app - .data - .radarr_data - .filtered_movies - .as_ref() - .unwrap() - .current_selection() - .id, - app - .data - .radarr_data - .filtered_movies - .as_ref() - .unwrap() - .current_selection() - .tmdb_id, - ) - } else { - ( - app.data.radarr_data.movies.current_selection().id, - app.data.radarr_data.movies.current_selection().tmdb_id, - ) - } + ( + app.data.radarr_data.movies.current_selection().id, + app.data.radarr_data.movies.current_selection().tmdb_id, + ) } async fn extract_collection_id(&mut self) -> i64 { - if self + self .app .lock() .await .data .radarr_data - .filtered_collections - .is_some() - { - self - .app - .lock() - .await - .data - .radarr_data - .filtered_collections - .as_ref() - .unwrap() - .current_selection() - .id - } else { - self - .app - .lock() - .await - .data - .radarr_data - .collections - .current_selection() - .id - } + .collections + .current_selection() + .id } async fn append_movie_id_param(&mut self, resource: &str) -> String { diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index 91c955c..3163496 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -454,7 +454,7 @@ mod test { &resource, ) .await; - app_arc.lock().await.data.radarr_data.search = Some("test term".into()); + app_arc.lock().await.data.radarr_data.add_movie_search = Some("test term".into()); let mut network = Network::new(&app_arc, CancellationToken::new()); network @@ -520,7 +520,7 @@ mod test { ); let (async_server, app_arc, _server) = mock_radarr_api(RequestMethod::Get, None, Some(json!([])), None, &resource).await; - app_arc.lock().await.data.radarr_data.search = Some("test term".into()); + app_arc.lock().await.data.radarr_data.add_movie_search = Some("test term".into()); let mut network = Network::new(&app_arc, CancellationToken::new()); network @@ -2639,12 +2639,12 @@ mod test { async fn test_extract_movie_id_filtered_movies() { let app_arc = Arc::new(Mutex::new(App::default())); let mut filtered_movies = StatefulTable::default(); - filtered_movies.set_items(vec![Movie { + filtered_movies.set_filtered_items(vec![Movie { id: 1, tmdb_id: 2, ..Movie::default() }]); - app_arc.lock().await.data.radarr_data.filtered_movies = Some(filtered_movies); + app_arc.lock().await.data.radarr_data.movies = filtered_movies; let mut network = Network::new(&app_arc, CancellationToken::new()); assert_eq!(network.extract_movie_id().await, (1, 2)); @@ -2672,11 +2672,11 @@ mod test { async fn test_extract_collection_id_filtered_collection() { let app_arc = Arc::new(Mutex::new(App::default())); let mut filtered_collections = StatefulTable::default(); - filtered_collections.set_items(vec![Collection { + filtered_collections.set_filtered_items(vec![Collection { id: 1, ..Collection::default() }]); - app_arc.lock().await.data.radarr_data.filtered_collections = Some(filtered_collections); + app_arc.lock().await.data.radarr_data.collections = filtered_collections; let mut network = Network::new(&app_arc, CancellationToken::new()); assert_eq!(network.extract_collection_id().await, 1); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 6d39e0e..2b5e304 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -220,32 +220,6 @@ pub fn draw_drop_down_popup( draw_popup_over(f, app, area, background_fn, drop_down_fn, 20, 30); } -pub fn draw_error_popup_over( - f: &mut Frame<'_>, - app: &mut App<'_>, - area: Rect, - message: &str, - background_fn: fn(&mut Frame<'_>, &mut App<'_>, Rect), -) { - background_fn(f, app, area); - draw_error_popup(f, message); -} - -pub fn draw_error_popup(f: &mut Frame<'_>, message: &str) { - let prompt_area = centered_rect(25, 8, f.size()); - f.render_widget(Clear, prompt_area); - f.render_widget(background_block(), prompt_area); - - let error_message = Paragraph::new(Text::from(message)) - .block(title_block_centered("Error").failure()) - .failure() - .bold() - .wrap(Wrap { trim: false }) - .alignment(Alignment::Center); - - f.render_widget(error_message, prompt_area); -} - fn draw_tabs(f: &mut Frame<'_>, area: Rect, title: &str, tab_state: &TabState) -> Rect { f.render_widget(title_block(title), area); @@ -485,12 +459,3 @@ pub fn draw_input_box_popup( .block(borderless_block()); f.render_widget(help, help_area); } - -pub fn draw_error_message_popup(f: &mut Frame<'_>, area: Rect, error_msg: &str) { - let input = Paragraph::new(error_msg) - .failure() - .alignment(Alignment::Center) - .block(layout_block()); - - f.render_widget(input, area); -} diff --git a/src/ui/radarr_ui/collections/collection_details_ui.rs b/src/ui/radarr_ui/collections/collection_details_ui.rs index 9343d8f..623fcf4 100644 --- a/src/ui/radarr_ui/collections/collection_details_ui.rs +++ b/src/ui/radarr_ui/collections/collection_details_ui.rs @@ -72,12 +72,7 @@ pub fn draw_collection_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) Layout::vertical([Constraint::Percentage(25), Constraint::Fill(0)]) .margin(1) .areas(area); - let collection_selection = - if let Some(filtered_collections) = app.data.radarr_data.filtered_collections.as_ref() { - filtered_collections.current_selection() - } else { - app.data.radarr_data.collections.current_selection() - }; + let collection_selection = app.data.radarr_data.collections.current_selection(); let quality_profile = app .data .radarr_data diff --git a/src/ui/radarr_ui/collections/edit_collection_ui.rs b/src/ui/radarr_ui/collections/edit_collection_ui.rs index 8c888ed..116ae55 100644 --- a/src/ui/radarr_ui/collections/edit_collection_ui.rs +++ b/src/ui/radarr_ui/collections/edit_collection_ui.rs @@ -89,36 +89,22 @@ impl DrawUi for EditCollectionUi { } fn draw_edit_collection_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - let (collection_title, collection_overview) = - if let Some(filtered_collections) = app.data.radarr_data.filtered_collections.as_ref() { - ( - filtered_collections.current_selection().title.text.clone(), - filtered_collections - .current_selection() - .overview - .clone() - .unwrap_or_default(), - ) - } else { - ( - app - .data - .radarr_data - .collections - .current_selection() - .title - .text - .clone(), - app - .data - .radarr_data - .collections - .current_selection() - .overview - .clone() - .unwrap_or_default(), - ) - }; + let collection_title = app + .data + .radarr_data + .collections + .current_selection() + .title + .text + .clone(); + let collection_overview = app + .data + .radarr_data + .collections + .current_selection() + .overview + .clone() + .unwrap_or_default(); let title = format!("Edit - {collection_title}"); let yes_no_value = app.data.radarr_data.prompt_confirm; let selected_block = app.data.radarr_data.selected_block.get_active_block(); diff --git a/src/ui/radarr_ui/collections/mod.rs b/src/ui/radarr_ui/collections/mod.rs index 0f4fc77..244a449 100644 --- a/src/ui/radarr_ui/collections/mod.rs +++ b/src/ui/radarr_ui/collections/mod.rs @@ -12,10 +12,10 @@ use crate::ui::radarr_ui::collections::collection_details_ui::CollectionDetailsU use crate::ui::radarr_ui::collections::edit_collection_ui::EditCollectionUi; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; +use crate::ui::widgets::error_message::ErrorMessage; use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::{ - draw_error_message_popup, draw_input_box_popup, draw_popup_over, draw_prompt_box, - draw_prompt_popup_over, DrawUi, + draw_input_box_popup, draw_popup_over, draw_prompt_box, draw_prompt_popup_over, DrawUi, }; mod collection_details_ui; @@ -101,19 +101,13 @@ impl DrawUi for CollectionsUi { } pub(super) fn draw_collections(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - let current_selection = - if let Some(filtered_collections) = app.data.radarr_data.filtered_collections.as_ref() { - filtered_collections.current_selection().clone() - } else if !app.data.radarr_data.collections.items.is_empty() { - app.data.radarr_data.collections.current_selection().clone() - } else { - Collection::default() - }; - let quality_profile_map = &app.data.radarr_data.quality_profile_map; - let content = match app.data.radarr_data.filtered_collections.as_mut() { - Some(filtered_collections) if !app.data.radarr_data.is_filtering => Some(filtered_collections), - _ => Some(&mut app.data.radarr_data.collections), + let current_selection = if !app.data.radarr_data.collections.items.is_empty() { + app.data.radarr_data.collections.current_selection().clone() + } else { + Collection::default() }; + let quality_profile_map = &app.data.radarr_data.quality_profile_map; + let content = Some(&mut app.data.radarr_data.collections); let collections_table_footer = app .data .radarr_data @@ -187,7 +181,7 @@ fn draw_collection_search_box(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) f, area, "Search", - app.data.radarr_data.search.as_ref().unwrap(), + app.data.radarr_data.collections.search.as_ref().unwrap(), ); } @@ -196,14 +190,17 @@ fn draw_filter_collections_box(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) f, area, "Filter", - app.data.radarr_data.filter.as_ref().unwrap(), + app.data.radarr_data.collections.filter.as_ref().unwrap(), ) } fn draw_search_collection_error_box(f: &mut Frame<'_>, _: &mut App<'_>, area: Rect) { - draw_error_message_popup(f, area, "Collection not found!"); + f.render_widget(ErrorMessage::new("Collection not found!"), area); } fn draw_filter_collections_error_box(f: &mut Frame<'_>, _: &mut App<'_>, area: Rect) { - draw_error_message_popup(f, area, "No collections found matching the given filter!"); + f.render_widget( + ErrorMessage::new("No collections found matching the given filter!"), + area, + ); } diff --git a/src/ui/radarr_ui/library/add_movie_ui.rs b/src/ui/radarr_ui/library/add_movie_ui.rs index a063a43..5d470ea 100644 --- a/src/ui/radarr_ui/library/add_movie_ui.rs +++ b/src/ui/radarr_ui/library/add_movie_ui.rs @@ -17,11 +17,12 @@ use crate::ui::utils::{ title_block_centered, }; use crate::ui::widgets::button::Button; +use crate::ui::widgets::error_message::ErrorMessage; use crate::ui::widgets::input_box::InputBox; use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::popup::Popup; use crate::ui::{ - draw_drop_down_popup, draw_error_popup, draw_error_popup_over, draw_large_popup_over, - draw_medium_popup_over, draw_selectable_list, DrawUi, + draw_drop_down_popup, draw_large_popup_over, draw_medium_popup_over, draw_selectable_list, DrawUi, }; use crate::utils::convert_runtime; use crate::{render_selectable_input_box, App}; @@ -68,13 +69,17 @@ impl DrawUi for AddMovieUi { draw_medium_popup_over(f, app, area, draw_add_movie_search, draw_confirmation_popup); } } - ActiveRadarrBlock::AddMovieAlreadyInLibrary => draw_error_popup_over( - f, - app, - area, - "This film is already in your library", - draw_add_movie_search, - ), + ActiveRadarrBlock::AddMovieAlreadyInLibrary => { + draw_add_movie_search(f, app, area); + f.render_widget( + Popup::new( + ErrorMessage::new("This film is already in your library"), + 25, + 8, + ), + f.size(), + ); + } _ => (), }; @@ -108,11 +113,11 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ]) .margin(1) .areas(area); - let block_content = &app.data.radarr_data.search.as_ref().unwrap().text; + let block_content = &app.data.radarr_data.add_movie_search.as_ref().unwrap().text; let offset = *app .data .radarr_data - .search + .add_movie_search .as_ref() .unwrap() .offset @@ -197,9 +202,11 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let help_paragraph = Paragraph::new(help_text) .block(borderless_block()) .alignment(Alignment::Center); + let error_message = ErrorMessage::new("No movies found matching your query!"); + let error_message_popup = Popup::new(error_message, 25, 8); f.render_widget(layout_block(), results_area); - draw_error_popup(f, "No movies found matching your query!"); + f.render_widget(error_message_popup, f.size()); f.render_widget(help_paragraph, help_area); } ActiveRadarrBlock::AddMovieSearchResults diff --git a/src/ui/radarr_ui/library/edit_movie_ui.rs b/src/ui/radarr_ui/library/edit_movie_ui.rs index 7cfc458..c77d667 100644 --- a/src/ui/radarr_ui/library/edit_movie_ui.rs +++ b/src/ui/radarr_ui/library/edit_movie_ui.rs @@ -19,8 +19,8 @@ use crate::ui::widgets::button::Button; use crate::ui::widgets::checkbox::Checkbox; use crate::ui::widgets::input_box::InputBox; use crate::ui::{ - draw_drop_down_popup, draw_large_popup_over_background_fn_with_ui, - draw_medium_popup_over, draw_popup, draw_selectable_list, DrawUi, + draw_drop_down_popup, draw_large_popup_over_background_fn_with_ui, draw_medium_popup_over, + draw_popup, draw_selectable_list, DrawUi, }; #[cfg(test)] @@ -91,31 +91,21 @@ impl DrawUi for EditMovieUi { } fn draw_edit_movie_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - let (movie_title, movie_overview) = - if let Some(filtered_movies) = app.data.radarr_data.filtered_movies.as_ref() { - ( - filtered_movies.current_selection().title.text.clone(), - filtered_movies.current_selection().overview.clone(), - ) - } else { - ( - app - .data - .radarr_data - .movies - .current_selection() - .title - .text - .clone(), - app - .data - .radarr_data - .movies - .current_selection() - .overview - .clone(), - ) - }; + let movie_title = app + .data + .radarr_data + .movies + .current_selection() + .title + .text + .clone(); + let movie_overview = app + .data + .radarr_data + .movies + .current_selection() + .overview + .clone(); let title = format!("Edit - {movie_title}"); let yes_no_value = app.data.radarr_data.prompt_confirm; let selected_block = app.data.radarr_data.selected_block.get_active_block(); diff --git a/src/ui/radarr_ui/library/mod.rs b/src/ui/radarr_ui/library/mod.rs index 708368f..1b4e838 100644 --- a/src/ui/radarr_ui/library/mod.rs +++ b/src/ui/radarr_ui/library/mod.rs @@ -12,10 +12,10 @@ use crate::ui::radarr_ui::library::delete_movie_ui::DeleteMovieUi; use crate::ui::radarr_ui::library::edit_movie_ui::EditMovieUi; use crate::ui::radarr_ui::library::movie_details_ui::MovieDetailsUi; use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; +use crate::ui::widgets::error_message::ErrorMessage; use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::{ - draw_error_message_popup, draw_input_box_popup, draw_popup_over, draw_prompt_box, - draw_prompt_popup_over, DrawUi, + draw_input_box_popup, draw_popup_over, draw_prompt_box, draw_prompt_popup_over, DrawUi, }; use crate::utils::{convert_runtime, convert_to_gb}; @@ -92,21 +92,15 @@ impl DrawUi for LibraryUi { } pub(super) fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - let current_selection = - if let Some(filtered_movies) = app.data.radarr_data.filtered_movies.as_ref() { - filtered_movies.current_selection().clone() - } else if !app.data.radarr_data.movies.items.is_empty() { - app.data.radarr_data.movies.current_selection().clone() - } else { - Movie::default() - }; + let current_selection = if !app.data.radarr_data.movies.items.is_empty() { + app.data.radarr_data.movies.current_selection().clone() + } else { + Movie::default() + }; let quality_profile_map = &app.data.radarr_data.quality_profile_map; let tags_map = &app.data.radarr_data.tags_map; let downloads_vec = &app.data.radarr_data.downloads.items; - let content = match app.data.radarr_data.filtered_movies.as_mut() { - Some(filtered_movies) if !app.data.radarr_data.is_filtering => Some(filtered_movies), - _ => Some(&mut app.data.radarr_data.movies), - }; + let content = Some(&mut app.data.radarr_data.movies); let help_footer = app .data .radarr_data @@ -203,7 +197,7 @@ fn draw_movie_search_box(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { f, area, "Search", - app.data.radarr_data.search.as_ref().unwrap(), + app.data.radarr_data.movies.search.as_ref().unwrap(), ); } @@ -212,14 +206,17 @@ fn draw_filter_movies_box(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { f, area, "Filter", - app.data.radarr_data.filter.as_ref().unwrap(), + app.data.radarr_data.movies.filter.as_ref().unwrap(), ) } fn draw_search_movie_error_box(f: &mut Frame<'_>, _: &mut App<'_>, area: Rect) { - draw_error_message_popup(f, area, "Movie not found!"); + f.render_widget(ErrorMessage::new("Movie not found!"), area); } fn draw_filter_movies_error_box(f: &mut Frame<'_>, _: &mut App<'_>, area: Rect) { - draw_error_message_popup(f, area, "No movies found matching the given filter!"); + f.render_widget( + ErrorMessage::new("No movies found matching the given filter!"), + area, + ); } diff --git a/src/ui/radarr_ui/system/system_details_ui.rs b/src/ui/radarr_ui/system/system_details_ui.rs index 980bb35..0f1d013 100644 --- a/src/ui/radarr_ui/system/system_details_ui.rs +++ b/src/ui/radarr_ui/system/system_details_ui.rs @@ -85,11 +85,6 @@ fn draw_logs_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn draw_tasks_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let tasks_popup_table = |f: &mut Frame<'_>, app: &mut App<'_>, area: Rect| { let help_footer = Some(build_context_clue_string(&SYSTEM_TASKS_CONTEXT_CLUES)); - // let context_area = draw_help_footer_and_get_content_area( - // f, - // area, - // help_footer, - // ); let tasks_row_mapping = |task: &Task| { let task_props = extract_task_props(task); diff --git a/src/ui/widgets/button.rs b/src/ui/widgets/button.rs index f029706..024cb6d 100644 --- a/src/ui/widgets/button.rs +++ b/src/ui/widgets/button.rs @@ -40,7 +40,7 @@ impl<'a> Button<'a> { self } - fn render_button_with_icon(&self, area: Rect, buf: &mut Buffer) { + fn render_button_with_icon(self, area: Rect, buf: &mut Buffer) { let [title_area, icon_area] = Layout::horizontal([ Constraint::Length(self.title.len() as u16), Constraint::Percentage(25), @@ -63,7 +63,7 @@ impl<'a> Button<'a> { } } - fn render_labeled_button(&self, area: Rect, buf: &mut Buffer) { + fn render_labeled_button(self, area: Rect, buf: &mut Buffer) { let [label_area, button_area] = Layout::horizontal([Constraint::Percentage(48), Constraint::Percentage(48)]).areas(area); let label_paragraph = Paragraph::new(Text::from(format!("\n{}: ", self.label.unwrap()))) @@ -79,7 +79,7 @@ impl<'a> Button<'a> { } } - fn render_button(&self, area: Rect, buf: &mut Buffer) { + fn render_button(self, area: Rect, buf: &mut Buffer) { Paragraph::new(Text::from(self.title)) .block(layout_block()) .alignment(Alignment::Center) diff --git a/src/ui/widgets/checkbox.rs b/src/ui/widgets/checkbox.rs index 7747b7c..5fa280a 100644 --- a/src/ui/widgets/checkbox.rs +++ b/src/ui/widgets/checkbox.rs @@ -31,7 +31,7 @@ impl<'a> Checkbox<'a> { self } - fn render_checkbox(&self, area: Rect, buf: &mut Buffer) { + fn render_checkbox(self, area: Rect, buf: &mut Buffer) { let check = if self.is_checked { "✔" } else { "" }; let [label_area, checkbox_area] = Layout::horizontal([Constraint::Percentage(48), Constraint::Percentage(48)]).areas(area); diff --git a/src/ui/widgets/error_message.rs b/src/ui/widgets/error_message.rs new file mode 100644 index 0000000..c631a0b --- /dev/null +++ b/src/ui/widgets/error_message.rs @@ -0,0 +1,36 @@ +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::title_block_centered; +use ratatui::buffer::Buffer; +use ratatui::layout::{Alignment, Rect}; +use ratatui::style::Stylize; +use ratatui::text::Text; +use ratatui::widgets::{Paragraph, Widget}; + +pub struct ErrorMessage<'a> { + text: Text<'a>, +} + +impl<'a> ErrorMessage<'a> { + pub fn new(message: T) -> Self + where + T: Into>, + { + ErrorMessage { + text: message.into(), + } + } + + fn render_error_message(self, area: Rect, buf: &mut Buffer) { + Paragraph::new(self.text) + .failure() + .alignment(Alignment::Center) + .block(title_block_centered("Error").failure().bold()) + .render(area, buf); + } +} + +impl<'a> Widget for ErrorMessage<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + self.render_error_message(area, buf); + } +} diff --git a/src/ui/widgets/input_box.rs b/src/ui/widgets/input_box.rs index 8ce702e..e7c2e80 100644 --- a/src/ui/widgets/input_box.rs +++ b/src/ui/widgets/input_box.rs @@ -47,12 +47,12 @@ impl<'a> InputBox<'a> { self.label = Some(label); self } - + pub fn offset(mut self, offset: usize) -> InputBox<'a> { self.offset = offset; self } - + pub fn cursor_after_string(mut self, cursor_after_string: bool) -> InputBox<'a> { self.cursor_after_string = cursor_after_string; self @@ -89,7 +89,7 @@ impl<'a> InputBox<'a> { } } - fn render_input_box(&self, area: Rect, buf: &mut Buffer) { + fn render_input_box(self, area: Rect, buf: &mut Buffer) { let style = if matches!(self.is_highlighted, Some(true)) && matches!(self.is_selected, Some(false)) { Style::new().system_function().bold() @@ -99,7 +99,7 @@ impl<'a> InputBox<'a> { let input_box_paragraph = Paragraph::new(Text::from(self.content)) .style(style) - .block(self.block.clone()); + .block(self.block); if let Some(label) = self.label { let [label_area, text_box_area] = diff --git a/src/ui/widgets/loading_block.rs b/src/ui/widgets/loading_block.rs index 7dfb5c4..41df126 100644 --- a/src/ui/widgets/loading_block.rs +++ b/src/ui/widgets/loading_block.rs @@ -14,14 +14,14 @@ impl<'a> LoadingBlock<'a> { Self { is_loading, block } } - fn render_loading_block(&self, area: Rect, buf: &mut Buffer) { + fn render_loading_block(self, area: Rect, buf: &mut Buffer) { if self.is_loading { Paragraph::new(Text::from("\n\n Loading ...\n\n")) .system_function() - .block(self.block.clone()) + .block(self.block) .render(area, buf); } else { - self.block.clone().render(area, buf); + self.block.render(area, buf); } } } diff --git a/src/ui/widgets/managarr_table.rs b/src/ui/widgets/managarr_table.rs index e7c8f69..5a56286 100644 --- a/src/ui/widgets/managarr_table.rs +++ b/src/ui/widgets/managarr_table.rs @@ -86,12 +86,12 @@ where self } - pub fn highlight_rows(mut self, hightlight_rows: bool) -> Self { - self.highlight_rows = hightlight_rows; + pub fn highlight_rows(mut self, highlight_rows: bool) -> Self { + self.highlight_rows = highlight_rows; self } - fn render_table(&mut self, area: Rect, buf: &mut Buffer) { + fn render_table(self, area: Rect, buf: &mut Buffer) { let table_area = if let Some(ref footer) = self.footer { let [content_area, footer_area] = Layout::vertical([Constraint::Fill(0), Constraint::Length(2)]) @@ -109,18 +109,26 @@ where }; let loading_block = LoadingBlock::new(self.is_loading, self.block.clone()); - if let Some(ref mut content) = self.content { - if !content.items.is_empty() { - let rows = content.items.iter().map(&self.row_mapper); + if let Some(content) = self.content { + let (table_contents, table_state) = if content.filtered_items.is_some() { + ( + content.filtered_items.as_ref().unwrap(), + content.filtered_state.as_mut().unwrap(), + ) + } else { + (&content.items, &mut content.state) + }; + if !table_contents.is_empty() { + let rows = table_contents.iter().map(&self.row_mapper); - let headers = Row::new(self.table_headers.clone()) + let headers = Row::new(self.table_headers) .default() .bold() .bottom_margin(0); let mut table = Table::new(rows, &self.constraints) .header(headers) - .block(self.block.clone()); + .block(self.block); if self.highlight_rows { table = table @@ -128,7 +136,7 @@ where .highlight_symbol(HIGHLIGHT_SYMBOL); } - StatefulWidget::render(table, table_area, buf, &mut content.state); + StatefulWidget::render(table, table_area, buf, table_state); } else { loading_block.render(table_area, buf); } @@ -142,7 +150,7 @@ impl<'a, T, F> Widget for ManagarrTable<'a, T, F> where F: Fn(&T) -> Row<'a>, { - fn render(mut self, area: Rect, buf: &mut Buffer) { + fn render(self, area: Rect, buf: &mut Buffer) { self.render_table(area, buf); } } diff --git a/src/ui/widgets/mod.rs b/src/ui/widgets/mod.rs index 19659b2..7e85c31 100644 --- a/src/ui/widgets/mod.rs +++ b/src/ui/widgets/mod.rs @@ -1,5 +1,7 @@ pub(super) mod button; pub(super) mod checkbox; +pub(super) mod error_message; pub(super) mod input_box; pub(super) mod loading_block; pub(super) mod managarr_table; +pub(super) mod popup; diff --git a/src/ui/widgets/popup.rs b/src/ui/widgets/popup.rs new file mode 100644 index 0000000..74a30d4 --- /dev/null +++ b/src/ui/widgets/popup.rs @@ -0,0 +1,34 @@ +use crate::ui::utils::{background_block, centered_rect}; +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::widgets::{Clear, Widget}; + +pub struct Popup { + widget: T, + percent_x: u16, + percent_y: u16, +} + +impl Popup { + pub fn new(widget: T, percent_x: u16, percent_y: u16) -> Self { + Self { + widget, + percent_x, + percent_y, + } + } + + fn render_popup(self, area: Rect, buf: &mut Buffer) { + let popup_area = centered_rect(self.percent_x, self.percent_y, area); + + Clear.render(popup_area, buf); + background_block().render(popup_area, buf); + self.widget.render(popup_area, buf); + } +} + +impl Widget for Popup { + fn render(self, area: Rect, buf: &mut Buffer) { + self.render_popup(area, buf); + } +} diff --git a/src/utils.rs b/src/utils.rs index 8c7a774..6e046c2 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,7 +2,6 @@ use log::LevelFilter; use log4rs::append::file::FileAppender; use log4rs::config::{Appender, Root}; use log4rs::encode::pattern::PatternEncoder; -use regex::Regex; #[cfg(test)] #[path = "utils_tests.rs"] @@ -37,10 +36,3 @@ pub fn convert_runtime(runtime: i64) -> (i64, i64) { (hours, minutes) } - -pub fn strip_non_search_characters(input: &str) -> String { - Regex::new(r"[^a-zA-Z0-9.,/'\-:\s]") - .unwrap() - .replace_all(&input.to_lowercase(), "") - .to_string() -} diff --git a/src/utils_tests.rs b/src/utils_tests.rs index 64bc321..6c74458 100644 --- a/src/utils_tests.rs +++ b/src/utils_tests.rs @@ -2,7 +2,7 @@ mod tests { use pretty_assertions::assert_eq; - use crate::utils::{convert_runtime, convert_to_gb, strip_non_search_characters}; + use crate::utils::{convert_runtime, convert_to_gb}; #[test] fn test_convert_to_gb() { @@ -17,12 +17,4 @@ mod tests { assert_eq!(hours, 2); assert_eq!(minutes, 34); } - - #[test] - fn test_strip_non_alphanumeric_characters() { - assert_eq!( - strip_non_search_characters("Te$t S7r!ng::'~-@_`,(.)/*}^&%#+="), - "tet s7rng::'-,./".to_owned() - ) - } }