From 47b609369b43af139932aafa3a88e506aa226d41 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Sat, 7 Dec 2024 19:20:13 -0700 Subject: [PATCH] refactor(handler): Created a macro to handle all table key events to reduce code duplication and make future implementations faster; Only refactored the Sonarr library to use it thus far --- Cargo.lock | 1 + Cargo.toml | 1 + src/handlers/handler_test_utils.rs | 11 + src/handlers/handlers_tests.rs | 12 + src/handlers/mod.rs | 1 + .../library/library_handler_tests.rs | 61 ++- src/handlers/sonarr_handlers/library/mod.rs | 276 ++---------- .../library/series_details_handler.rs | 42 +- src/handlers/table_handler.rs | 401 ++++++++++++++++++ 9 files changed, 547 insertions(+), 259 deletions(-) create mode 100644 src/handlers/table_handler.rs diff --git a/Cargo.lock b/Cargo.lock index e673269..b013f5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1349,6 +1349,7 @@ dependencies = [ "managarr-tree-widget", "mockall", "mockito", + "paste", "pretty_assertions", "ratatui", "regex", diff --git a/Cargo.toml b/Cargo.toml index 0006109..82e13d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ managarr-tree-widget = "0.24.0" indicatif = "0.17.9" derive_setters = "0.1.6" deunicode = "1.6.0" +paste = "1.0.15" [dev-dependencies] assert_cmd = "2.0.16" diff --git a/src/handlers/handler_test_utils.rs b/src/handlers/handler_test_utils.rs index a4b7112..e7f6eb2 100644 --- a/src/handlers/handler_test_utils.rs +++ b/src/handlers/handler_test_utils.rs @@ -103,6 +103,7 @@ mod test_utils { #[rstest] fn $func(#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key) { let mut app = App::default(); + app.push_navigation_stack($block.into()); app .data .$servarr_data @@ -129,6 +130,7 @@ mod test_utils { #[rstest] fn $func(#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key) { let mut app = App::default(); + app.push_navigation_stack($block.into()); app .data .$servarr_data @@ -155,6 +157,7 @@ mod test_utils { #[rstest] fn $func(#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key) { let mut app = App::default(); + app.push_navigation_stack($block.into()); app.data.$servarr_data.$data_ref.set_items($items); $handler::with(key, &mut app, $block, $context).handle(); @@ -177,6 +180,7 @@ mod test_utils { #[rstest] fn $func(#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key) { let mut app = App::default(); + app.push_navigation_stack($block.into()); app.data.$servarr_data.$data_ref.set_items($items); $handler::with(key, &mut app, $block, $context).handle(); @@ -214,6 +218,7 @@ mod test_utils { #[test] fn $func() { let mut app = App::default(); + app.push_navigation_stack($block.into()); app.data.$servarr_data.$data_ref.set_items(vec![ "Test 1".to_owned(), "Test 2".to_owned(), @@ -240,6 +245,7 @@ mod test_utils { #[test] fn $func() { let mut app = App::default(); + app.push_navigation_stack($block.into()); app .data .$servarr_data @@ -266,6 +272,7 @@ mod test_utils { #[test] fn $func() { let mut app = App::default(); + app.push_navigation_stack($block.into()); app.data.$servarr_data.$data_ref.set_items($items); $handler::with(DEFAULT_KEYBINDINGS.end.key, &mut app, $block, $context).handle(); @@ -288,6 +295,7 @@ mod test_utils { #[test] fn $func() { let mut app = App::default(); + app.push_navigation_stack($block.into()); app.data.$servarr_data.$data_ref.set_items($items); $handler::with(DEFAULT_KEYBINDINGS.end.key, &mut app, $block, $context).handle(); @@ -328,6 +336,9 @@ mod test_utils { $crate::models::sonarr_models::SonarrHistoryItem::default(), ]); app.data.sonarr_data.series_history = Some(series_history); + app.data.sonarr_data.series.set_items(vec![ + $crate::models::sonarr_models::Series::default(), + ]); app.push_navigation_stack($base.into()); app.push_navigation_stack($active_block.into()); diff --git a/src/handlers/handlers_tests.rs b/src/handlers/handlers_tests.rs index d204e18..0b0fa44 100644 --- a/src/handlers/handlers_tests.rs +++ b/src/handlers/handlers_tests.rs @@ -1,5 +1,7 @@ #[cfg(test)] mod tests { + use crate::models::radarr_models::Movie; + use crate::models::sonarr_models::Series; use pretty_assertions::assert_eq; use rstest::rstest; @@ -30,6 +32,16 @@ mod tests { let mut app = App::default(); app.push_navigation_stack(base_block); app.push_navigation_stack(top_block); + app + .data + .sonarr_data + .series + .set_items(vec![Series::default()]); + app + .data + .radarr_data + .movies + .set_items(vec![Movie::default()]); handle_events(DEFAULT_KEYBINDINGS.esc.key, &mut app); diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 1f6f88e..e3d2ddc 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -16,6 +16,7 @@ mod handlers_tests; #[cfg(test)] #[path = "handler_test_utils.rs"] pub mod handler_test_utils; +mod table_handler; pub trait KeyEventHandler<'a, 'b, T: Into + Copy> { fn handle_key_event(&mut self) { diff --git a/src/handlers/sonarr_handlers/library/library_handler_tests.rs b/src/handlers/sonarr_handlers/library/library_handler_tests.rs index 2da1cc3..2e193c2 100644 --- a/src/handlers/sonarr_handlers/library/library_handler_tests.rs +++ b/src/handlers/sonarr_handlers/library/library_handler_tests.rs @@ -195,6 +195,7 @@ mod tests { #[test] fn test_series_search_box_home_end_keys() { let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::SearchSeries.into()); app .data .sonarr_data @@ -248,6 +249,7 @@ mod tests { #[test] fn test_series_filter_box_home_end_keys() { let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::FilterSeries.into()); app .data .sonarr_data @@ -473,6 +475,8 @@ mod tests { #[test] fn test_series_search_box_left_right_keys() { let mut app = App::default(); + app.data.sonarr_data.series.set_items(vec![Series::default()]); + app.push_navigation_stack(ActiveSonarrBlock::SearchSeries.into()); app.data.sonarr_data.series.search = Some("Test".into()); LibraryHandler::with( @@ -521,6 +525,8 @@ mod tests { #[test] fn test_series_filter_box_left_right_keys() { let mut app = App::default(); + app.data.sonarr_data.series.set_items(vec![Series::default()]); + app.push_navigation_stack(ActiveSonarrBlock::FilterSeries.into()); app.data.sonarr_data.series.filter = Some("Test".into()); LibraryHandler::with( @@ -860,6 +866,7 @@ mod tests { app.push_navigation_stack(active_sonarr_block.into()); app.data.sonarr_data = create_test_sonarr_data(); app.data.sonarr_data.series.search = Some("Test".into()); + app.data.sonarr_data.series.set_items(vec![Series::default()]); LibraryHandler::with(ESC_KEY, &mut app, active_sonarr_block, None).handle(); @@ -868,15 +875,10 @@ mod tests { assert_eq!(app.data.sonarr_data.series.search, None); } - #[rstest] - fn test_filter_series_block_esc( - #[values(ActiveSonarrBlock::FilterSeries, ActiveSonarrBlock::FilterSeriesError)] - active_sonarr_block: ActiveSonarrBlock, - ) { + #[test] + fn test_series_block_esc_resets_filter_if_already_set() { let mut app = App::default(); - app.should_ignore_quit_key = true; app.push_navigation_stack(ActiveSonarrBlock::Series.into()); - app.push_navigation_stack(active_sonarr_block.into()); app.data.sonarr_data = create_test_sonarr_data(); app.data.sonarr_data.series = StatefulTable { filter: Some("Test".into()), @@ -884,8 +886,32 @@ mod tests { filtered_state: Some(TableState::default()), ..StatefulTable::default() }; + app.data.sonarr_data.series.set_items(vec![Series::default()]); - LibraryHandler::with(ESC_KEY, &mut app, active_sonarr_block, None).handle(); + LibraryHandler::with(ESC_KEY, &mut app, ActiveSonarrBlock::Series, None).handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + assert_eq!(app.data.sonarr_data.series.filter, None); + assert_eq!(app.data.sonarr_data.series.filtered_items, None); + assert_eq!(app.data.sonarr_data.series.filtered_state, None); + } + + #[test] + fn test_filter_series_error_block_esc() { + let mut app = App::default(); + app.should_ignore_quit_key = true; + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::FilterSeriesError.into()); + app.data.sonarr_data = create_test_sonarr_data(); + app.data.sonarr_data.series = StatefulTable { + filter: Some("Test".into()), + filtered_items: Some(Vec::new()), + filtered_state: Some(TableState::default()), + ..StatefulTable::default() + }; + app.data.sonarr_data.series.set_items(vec![Series::default()]); + + LibraryHandler::with(ESC_KEY, &mut app, ActiveSonarrBlock::FilterSeriesError, None).handle(); assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); assert!(!app.should_ignore_quit_key); @@ -916,6 +942,7 @@ mod tests { #[test] fn test_series_sort_prompt_block_esc() { let mut app = App::default(); + app.data.sonarr_data.series.set_items(vec![Series::default()]); app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app.push_navigation_stack(ActiveSonarrBlock::SeriesSortPrompt.into()); @@ -932,22 +959,11 @@ mod tests { app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app.data.sonarr_data = create_test_sonarr_data(); - app.data.sonarr_data.series = 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, ActiveSonarrBlock::Series, None).handle(); assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); assert!(app.error.text.is_empty()); - assert_eq!(app.data.sonarr_data.series.search, None); - assert_eq!(app.data.sonarr_data.series.filter, None); - assert_eq!(app.data.sonarr_data.series.filtered_items, None); - assert_eq!(app.data.sonarr_data.series.filtered_state, None); } } @@ -968,6 +984,7 @@ mod tests { #[test] fn test_search_series_key() { let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app .data .sonarr_data @@ -1020,6 +1037,7 @@ mod tests { #[test] fn test_filter_series_key() { let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app .data .sonarr_data @@ -1274,6 +1292,7 @@ mod tests { #[test] fn test_search_series_box_backspace_key() { let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::SearchSeries.into()); app.data.sonarr_data.series.search = Some("Test".into()); app .data @@ -1298,6 +1317,7 @@ mod tests { #[test] fn test_filter_series_box_backspace_key() { let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::FilterSeries.into()); app .data .sonarr_data @@ -1322,6 +1342,7 @@ mod tests { #[test] fn test_search_series_box_char_key() { let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::SearchSeries.into()); app .data .sonarr_data @@ -1346,6 +1367,7 @@ mod tests { #[test] fn test_filter_series_box_char_key() { let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::FilterSeries.into()); app .data .sonarr_data @@ -1370,6 +1392,7 @@ mod tests { #[test] fn test_sort_key() { let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app .data .sonarr_data diff --git a/src/handlers/sonarr_handlers/library/mod.rs b/src/handlers/sonarr_handlers/library/mod.rs index c91ffc1..a5ff91e 100644 --- a/src/handlers/sonarr_handlers/library/mod.rs +++ b/src/handlers/sonarr_handlers/library/mod.rs @@ -6,7 +6,7 @@ use edit_series_handler::EditSeriesHandler; use crate::{ app::App, event::Key, - handle_text_box_keys, handle_text_box_left_right_keys, + handle_table_events, handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}, models::{ servarr_data::sonarr::sonarr_data::{ @@ -23,6 +23,7 @@ use crate::{ use super::handle_change_tab_left_right_keys; use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::handlers::sonarr_handlers::library::series_details_handler::SeriesDetailsHandler; +use crate::handlers::table_handler::TableHandlingProps; mod add_series_handler; mod delete_series_handler; @@ -39,25 +40,43 @@ pub(super) struct LibraryHandler<'a, 'b> { context: Option, } +impl<'a, 'b> LibraryHandler<'a, 'b> { + handle_table_events!(self, series, self.app.data.sonarr_data.series, Series); +} + impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for LibraryHandler<'a, 'b> { fn handle(&mut self) { - match self.active_sonarr_block { - _ if AddSeriesHandler::accepts(self.active_sonarr_block) => { - AddSeriesHandler::with(self.key, self.app, self.active_sonarr_block, self.context).handle(); + let series_table_handling_props = TableHandlingProps::new(ActiveSonarrBlock::Series.into()) + .sorting_block(ActiveSonarrBlock::SeriesSortPrompt.into()) + .sort_by_fn(|a: &Series, b: &Series| a.id.cmp(&b.id)) + .sort_options(series_sorting_options()) + .searching_block(ActiveSonarrBlock::SearchSeries.into()) + .search_error_block(ActiveSonarrBlock::SearchSeriesError.into()) + .search_field_fn(|series| &series.title.text) + .filtering_block(ActiveSonarrBlock::FilterSeries.into()) + .filter_error_block(ActiveSonarrBlock::FilterSeriesError.into()) + .filter_field_fn(|series| &series.title.text); + + if !self.handle_series_table_events(series_table_handling_props) { + match self.active_sonarr_block { + _ if AddSeriesHandler::accepts(self.active_sonarr_block) => { + AddSeriesHandler::with(self.key, self.app, self.active_sonarr_block, self.context) + .handle(); + } + _ if DeleteSeriesHandler::accepts(self.active_sonarr_block) => { + DeleteSeriesHandler::with(self.key, self.app, self.active_sonarr_block, self.context) + .handle(); + } + _ if EditSeriesHandler::accepts(self.active_sonarr_block) => { + EditSeriesHandler::with(self.key, self.app, self.active_sonarr_block, self.context) + .handle(); + } + _ if SeriesDetailsHandler::accepts(self.active_sonarr_block) => { + SeriesDetailsHandler::with(self.key, self.app, self.active_sonarr_block, self.context) + .handle(); + } + _ => self.handle_key_event(), } - _ if DeleteSeriesHandler::accepts(self.active_sonarr_block) => { - DeleteSeriesHandler::with(self.key, self.app, self.active_sonarr_block, self.context) - .handle(); - } - _ if EditSeriesHandler::accepts(self.active_sonarr_block) => { - EditSeriesHandler::with(self.key, self.app, self.active_sonarr_block, self.context) - .handle(); - } - _ if SeriesDetailsHandler::accepts(self.active_sonarr_block) => { - SeriesDetailsHandler::with(self.key, self.app, self.active_sonarr_block, self.context) - .handle(); - } - _ => self.handle_key_event(), } } @@ -91,109 +110,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for LibraryHandler<'a, ' !self.app.is_loading && !self.app.data.sonarr_data.series.is_empty() } - fn handle_scroll_up(&mut self) { - match self.active_sonarr_block { - ActiveSonarrBlock::Series => self.app.data.sonarr_data.series.scroll_up(), - ActiveSonarrBlock::SeriesSortPrompt => self - .app - .data - .sonarr_data - .series - .sort - .as_mut() - .unwrap() - .scroll_up(), - _ => (), - } - } + fn handle_scroll_up(&mut self) {} - fn handle_scroll_down(&mut self) { - match self.active_sonarr_block { - ActiveSonarrBlock::Series => self.app.data.sonarr_data.series.scroll_down(), - ActiveSonarrBlock::SeriesSortPrompt => self - .app - .data - .sonarr_data - .series - .sort - .as_mut() - .unwrap() - .scroll_down(), - _ => (), - } - } + fn handle_scroll_down(&mut self) {} - fn handle_home(&mut self) { - match self.active_sonarr_block { - ActiveSonarrBlock::Series => self.app.data.sonarr_data.series.scroll_to_top(), - ActiveSonarrBlock::SearchSeries => { - self - .app - .data - .sonarr_data - .series - .search - .as_mut() - .unwrap() - .scroll_home(); - } - ActiveSonarrBlock::FilterSeries => { - self - .app - .data - .sonarr_data - .series - .filter - .as_mut() - .unwrap() - .scroll_home(); - } - ActiveSonarrBlock::SeriesSortPrompt => self - .app - .data - .sonarr_data - .series - .sort - .as_mut() - .unwrap() - .scroll_to_top(), - _ => (), - } - } + fn handle_home(&mut self) {} - fn handle_end(&mut self) { - match self.active_sonarr_block { - ActiveSonarrBlock::Series => self.app.data.sonarr_data.series.scroll_to_bottom(), - ActiveSonarrBlock::SearchSeries => self - .app - .data - .sonarr_data - .series - .search - .as_mut() - .unwrap() - .reset_offset(), - ActiveSonarrBlock::FilterSeries => self - .app - .data - .sonarr_data - .series - .filter - .as_mut() - .unwrap() - .reset_offset(), - ActiveSonarrBlock::SeriesSortPrompt => self - .app - .data - .sonarr_data - .series - .sort - .as_mut() - .unwrap() - .scroll_to_bottom(), - _ => (), - } - } + fn handle_end(&mut self) {} fn handle_delete(&mut self) { if self.active_sonarr_block == ActiveSonarrBlock::Series { @@ -209,20 +132,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for LibraryHandler<'a, ' match self.active_sonarr_block { ActiveSonarrBlock::Series => handle_change_tab_left_right_keys(self.app, self.key), ActiveSonarrBlock::UpdateAllSeriesPrompt => handle_prompt_toggle(self.app, self.key), - ActiveSonarrBlock::SearchSeries => { - handle_text_box_left_right_keys!( - self, - self.key, - self.app.data.sonarr_data.series.search.as_mut().unwrap() - ) - } - ActiveSonarrBlock::FilterSeries => { - handle_text_box_left_right_keys!( - self, - self.key, - self.app.data.sonarr_data.series.filter.as_mut().unwrap() - ) - } _ => (), } } @@ -232,44 +141,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for LibraryHandler<'a, ' ActiveSonarrBlock::Series => self .app .push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into()), - ActiveSonarrBlock::SearchSeries => { - self.app.pop_navigation_stack(); - self.app.should_ignore_quit_key = false; - - if self.app.data.sonarr_data.series.search.is_some() { - let has_match = self - .app - .data - .sonarr_data - .series - .apply_search(|series| &series.title.text); - - if !has_match { - self - .app - .push_navigation_stack(ActiveSonarrBlock::SearchSeriesError.into()); - } - } - } - ActiveSonarrBlock::FilterSeries => { - self.app.pop_navigation_stack(); - self.app.should_ignore_quit_key = false; - - if self.app.data.sonarr_data.series.filter.is_some() { - let has_matches = self - .app - .data - .sonarr_data - .series - .apply_filter(|series| &series.title.text); - - if !has_matches { - self - .app - .push_navigation_stack(ActiveSonarrBlock::FilterSeriesError.into()); - } - } - } ActiveSonarrBlock::UpdateAllSeriesPrompt => { if self.app.data.sonarr_data.prompt_confirm { self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::UpdateAllSeries); @@ -277,44 +148,17 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for LibraryHandler<'a, ' self.app.pop_navigation_stack(); } - ActiveSonarrBlock::SeriesSortPrompt => { - self - .app - .data - .sonarr_data - .series - .items - .sort_by(|a, b| a.id.cmp(&b.id)); - self.app.data.sonarr_data.series.apply_sorting(); - - self.app.pop_navigation_stack(); - } _ => (), } } fn handle_esc(&mut self) { match self.active_sonarr_block { - ActiveSonarrBlock::FilterSeries | ActiveSonarrBlock::FilterSeriesError => { - self.app.pop_navigation_stack(); - self.app.data.sonarr_data.series.reset_filter(); - self.app.should_ignore_quit_key = false; - } - ActiveSonarrBlock::SearchSeries | ActiveSonarrBlock::SearchSeriesError => { - self.app.pop_navigation_stack(); - self.app.data.sonarr_data.series.reset_search(); - self.app.should_ignore_quit_key = false; - } ActiveSonarrBlock::UpdateAllSeriesPrompt => { self.app.pop_navigation_stack(); self.app.data.sonarr_data.prompt_confirm = false; } - ActiveSonarrBlock::SeriesSortPrompt => { - self.app.pop_navigation_stack(); - } _ => { - self.app.data.sonarr_data.series.reset_search(); - self.app.data.sonarr_data.series.reset_filter(); handle_clear_errors(self.app); } } @@ -324,21 +168,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for LibraryHandler<'a, ' let key = self.key; match self.active_sonarr_block { ActiveSonarrBlock::Series => match self.key { - _ if key == DEFAULT_KEYBINDINGS.search.key => { - self - .app - .push_navigation_stack(ActiveSonarrBlock::SearchSeries.into()); - self.app.data.sonarr_data.series.search = Some(HorizontallyScrollableText::default()); - self.app.should_ignore_quit_key = true; - } - _ if key == DEFAULT_KEYBINDINGS.filter.key => { - self - .app - .push_navigation_stack(ActiveSonarrBlock::FilterSeries.into()); - self.app.data.sonarr_data.series.reset_filter(); - self.app.data.sonarr_data.series.filter = Some(HorizontallyScrollableText::default()); - self.app.should_ignore_quit_key = true; - } _ if key == DEFAULT_KEYBINDINGS.edit.key => { self.app.push_navigation_stack( ( @@ -366,33 +195,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for LibraryHandler<'a, ' _ if key == DEFAULT_KEYBINDINGS.refresh.key => { self.app.should_refresh = true; } - _ if key == DEFAULT_KEYBINDINGS.sort.key => { - self - .app - .data - .sonarr_data - .series - .sorting(series_sorting_options()); - self - .app - .push_navigation_stack(ActiveSonarrBlock::SeriesSortPrompt.into()); - } _ => (), }, - ActiveSonarrBlock::SearchSeries => { - handle_text_box_keys!( - self, - key, - self.app.data.sonarr_data.series.search.as_mut().unwrap() - ) - } - ActiveSonarrBlock::FilterSeries => { - handle_text_box_keys!( - self, - key, - self.app.data.sonarr_data.series.filter.as_mut().unwrap() - ) - } ActiveSonarrBlock::UpdateAllSeriesPrompt => { if key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.sonarr_data.prompt_confirm = true; diff --git a/src/handlers/sonarr_handlers/library/series_details_handler.rs b/src/handlers/sonarr_handlers/library/series_details_handler.rs index e47fa18..950ee72 100644 --- a/src/handlers/sonarr_handlers/library/series_details_handler.rs +++ b/src/handlers/sonarr_handlers/library/series_details_handler.rs @@ -436,8 +436,24 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeriesDetailsHandler self.app.pop_navigation_stack(); } ActiveSonarrBlock::SeriesHistory => { - if self.app.data.sonarr_data.series_history.as_ref().expect("Series history is not populated").filtered_items.is_some() { - self.app.data.sonarr_data.series_history.as_mut().expect("Series history is not populated").reset_filter(); + if self + .app + .data + .sonarr_data + .series_history + .as_ref() + .expect("Series history is not populated") + .filtered_items + .is_some() + { + self + .app + .data + .sonarr_data + .series_history + .as_mut() + .expect("Series history is not populated") + .reset_filter(); } else { self.app.pop_navigation_stack(); self.app.data.sonarr_data.reset_series_info_tabs(); @@ -577,14 +593,32 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeriesDetailsHandler handle_text_box_keys!( self, key, - self.app.data.sonarr_data.series_history.as_mut().expect("Series history should be populated").search.as_mut().unwrap() + self + .app + .data + .sonarr_data + .series_history + .as_mut() + .expect("Series history should be populated") + .search + .as_mut() + .unwrap() ) } ActiveSonarrBlock::FilterSeriesHistory => { handle_text_box_keys!( self, key, - self.app.data.sonarr_data.series_history.as_mut().expect("Series history should be populated").filter.as_mut().unwrap() + self + .app + .data + .sonarr_data + .series_history + .as_mut() + .expect("Series history should be populated") + .filter + .as_mut() + .unwrap() ) } ActiveSonarrBlock::AutomaticallySearchSeriesPrompt => { diff --git a/src/handlers/table_handler.rs b/src/handlers/table_handler.rs new file mode 100644 index 0000000..b05759f --- /dev/null +++ b/src/handlers/table_handler.rs @@ -0,0 +1,401 @@ +use crate::models::stateful_table::SortOption; +use crate::models::Route; +use derive_setters::Setters; +use std::cmp::Ordering; +use std::fmt::Debug; + +#[derive(Setters)] +pub struct TableHandlingProps +where + T: Clone + PartialEq + Eq + Debug + Default, +{ + #[setters(strip_option)] + pub sorting_block: Option, + #[setters(strip_option)] + pub sort_options: Option>>, + #[setters(strip_option)] + pub sort_by_fn: Option Ordering>, + #[setters(strip_option)] + pub searching_block: Option, + #[setters(strip_option)] + pub search_error_block: Option, + #[setters(strip_option)] + pub search_field_fn: Option &str>, + #[setters(strip_option)] + pub filtering_block: Option, + #[setters(strip_option)] + pub filter_error_block: Option, + #[setters(strip_option)] + pub filter_field_fn: Option &str>, + #[setters(skip)] + pub table_block: Route, +} + +#[macro_export] +macro_rules! handle_table_events { + ($self:expr, $name:ty, $table:expr, $row:ident) => { + paste::paste! { + fn [](&mut $self, props: $crate::handlers::table_handler::TableHandlingProps<$row>) -> bool { + if $self.is_ready() { + match $self.key { + _ if $self.key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.up.key => $self.[](props), + _ if $self.key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.down.key => $self.[](props), + _ if $self.key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.home.key => $self.[](props), + _ if $self.key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.end.key => $self.[](props), + _ if $self.key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.left.key + || $self.key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.right.key => + { + $self.[](props) + } + _ if $self.key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.submit.key => $self.[](props), + _ if $self.key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.esc.key => $self.[](props), + _ if props.searching_block.is_some() + && $self.app.get_current_route() == *props.searching_block.as_ref().unwrap() => + { + $self.[]() + } + _ if props.filtering_block.is_some() + && $self.app.get_current_route() == *props.filtering_block.as_ref().unwrap() => + { + $self.[]() + } + _ if $self.key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.filter.key => $self.[](props), + _ if $self.key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.search.key => $self.[](props), + _ if $self.key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.sort.key => $self.[](props), + _ => false, + } + } else { + false + } + } + + fn [](&mut $self, props: $crate::handlers::table_handler::TableHandlingProps<$row>) -> bool { + match $self.app.get_current_route() { + _ if props.table_block == $self.app.get_current_route() => { + $table.scroll_up(); + true + } + _ if props.sorting_block.is_some() + && $self.app.get_current_route() == *props.sorting_block.as_ref().unwrap() => + { + $table.sort.as_mut().unwrap().scroll_up(); + true + } + _ => false, + } + } + + fn [](&mut $self, props: $crate::handlers::table_handler::TableHandlingProps<$row>) -> bool { + match $self.app.get_current_route() { + _ if props.table_block == $self.app.get_current_route() => { + $table.scroll_down(); + true + } + _ if props.sorting_block.is_some() + && $self.app.get_current_route() == *props.sorting_block.as_ref().unwrap() => + { + $table + .sort + .as_mut() + .unwrap() + .scroll_down(); + true + } + _ => false, + } + } + + fn [](&mut $self, props: $crate::handlers::table_handler::TableHandlingProps<$row>) -> bool { + match $self.app.get_current_route() { + _ if props.table_block == $self.app.get_current_route() => { + $table.scroll_to_top(); + true + } + _ if props.sorting_block.is_some() + && $self.app.get_current_route() == *props.sorting_block.as_ref().unwrap() => + { + $table + .sort + .as_mut() + .unwrap() + .scroll_to_top(); + true + } + _ if props.searching_block.is_some() + && $self.app.get_current_route() == *props.searching_block.as_ref().unwrap() => + { + $table + .search + .as_mut() + .unwrap() + .scroll_home(); + true + } + _ if props.filtering_block.is_some() + && $self.app.get_current_route() == *props.filtering_block.as_ref().unwrap() => + { + $table + .filter + .as_mut() + .unwrap() + .scroll_home(); + true + } + _ => false, + } + } + + fn [](&mut $self, props: $crate::handlers::table_handler::TableHandlingProps<$row>) -> bool { + match $self.app.get_current_route() { + _ if props.table_block == $self.app.get_current_route() => { + $table.scroll_to_bottom(); + true + } + _ if props.sorting_block.is_some() + && $self.app.get_current_route() == *props.sorting_block.as_ref().unwrap() => + { + $table + .sort + .as_mut() + .unwrap() + .scroll_to_bottom(); + true + } + _ if props.searching_block.is_some() + && $self.app.get_current_route() == *props.searching_block.as_ref().unwrap() => + { + $table + .search + .as_mut() + .unwrap() + .reset_offset(); + true + } + _ if props.filtering_block.is_some() + && $self.app.get_current_route() == *props.filtering_block.as_ref().unwrap() => + { + $table + .filter + .as_mut() + .unwrap() + .reset_offset(); + true + } + _ => false, + } + } + + fn [](&mut $self, props: $crate::handlers::table_handler::TableHandlingProps<$row>) -> bool { + match $self.app.get_current_route() { + _ if props.searching_block.is_some() + && $self.app.get_current_route() == *props.searching_block.as_ref().unwrap() => + { + $crate::handle_text_box_left_right_keys!( + $self, + $self.key, + $table.search.as_mut().unwrap() + ); + true + } + _ if props.filtering_block.is_some() + && $self.app.get_current_route() == *props.filtering_block.as_ref().unwrap() => + { + $crate::handle_text_box_left_right_keys!( + $self, + $self.key, + $table.filter.as_mut().unwrap() + ); + true + } + _ => false, + } + } + + fn [](&mut $self, props: $crate::handlers::table_handler::TableHandlingProps<$row>) -> bool { + match $self.app.get_current_route() { + _ if props.sorting_block.is_some() + && $self.app.get_current_route() == *props.sorting_block.as_ref().unwrap() => + { + let sort_by_fn = props.sort_by_fn.expect("Sort by function is required"); + + $table.items.sort_by(sort_by_fn); + $table.apply_sorting(); + $self.app.pop_navigation_stack(); + + true + } + _ if props.searching_block.is_some() + && $self.app.get_current_route() == *props.searching_block.as_ref().unwrap() => + { + $self.app.pop_navigation_stack(); + $self.app.should_ignore_quit_key = false; + + if $table.search.is_some() { + let search_field_fn = props + .search_field_fn + .expect("Search field function is required"); + let has_match = $table.apply_search(search_field_fn); + + if !has_match { + $self.app.push_navigation_stack( + props + .search_error_block + .expect("Search error block is undefined"), + ); + } + } + + true + } + _ if props.filtering_block.is_some() + && $self.app.get_current_route() == *props.filtering_block.as_ref().unwrap() => + { + $self.app.pop_navigation_stack(); + $self.app.should_ignore_quit_key = false; + + if $table.filter.is_some() { + let filter_field_fn = props + .filter_field_fn + .expect("Search field function is required"); + let has_match = $table.apply_filter(filter_field_fn); + + if !has_match { + $self.app.push_navigation_stack( + props + .filter_error_block + .expect("Search error block is undefined"), + ); + } + } + + true + } + _ => false, + } + } + + fn [](&mut $self, props: $crate::handlers::table_handler::TableHandlingProps<$row>) -> bool { + match $self.app.get_current_route() { + _ if props.sorting_block.is_some() + && $self.app.get_current_route() == *props.sorting_block.as_ref().unwrap() => + { + $self.app.pop_navigation_stack(); + true + } + _ if (props.searching_block.is_some() + && $self.app.get_current_route() == *props.searching_block.as_ref().unwrap()) + || (props.search_error_block.is_some() + && $self.app.get_current_route() == *props.search_error_block.as_ref().unwrap()) => + { + $self.app.pop_navigation_stack(); + $table.reset_search(); + $self.app.should_ignore_quit_key = false; + true + } + _ if (props.filtering_block.is_some() + && $self.app.get_current_route() == *props.filtering_block.as_ref().unwrap()) + || (props.filter_error_block.is_some() + && $self.app.get_current_route() == *props.filter_error_block.as_ref().unwrap()) => + { + $self.app.pop_navigation_stack(); + $table.reset_filter(); + $self.app.should_ignore_quit_key = false; + true + } + _ if props.table_block == $self.app.get_current_route() + && $table.filtered_items.is_some() => + { + $table.reset_filter(); + true + } + _ => false, + } + } + + fn [](&mut $self, props: $crate::handlers::table_handler::TableHandlingProps<$row>) -> bool { + if matches!($self.app.get_current_route(), _ if props.table_block == $self.app.get_current_route()) { + $self + .app + .push_navigation_stack(props.filtering_block.expect("Filtering block is undefined").into()); + $table.reset_filter(); + $table.filter = Some(HorizontallyScrollableText::default()); + $self.app.should_ignore_quit_key = true; + + true + } else { + false + } + } + + fn [](&mut $self, props: $crate::handlers::table_handler::TableHandlingProps<$row>) -> bool { + if matches!($self.app.get_current_route(), _ if props.table_block == $self.app.get_current_route()) { + $self + .app + .push_navigation_stack(props.searching_block.expect("Searching block is undefined")); + $table.search = Some(HorizontallyScrollableText::default()); + $self.app.should_ignore_quit_key = true; + + true + } else { + false + } + } + + fn [](&mut $self, props: $crate::handlers::table_handler::TableHandlingProps<$row>) -> bool { + if matches!($self.app.get_current_route(), _ if props.table_block == $self.app.get_current_route()) { + $table.sorting( + props + .sort_options + .as_ref() + .expect("Sort options are undefined") + .clone(), + ); + $self + .app + .push_navigation_stack(props.sorting_block.expect("Sorting block is undefined")); + true + } else { + false + } + } + + fn [](&mut $self) -> bool { + $crate::handle_text_box_keys!( + $self, + $self.key, + $table.search.as_mut().unwrap() + ); + true + } + + fn [](&mut $self) -> bool { + $crate::handle_text_box_keys!( + $self, + $self.key, + $table.filter.as_mut().unwrap() + ); + true + } + } + }; +} + +impl TableHandlingProps +where + T: Clone + PartialEq + Eq + Debug + Default, +{ + pub fn new(table_block: Route) -> Self { + TableHandlingProps { + sorting_block: None, + sort_options: None, + sort_by_fn: None, + searching_block: None, + search_error_block: None, + search_field_fn: None, + filtering_block: None, + filter_error_block: None, + filter_field_fn: None, + table_block, + } + } +}