diff --git a/src/ui/readarr_ui/library/author_details_ui.rs b/src/ui/readarr_ui/library/author_details_ui.rs index 7817b7fb..3a231887 100644 --- a/src/ui/readarr_ui/library/author_details_ui.rs +++ b/src/ui/readarr_ui/library/author_details_ui.rs @@ -14,6 +14,7 @@ use crate::models::servarr_data::readarr::readarr_data::{ AUTHOR_DETAILS_BLOCKS, ActiveReadarrBlock, }; use crate::ui::readarr_ui::library::book_details_ui::BookDetailsUi; +use crate::ui::readarr_ui::library::delete_book_ui::DeleteBookUi; use crate::ui::readarr_ui::readarr_ui_utils::create_history_event_details; use crate::ui::styles::{ManagarrStyle, secondary_style}; use crate::ui::utils::decorate_peer_style; @@ -42,7 +43,9 @@ impl DrawUi for AuthorDetailsUi { let Route::Readarr(active_readarr_block, _) = route else { return false; }; - BookDetailsUi::accepts(route) || AUTHOR_DETAILS_BLOCKS.contains(&active_readarr_block) + BookDetailsUi::accepts(route) + || DeleteBookUi::accepts(route) + || AUTHOR_DETAILS_BLOCKS.contains(&active_readarr_block) } fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { @@ -75,6 +78,7 @@ impl DrawUi for AuthorDetailsUi { draw_author_details(f, app, content_area); match active_readarr_block { + _ if DeleteBookUi::accepts(route) => DeleteBookUi::draw(f, app, _area), ActiveReadarrBlock::AuthorHistoryDetails => { draw_author_history_item_details_popup(f, app); } diff --git a/src/ui/readarr_ui/library/author_details_ui_tests.rs b/src/ui/readarr_ui/library/author_details_ui_tests.rs index 2358f45b..332dc656 100644 --- a/src/ui/readarr_ui/library/author_details_ui_tests.rs +++ b/src/ui/readarr_ui/library/author_details_ui_tests.rs @@ -7,7 +7,8 @@ mod tests { use crate::models::readarr_models::{Book, BookStatistics}; use crate::models::servarr_data::readarr::readarr_data::{ - AUTHOR_DETAILS_BLOCKS, ActiveReadarrBlock, BOOK_DETAILS_BLOCKS, EDITION_DETAILS_BLOCKS, + AUTHOR_DETAILS_BLOCKS, ActiveReadarrBlock, BOOK_DETAILS_BLOCKS, DELETE_BOOK_BLOCKS, + EDITION_DETAILS_BLOCKS, }; use crate::ui::DrawUi; use crate::ui::readarr_ui::library::author_details_ui::{ @@ -20,6 +21,7 @@ mod tests { let mut author_details_blocks = AUTHOR_DETAILS_BLOCKS.to_vec(); author_details_blocks.extend(BOOK_DETAILS_BLOCKS); author_details_blocks.extend(EDITION_DETAILS_BLOCKS); + author_details_blocks.extend(DELETE_BOOK_BLOCKS); ActiveReadarrBlock::iter().for_each(|active_readarr_block| { if author_details_blocks.contains(&active_readarr_block) { @@ -150,8 +152,11 @@ mod tests { use rstest::rstest; use crate::app::App; + use crate::models::BlockSelectionState; use crate::models::readarr_models::{AuthorStatistics, BookStatistics}; - use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock; + use crate::models::servarr_data::readarr::readarr_data::{ + ActiveReadarrBlock, DELETE_BOOK_SELECTION_BLOCKS, + }; use crate::models::stateful_table::StatefulTable; use crate::ui::DrawUi; use crate::ui::readarr_ui::library::author_details_ui::AuthorDetailsUi; @@ -311,5 +316,21 @@ mod tests { insta::assert_snapshot!(output); } + + #[test] + fn test_author_details_ui_renders_delete_book_over_author_details() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveReadarrBlock::AuthorDetails.into()); + app.data.readarr_data.delete_files = true; + app.data.readarr_data.add_import_list_exclusion = false; + app.data.readarr_data.selected_block = BlockSelectionState::new(DELETE_BOOK_SELECTION_BLOCKS); + app.push_navigation_stack(ActiveReadarrBlock::DeleteBookPrompt.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + AuthorDetailsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } } } diff --git a/src/ui/readarr_ui/library/delete_book_ui.rs b/src/ui/readarr_ui/library/delete_book_ui.rs new file mode 100644 index 00000000..799e1b57 --- /dev/null +++ b/src/ui/readarr_ui/library/delete_book_ui.rs @@ -0,0 +1,75 @@ +use ratatui::Frame; +use ratatui::layout::Rect; + +use crate::app::App; +use crate::models::Route; +use crate::models::servarr_data::readarr::readarr_data::{ActiveReadarrBlock, DELETE_BOOK_BLOCKS}; +use crate::ui::DrawUi; +use crate::ui::widgets::checkbox::Checkbox; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; +use crate::ui::widgets::popup::{Popup, Size}; + +#[cfg(test)] +#[path = "delete_book_ui_tests.rs"] +mod delete_book_ui_tests; + +pub(super) struct DeleteBookUi; + +impl DrawUi for DeleteBookUi { + fn accepts(route: Route) -> bool { + let Route::Readarr(active_readarr_block, _) = route else { + return false; + }; + DELETE_BOOK_BLOCKS.contains(&active_readarr_block) + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + if matches!( + app.get_current_route(), + Route::Readarr(ActiveReadarrBlock::DeleteBookPrompt, _) + ) { + let selected_block = app.data.readarr_data.selected_block.get_active_block(); + let highlights = delete_book_prompt_highlights(selected_block); + let prompt = format!( + "Do you really want to delete the book: \n{}?", + app.data.readarr_data.books.current_selection().title.text + ); + let checkboxes = vec![ + Checkbox::new("Delete Book Files") + .checked(app.data.readarr_data.delete_files) + .highlighted(highlights.delete_files), + Checkbox::new("Add List Exclusion") + .checked(app.data.readarr_data.add_import_list_exclusion) + .highlighted(highlights.add_import_list_exclusion), + ]; + let confirmation_prompt = ConfirmationPrompt::new() + .title("Delete Book") + .prompt(&prompt) + .checkboxes(checkboxes) + .yes_no_highlighted(highlights.confirm) + .yes_no_value(app.data.readarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + } +} + +#[derive(PartialEq, Eq)] +#[cfg_attr(test, derive(Debug))] +struct DeleteBookPromptHighlights { + delete_files: bool, + add_import_list_exclusion: bool, + confirm: bool, +} + +fn delete_book_prompt_highlights(selected_block: ActiveReadarrBlock) -> DeleteBookPromptHighlights { + DeleteBookPromptHighlights { + delete_files: selected_block == ActiveReadarrBlock::DeleteBookToggleDeleteFile, + add_import_list_exclusion: selected_block + == ActiveReadarrBlock::DeleteBookToggleAddListExclusion, + confirm: selected_block == ActiveReadarrBlock::DeleteBookConfirmPrompt, + } +} diff --git a/src/ui/readarr_ui/library/delete_book_ui_tests.rs b/src/ui/readarr_ui/library/delete_book_ui_tests.rs new file mode 100644 index 00000000..a1dea0f1 --- /dev/null +++ b/src/ui/readarr_ui/library/delete_book_ui_tests.rs @@ -0,0 +1,153 @@ +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::models::servarr_data::readarr::readarr_data::{ + ActiveReadarrBlock, DELETE_BOOK_BLOCKS, DELETE_BOOK_SELECTION_BLOCKS, + }; + use crate::ui::DrawUi; + use crate::ui::readarr_ui::library::delete_book_ui::{ + DeleteBookPromptHighlights, DeleteBookUi, delete_book_prompt_highlights, + }; + + #[test] + fn test_delete_book_ui_accepts() { + ActiveReadarrBlock::iter().for_each(|active_readarr_block| { + if DELETE_BOOK_BLOCKS.contains(&active_readarr_block) { + assert!(DeleteBookUi::accepts(active_readarr_block.into())); + } else { + assert!(!DeleteBookUi::accepts(active_readarr_block.into())); + } + }); + } + + #[rstest] + #[case( + ActiveReadarrBlock::DeleteBookToggleDeleteFile, + DeleteBookPromptHighlights { + delete_files: true, + add_import_list_exclusion: false, + confirm: false, + } + )] + #[case( + ActiveReadarrBlock::DeleteBookToggleAddListExclusion, + DeleteBookPromptHighlights { + delete_files: false, + add_import_list_exclusion: true, + confirm: false, + } + )] + #[case( + ActiveReadarrBlock::DeleteBookConfirmPrompt, + DeleteBookPromptHighlights { + delete_files: false, + add_import_list_exclusion: false, + confirm: true, + } + )] + #[case( + ActiveReadarrBlock::BookDetails, + DeleteBookPromptHighlights { + delete_files: false, + add_import_list_exclusion: false, + confirm: false, + } + )] + fn test_delete_book_prompt_highlights( + #[case] selected_block: ActiveReadarrBlock, + #[case] expected_highlights: DeleteBookPromptHighlights, + ) { + let highlights = delete_book_prompt_highlights(selected_block); + + assert_eq!(highlights, expected_highlights); + } + + #[test] + fn test_delete_book_prompt_highlights_covers_every_selection_step() { + for step in DELETE_BOOK_SELECTION_BLOCKS { + let highlights = delete_book_prompt_highlights(step[0]); + + assert_ne!( + highlights, + delete_book_prompt_highlights(ActiveReadarrBlock::BookDetails) + ); + } + } + + mod snapshot_tests { + use rstest::rstest; + + use super::*; + use crate::app::App; + use crate::models::BlockSelectionState; + use crate::ui::ui_test_utils::test_utils::{TerminalSize, render_to_string_with_app}; + + fn seed_distinct_delete_preferences(app: &mut App<'_>) { + app.data.readarr_data.delete_files = true; + app.data.readarr_data.add_import_list_exclusion = false; + } + + #[rstest] + #[case(0, ActiveReadarrBlock::DeleteBookToggleDeleteFile)] + #[case(1, ActiveReadarrBlock::DeleteBookToggleAddListExclusion)] + #[case(2, ActiveReadarrBlock::DeleteBookConfirmPrompt)] + fn test_delete_book_ui_renders_step( + #[case] step: usize, + #[case] selected_readarr_block: ActiveReadarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveReadarrBlock::DeleteBookPrompt.into()); + seed_distinct_delete_preferences(&mut app); + let mut selected_block = BlockSelectionState::new(DELETE_BOOK_SELECTION_BLOCKS); + selected_block.set_index(0, step); + app.data.readarr_data.selected_block = selected_block; + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + DeleteBookUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!( + format!("delete_book_step_{step}_{selected_readarr_block}"), + output + ); + } + + #[test] + fn test_delete_book_ui_renders_inverted_delete_preferences() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveReadarrBlock::DeleteBookPrompt.into()); + app.data.readarr_data.delete_files = false; + app.data.readarr_data.add_import_list_exclusion = true; + app.data.readarr_data.selected_block = BlockSelectionState::new(DELETE_BOOK_SELECTION_BLOCKS); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + DeleteBookUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[rstest] + #[case(ActiveReadarrBlock::DeleteBookConfirmPrompt)] + #[case(ActiveReadarrBlock::DeleteBookToggleDeleteFile)] + #[case(ActiveReadarrBlock::DeleteBookToggleAddListExclusion)] + fn test_delete_book_ui_renders_nothing_when_route_is_not_the_prompt( + #[case] active_readarr_block: ActiveReadarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_readarr_block.into()); + seed_distinct_delete_preferences(&mut app); + app.data.readarr_data.selected_block = BlockSelectionState::new(DELETE_BOOK_SELECTION_BLOCKS); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + DeleteBookUi::draw(f, app, f.area()); + }); + + assert!(!output.contains("Delete Book")); + assert!(!output.contains("Add List Exclusion")); + } + } +} diff --git a/src/ui/readarr_ui/library/library_ui_tests.rs b/src/ui/readarr_ui/library/library_ui_tests.rs index 2e56081a..1d0b4406 100644 --- a/src/ui/readarr_ui/library/library_ui_tests.rs +++ b/src/ui/readarr_ui/library/library_ui_tests.rs @@ -5,7 +5,8 @@ mod tests { use crate::models::readarr_models::{Author, AuthorStatistics, AuthorStatus}; use crate::models::servarr_data::readarr::readarr_data::{ ADD_AUTHOR_BLOCKS, AUTHOR_DETAILS_BLOCKS, ActiveReadarrBlock, BOOK_DETAILS_BLOCKS, - DELETE_AUTHOR_BLOCKS, EDIT_AUTHOR_BLOCKS, EDITION_DETAILS_BLOCKS, LIBRARY_BLOCKS, + DELETE_AUTHOR_BLOCKS, DELETE_BOOK_BLOCKS, EDIT_AUTHOR_BLOCKS, EDITION_DETAILS_BLOCKS, + LIBRARY_BLOCKS, }; use crate::ui::DrawUi; use crate::ui::readarr_ui::library::{LibraryUi, decorate_author_row_with_style}; @@ -21,6 +22,7 @@ mod tests { blocks.extend(EDITION_DETAILS_BLOCKS); blocks.extend(ADD_AUTHOR_BLOCKS); blocks.extend(DELETE_AUTHOR_BLOCKS); + blocks.extend(DELETE_BOOK_BLOCKS); blocks.extend(EDIT_AUTHOR_BLOCKS); for active_readarr_block in ActiveReadarrBlock::iter() { diff --git a/src/ui/readarr_ui/library/mod.rs b/src/ui/readarr_ui/library/mod.rs index c1883812..2467a643 100644 --- a/src/ui/readarr_ui/library/mod.rs +++ b/src/ui/readarr_ui/library/mod.rs @@ -32,6 +32,7 @@ mod add_author_ui; mod author_details_ui; mod book_details_ui; mod delete_author_ui; +mod delete_book_ui; mod edit_author_ui; mod edition_details_ui; diff --git a/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__author_details_ui__author_details_ui_tests__tests__snapshot_tests__author_details_ui_renders_delete_book_over_author_details.snap b/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__author_details_ui__author_details_ui_tests__tests__snapshot_tests__author_details_ui_renders_delete_book_over_author_details.snap new file mode 100644 index 00000000..4a83f5a9 --- /dev/null +++ b/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__author_details_ui__author_details_ui_tests__tests__snapshot_tests__author_details_ui_renders_delete_book_over_author_details.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/readarr_ui/library/author_details_ui_tests.rs +expression: output +--- + + + + ╭ Test Author ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Author: Test Author │ + │Overview: some interesting description of the author │ + │Status: Continuing │ + │Genres: science fiction │ + │Rating: N/A │ + │Path: /nfs/books/Test Author │ + │Quality Profile: Standard │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │ │ + │ │ + │ │ + │╭ Author Details ───────────────────────╭────────────────────── Delete Book ──────────────────────╮───────────────────────────────────────────╮│ + ││ Books │ History │ Author Search │ Do you really want to delete the book: │ ││ + ││─────────────────────────────────────────│ Test Book? │───────────────────────────────────────────││ + ││ Monitored Title │ │Date Size ││ + ││=> 🏷 Test Book │ │20 0.00 GB ││ + ││ │ ╭───╮ │ ││ + ││ │ Delete Book Files: │ ✔ │ │ ││ + ││ │ ╰───╯ │ ││ + ││ │ ╭───╮ │ ││ + ││ │ Add List Exclusion: │ │ │ ││ + ││ │ ╰───╯ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │╭────────────────────────────╮╭───────────────────────────╮│ ││ + ││ ││ Yes ││ No ││ ││ + ││ │╰────────────────────────────╯╰───────────────────────────╯│ ││ + ││ ╰───────────────────────────────────────────────────────────╯ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__delete_book_ui__delete_book_ui_tests__tests__snapshot_tests__delete_book_step_0_DeleteBookToggleDeleteFile.snap b/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__delete_book_ui__delete_book_ui_tests__tests__snapshot_tests__delete_book_step_0_DeleteBookToggleDeleteFile.snap new file mode 100644 index 00000000..2b7fcb09 --- /dev/null +++ b/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__delete_book_ui__delete_book_ui_tests__tests__snapshot_tests__delete_book_step_0_DeleteBookToggleDeleteFile.snap @@ -0,0 +1,38 @@ +--- +source: src/ui/readarr_ui/library/delete_book_ui_tests.rs +expression: output +--- + + + + + + + + + + + + + + + + + ╭────────────────────── Delete Book ──────────────────────╮ + │ Do you really want to delete the book: │ + │ Test Book? │ + │ │ + │ │ + │ ╭───╮ │ + │ Delete Book Files: │ ✔ │ │ + │ ╰───╯ │ + │ ╭───╮ │ + │ Add List Exclusion: │ │ │ + │ ╰───╯ │ + │ │ + │ │ + │ │ + │╭────────────────────────────╮╭───────────────────────────╮│ + ││ Yes ││ No ││ + │╰────────────────────────────╯╰───────────────────────────╯│ + ╰───────────────────────────────────────────────────────────╯ diff --git a/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__delete_book_ui__delete_book_ui_tests__tests__snapshot_tests__delete_book_step_1_DeleteBookToggleAddListExclusion.snap b/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__delete_book_ui__delete_book_ui_tests__tests__snapshot_tests__delete_book_step_1_DeleteBookToggleAddListExclusion.snap new file mode 100644 index 00000000..2b7fcb09 --- /dev/null +++ b/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__delete_book_ui__delete_book_ui_tests__tests__snapshot_tests__delete_book_step_1_DeleteBookToggleAddListExclusion.snap @@ -0,0 +1,38 @@ +--- +source: src/ui/readarr_ui/library/delete_book_ui_tests.rs +expression: output +--- + + + + + + + + + + + + + + + + + ╭────────────────────── Delete Book ──────────────────────╮ + │ Do you really want to delete the book: │ + │ Test Book? │ + │ │ + │ │ + │ ╭───╮ │ + │ Delete Book Files: │ ✔ │ │ + │ ╰───╯ │ + │ ╭───╮ │ + │ Add List Exclusion: │ │ │ + │ ╰───╯ │ + │ │ + │ │ + │ │ + │╭────────────────────────────╮╭───────────────────────────╮│ + ││ Yes ││ No ││ + │╰────────────────────────────╯╰───────────────────────────╯│ + ╰───────────────────────────────────────────────────────────╯ diff --git a/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__delete_book_ui__delete_book_ui_tests__tests__snapshot_tests__delete_book_step_2_DeleteBookConfirmPrompt.snap b/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__delete_book_ui__delete_book_ui_tests__tests__snapshot_tests__delete_book_step_2_DeleteBookConfirmPrompt.snap new file mode 100644 index 00000000..2b7fcb09 --- /dev/null +++ b/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__delete_book_ui__delete_book_ui_tests__tests__snapshot_tests__delete_book_step_2_DeleteBookConfirmPrompt.snap @@ -0,0 +1,38 @@ +--- +source: src/ui/readarr_ui/library/delete_book_ui_tests.rs +expression: output +--- + + + + + + + + + + + + + + + + + ╭────────────────────── Delete Book ──────────────────────╮ + │ Do you really want to delete the book: │ + │ Test Book? │ + │ │ + │ │ + │ ╭───╮ │ + │ Delete Book Files: │ ✔ │ │ + │ ╰───╯ │ + │ ╭───╮ │ + │ Add List Exclusion: │ │ │ + │ ╰───╯ │ + │ │ + │ │ + │ │ + │╭────────────────────────────╮╭───────────────────────────╮│ + ││ Yes ││ No ││ + │╰────────────────────────────╯╰───────────────────────────╯│ + ╰───────────────────────────────────────────────────────────╯ diff --git a/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__delete_book_ui__delete_book_ui_tests__tests__snapshot_tests__delete_book_ui_renders_inverted_delete_preferences.snap b/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__delete_book_ui__delete_book_ui_tests__tests__snapshot_tests__delete_book_ui_renders_inverted_delete_preferences.snap new file mode 100644 index 00000000..d472cc86 --- /dev/null +++ b/src/ui/readarr_ui/library/snapshots/managarr__ui__readarr_ui__library__delete_book_ui__delete_book_ui_tests__tests__snapshot_tests__delete_book_ui_renders_inverted_delete_preferences.snap @@ -0,0 +1,38 @@ +--- +source: src/ui/readarr_ui/library/delete_book_ui_tests.rs +expression: output +--- + + + + + + + + + + + + + + + + + ╭────────────────────── Delete Book ──────────────────────╮ + │ Do you really want to delete the book: │ + │ Test Book? │ + │ │ + │ │ + │ ╭───╮ │ + │ Delete Book Files: │ │ │ + │ ╰───╯ │ + │ ╭───╮ │ + │ Add List Exclusion: │ ✔ │ │ + │ ╰───╯ │ + │ │ + │ │ + │ │ + │╭────────────────────────────╮╭───────────────────────────╮│ + ││ Yes ││ No ││ + │╰────────────────────────────╯╰───────────────────────────╯│ + ╰───────────────────────────────────────────────────────────╯