feat(readarr): add the delete book UI

This commit is contained in:
Dark-Alex-17 committed 2026-09-03 21:58:35 -06:00
1 parent 2b07ad96f1
commit 773719dbd2
11 files changed
+464 -4

No files matched your search

@@ -14,6 +14,7 @@ use crate::models::servarr_data::readarr::readarr_data::{
AUTHOR_DETAILS_BLOCKS, ActiveReadarrBlock, AUTHOR_DETAILS_BLOCKS, ActiveReadarrBlock,
}; };
use crate::ui::readarr_ui::library::book_details_ui::BookDetailsUi; 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::readarr_ui::readarr_ui_utils::create_history_event_details;
use crate::ui::styles::{ManagarrStyle, secondary_style}; use crate::ui::styles::{ManagarrStyle, secondary_style};
use crate::ui::utils::decorate_peer_style; use crate::ui::utils::decorate_peer_style;
@@ -42,7 +43,9 @@ impl DrawUi for AuthorDetailsUi {
let Route::Readarr(active_readarr_block, _) = route else { let Route::Readarr(active_readarr_block, _) = route else {
return false; 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) { 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); draw_author_details(f, app, content_area);
match active_readarr_block { match active_readarr_block {
_ if DeleteBookUi::accepts(route) => DeleteBookUi::draw(f, app, _area),
ActiveReadarrBlock::AuthorHistoryDetails => { ActiveReadarrBlock::AuthorHistoryDetails => {
draw_author_history_item_details_popup(f, app); draw_author_history_item_details_popup(f, app);
} }
@@ -7,7 +7,8 @@ mod tests {
use crate::models::readarr_models::{Book, BookStatistics}; use crate::models::readarr_models::{Book, BookStatistics};
use crate::models::servarr_data::readarr::readarr_data::{ 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::DrawUi;
use crate::ui::readarr_ui::library::author_details_ui::{ 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(); let mut author_details_blocks = AUTHOR_DETAILS_BLOCKS.to_vec();
author_details_blocks.extend(BOOK_DETAILS_BLOCKS); author_details_blocks.extend(BOOK_DETAILS_BLOCKS);
author_details_blocks.extend(EDITION_DETAILS_BLOCKS); author_details_blocks.extend(EDITION_DETAILS_BLOCKS);
author_details_blocks.extend(DELETE_BOOK_BLOCKS);
ActiveReadarrBlock::iter().for_each(|active_readarr_block| { ActiveReadarrBlock::iter().for_each(|active_readarr_block| {
if author_details_blocks.contains(&active_readarr_block) { if author_details_blocks.contains(&active_readarr_block) {
@@ -150,8 +152,11 @@ mod tests {
use rstest::rstest; use rstest::rstest;
use crate::app::App; use crate::app::App;
use crate::models::BlockSelectionState;
use crate::models::readarr_models::{AuthorStatistics, BookStatistics}; 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::models::stateful_table::StatefulTable;
use crate::ui::DrawUi; use crate::ui::DrawUi;
use crate::ui::readarr_ui::library::author_details_ui::AuthorDetailsUi; use crate::ui::readarr_ui::library::author_details_ui::AuthorDetailsUi;
@@ -311,5 +316,21 @@ mod tests {
insta::assert_snapshot!(output); 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);
}
} }
} }
@@ -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,
}
}
@@ -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"));
}
}
}
@@ -5,7 +5,8 @@ mod tests {
use crate::models::readarr_models::{Author, AuthorStatistics, AuthorStatus}; use crate::models::readarr_models::{Author, AuthorStatistics, AuthorStatus};
use crate::models::servarr_data::readarr::readarr_data::{ use crate::models::servarr_data::readarr::readarr_data::{
ADD_AUTHOR_BLOCKS, AUTHOR_DETAILS_BLOCKS, ActiveReadarrBlock, BOOK_DETAILS_BLOCKS, 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::DrawUi;
use crate::ui::readarr_ui::library::{LibraryUi, decorate_author_row_with_style}; 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(EDITION_DETAILS_BLOCKS);
blocks.extend(ADD_AUTHOR_BLOCKS); blocks.extend(ADD_AUTHOR_BLOCKS);
blocks.extend(DELETE_AUTHOR_BLOCKS); blocks.extend(DELETE_AUTHOR_BLOCKS);
blocks.extend(DELETE_BOOK_BLOCKS);
blocks.extend(EDIT_AUTHOR_BLOCKS); blocks.extend(EDIT_AUTHOR_BLOCKS);
for active_readarr_block in ActiveReadarrBlock::iter() { for active_readarr_block in ActiveReadarrBlock::iter() {
+1
View File
@@ -32,6 +32,7 @@ mod add_author_ui;
mod author_details_ui; mod author_details_ui;
mod book_details_ui; mod book_details_ui;
mod delete_author_ui; mod delete_author_ui;
mod delete_book_ui;
mod edit_author_ui; mod edit_author_ui;
mod edition_details_ui; mod edition_details_ui;
@@ -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 ││ ││
││ │╰────────────────────────────╯╰───────────────────────────╯│ ││
││ ╰───────────────────────────────────────────────────────────╯ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
││ ││
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -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 ││
│╰────────────────────────────╯╰───────────────────────────╯│
╰───────────────────────────────────────────────────────────╯
@@ -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 ││
│╰────────────────────────────╯╰───────────────────────────╯│
╰───────────────────────────────────────────────────────────╯
@@ -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 ││
│╰────────────────────────────╯╰───────────────────────────╯│
╰───────────────────────────────────────────────────────────╯
@@ -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 ││
│╰────────────────────────────╯╰───────────────────────────╯│
╰───────────────────────────────────────────────────────────╯