From bd96c4ab235198739cf610754dcb32113c25aced Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Thu, 3 Sep 2026 23:54:49 -0600 Subject: [PATCH] feat(readarr): add the blocklist handler --- .../blocklist/blocklist_handler_tests.rs | 627 ++++++++++++++++++ .../readarr_handlers/blocklist/mod.rs | 224 +++++++ src/handlers/readarr_handlers/mod.rs | 5 + .../readarr_handlers/readarr_handler_tests.rs | 18 + 4 files changed, 874 insertions(+) create mode 100644 src/handlers/readarr_handlers/blocklist/blocklist_handler_tests.rs create mode 100644 src/handlers/readarr_handlers/blocklist/mod.rs diff --git a/src/handlers/readarr_handlers/blocklist/blocklist_handler_tests.rs b/src/handlers/readarr_handlers/blocklist/blocklist_handler_tests.rs new file mode 100644 index 00000000..e3964cc7 --- /dev/null +++ b/src/handlers/readarr_handlers/blocklist/blocklist_handler_tests.rs @@ -0,0 +1,627 @@ +#[cfg(test)] +mod tests { + use std::cmp::Ordering; + + use chrono::DateTime; + use pretty_assertions::{assert_eq, assert_str_eq}; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_navigation_pushed; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::readarr_handlers::blocklist::{BlocklistHandler, blocklist_sorting_options}; + use crate::models::readarr_models::{Author, BlocklistItem}; + use crate::models::servarr_data::readarr::readarr_data::{ActiveReadarrBlock, BLOCKLIST_BLOCKS}; + use crate::models::servarr_models::{Quality, QualityWrapper}; + + mod test_handle_delete { + use pretty_assertions::assert_eq; + + use super::*; + + const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key; + + #[test] + fn test_delete_blocklist_item_prompt() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into()); + app.data.readarr_data.blocklist.set_items(blocklist_vec()); + + BlocklistHandler::new(DELETE_KEY, &mut app, ActiveReadarrBlock::Blocklist, None).handle(); + + assert_navigation_pushed!(app, ActiveReadarrBlock::DeleteBlocklistItemPrompt.into()); + } + + #[test] + fn test_delete_blocklist_item_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into()); + app.data.readarr_data.blocklist.set_items(blocklist_vec()); + + BlocklistHandler::new(DELETE_KEY, &mut app, ActiveReadarrBlock::Blocklist, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveReadarrBlock::Blocklist.into() + ); + } + } + + mod test_handle_left_right_action { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + use crate::assert_navigation_pushed; + + #[rstest] + fn test_blocklist_tab_left(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into()); + app.is_loading = is_ready; + app.data.readarr_data.main_tabs.set_index(2); + + BlocklistHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveReadarrBlock::Blocklist, + None, + ) + .handle(); + + assert_eq!( + app.data.readarr_data.main_tabs.get_active_route(), + ActiveReadarrBlock::Downloads.into() + ); + assert_navigation_pushed!(app, ActiveReadarrBlock::Downloads.into()); + } + + #[rstest] + fn test_blocklist_tab_right(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into()); + app.is_loading = is_ready; + app.data.readarr_data.main_tabs.set_index(2); + + BlocklistHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveReadarrBlock::Blocklist, + None, + ) + .handle(); + + assert_eq!( + app.data.readarr_data.main_tabs.get_active_route(), + ActiveReadarrBlock::History.into() + ); + assert_navigation_pushed!(app, ActiveReadarrBlock::History.into()); + } + + #[rstest] + fn test_blocklist_left_right_prompt_toggle( + #[values( + ActiveReadarrBlock::DeleteBlocklistItemPrompt, + ActiveReadarrBlock::BlocklistClearAllItemsPrompt + )] + active_readarr_block: ActiveReadarrBlock, + #[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into()); + + BlocklistHandler::new(key, &mut app, active_readarr_block, None).handle(); + + assert!(app.data.readarr_data.prompt_confirm); + + BlocklistHandler::new(key, &mut app, active_readarr_block, None).handle(); + + assert!(!app.data.readarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use crate::assert_navigation_popped; + use crate::network::readarr_network::ReadarrEvent; + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_blocklist_submit() { + let mut app = App::test_default(); + app.data.readarr_data.blocklist.set_items(blocklist_vec()); + app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into()); + + BlocklistHandler::new(SUBMIT_KEY, &mut app, ActiveReadarrBlock::Blocklist, None).handle(); + + assert_navigation_pushed!(app, ActiveReadarrBlock::BlocklistItemDetails.into()); + } + + #[test] + fn test_blocklist_submit_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.data.readarr_data.blocklist.set_items(blocklist_vec()); + app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into()); + + BlocklistHandler::new(SUBMIT_KEY, &mut app, ActiveReadarrBlock::Blocklist, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveReadarrBlock::Blocklist.into() + ); + } + + #[rstest] + #[case( + ActiveReadarrBlock::Blocklist, + ActiveReadarrBlock::DeleteBlocklistItemPrompt, + ReadarrEvent::DeleteBlocklistItem(3) + )] + #[case( + ActiveReadarrBlock::Blocklist, + ActiveReadarrBlock::BlocklistClearAllItemsPrompt, + ReadarrEvent::ClearBlocklist + )] + fn test_blocklist_prompt_confirm_submit( + #[case] base_route: ActiveReadarrBlock, + #[case] prompt_block: ActiveReadarrBlock, + #[case] expected_action: ReadarrEvent, + ) { + let mut app = App::test_default(); + app.data.readarr_data.blocklist.set_items(blocklist_vec()); + app.data.readarr_data.prompt_confirm = true; + app.push_navigation_stack(base_route.into()); + app.push_navigation_stack(prompt_block.into()); + + BlocklistHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle(); + + assert!(app.data.readarr_data.prompt_confirm); + assert_some_eq_x!( + &app.data.readarr_data.prompt_confirm_action, + &expected_action + ); + assert_navigation_popped!(app, base_route.into()); + } + + #[rstest] + fn test_blocklist_prompt_decline_submit( + #[values( + ActiveReadarrBlock::DeleteBlocklistItemPrompt, + ActiveReadarrBlock::BlocklistClearAllItemsPrompt + )] + prompt_block: ActiveReadarrBlock, + ) { + let mut app = App::test_default(); + app.data.readarr_data.blocklist.set_items(blocklist_vec()); + app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into()); + app.push_navigation_stack(prompt_block.into()); + + BlocklistHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle(); + + assert!(!app.data.readarr_data.prompt_confirm); + assert_none!(app.data.readarr_data.prompt_confirm_action); + assert_navigation_popped!(app, ActiveReadarrBlock::Blocklist.into()); + } + } + + mod test_handle_esc { + use rstest::rstest; + + use super::*; + use crate::assert_navigation_popped; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + #[case( + ActiveReadarrBlock::Blocklist, + ActiveReadarrBlock::DeleteBlocklistItemPrompt + )] + #[case( + ActiveReadarrBlock::Blocklist, + ActiveReadarrBlock::BlocklistClearAllItemsPrompt + )] + fn test_blocklist_prompt_blocks_esc( + #[case] base_block: ActiveReadarrBlock, + #[case] prompt_block: ActiveReadarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(base_block.into()); + app.push_navigation_stack(prompt_block.into()); + app.data.readarr_data.prompt_confirm = true; + + BlocklistHandler::new(ESC_KEY, &mut app, prompt_block, None).handle(); + + assert_navigation_popped!(app, base_block.into()); + assert!(!app.data.readarr_data.prompt_confirm); + } + + #[test] + fn test_esc_blocklist_item_details() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into()); + app.push_navigation_stack(ActiveReadarrBlock::BlocklistItemDetails.into()); + + BlocklistHandler::new( + ESC_KEY, + &mut app, + ActiveReadarrBlock::BlocklistItemDetails, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::Blocklist.into()); + } + + #[rstest] + fn test_default_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.error = "test error".to_owned().into(); + app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into()); + app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into()); + + BlocklistHandler::new(ESC_KEY, &mut app, ActiveReadarrBlock::Blocklist, None).handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::Blocklist.into()); + assert_is_empty!(app.error.text); + } + } + + mod test_handle_key_char { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::network::readarr_network::ReadarrEvent; + + use super::*; + use crate::{assert_navigation_popped, assert_navigation_pushed}; + + #[test] + fn test_refresh_blocklist_key() { + let mut app = App::test_default(); + app.data.readarr_data.blocklist.set_items(blocklist_vec()); + app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into()); + + BlocklistHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveReadarrBlock::Blocklist, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveReadarrBlock::Blocklist.into()); + assert!(app.should_refresh); + } + + #[test] + fn test_refresh_blocklist_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.data.readarr_data.blocklist.set_items(blocklist_vec()); + app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into()); + + BlocklistHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveReadarrBlock::Blocklist, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveReadarrBlock::Blocklist.into() + ); + assert!(!app.should_refresh); + } + + #[test] + fn test_clear_blocklist_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into()); + app.data.readarr_data.blocklist.set_items(blocklist_vec()); + + BlocklistHandler::new( + DEFAULT_KEYBINDINGS.clear.key, + &mut app, + ActiveReadarrBlock::Blocklist, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveReadarrBlock::BlocklistClearAllItemsPrompt.into()); + } + + #[test] + fn test_clear_blocklist_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into()); + app.data.readarr_data.blocklist.set_items(blocklist_vec()); + + BlocklistHandler::new( + DEFAULT_KEYBINDINGS.clear.key, + &mut app, + ActiveReadarrBlock::Blocklist, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveReadarrBlock::Blocklist.into() + ); + } + + #[rstest] + #[case( + ActiveReadarrBlock::Blocklist, + ActiveReadarrBlock::DeleteBlocklistItemPrompt, + ReadarrEvent::DeleteBlocklistItem(3) + )] + #[case( + ActiveReadarrBlock::Blocklist, + ActiveReadarrBlock::BlocklistClearAllItemsPrompt, + ReadarrEvent::ClearBlocklist + )] + fn test_blocklist_prompt_confirm( + #[case] base_route: ActiveReadarrBlock, + #[case] prompt_block: ActiveReadarrBlock, + #[case] expected_action: ReadarrEvent, + ) { + let mut app = App::test_default(); + app.data.readarr_data.blocklist.set_items(blocklist_vec()); + app.push_navigation_stack(base_route.into()); + app.push_navigation_stack(prompt_block.into()); + + BlocklistHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + prompt_block, + None, + ) + .handle(); + + assert!(app.data.readarr_data.prompt_confirm); + assert_some_eq_x!( + &app.data.readarr_data.prompt_confirm_action, + &expected_action + ); + assert_navigation_popped!(app, base_route.into()); + } + } + + #[test] + fn test_blocklist_sorting_options_author_name() { + let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = |a, b| { + a.author + .author_name + .text + .to_lowercase() + .cmp(&b.author.author_name.text.to_lowercase()) + }; + let mut expected_blocklist_vec = blocklist_vec(); + expected_blocklist_vec.sort_by(expected_cmp_fn); + + let sort_option = blocklist_sorting_options()[0].clone(); + let mut sorted_blocklist_vec = blocklist_vec(); + sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_blocklist_vec, expected_blocklist_vec); + assert_str_eq!(sort_option.name, "Author Name"); + } + + #[test] + fn test_blocklist_sorting_options_source_title() { + let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = |a, b| { + a.source_title + .to_lowercase() + .cmp(&b.source_title.to_lowercase()) + }; + let mut expected_blocklist_vec = blocklist_vec(); + expected_blocklist_vec.sort_by(expected_cmp_fn); + + let sort_option = blocklist_sorting_options()[1].clone(); + let mut sorted_blocklist_vec = blocklist_vec(); + sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_blocklist_vec, expected_blocklist_vec); + assert_str_eq!(sort_option.name, "Source Title"); + } + + #[test] + fn test_blocklist_sorting_options_quality() { + let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = |a, b| { + a.quality + .quality + .name + .to_lowercase() + .cmp(&b.quality.quality.name.to_lowercase()) + }; + let mut expected_blocklist_vec = blocklist_vec(); + expected_blocklist_vec.sort_by(expected_cmp_fn); + + let sort_option = blocklist_sorting_options()[2].clone(); + let mut sorted_blocklist_vec = blocklist_vec(); + sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_blocklist_vec, expected_blocklist_vec); + assert_str_eq!(sort_option.name, "Quality"); + } + + #[test] + fn test_blocklist_sorting_options_date() { + let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = + |a, b| a.date.cmp(&b.date); + let mut expected_blocklist_vec = blocklist_vec(); + expected_blocklist_vec.sort_by(expected_cmp_fn); + + let sort_option = blocklist_sorting_options()[3].clone(); + let mut sorted_blocklist_vec = blocklist_vec(); + sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_blocklist_vec, expected_blocklist_vec); + assert_str_eq!(sort_option.name, "Date"); + } + + #[test] + fn test_blocklist_handler_accepts() { + ActiveReadarrBlock::iter().for_each(|active_readarr_block| { + if BLOCKLIST_BLOCKS.contains(&active_readarr_block) { + assert!(BlocklistHandler::accepts(active_readarr_block)); + } else { + assert!(!BlocklistHandler::accepts(active_readarr_block)); + } + }) + } + + #[rstest] + fn test_blocklist_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = BlocklistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_extract_blocklist_item_id() { + let mut app = App::test_default(); + app.data.readarr_data.blocklist.set_items(blocklist_vec()); + app.data.readarr_data.blocklist.select_index(Some(1)); + + let blocklist_item_id = BlocklistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::Blocklist, + None, + ) + .extract_blocklist_item_id(); + + assert_eq!(blocklist_item_id, 2); + } + + #[test] + fn test_blocklist_handler_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into()); + app.is_loading = true; + + let handler = BlocklistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::Blocklist, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_blocklist_handler_not_ready_when_blocklist_is_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into()); + app.is_loading = false; + + let handler = BlocklistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::Blocklist, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_blocklist_handler_ready_when_not_loading_and_blocklist_is_not_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::Blocklist.into()); + app.is_loading = false; + app + .data + .readarr_data + .blocklist + .set_items(vec![BlocklistItem::default()]); + + let handler = BlocklistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::Blocklist, + None, + ); + + assert!(handler.is_ready()); + } + + fn blocklist_vec() -> Vec { + vec![ + BlocklistItem { + id: 3, + source_title: "test 2".to_owned(), + quality: QualityWrapper { + quality: Quality { + name: "AZW3".to_owned(), + }, + }, + date: DateTime::from(DateTime::parse_from_rfc3339("2024-03-10T07:28:45Z").unwrap()), + author: Author { + author_name: "Zora Neale".into(), + ..Author::default() + }, + ..BlocklistItem::default() + }, + BlocklistItem { + id: 2, + source_title: "test 3".to_owned(), + quality: QualityWrapper { + quality: Quality { + name: "MOBI".to_owned(), + }, + }, + date: DateTime::from(DateTime::parse_from_rfc3339("2024-01-10T07:28:45Z").unwrap()), + author: Author { + author_name: "Mary Shelley".into(), + ..Author::default() + }, + ..BlocklistItem::default() + }, + BlocklistItem { + id: 1, + source_title: "test 1".to_owned(), + quality: QualityWrapper { + quality: Quality { + name: "EPUB".to_owned(), + }, + }, + date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()), + author: Author { + author_name: "".into(), + ..Author::default() + }, + ..BlocklistItem::default() + }, + ] + } +} diff --git a/src/handlers/readarr_handlers/blocklist/mod.rs b/src/handlers/readarr_handlers/blocklist/mod.rs new file mode 100644 index 00000000..c24243e2 --- /dev/null +++ b/src/handlers/readarr_handlers/blocklist/mod.rs @@ -0,0 +1,224 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::readarr_handlers::handle_change_tab_left_right_keys; +use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; +use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; +use crate::matches_key; +use crate::models::Route; +use crate::models::readarr_models::BlocklistItem; +use crate::models::servarr_data::readarr::readarr_data::{ActiveReadarrBlock, BLOCKLIST_BLOCKS}; +use crate::models::stateful_table::SortOption; +use crate::network::readarr_network::ReadarrEvent; + +#[cfg(test)] +#[path = "blocklist_handler_tests.rs"] +mod blocklist_handler_tests; + +pub(super) struct BlocklistHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_readarr_block: ActiveReadarrBlock, + _context: Option, +} + +impl BlocklistHandler<'_, '_> { + fn extract_blocklist_item_id(&self) -> i64 { + self.app.data.readarr_data.blocklist.current_selection().id + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for BlocklistHandler<'a, 'b> { + fn handle(&mut self) { + let blocklist_table_handling_config = + TableHandlingConfig::new(ActiveReadarrBlock::Blocklist.into()) + .sorting_block(ActiveReadarrBlock::BlocklistSortPrompt.into()) + .sort_options(blocklist_sorting_options()); + + if !handle_table( + self, + |app| &mut app.data.readarr_data.blocklist, + blocklist_table_handling_config, + ) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveReadarrBlock) -> bool { + BLOCKLIST_BLOCKS.contains(&active_block) + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveReadarrBlock, + context: Option, + ) -> Self { + BlocklistHandler { + key, + app, + active_readarr_block: active_block, + _context: context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && !self.app.data.readarr_data.blocklist.is_empty() + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) { + if self.active_readarr_block == ActiveReadarrBlock::Blocklist { + self + .app + .push_navigation_stack(ActiveReadarrBlock::DeleteBlocklistItemPrompt.into()); + } + } + + fn handle_left_right_action(&mut self) { + match self.active_readarr_block { + ActiveReadarrBlock::Blocklist => handle_change_tab_left_right_keys(self.app, self.key), + ActiveReadarrBlock::DeleteBlocklistItemPrompt + | ActiveReadarrBlock::BlocklistClearAllItemsPrompt => { + handle_prompt_toggle(self.app, self.key) + } + _ => {} + } + } + + fn handle_submit(&mut self) { + match self.active_readarr_block { + ActiveReadarrBlock::DeleteBlocklistItemPrompt => { + if self.app.data.readarr_data.prompt_confirm { + self.app.data.readarr_data.prompt_confirm_action = Some( + ReadarrEvent::DeleteBlocklistItem(self.extract_blocklist_item_id()), + ); + } + + self.app.pop_navigation_stack(); + } + ActiveReadarrBlock::BlocklistClearAllItemsPrompt => { + if self.app.data.readarr_data.prompt_confirm { + self.app.data.readarr_data.prompt_confirm_action = Some(ReadarrEvent::ClearBlocklist); + } + + self.app.pop_navigation_stack(); + } + ActiveReadarrBlock::Blocklist => { + self + .app + .push_navigation_stack(ActiveReadarrBlock::BlocklistItemDetails.into()); + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_readarr_block { + ActiveReadarrBlock::DeleteBlocklistItemPrompt + | ActiveReadarrBlock::BlocklistClearAllItemsPrompt => { + self.app.pop_navigation_stack(); + self.app.data.readarr_data.prompt_confirm = false; + } + ActiveReadarrBlock::BlocklistItemDetails | ActiveReadarrBlock::BlocklistSortPrompt => { + self.app.pop_navigation_stack(); + } + _ => handle_clear_errors(self.app), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_readarr_block { + ActiveReadarrBlock::Blocklist => match self.key { + _ if matches_key!(refresh, key) => { + self.app.should_refresh = true; + } + _ if matches_key!(clear, key) => { + self + .app + .push_navigation_stack(ActiveReadarrBlock::BlocklistClearAllItemsPrompt.into()); + } + _ => (), + }, + ActiveReadarrBlock::DeleteBlocklistItemPrompt => { + if matches_key!(confirm, key) { + self.app.data.readarr_data.prompt_confirm = true; + self.app.data.readarr_data.prompt_confirm_action = Some( + ReadarrEvent::DeleteBlocklistItem(self.extract_blocklist_item_id()), + ); + + self.app.pop_navigation_stack(); + } + } + ActiveReadarrBlock::BlocklistClearAllItemsPrompt => { + if matches_key!(confirm, key) { + self.app.data.readarr_data.prompt_confirm = true; + self.app.data.readarr_data.prompt_confirm_action = Some(ReadarrEvent::ClearBlocklist); + + self.app.pop_navigation_stack(); + } + } + _ => (), + } + } + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} + +fn blocklist_sorting_options() -> Vec> { + vec![ + SortOption { + name: "Author Name", + cmp_fn: Some(|a, b| { + a.author + .author_name + .text + .to_lowercase() + .cmp(&b.author.author_name.text.to_lowercase()) + }), + }, + SortOption { + name: "Source Title", + cmp_fn: Some(|a, b| { + a.source_title + .to_lowercase() + .cmp(&b.source_title.to_lowercase()) + }), + }, + SortOption { + name: "Quality", + cmp_fn: Some(|a, b| { + a.quality + .quality + .name + .to_lowercase() + .cmp(&b.quality.quality.name.to_lowercase()) + }), + }, + SortOption { + name: "Date", + cmp_fn: Some(|a, b| a.date.cmp(&b.date)), + }, + ] +} diff --git a/src/handlers/readarr_handlers/mod.rs b/src/handlers/readarr_handlers/mod.rs index 477598d5..2de29fc0 100644 --- a/src/handlers/readarr_handlers/mod.rs +++ b/src/handlers/readarr_handlers/mod.rs @@ -1,4 +1,5 @@ use super::KeyEventHandler; +use crate::handlers::readarr_handlers::blocklist::BlocklistHandler; use crate::handlers::readarr_handlers::library::LibraryHandler; use crate::models::Route; use crate::{ @@ -6,6 +7,7 @@ use crate::{ models::servarr_data::readarr::readarr_data::ActiveReadarrBlock, }; +mod blocklist; mod history; mod library; @@ -26,6 +28,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for ReadarrHandler<'a, _ if LibraryHandler::accepts(self.active_readarr_block) => { LibraryHandler::new(self.key, self.app, self.active_readarr_block, self.context).handle(); } + _ if BlocklistHandler::accepts(self.active_readarr_block) => { + BlocklistHandler::new(self.key, self.app, self.active_readarr_block, self.context).handle(); + } _ => self.handle_key_event(), } } diff --git a/src/handlers/readarr_handlers/readarr_handler_tests.rs b/src/handlers/readarr_handlers/readarr_handler_tests.rs index d467d874..d0806dd2 100644 --- a/src/handlers/readarr_handlers/readarr_handler_tests.rs +++ b/src/handlers/readarr_handlers/readarr_handler_tests.rs @@ -176,4 +176,22 @@ mod tests { active_readarr_block ); } + + #[rstest] + fn test_delegates_blocklist_blocks_to_blocklist_handler( + #[values( + ActiveReadarrBlock::Blocklist, + ActiveReadarrBlock::BlocklistItemDetails, + ActiveReadarrBlock::DeleteBlocklistItemPrompt, + ActiveReadarrBlock::BlocklistClearAllItemsPrompt, + ActiveReadarrBlock::BlocklistSortPrompt + )] + active_readarr_block: ActiveReadarrBlock, + ) { + test_handler_delegation!( + ReadarrHandler, + ActiveReadarrBlock::Blocklist, + active_readarr_block + ); + } }