diff --git a/src/handlers/readarr_handlers/library/book_details_handler.rs b/src/handlers/readarr_handlers/library/book_details_handler.rs index ee16c610..b49a60d9 100644 --- a/src/handlers/readarr_handlers/library/book_details_handler.rs +++ b/src/handlers/readarr_handlers/library/book_details_handler.rs @@ -13,6 +13,7 @@ use crate::network::readarr_network::ReadarrEvent; use indoc::formatdoc; use super::author_details_handler::releases_sorting_options; +use super::edition_details_handler::EditionDetailsHandler; #[cfg(test)] #[path = "book_details_handler_tests.rs"] @@ -133,6 +134,16 @@ impl BookDetailsHandler<'_, '_> { impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for BookDetailsHandler<'a, 'b> { fn handle(&mut self) { + if EditionDetailsHandler::accepts(self.active_readarr_block) { + return EditionDetailsHandler::new( + self.key, + self.app, + self.active_readarr_block, + self._context, + ) + .handle(); + } + let editions_table_handling_config = TableHandlingConfig::new(ActiveReadarrBlock::BookDetails.into()) .searching_block(ActiveReadarrBlock::SearchEditions.into()) @@ -197,7 +208,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for BookDetailsHandler< } fn accepts(active_block: ActiveReadarrBlock) -> bool { - BOOK_DETAILS_BLOCKS.contains(&active_block) + EditionDetailsHandler::accepts(active_block) || BOOK_DETAILS_BLOCKS.contains(&active_block) } fn ignore_special_keys(&self) -> bool { diff --git a/src/handlers/readarr_handlers/library/book_details_handler_tests.rs b/src/handlers/readarr_handlers/library/book_details_handler_tests.rs index bd43b874..886ebdef 100644 --- a/src/handlers/readarr_handlers/library/book_details_handler_tests.rs +++ b/src/handlers/readarr_handlers/library/book_details_handler_tests.rs @@ -13,9 +13,10 @@ mod tests { }; use crate::models::servarr_data::readarr::modals::BookDetailsModal; use crate::models::servarr_data::readarr::readarr_data::{ - ActiveReadarrBlock, BOOK_DETAILS_BLOCKS, + ActiveReadarrBlock, BOOK_DETAILS_BLOCKS, EDITION_DETAILS_BLOCKS, }; use crate::models::servarr_models::{Quality, QualityWrapper}; + use crate::test_handler_delegation; mod test_handle_scroll_up_and_down { use super::*; @@ -866,8 +867,12 @@ mod tests { #[test] fn test_book_details_handler_accepts() { + let mut book_details_handler_blocks = Vec::new(); + book_details_handler_blocks.extend(BOOK_DETAILS_BLOCKS); + book_details_handler_blocks.extend(EDITION_DETAILS_BLOCKS); + ActiveReadarrBlock::iter().for_each(|active_readarr_block| { - if BOOK_DETAILS_BLOCKS.contains(&active_readarr_block) { + if book_details_handler_blocks.contains(&active_readarr_block) { assert!( BookDetailsHandler::accepts(active_readarr_block), "{active_readarr_block} is not accepted by the BookDetailsHandler" @@ -881,6 +886,15 @@ mod tests { }); } + #[test] + fn test_delegates_edition_details_blocks_to_edition_details_handler() { + test_handler_delegation!( + BookDetailsHandler, + ActiveReadarrBlock::BookDetails, + ActiveReadarrBlock::BookEditionDetails + ); + } + #[test] fn test_book_details_handler_is_not_ready_when_loading() { let mut app = App::test_default(); diff --git a/src/handlers/readarr_handlers/library/edition_details_handler.rs b/src/handlers/readarr_handlers/library/edition_details_handler.rs new file mode 100644 index 00000000..9235b866 --- /dev/null +++ b/src/handlers/readarr_handlers/library/edition_details_handler.rs @@ -0,0 +1,125 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::KeyEventHandler; +use crate::matches_key; +use crate::models::servarr_data::readarr::readarr_data::{ + ActiveReadarrBlock, EDITION_DETAILS_BLOCKS, +}; +use crate::models::{Route, Scrollable, ScrollableText}; + +#[cfg(test)] +#[path = "edition_details_handler_tests.rs"] +mod edition_details_handler_tests; + +pub(super) struct EditionDetailsHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_readarr_block: ActiveReadarrBlock, + _context: Option, +} + +impl EditionDetailsHandler<'_, '_> { + fn edition_details(&mut self) -> &mut ScrollableText { + &mut self + .app + .data + .readarr_data + .book_details_modal + .as_mut() + .expect("Book details modal is undefined") + .edition_details_modal + .as_mut() + .expect("Edition details modal is undefined") + .edition_details + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for EditionDetailsHandler<'a, 'b> { + fn accepts(active_block: ActiveReadarrBlock) -> bool { + EDITION_DETAILS_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, + ) -> EditionDetailsHandler<'a, 'b> { + EditionDetailsHandler { + key, + app, + active_readarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + if self.app.is_loading { + return false; + } + + let Some(book_details_modal) = &self.app.data.readarr_data.book_details_modal else { + return false; + }; + + book_details_modal.edition_details_modal.is_some() + } + + fn handle_scroll_up(&mut self) { + self.edition_details().scroll_up(); + } + + fn handle_scroll_down(&mut self) { + self.edition_details().scroll_down(); + } + + fn handle_home(&mut self) { + self.edition_details().scroll_to_top(); + } + + fn handle_end(&mut self) { + self.edition_details().scroll_to_bottom(); + } + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) {} + + fn handle_submit(&mut self) {} + + fn handle_esc(&mut self) { + self.app.pop_navigation_stack(); + self + .app + .data + .readarr_data + .book_details_modal + .as_mut() + .expect("Book details modal is undefined") + .edition_details_modal = None; + } + + fn handle_char_key_event(&mut self) { + if matches_key!(refresh, self.key) { + self + .app + .pop_and_push_navigation_stack(self.active_readarr_block.into()); + } + } + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} diff --git a/src/handlers/readarr_handlers/library/edition_details_handler_tests.rs b/src/handlers/readarr_handlers/library/edition_details_handler_tests.rs new file mode 100644 index 00000000..f41215f3 --- /dev/null +++ b/src/handlers/readarr_handlers/library/edition_details_handler_tests.rs @@ -0,0 +1,420 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::handlers::KeyEventHandler; + use crate::handlers::readarr_handlers::library::edition_details_handler::EditionDetailsHandler; + use crate::models::ScrollableText; + use crate::models::servarr_data::readarr::modals::{BookDetailsModal, EditionDetailsModal}; + use crate::models::servarr_data::readarr::readarr_data::{ + ActiveReadarrBlock, EDITION_DETAILS_BLOCKS, + }; + + mod test_handle_scroll_up_and_down { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_edition_details_scroll_down() { + let mut app = app_with_edition_details(); + + EditionDetailsHandler::new( + DEFAULT_KEYBINDINGS.down.key, + &mut app, + ActiveReadarrBlock::BookEditionDetails, + None, + ) + .handle(); + + assert_eq!(edition_details_offset(&app), 1); + } + + #[test] + fn test_edition_details_scroll_up() { + let mut app = app_with_edition_details(); + set_offset(&mut app, 2); + + EditionDetailsHandler::new( + DEFAULT_KEYBINDINGS.up.key, + &mut app, + ActiveReadarrBlock::BookEditionDetails, + None, + ) + .handle(); + + assert_eq!(edition_details_offset(&app), 1); + } + + #[test] + fn test_edition_details_scroll_down_no_op_when_not_ready() { + let mut app = app_with_edition_details(); + app.is_loading = true; + + EditionDetailsHandler::new( + DEFAULT_KEYBINDINGS.down.key, + &mut app, + ActiveReadarrBlock::BookEditionDetails, + None, + ) + .handle(); + + assert_eq!(edition_details_offset(&app), 0); + } + + #[test] + fn test_edition_details_scroll_up_no_op_when_not_ready() { + let mut app = app_with_edition_details(); + set_offset(&mut app, 2); + app.is_loading = true; + + EditionDetailsHandler::new( + DEFAULT_KEYBINDINGS.up.key, + &mut app, + ActiveReadarrBlock::BookEditionDetails, + None, + ) + .handle(); + + assert_eq!(edition_details_offset(&app), 2); + } + + #[test] + fn test_edition_details_scrolls_past_the_first_screen() { + let mut app = app_with_edition_details(); + + for _ in 0..4 { + EditionDetailsHandler::new( + DEFAULT_KEYBINDINGS.down.key, + &mut app, + ActiveReadarrBlock::BookEditionDetails, + None, + ) + .handle(); + } + + assert_eq!(edition_details_offset(&app), 4); + } + } + + mod test_handle_home_end { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_edition_details_end() { + let mut app = app_with_edition_details(); + + EditionDetailsHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveReadarrBlock::BookEditionDetails, + None, + ) + .handle(); + + assert_eq!(edition_details_offset(&app), 5); + } + + #[test] + fn test_edition_details_home() { + let mut app = app_with_edition_details(); + set_offset(&mut app, 4); + + EditionDetailsHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveReadarrBlock::BookEditionDetails, + None, + ) + .handle(); + + assert_eq!(edition_details_offset(&app), 0); + } + + #[test] + fn test_edition_details_end_no_op_when_not_ready() { + let mut app = app_with_edition_details(); + app.is_loading = true; + + EditionDetailsHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveReadarrBlock::BookEditionDetails, + None, + ) + .handle(); + + assert_eq!(edition_details_offset(&app), 0); + } + + #[test] + fn test_edition_details_home_no_op_when_not_ready() { + let mut app = app_with_edition_details(); + set_offset(&mut app, 4); + app.is_loading = true; + + EditionDetailsHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveReadarrBlock::BookEditionDetails, + None, + ) + .handle(); + + assert_eq!(edition_details_offset(&app), 4); + } + } + + mod test_handle_esc { + use super::*; + use crate::{assert_modal_absent, assert_modal_present, assert_navigation_popped}; + + #[test] + fn test_edition_details_esc_tears_down_the_edition_details_modal() { + let mut app = app_with_edition_details(); + + EditionDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::BookEditionDetails, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::BookDetails.into()); + assert_modal_absent!( + app + .data + .readarr_data + .book_details_modal + .as_ref() + .unwrap() + .edition_details_modal + ); + assert_modal_present!(app.data.readarr_data.book_details_modal); + } + + #[test] + fn test_edition_details_esc_tears_down_the_modal_even_when_loading() { + let mut app = app_with_edition_details(); + app.is_loading = true; + + EditionDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::BookEditionDetails, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::BookDetails.into()); + assert_modal_absent!( + app + .data + .readarr_data + .book_details_modal + .as_ref() + .unwrap() + .edition_details_modal + ); + } + } + + mod test_handle_key_char { + use super::*; + use crate::assert_navigation_pushed; + + #[test] + fn test_refresh_key() { + let mut app = app_with_edition_details(); + + EditionDetailsHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveReadarrBlock::BookEditionDetails, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveReadarrBlock::BookEditionDetails.into()); + } + + #[test] + fn test_refresh_key_no_op_when_not_ready() { + let mut app = app_with_edition_details(); + app.is_loading = true; + app.push_navigation_stack(ActiveReadarrBlock::BookHistory.into()); + + EditionDetailsHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveReadarrBlock::BookEditionDetails, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveReadarrBlock::BookHistory.into()); + } + } + + #[test] + fn test_edition_details_handler_accepts() { + ActiveReadarrBlock::iter().for_each(|active_readarr_block| { + if EDITION_DETAILS_BLOCKS.contains(&active_readarr_block) { + assert!( + EditionDetailsHandler::accepts(active_readarr_block), + "{active_readarr_block} is not accepted by the EditionDetailsHandler" + ); + } else { + assert!( + !EditionDetailsHandler::accepts(active_readarr_block), + "{active_readarr_block} is wrongly accepted by the EditionDetailsHandler" + ); + } + }); + } + + #[test] + fn test_edition_details_handler_dispatches_no_network_events() { + let mut app = app_with_edition_details(); + + for key in [ + DEFAULT_KEYBINDINGS.up.key, + DEFAULT_KEYBINDINGS.down.key, + DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.delete.key, + DEFAULT_KEYBINDINGS.left.key, + DEFAULT_KEYBINDINGS.right.key, + DEFAULT_KEYBINDINGS.submit.key, + DEFAULT_KEYBINDINGS.refresh.key, + ] { + EditionDetailsHandler::new(key, &mut app, ActiveReadarrBlock::BookEditionDetails, None) + .handle(); + + assert_none!(app.data.readarr_data.prompt_confirm_action); + assert!(!app.data.readarr_data.prompt_confirm); + } + } + + #[test] + fn test_edition_details_handler_is_not_ready_when_loading() { + let mut app = app_with_edition_details(); + app.is_loading = true; + + let handler = EditionDetailsHandler::new( + DEFAULT_KEYBINDINGS.up.key, + &mut app, + ActiveReadarrBlock::BookEditionDetails, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_edition_details_handler_is_not_ready_when_the_book_details_modal_is_absent() { + let mut app = App::test_default(); + + let handler = EditionDetailsHandler::new( + DEFAULT_KEYBINDINGS.up.key, + &mut app, + ActiveReadarrBlock::BookEditionDetails, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_edition_details_handler_is_not_ready_when_the_edition_details_modal_is_absent() { + let mut app = App::test_default(); + app.data.readarr_data.book_details_modal = Some(BookDetailsModal::default()); + + let handler = EditionDetailsHandler::new( + DEFAULT_KEYBINDINGS.up.key, + &mut app, + ActiveReadarrBlock::BookEditionDetails, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_edition_details_handler_is_ready() { + let mut app = app_with_edition_details(); + + let handler = EditionDetailsHandler::new( + DEFAULT_KEYBINDINGS.up.key, + &mut app, + ActiveReadarrBlock::BookEditionDetails, + None, + ); + + assert!(handler.is_ready()); + } + + #[test] + fn test_edition_details_handler_ignores_special_keys_for_textbox_input() { + let mut app = app_with_edition_details(); + app.ignore_special_keys_for_textbox_input = true; + + let handler = EditionDetailsHandler::new( + DEFAULT_KEYBINDINGS.up.key, + &mut app, + ActiveReadarrBlock::BookEditionDetails, + None, + ); + + assert!(handler.ignore_special_keys()); + } + + fn app_with_edition_details() -> App<'static> { + let mut app = App::test_default(); + let book_details_modal = BookDetailsModal { + edition_details_modal: Some(EditionDetailsModal { + edition_details: ScrollableText::with_string( + "Title: Test Edition\nFormat: Paperback\nLanguage: eng\nPublisher: Test\nPage Count: 662\nOverview: A long overview" + .to_owned(), + ), + }), + ..BookDetailsModal::default() + }; + app.data.readarr_data.book_details_modal = Some(book_details_modal); + app.push_navigation_stack(ActiveReadarrBlock::BookDetails.into()); + app.push_navigation_stack(ActiveReadarrBlock::BookEditionDetails.into()); + + app + } + + fn edition_details_offset(app: &App<'_>) -> u16 { + app + .data + .readarr_data + .book_details_modal + .as_ref() + .unwrap() + .edition_details_modal + .as_ref() + .unwrap() + .edition_details + .offset + } + + fn set_offset(app: &mut App<'_>, offset: u16) { + app + .data + .readarr_data + .book_details_modal + .as_mut() + .unwrap() + .edition_details_modal + .as_mut() + .unwrap() + .edition_details + .offset = offset; + } +} diff --git a/src/handlers/readarr_handlers/library/library_handler_tests.rs b/src/handlers/readarr_handlers/library/library_handler_tests.rs index f2af8d74..f474ecd6 100644 --- a/src/handlers/readarr_handlers/library/library_handler_tests.rs +++ b/src/handlers/readarr_handlers/library/library_handler_tests.rs @@ -14,7 +14,8 @@ mod tests { use crate::models::HorizontallyScrollableText; use crate::models::readarr_models::{Author, AuthorStatistics, AuthorStatus}; use crate::models::servarr_data::readarr::readarr_data::{ - AUTHOR_DETAILS_BLOCKS, ActiveReadarrBlock, BOOK_DETAILS_BLOCKS, LIBRARY_BLOCKS, + AUTHOR_DETAILS_BLOCKS, ActiveReadarrBlock, BOOK_DETAILS_BLOCKS, EDITION_DETAILS_BLOCKS, + LIBRARY_BLOCKS, }; use crate::test_handler_delegation; @@ -894,6 +895,7 @@ mod tests { library_handler_blocks.extend(LIBRARY_BLOCKS); library_handler_blocks.extend(AUTHOR_DETAILS_BLOCKS); library_handler_blocks.extend(BOOK_DETAILS_BLOCKS); + library_handler_blocks.extend(EDITION_DETAILS_BLOCKS); ActiveReadarrBlock::iter().for_each(|readarr_block| { if library_handler_blocks.contains(&readarr_block) { diff --git a/src/handlers/readarr_handlers/library/mod.rs b/src/handlers/readarr_handlers/library/mod.rs index 9dd3fe36..d4093bae 100644 --- a/src/handlers/readarr_handlers/library/mod.rs +++ b/src/handlers/readarr_handlers/library/mod.rs @@ -21,6 +21,7 @@ use crate::models::Route; mod author_details_handler; mod book_details_handler; +mod edition_details_handler; pub(in crate::handlers::readarr_handlers) use { author_details_handler::AuthorDetailsHandler, book_details_handler::BookDetailsHandler,