diff --git a/src/handlers/readarr_handlers/library/author_details_handler.rs b/src/handlers/readarr_handlers/library/author_details_handler.rs index d10758a8..26fa82a8 100644 --- a/src/handlers/readarr_handlers/library/author_details_handler.rs +++ b/src/handlers/readarr_handlers/library/author_details_handler.rs @@ -389,7 +389,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for AuthorDetailsHandle } } -fn releases_sorting_options() -> Vec> { +pub(in crate::handlers::readarr_handlers::library) fn releases_sorting_options() +-> Vec> { vec![ SortOption { name: "Source", diff --git a/src/handlers/readarr_handlers/library/book_details_handler.rs b/src/handlers/readarr_handlers/library/book_details_handler.rs new file mode 100644 index 00000000..ee16c610 --- /dev/null +++ b/src/handlers/readarr_handlers/library/book_details_handler.rs @@ -0,0 +1,492 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::readarr_handlers::history::history_sorting_options; +use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; +use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; +use crate::matches_key; +use crate::models::readarr_models::{Edition, Ratings, ReadarrHistoryItem, ReadarrRelease}; +use crate::models::servarr_data::readarr::modals::EditionDetailsModal; +use crate::models::servarr_data::readarr::readarr_data::{ActiveReadarrBlock, BOOK_DETAILS_BLOCKS}; +use crate::models::servarr_models::ReleaseDownloadBody; +use crate::models::{Route, ScrollableText}; +use crate::network::readarr_network::ReadarrEvent; +use indoc::formatdoc; + +use super::author_details_handler::releases_sorting_options; + +#[cfg(test)] +#[path = "book_details_handler_tests.rs"] +mod book_details_handler_tests; + +pub(in crate::handlers::readarr_handlers) struct BookDetailsHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_readarr_block: ActiveReadarrBlock, + _context: Option, +} + +impl BookDetailsHandler<'_, '_> { + fn extract_book_id(&self) -> i64 { + self.app.data.readarr_data.books.current_selection().id + } + + fn extract_book_file_id(&self) -> i64 { + self + .app + .data + .readarr_data + .book_details_modal + .as_ref() + .expect("Book details modal is undefined") + .book_files + .current_selection() + .id + } + + fn book_files_are_populated(&self) -> bool { + self + .app + .data + .readarr_data + .book_details_modal + .as_ref() + .is_some_and(|modal| !modal.book_files.is_empty()) + } + + fn editions_are_populated(&self) -> bool { + self + .app + .data + .readarr_data + .book_details_modal + .as_ref() + .is_some_and(|modal| !modal.editions.is_empty()) + } + + fn build_edition_details_modal(&mut self) { + let Edition { + title, + overview, + format, + publisher, + page_count, + release_date, + isbn13, + asin, + ratings, + language, + monitored, + .. + } = self + .app + .data + .readarr_data + .book_details_modal + .as_ref() + .expect("Book details modal is undefined") + .editions + .current_selection() + .clone(); + let format = format.unwrap_or_default(); + let language = language.unwrap_or_default(); + let publisher = publisher.unwrap_or_default(); + let page_count = page_count + .map(|count| count.to_string()) + .unwrap_or_default(); + let release_date = release_date + .map(|date| date.to_string()) + .unwrap_or_default(); + let isbn13 = isbn13.unwrap_or_default(); + let asin = asin.unwrap_or_default(); + let ratings = ratings + .map(|Ratings { votes, value, .. }| format!("{value} ({votes} votes)")) + .unwrap_or_default(); + let overview = overview.unwrap_or_default(); + let details = formatdoc!( + " + Title: {title} + Format: {format} + Language: {language} + Publisher: {publisher} + Page Count: {page_count} + Release Date: {release_date} + ISBN13: {isbn13} + ASIN: {asin} + Ratings: {ratings} + Monitored: {monitored} + Overview: {overview} + " + ); + + self + .app + .data + .readarr_data + .book_details_modal + .as_mut() + .expect("Book details modal is undefined") + .edition_details_modal = Some(EditionDetailsModal { + edition_details: ScrollableText::with_string(details), + }); + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for BookDetailsHandler<'a, 'b> { + fn handle(&mut self) { + let editions_table_handling_config = + TableHandlingConfig::new(ActiveReadarrBlock::BookDetails.into()) + .searching_block(ActiveReadarrBlock::SearchEditions.into()) + .search_error_block(ActiveReadarrBlock::SearchEditionsError.into()) + .search_field_fn(|edition: &Edition| &edition.title); + + let book_history_table_handling_config = + TableHandlingConfig::new(ActiveReadarrBlock::BookHistory.into()) + .sorting_block(ActiveReadarrBlock::BookHistorySortPrompt.into()) + .sort_options(history_sorting_options()) + .searching_block(ActiveReadarrBlock::SearchBookHistory.into()) + .search_error_block(ActiveReadarrBlock::SearchBookHistoryError.into()) + .search_field_fn(|history_item: &ReadarrHistoryItem| &history_item.source_title.text) + .filtering_block(ActiveReadarrBlock::FilterBookHistory.into()) + .filter_error_block(ActiveReadarrBlock::FilterBookHistoryError.into()) + .filter_field_fn(|history_item: &ReadarrHistoryItem| &history_item.source_title.text); + + let book_releases_table_handling_config = + TableHandlingConfig::new(ActiveReadarrBlock::ManualBookSearch.into()) + .sorting_block(ActiveReadarrBlock::ManualBookSearchSortPrompt.into()) + .sort_options(releases_sorting_options()); + + if !handle_table( + self, + |app| { + &mut app + .data + .readarr_data + .book_details_modal + .as_mut() + .expect("Book details modal is undefined") + .editions + }, + editions_table_handling_config, + ) && !handle_table( + self, + |app| { + &mut app + .data + .readarr_data + .book_details_modal + .as_mut() + .expect("Book details modal is undefined") + .book_history + }, + book_history_table_handling_config, + ) && !handle_table( + self, + |app| { + &mut app + .data + .readarr_data + .book_details_modal + .as_mut() + .expect("Book details modal is undefined") + .book_releases + }, + book_releases_table_handling_config, + ) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveReadarrBlock) -> bool { + BOOK_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, + ) -> BookDetailsHandler<'a, 'b> { + BookDetailsHandler { + 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; + }; + + match self.active_readarr_block { + ActiveReadarrBlock::BookDetails => !book_details_modal.editions.is_empty(), + ActiveReadarrBlock::BookHistory => !book_details_modal.book_history.is_empty(), + ActiveReadarrBlock::BookFileInfo => !book_details_modal.book_files.is_empty(), + ActiveReadarrBlock::ManualBookSearch => !book_details_modal.book_releases.is_empty(), + _ => true, + } + } + + 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::BookDetails + && self.book_files_are_populated() + { + self + .app + .push_navigation_stack(ActiveReadarrBlock::DeleteBookFilePrompt.into()); + } + } + + fn handle_left_right_action(&mut self) { + match self.active_readarr_block { + ActiveReadarrBlock::BookDetails + | ActiveReadarrBlock::BookHistory + | ActiveReadarrBlock::BookFileInfo + | ActiveReadarrBlock::ManualBookSearch => match self.key { + _ if matches_key!(left, self.key) => { + self + .app + .data + .readarr_data + .book_details_modal + .as_mut() + .expect("Book details modal is undefined") + .book_details_tabs + .previous(); + self.app.pop_and_push_navigation_stack( + self + .app + .data + .readarr_data + .book_details_modal + .as_ref() + .expect("Book details modal is undefined") + .book_details_tabs + .get_active_route(), + ); + } + _ if matches_key!(right, self.key) => { + self + .app + .data + .readarr_data + .book_details_modal + .as_mut() + .expect("Book details modal is undefined") + .book_details_tabs + .next(); + self.app.pop_and_push_navigation_stack( + self + .app + .data + .readarr_data + .book_details_modal + .as_ref() + .expect("Book details modal is undefined") + .book_details_tabs + .get_active_route(), + ); + } + _ => (), + }, + ActiveReadarrBlock::AutomaticallySearchBookPrompt + | ActiveReadarrBlock::ManualBookSearchConfirmPrompt + | ActiveReadarrBlock::DeleteBookFilePrompt => { + handle_prompt_toggle(self.app, self.key); + } + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_readarr_block { + ActiveReadarrBlock::BookDetails if self.editions_are_populated() => { + self.build_edition_details_modal(); + self + .app + .push_navigation_stack(ActiveReadarrBlock::BookEditionDetails.into()); + } + ActiveReadarrBlock::BookHistory => self + .app + .push_navigation_stack(ActiveReadarrBlock::BookHistoryDetails.into()), + ActiveReadarrBlock::ManualBookSearch => { + self + .app + .push_navigation_stack(ActiveReadarrBlock::ManualBookSearchConfirmPrompt.into()); + } + ActiveReadarrBlock::ManualBookSearchConfirmPrompt => { + if self.app.data.readarr_data.prompt_confirm { + let ReadarrRelease { + guid, indexer_id, .. + } = self + .app + .data + .readarr_data + .book_details_modal + .as_ref() + .expect("Book details modal is undefined") + .book_releases + .current_selection() + .clone(); + let params = ReleaseDownloadBody { guid, indexer_id }; + self.app.data.readarr_data.prompt_confirm_action = + Some(ReadarrEvent::DownloadRelease(params)); + } + + self.app.pop_navigation_stack(); + } + ActiveReadarrBlock::AutomaticallySearchBookPrompt => { + if self.app.data.readarr_data.prompt_confirm { + self.app.data.readarr_data.prompt_confirm_action = Some( + ReadarrEvent::TriggerAutomaticBookSearch(self.extract_book_id()), + ); + } + + self.app.pop_navigation_stack(); + } + ActiveReadarrBlock::DeleteBookFilePrompt => { + if self.app.data.readarr_data.prompt_confirm { + self.app.data.readarr_data.prompt_confirm_action = + Some(ReadarrEvent::DeleteBookFile(self.extract_book_file_id())); + } + + self.app.pop_navigation_stack(); + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_readarr_block { + ActiveReadarrBlock::BookDetails + | ActiveReadarrBlock::BookFileInfo + | ActiveReadarrBlock::ManualBookSearch => { + self.app.pop_navigation_stack(); + self.app.data.readarr_data.book_details_modal = None; + } + ActiveReadarrBlock::BookHistoryDetails => { + self.app.pop_navigation_stack(); + } + ActiveReadarrBlock::BookHistory => { + if self + .app + .data + .readarr_data + .book_details_modal + .as_ref() + .expect("Book details modal is undefined") + .book_history + .filtered_items + .is_some() + { + self + .app + .data + .readarr_data + .book_details_modal + .as_mut() + .expect("Book details modal is undefined") + .book_history + .reset_filter(); + } else { + self.app.pop_navigation_stack(); + self.app.data.readarr_data.book_details_modal = None; + } + } + ActiveReadarrBlock::AutomaticallySearchBookPrompt + | ActiveReadarrBlock::ManualBookSearchConfirmPrompt + | ActiveReadarrBlock::DeleteBookFilePrompt => { + self.app.pop_navigation_stack(); + self.app.data.readarr_data.prompt_confirm = false; + } + _ => (), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_readarr_block { + ActiveReadarrBlock::BookDetails + | ActiveReadarrBlock::BookHistory + | ActiveReadarrBlock::BookFileInfo + | ActiveReadarrBlock::ManualBookSearch => match key { + _ if matches_key!(refresh, key) => { + self + .app + .pop_and_push_navigation_stack(self.active_readarr_block.into()); + } + _ if matches_key!(auto_search, key) => { + self + .app + .push_navigation_stack(ActiveReadarrBlock::AutomaticallySearchBookPrompt.into()); + } + _ => (), + }, + ActiveReadarrBlock::AutomaticallySearchBookPrompt if matches_key!(confirm, key) => { + self.app.data.readarr_data.prompt_confirm = true; + self.app.data.readarr_data.prompt_confirm_action = Some( + ReadarrEvent::TriggerAutomaticBookSearch(self.extract_book_id()), + ); + + self.app.pop_navigation_stack(); + } + ActiveReadarrBlock::DeleteBookFilePrompt if matches_key!(confirm, key) => { + self.app.data.readarr_data.prompt_confirm = true; + self.app.data.readarr_data.prompt_confirm_action = + Some(ReadarrEvent::DeleteBookFile(self.extract_book_file_id())); + + self.app.pop_navigation_stack(); + } + ActiveReadarrBlock::ManualBookSearchConfirmPrompt if matches_key!(confirm, key) => { + self.app.data.readarr_data.prompt_confirm = true; + let ReadarrRelease { + guid, indexer_id, .. + } = self + .app + .data + .readarr_data + .book_details_modal + .as_ref() + .expect("Book details modal is undefined") + .book_releases + .current_selection() + .clone(); + let params = ReleaseDownloadBody { guid, indexer_id }; + self.app.data.readarr_data.prompt_confirm_action = + Some(ReadarrEvent::DownloadRelease(params)); + + 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() + } +} diff --git a/src/handlers/readarr_handlers/library/book_details_handler_tests.rs b/src/handlers/readarr_handlers/library/book_details_handler_tests.rs new file mode 100644 index 00000000..bd43b874 --- /dev/null +++ b/src/handlers/readarr_handlers/library/book_details_handler_tests.rs @@ -0,0 +1,1084 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::readarr_handlers::library::book_details_handler::BookDetailsHandler; + use crate::models::HorizontallyScrollableText; + use crate::models::readarr_models::{ + Book, BookFile, Edition, Ratings, ReadarrHistoryItem, ReadarrRelease, + }; + use crate::models::servarr_data::readarr::modals::BookDetailsModal; + use crate::models::servarr_data::readarr::readarr_data::{ + ActiveReadarrBlock, BOOK_DETAILS_BLOCKS, + }; + use crate::models::servarr_models::{Quality, QualityWrapper}; + + mod test_handle_scroll_up_and_down { + use super::*; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::event::Key; + use pretty_assertions::assert_str_eq; + use rstest::rstest; + + #[rstest] + fn test_editions_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::test_default(); + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::BookDetails.into()); + + BookDetailsHandler::new(key, &mut app, ActiveReadarrBlock::BookDetails, None).handle(); + + assert_str_eq!(selected_edition(&app).title, "Test Edition 2"); + } + + #[rstest] + fn test_editions_scroll_no_op_when_not_ready( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::test_default(); + app.is_loading = true; + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::BookDetails.into()); + + BookDetailsHandler::new(key, &mut app, ActiveReadarrBlock::BookDetails, None).handle(); + + assert_str_eq!(selected_edition(&app).title, "Test Edition 1"); + } + + #[rstest] + fn test_book_history_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::test_default(); + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::BookHistory.into()); + + BookDetailsHandler::new(key, &mut app, ActiveReadarrBlock::BookHistory, None).handle(); + + assert_str_eq!( + app + .data + .readarr_data + .book_details_modal + .as_ref() + .unwrap() + .book_history + .current_selection() + .source_title + .text, + "Test History 2" + ); + } + + #[rstest] + fn test_book_releases_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::test_default(); + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::ManualBookSearch.into()); + + BookDetailsHandler::new(key, &mut app, ActiveReadarrBlock::ManualBookSearch, None).handle(); + + assert_str_eq!( + app + .data + .readarr_data + .book_details_modal + .as_ref() + .unwrap() + .book_releases + .current_selection() + .title + .text, + "Test Release 2" + ); + } + } + + mod test_handle_home_end { + use super::*; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use pretty_assertions::assert_str_eq; + + #[test] + fn test_editions_home_end() { + let mut app = App::test_default(); + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::BookDetails.into()); + + BookDetailsHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveReadarrBlock::BookDetails, + None, + ) + .handle(); + + assert_str_eq!(selected_edition(&app).title, "Test Edition 2"); + + BookDetailsHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveReadarrBlock::BookDetails, + None, + ) + .handle(); + + assert_str_eq!(selected_edition(&app).title, "Test Edition 1"); + } + + #[test] + fn test_editions_home_end_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::BookDetails.into()); + + BookDetailsHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveReadarrBlock::BookDetails, + None, + ) + .handle(); + + assert_str_eq!(selected_edition(&app).title, "Test Edition 1"); + } + } + + mod test_handle_delete { + use super::*; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_navigation_pushed; + use crate::event::Key; + + const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key; + + #[test] + fn test_delete_book_file_prompt() { + let mut app = App::test_default(); + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::BookDetails.into()); + + BookDetailsHandler::new(DELETE_KEY, &mut app, ActiveReadarrBlock::BookDetails, None).handle(); + + assert_navigation_pushed!(app, ActiveReadarrBlock::DeleteBookFilePrompt.into()); + } + + #[test] + fn test_delete_book_file_prompt_is_a_no_op_when_book_files_are_empty() { + let mut app = App::test_default(); + let mut modal = book_details_modal(); + modal.book_files.set_items(Vec::new()); + app.data.readarr_data.book_details_modal = Some(modal); + app.push_navigation_stack(ActiveReadarrBlock::BookDetails.into()); + + BookDetailsHandler::new(DELETE_KEY, &mut app, ActiveReadarrBlock::BookDetails, None).handle(); + + assert_navigation_pushed!(app, ActiveReadarrBlock::BookDetails.into()); + } + + #[test] + fn test_delete_book_file_prompt_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::BookDetails.into()); + + BookDetailsHandler::new(DELETE_KEY, &mut app, ActiveReadarrBlock::BookDetails, None).handle(); + + assert_navigation_pushed!(app, ActiveReadarrBlock::BookDetails.into()); + } + } + + mod test_handle_left_right_action { + use super::*; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_navigation_pushed; + use crate::event::Key; + use rstest::rstest; + + #[rstest] + #[case(ActiveReadarrBlock::BookDetails, ActiveReadarrBlock::BookHistory)] + #[case(ActiveReadarrBlock::BookHistory, ActiveReadarrBlock::ManualBookSearch)] + #[case(ActiveReadarrBlock::ManualBookSearch, ActiveReadarrBlock::BookDetails)] + fn test_book_details_tabs_left_right_action( + #[case] left_block: ActiveReadarrBlock, + #[case] right_block: ActiveReadarrBlock, + #[values(true, false)] is_loading: bool, + ) { + let mut app = App::test_default_fully_populated(); + app.is_loading = is_loading; + app.push_navigation_stack(right_block.into()); + let tab_index = app + .data + .readarr_data + .book_details_modal + .as_ref() + .unwrap() + .book_details_tabs + .tabs + .iter() + .position(|tab_route| tab_route.route == right_block.into()) + .unwrap_or_default(); + app + .data + .readarr_data + .book_details_modal + .as_mut() + .unwrap() + .book_details_tabs + .index = tab_index; + + BookDetailsHandler::new(DEFAULT_KEYBINDINGS.left.key, &mut app, right_block, None).handle(); + + assert_navigation_pushed!(app, left_block.into()); + + BookDetailsHandler::new(DEFAULT_KEYBINDINGS.right.key, &mut app, left_block, None).handle(); + + assert_navigation_pushed!(app, right_block.into()); + } + + #[rstest] + fn test_left_right_prompt_toggle( + #[values( + ActiveReadarrBlock::AutomaticallySearchBookPrompt, + ActiveReadarrBlock::ManualBookSearchConfirmPrompt, + ActiveReadarrBlock::DeleteBookFilePrompt + )] + active_readarr_block: ActiveReadarrBlock, + #[values(Key::Left, Key::Right)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(active_readarr_block.into()); + + BookDetailsHandler::new(key, &mut app, active_readarr_block, None).handle(); + + assert!(app.data.readarr_data.prompt_confirm); + + BookDetailsHandler::new(key, &mut app, active_readarr_block, None).handle(); + + assert!(!app.data.readarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use super::*; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::event::Key; + use crate::models::servarr_models::ReleaseDownloadBody; + use crate::network::readarr_network::ReadarrEvent; + use crate::{assert_modal_absent, assert_modal_present, assert_navigation_pushed}; + use pretty_assertions::assert_eq; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_book_details_submit_builds_the_edition_details_modal_and_pushes_the_popup() { + let mut app = App::test_default(); + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::BookDetails.into()); + + BookDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveReadarrBlock::BookDetails, None).handle(); + + assert_navigation_pushed!(app, ActiveReadarrBlock::BookEditionDetails.into()); + assert_modal_present!( + app + .data + .readarr_data + .book_details_modal + .as_ref() + .unwrap() + .edition_details_modal + ); + assert!(edition_details_text(&app).contains("Title: Test Edition 1")); + assert!(edition_details_text(&app).contains("Format: Paperback")); + assert!(edition_details_text(&app).contains("ISBN13: isbn-1")); + assert!(edition_details_text(&app).contains("ASIN: asin-1")); + assert!(edition_details_text(&app).contains("Overview: Overview 1")); + } + + #[test] + fn test_book_details_submit_dispatches_no_network_event() { + let mut app = App::test_default(); + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::BookDetails.into()); + + BookDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveReadarrBlock::BookDetails, None).handle(); + + assert_none!(app.data.readarr_data.prompt_confirm_action); + } + + #[test] + fn test_book_details_submit_rebuilds_the_modal_when_a_different_edition_is_selected() { + let mut app = App::test_default(); + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::BookDetails.into()); + + BookDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveReadarrBlock::BookDetails, None).handle(); + + assert!(edition_details_text(&app).contains("Title: Test Edition 1")); + assert!(edition_details_text(&app).contains("Format: Paperback")); + + app.pop_navigation_stack(); + BookDetailsHandler::new( + DEFAULT_KEYBINDINGS.down.key, + &mut app, + ActiveReadarrBlock::BookDetails, + None, + ) + .handle(); + + BookDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveReadarrBlock::BookDetails, None).handle(); + + assert!(edition_details_text(&app).contains("Title: Test Edition 2")); + assert!(edition_details_text(&app).contains("Format: Hardcover")); + assert!(edition_details_text(&app).contains("ISBN13: isbn-2")); + assert!(!edition_details_text(&app).contains("Test Edition 1")); + } + + #[test] + fn test_book_details_submit_is_a_no_op_when_the_editions_table_is_empty() { + let mut app = App::test_default(); + let mut modal = book_details_modal(); + modal.editions.set_items(Vec::new()); + app.data.readarr_data.book_details_modal = Some(modal); + app.push_navigation_stack(ActiveReadarrBlock::BookDetails.into()); + + BookDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveReadarrBlock::BookDetails, None).handle(); + + assert_navigation_pushed!(app, ActiveReadarrBlock::BookDetails.into()); + assert_modal_absent!( + app + .data + .readarr_data + .book_details_modal + .as_ref() + .unwrap() + .edition_details_modal + ); + } + + #[test] + fn test_book_details_submit_is_a_no_op_when_the_book_details_modal_is_absent() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::BookDetails.into()); + + BookDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveReadarrBlock::BookDetails, None).handle(); + + assert_navigation_pushed!(app, ActiveReadarrBlock::BookDetails.into()); + assert_modal_absent!(app.data.readarr_data.book_details_modal); + } + + #[test] + fn test_book_details_submit_with_a_single_edition() { + let mut app = App::test_default(); + let mut modal = book_details_modal(); + modal.editions.set_items(vec![editions_vec()[0].clone()]); + app.data.readarr_data.book_details_modal = Some(modal); + app.push_navigation_stack(ActiveReadarrBlock::BookDetails.into()); + + BookDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveReadarrBlock::BookDetails, None).handle(); + + assert_navigation_pushed!(app, ActiveReadarrBlock::BookEditionDetails.into()); + assert!(edition_details_text(&app).contains("Title: Test Edition 1")); + } + + #[test] + fn test_book_details_submit_renders_every_optional_field_when_absent() { + let mut app = App::test_default(); + let mut modal = book_details_modal(); + modal.editions.set_items(vec![Edition { + title: "Bare Edition".to_owned(), + ..Edition::default() + }]); + app.data.readarr_data.book_details_modal = Some(modal); + app.push_navigation_stack(ActiveReadarrBlock::BookDetails.into()); + + BookDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveReadarrBlock::BookDetails, None).handle(); + + assert_navigation_pushed!(app, ActiveReadarrBlock::BookEditionDetails.into()); + assert!(edition_details_text(&app).contains("Title: Bare Edition")); + assert!(edition_details_text(&app).contains("Format: \n")); + assert!(edition_details_text(&app).contains("Ratings: \n")); + } + + #[test] + fn test_book_details_submit_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::BookDetails.into()); + + BookDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveReadarrBlock::BookDetails, None).handle(); + + assert_navigation_pushed!(app, ActiveReadarrBlock::BookDetails.into()); + assert_modal_absent!( + app + .data + .readarr_data + .book_details_modal + .as_ref() + .unwrap() + .edition_details_modal + ); + } + + #[test] + fn test_book_history_submit() { + let mut app = App::test_default(); + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::BookHistory.into()); + + BookDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveReadarrBlock::BookHistory, None).handle(); + + assert_navigation_pushed!(app, ActiveReadarrBlock::BookHistoryDetails.into()); + } + + #[test] + fn test_book_history_submit_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::BookHistory.into()); + + BookDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveReadarrBlock::BookHistory, None).handle(); + + assert_navigation_pushed!(app, ActiveReadarrBlock::BookHistory.into()); + } + + #[test] + fn test_manual_book_search_submit() { + let mut app = App::test_default(); + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::ManualBookSearch.into()); + + BookDetailsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::ManualBookSearch, + None, + ) + .handle(); + + assert_navigation_pushed!( + app, + ActiveReadarrBlock::ManualBookSearchConfirmPrompt.into() + ); + } + + #[test] + fn test_manual_book_search_confirm_prompt_submit() { + let mut app = App::test_default(); + app.data.readarr_data.prompt_confirm = true; + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::ManualBookSearch.into()); + app.push_navigation_stack(ActiveReadarrBlock::ManualBookSearchConfirmPrompt.into()); + + BookDetailsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::ManualBookSearchConfirmPrompt, + None, + ) + .handle(); + + assert_eq!( + app.data.readarr_data.prompt_confirm_action, + Some(ReadarrEvent::DownloadRelease(ReleaseDownloadBody { + guid: "guid-1".to_owned(), + indexer_id: 1, + })) + ); + assert_navigation_pushed!(app, ActiveReadarrBlock::ManualBookSearch.into()); + } + + #[test] + fn test_manual_book_search_confirm_prompt_submit_no_confirmation() { + let mut app = App::test_default(); + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::ManualBookSearch.into()); + app.push_navigation_stack(ActiveReadarrBlock::ManualBookSearchConfirmPrompt.into()); + + BookDetailsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::ManualBookSearchConfirmPrompt, + None, + ) + .handle(); + + assert_none!(app.data.readarr_data.prompt_confirm_action); + assert_navigation_pushed!(app, ActiveReadarrBlock::ManualBookSearch.into()); + } + + #[test] + fn test_automatically_search_book_prompt_submit() { + let mut app = App::test_default(); + app.data.readarr_data.prompt_confirm = true; + app.data.readarr_data.books.set_items(books_vec()); + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::BookDetails.into()); + app.push_navigation_stack(ActiveReadarrBlock::AutomaticallySearchBookPrompt.into()); + + BookDetailsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::AutomaticallySearchBookPrompt, + None, + ) + .handle(); + + assert_eq!( + app.data.readarr_data.prompt_confirm_action, + Some(ReadarrEvent::TriggerAutomaticBookSearch(7)) + ); + assert_navigation_pushed!(app, ActiveReadarrBlock::BookDetails.into()); + } + + #[test] + fn test_automatically_search_book_prompt_submit_no_confirmation() { + let mut app = App::test_default(); + app.data.readarr_data.books.set_items(books_vec()); + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::BookDetails.into()); + app.push_navigation_stack(ActiveReadarrBlock::AutomaticallySearchBookPrompt.into()); + + BookDetailsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::AutomaticallySearchBookPrompt, + None, + ) + .handle(); + + assert_none!(app.data.readarr_data.prompt_confirm_action); + assert_navigation_pushed!(app, ActiveReadarrBlock::BookDetails.into()); + } + + #[test] + fn test_delete_book_file_prompt_submit() { + let mut app = App::test_default(); + app.data.readarr_data.prompt_confirm = true; + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::BookDetails.into()); + app.push_navigation_stack(ActiveReadarrBlock::DeleteBookFilePrompt.into()); + + BookDetailsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::DeleteBookFilePrompt, + None, + ) + .handle(); + + assert_eq!( + app.data.readarr_data.prompt_confirm_action, + Some(ReadarrEvent::DeleteBookFile(11)) + ); + assert_navigation_pushed!(app, ActiveReadarrBlock::BookDetails.into()); + } + + #[test] + fn test_delete_book_file_prompt_submit_no_confirmation() { + let mut app = App::test_default(); + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::BookDetails.into()); + app.push_navigation_stack(ActiveReadarrBlock::DeleteBookFilePrompt.into()); + + BookDetailsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::DeleteBookFilePrompt, + None, + ) + .handle(); + + assert_none!(app.data.readarr_data.prompt_confirm_action); + assert_navigation_pushed!(app, ActiveReadarrBlock::BookDetails.into()); + } + } + + mod test_handle_esc { + use super::*; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::event::Key; + use crate::{assert_modal_absent, assert_modal_present, assert_navigation_popped}; + use rstest::rstest; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_book_details_tabs_esc_tears_down_the_book_details_modal( + #[values( + ActiveReadarrBlock::BookDetails, + ActiveReadarrBlock::BookFileInfo, + ActiveReadarrBlock::ManualBookSearch + )] + active_readarr_block: ActiveReadarrBlock, + ) { + let mut app = App::test_default(); + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::AuthorDetails.into()); + app.push_navigation_stack(active_readarr_block.into()); + + BookDetailsHandler::new(ESC_KEY, &mut app, active_readarr_block, None).handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::AuthorDetails.into()); + assert_modal_absent!(app.data.readarr_data.book_details_modal); + } + + #[test] + fn test_book_history_esc_tears_down_the_book_details_modal() { + let mut app = App::test_default(); + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::AuthorDetails.into()); + app.push_navigation_stack(ActiveReadarrBlock::BookHistory.into()); + + BookDetailsHandler::new(ESC_KEY, &mut app, ActiveReadarrBlock::BookHistory, None).handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::AuthorDetails.into()); + assert_modal_absent!(app.data.readarr_data.book_details_modal); + } + + #[test] + fn test_book_history_esc_resets_the_filter_first() { + let mut app = App::test_default(); + let mut modal = book_details_modal(); + modal.book_history.filtered_items = Some(history_vec()); + app.data.readarr_data.book_details_modal = Some(modal); + app.push_navigation_stack(ActiveReadarrBlock::AuthorDetails.into()); + app.push_navigation_stack(ActiveReadarrBlock::BookHistory.into()); + + BookDetailsHandler::new(ESC_KEY, &mut app, ActiveReadarrBlock::BookHistory, None).handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::BookHistory.into()); + assert_none!( + app + .data + .readarr_data + .book_details_modal + .as_ref() + .unwrap() + .book_history + .filtered_items + ); + } + + #[test] + fn test_book_history_details_esc() { + let mut app = App::test_default(); + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::BookHistory.into()); + app.push_navigation_stack(ActiveReadarrBlock::BookHistoryDetails.into()); + + BookDetailsHandler::new( + ESC_KEY, + &mut app, + ActiveReadarrBlock::BookHistoryDetails, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::BookHistory.into()); + assert_modal_present!(app.data.readarr_data.book_details_modal); + } + + #[rstest] + fn test_prompt_esc( + #[values( + ActiveReadarrBlock::AutomaticallySearchBookPrompt, + ActiveReadarrBlock::ManualBookSearchConfirmPrompt, + ActiveReadarrBlock::DeleteBookFilePrompt + )] + active_readarr_block: ActiveReadarrBlock, + ) { + let mut app = App::test_default(); + app.data.readarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveReadarrBlock::BookDetails.into()); + app.push_navigation_stack(active_readarr_block.into()); + + BookDetailsHandler::new(ESC_KEY, &mut app, active_readarr_block, None).handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::BookDetails.into()); + assert!(!app.data.readarr_data.prompt_confirm); + } + } + + mod test_handle_key_char { + use super::*; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::models::servarr_models::ReleaseDownloadBody; + use crate::network::readarr_network::ReadarrEvent; + use crate::{assert_navigation_popped, assert_navigation_pushed}; + use pretty_assertions::assert_eq; + use rstest::rstest; + + #[rstest] + fn test_refresh_key( + #[values( + ActiveReadarrBlock::BookDetails, + ActiveReadarrBlock::BookHistory, + ActiveReadarrBlock::ManualBookSearch + )] + active_readarr_block: ActiveReadarrBlock, + ) { + let mut app = App::test_default(); + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(active_readarr_block.into()); + + BookDetailsHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + active_readarr_block, + None, + ) + .handle(); + + assert_navigation_pushed!(app, active_readarr_block.into()); + } + + #[rstest] + fn test_auto_search_key( + #[values( + ActiveReadarrBlock::BookDetails, + ActiveReadarrBlock::BookHistory, + ActiveReadarrBlock::ManualBookSearch + )] + active_readarr_block: ActiveReadarrBlock, + ) { + let mut app = App::test_default(); + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(active_readarr_block.into()); + + BookDetailsHandler::new( + DEFAULT_KEYBINDINGS.auto_search.key, + &mut app, + active_readarr_block, + None, + ) + .handle(); + + assert_navigation_pushed!( + app, + ActiveReadarrBlock::AutomaticallySearchBookPrompt.into() + ); + } + + #[test] + fn test_auto_search_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::BookDetails.into()); + + BookDetailsHandler::new( + DEFAULT_KEYBINDINGS.auto_search.key, + &mut app, + ActiveReadarrBlock::BookDetails, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveReadarrBlock::BookDetails.into()); + } + + #[test] + fn test_automatically_search_book_prompt_confirm() { + let mut app = App::test_default(); + app.data.readarr_data.books.set_items(books_vec()); + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::BookDetails.into()); + app.push_navigation_stack(ActiveReadarrBlock::AutomaticallySearchBookPrompt.into()); + + BookDetailsHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveReadarrBlock::AutomaticallySearchBookPrompt, + None, + ) + .handle(); + + assert!(app.data.readarr_data.prompt_confirm); + assert_eq!( + app.data.readarr_data.prompt_confirm_action, + Some(ReadarrEvent::TriggerAutomaticBookSearch(7)) + ); + assert_navigation_popped!(app, ActiveReadarrBlock::BookDetails.into()); + } + + #[test] + fn test_delete_book_file_prompt_confirm() { + let mut app = App::test_default(); + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::BookDetails.into()); + app.push_navigation_stack(ActiveReadarrBlock::DeleteBookFilePrompt.into()); + + BookDetailsHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveReadarrBlock::DeleteBookFilePrompt, + None, + ) + .handle(); + + assert!(app.data.readarr_data.prompt_confirm); + assert_eq!( + app.data.readarr_data.prompt_confirm_action, + Some(ReadarrEvent::DeleteBookFile(11)) + ); + assert_navigation_popped!(app, ActiveReadarrBlock::BookDetails.into()); + } + + #[test] + fn test_manual_book_search_confirm_prompt_confirm() { + let mut app = App::test_default(); + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + app.push_navigation_stack(ActiveReadarrBlock::ManualBookSearch.into()); + app.push_navigation_stack(ActiveReadarrBlock::ManualBookSearchConfirmPrompt.into()); + + BookDetailsHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveReadarrBlock::ManualBookSearchConfirmPrompt, + None, + ) + .handle(); + + assert!(app.data.readarr_data.prompt_confirm); + assert_eq!( + app.data.readarr_data.prompt_confirm_action, + Some(ReadarrEvent::DownloadRelease(ReleaseDownloadBody { + guid: "guid-1".to_owned(), + indexer_id: 1, + })) + ); + assert_navigation_popped!(app, ActiveReadarrBlock::ManualBookSearch.into()); + } + } + + #[test] + fn test_book_details_handler_accepts() { + ActiveReadarrBlock::iter().for_each(|active_readarr_block| { + if BOOK_DETAILS_BLOCKS.contains(&active_readarr_block) { + assert!( + BookDetailsHandler::accepts(active_readarr_block), + "{active_readarr_block} is not accepted by the BookDetailsHandler" + ); + } else { + assert!( + !BookDetailsHandler::accepts(active_readarr_block), + "{active_readarr_block} is wrongly accepted by the BookDetailsHandler" + ); + } + }); + } + + #[test] + fn test_book_details_handler_is_not_ready_when_loading() { + let mut app = App::test_default(); + app.is_loading = true; + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + + let handler = + BookDetailsHandler::new(DELETE_KEY, &mut app, ActiveReadarrBlock::BookDetails, None); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_book_details_handler_is_not_ready_when_the_modal_is_absent() { + let mut app = App::test_default(); + + let handler = + BookDetailsHandler::new(DELETE_KEY, &mut app, ActiveReadarrBlock::BookDetails, None); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_book_details_handler_is_ready_per_tab() { + let mut app = App::test_default(); + app.data.readarr_data.book_details_modal = Some(book_details_modal()); + + for active_readarr_block in [ + ActiveReadarrBlock::BookDetails, + ActiveReadarrBlock::BookHistory, + ActiveReadarrBlock::BookFileInfo, + ActiveReadarrBlock::ManualBookSearch, + ] { + let handler = BookDetailsHandler::new(DELETE_KEY, &mut app, active_readarr_block, None); + + assert!(handler.is_ready(), "{active_readarr_block} should be ready"); + } + } + + #[test] + fn test_book_details_handler_is_not_ready_when_its_tab_table_is_empty() { + let mut app = App::test_default(); + app.data.readarr_data.book_details_modal = Some(BookDetailsModal::default()); + + for active_readarr_block in [ + ActiveReadarrBlock::BookDetails, + ActiveReadarrBlock::BookHistory, + ActiveReadarrBlock::BookFileInfo, + ActiveReadarrBlock::ManualBookSearch, + ] { + let handler = BookDetailsHandler::new(DELETE_KEY, &mut app, active_readarr_block, None); + + assert!( + !handler.is_ready(), + "{active_readarr_block} should not be ready" + ); + } + } + + #[test] + fn test_book_details_handler_ignores_special_keys_for_textbox_input() { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = true; + + let handler = + BookDetailsHandler::new(DELETE_KEY, &mut app, ActiveReadarrBlock::BookDetails, None); + + assert!(handler.ignore_special_keys()); + } + + const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key; + + fn selected_edition(app: &App<'_>) -> Edition { + app + .data + .readarr_data + .book_details_modal + .as_ref() + .unwrap() + .editions + .current_selection() + .clone() + } + + fn edition_details_text(app: &App<'_>) -> String { + app + .data + .readarr_data + .book_details_modal + .as_ref() + .unwrap() + .edition_details_modal + .as_ref() + .unwrap() + .edition_details + .get_text() + } + + fn book_details_modal() -> BookDetailsModal { + let mut book_details_modal = BookDetailsModal::default(); + book_details_modal.editions.set_items(editions_vec()); + book_details_modal.book_files.set_items(book_files_vec()); + book_details_modal.book_history.set_items(history_vec()); + book_details_modal.book_releases.set_items(release_vec()); + + book_details_modal + } + + fn editions_vec() -> Vec { + vec![ + Edition { + id: 1, + book_id: 1, + title: "Test Edition 1".to_owned(), + format: Some("Paperback".to_owned()), + isbn13: Some("isbn-1".to_owned()), + asin: Some("asin-1".to_owned()), + publisher: Some("Publisher 1".to_owned()), + language: Some("eng".to_owned()), + overview: Some("Overview 1".to_owned()), + page_count: Some(662), + monitored: true, + ratings: Some(Ratings { + votes: 10, + value: 4.5, + popularity: None, + }), + ..Edition::default() + }, + Edition { + id: 2, + book_id: 1, + title: "Test Edition 2".to_owned(), + format: Some("Hardcover".to_owned()), + isbn13: Some("isbn-2".to_owned()), + asin: Some("asin-2".to_owned()), + publisher: Some("Publisher 2".to_owned()), + language: Some("eng".to_owned()), + overview: Some("Overview 2".to_owned()), + page_count: Some(722), + monitored: false, + ..Edition::default() + }, + ] + } + + fn book_files_vec() -> Vec { + vec![BookFile { + id: 11, + author_id: 1, + book_id: 1, + path: "/nfs/books/Test Author/Test Book.epub".to_owned(), + size: 1024, + quality: QualityWrapper { + quality: Quality { + name: "EPUB".to_owned(), + }, + }, + ..BookFile::default() + }] + } + + fn books_vec() -> Vec { + vec![Book { + id: 7, + ..Book::default() + }] + } + + fn history_vec() -> Vec { + vec![ + ReadarrHistoryItem { + id: 1, + source_title: HorizontallyScrollableText::from("Test History 1"), + ..ReadarrHistoryItem::default() + }, + ReadarrHistoryItem { + id: 2, + source_title: HorizontallyScrollableText::from("Test History 2"), + ..ReadarrHistoryItem::default() + }, + ] + } + + fn release_vec() -> Vec { + vec![ + ReadarrRelease { + guid: "guid-1".to_owned(), + indexer_id: 1, + title: HorizontallyScrollableText::from("Test Release 1"), + ..ReadarrRelease::default() + }, + ReadarrRelease { + guid: "guid-2".to_owned(), + indexer_id: 2, + title: HorizontallyScrollableText::from("Test Release 2"), + ..ReadarrRelease::default() + }, + ] + } +} diff --git a/src/handlers/readarr_handlers/library/library_handler_tests.rs b/src/handlers/readarr_handlers/library/library_handler_tests.rs index 3918ca47..f2af8d74 100644 --- a/src/handlers/readarr_handlers/library/library_handler_tests.rs +++ b/src/handlers/readarr_handlers/library/library_handler_tests.rs @@ -14,8 +14,9 @@ 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, LIBRARY_BLOCKS, + AUTHOR_DETAILS_BLOCKS, ActiveReadarrBlock, BOOK_DETAILS_BLOCKS, LIBRARY_BLOCKS, }; + use crate::test_handler_delegation; mod test_handle_scroll_up_and_down { use pretty_assertions::assert_str_eq; @@ -878,11 +879,21 @@ mod tests { assert_str_eq!(sort_option.name, "Tags"); } + #[test] + fn test_delegates_book_details_blocks_to_book_details_handler() { + test_handler_delegation!( + LibraryHandler, + ActiveReadarrBlock::AuthorDetails, + ActiveReadarrBlock::BookDetails + ); + } + #[test] fn test_library_handler_accepts() { let mut library_handler_blocks = Vec::new(); library_handler_blocks.extend(LIBRARY_BLOCKS); library_handler_blocks.extend(AUTHOR_DETAILS_BLOCKS); + library_handler_blocks.extend(BOOK_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 69179113..9dd3fe36 100644 --- a/src/handlers/readarr_handlers/library/mod.rs +++ b/src/handlers/readarr_handlers/library/mod.rs @@ -20,8 +20,11 @@ use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::models::Route; mod author_details_handler; +mod book_details_handler; -pub(in crate::handlers::readarr_handlers) use author_details_handler::AuthorDetailsHandler; +pub(in crate::handlers::readarr_handlers) use { + author_details_handler::AuthorDetailsHandler, book_details_handler::BookDetailsHandler, +}; #[cfg(test)] #[path = "library_handler_tests.rs"] @@ -63,13 +66,19 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for LibraryHandler<'a, AuthorDetailsHandler::new(self.key, self.app, self.active_readarr_block, self.context) .handle(); } + _ if BookDetailsHandler::accepts(self.active_readarr_block) => { + BookDetailsHandler::new(self.key, self.app, self.active_readarr_block, self.context) + .handle(); + } _ => self.handle_key_event(), } } } fn accepts(active_block: ActiveReadarrBlock) -> bool { - AuthorDetailsHandler::accepts(active_block) || LIBRARY_BLOCKS.contains(&active_block) + AuthorDetailsHandler::accepts(active_block) + || BookDetailsHandler::accepts(active_block) + || LIBRARY_BLOCKS.contains(&active_block) } fn ignore_special_keys(&self) -> bool {