feat(readarr): add a scrollable overview popup to author details

An author's overview renders as the last line of a description box that
does not scroll, so a long overview overflows with no way to read the
rest. Pressing v on the author details Books tab now opens a popup
holding the full overview.

The popup owns its own ScrollableText, built fresh from the author's
overview when it opens and torn down on Esc so a reopened popup starts
at the top. Scrolling mutates only that owned state. The background
paragraph renders the Author record's overview, which carries no scroll
offset, so it cannot scroll along with the popup.

Add a view keybinding bound to v, cite it from the author details
context clues, and give the popup its own clue set so it does not
inherit the hints of the view underneath.
This commit is contained in:
Dark-Alex-17 committed 2026-09-08 18:05:52 -06:00
1 parent ee6035c75d
commit 27c458307f
59 files changed
+1493 -97

No files matched your search

+6
View File
@@ -27,6 +27,7 @@ generate_keybindings! {
filter, filter,
sort, sort,
edit, edit,
view,
logs, logs,
tasks, tasks,
test, test,
@@ -142,6 +143,11 @@ pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings {
alt: None, alt: None,
desc: "edit", desc: "edit",
}, },
view: KeyBinding {
key: Key::Char('v'),
alt: None,
desc: "view",
},
events: KeyBinding { events: KeyBinding {
key: Key::Char('e'), key: Key::Char('e'),
alt: None, alt: None,
+1
View File
@@ -26,6 +26,7 @@ mod test {
#[case(DEFAULT_KEYBINDINGS.filter, Key::Char('f'), None, "filter")] #[case(DEFAULT_KEYBINDINGS.filter, Key::Char('f'), None, "filter")]
#[case(DEFAULT_KEYBINDINGS.sort, Key::Char('o'), None, "sort")] #[case(DEFAULT_KEYBINDINGS.sort, Key::Char('o'), None, "sort")]
#[case(DEFAULT_KEYBINDINGS.edit, Key::Char('e'), None, "edit")] #[case(DEFAULT_KEYBINDINGS.edit, Key::Char('e'), None, "edit")]
#[case(DEFAULT_KEYBINDINGS.view, Key::Char('v'), None, "view")]
#[case(DEFAULT_KEYBINDINGS.events, Key::Char('e'), None, "events")] #[case(DEFAULT_KEYBINDINGS.events, Key::Char('e'), None, "events")]
#[case(DEFAULT_KEYBINDINGS.logs, Key::Char('L'), None, "logs")] #[case(DEFAULT_KEYBINDINGS.logs, Key::Char('L'), None, "logs")]
#[case(DEFAULT_KEYBINDINGS.tasks, Key::Char('t'), None, "tasks")] #[case(DEFAULT_KEYBINDINGS.tasks, Key::Char('t'), None, "tasks")]
+16 -4
View File
@@ -6,9 +6,9 @@ use crate::app::context_clues::{
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::models::Route; use crate::models::Route;
use crate::models::servarr_data::readarr::readarr_data::{ use crate::models::servarr_data::readarr::readarr_data::{
ADD_AUTHOR_BLOCKS, ADD_ROOT_FOLDER_BLOCKS, AUTHOR_DETAILS_BLOCKS, ActiveReadarrBlock, ADD_AUTHOR_BLOCKS, ADD_ROOT_FOLDER_BLOCKS, AUTHOR_DETAILS_BLOCKS, AUTHOR_OVERVIEW_BLOCKS,
BOOK_DETAILS_BLOCKS, DELETE_AUTHOR_BLOCKS, DELETE_BOOK_BLOCKS, EDIT_AUTHOR_BLOCKS, ActiveReadarrBlock, BOOK_DETAILS_BLOCKS, DELETE_AUTHOR_BLOCKS, DELETE_BOOK_BLOCKS,
EDIT_INDEXER_BLOCKS, EDITION_DETAILS_BLOCKS, INDEXER_SETTINGS_BLOCKS, EDIT_AUTHOR_BLOCKS, EDIT_INDEXER_BLOCKS, EDITION_DETAILS_BLOCKS, INDEXER_SETTINGS_BLOCKS,
}; };
#[cfg(test)] #[cfg(test)]
@@ -39,7 +39,7 @@ pub static ADD_AUTHOR_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.esc, "edit search"), (DEFAULT_KEYBINDINGS.esc, "edit search"),
]; ];
pub static AUTHOR_DETAILS_CONTEXT_CLUES: [ContextClue; 8] = [ pub static AUTHOR_DETAILS_CONTEXT_CLUES: [ContextClue; 9] = [
( (
DEFAULT_KEYBINDINGS.refresh, DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc, DEFAULT_KEYBINDINGS.refresh.desc,
@@ -56,6 +56,15 @@ pub static AUTHOR_DETAILS_CONTEXT_CLUES: [ContextClue; 8] = [
DEFAULT_KEYBINDINGS.auto_search, DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc, DEFAULT_KEYBINDINGS.auto_search.desc,
), ),
(DEFAULT_KEYBINDINGS.view, "view overview"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static AUTHOR_OVERVIEW_CONTEXT_CLUES: [ContextClue; 2] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
]; ];
@@ -168,6 +177,9 @@ impl ContextClueProvider for ReadarrContextClueProvider {
.readarr_data .readarr_data
.author_info_tabs .author_info_tabs
.get_active_route_contextual_help(), .get_active_route_contextual_help(),
_ if AUTHOR_OVERVIEW_BLOCKS.contains(&active_readarr_block) => {
Some(&AUTHOR_OVERVIEW_CONTEXT_CLUES)
}
_ if BOOK_DETAILS_BLOCKS.contains(&active_readarr_block) => app _ if BOOK_DETAILS_BLOCKS.contains(&active_readarr_block) => app
.data .data
.readarr_data .readarr_data
+36 -4
View File
@@ -8,10 +8,10 @@ mod tests {
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::readarr::readarr_context_clues::{ use crate::app::readarr::readarr_context_clues::{
ADD_AUTHOR_SEARCH_RESULTS_CONTEXT_CLUES, AUTHOR_DETAILS_CONTEXT_CLUES, ADD_AUTHOR_SEARCH_RESULTS_CONTEXT_CLUES, AUTHOR_DETAILS_CONTEXT_CLUES,
AUTHOR_HISTORY_CONTEXT_CLUES, AUTHORS_CONTEXT_CLUES, BOOK_DETAILS_CONTEXT_CLUES, AUTHOR_HISTORY_CONTEXT_CLUES, AUTHOR_OVERVIEW_CONTEXT_CLUES, AUTHORS_CONTEXT_CLUES,
BOOK_FILE_CONTEXT_CLUES, BOOK_HISTORY_CONTEXT_CLUES, EDITION_DETAILS_CONTEXT_CLUES, BOOK_DETAILS_CONTEXT_CLUES, BOOK_FILE_CONTEXT_CLUES, BOOK_HISTORY_CONTEXT_CLUES,
MANUAL_AUTHOR_SEARCH_CONTEXT_CLUES, MANUAL_BOOK_SEARCH_CONTEXT_CLUES, EDITION_DETAILS_CONTEXT_CLUES, MANUAL_AUTHOR_SEARCH_CONTEXT_CLUES,
ReadarrContextClueProvider, MANUAL_BOOK_SEARCH_CONTEXT_CLUES, ReadarrContextClueProvider,
}; };
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::servarr_data::readarr::modals::BookDetailsModal; use crate::models::servarr_data::readarr::modals::BookDetailsModal;
@@ -115,6 +115,10 @@ mod tests {
DEFAULT_KEYBINDINGS.auto_search.desc, DEFAULT_KEYBINDINGS.auto_search.desc,
) )
); );
assert_some_eq_x!(
author_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.view, "view overview")
);
assert_some_eq_x!( assert_some_eq_x!(
author_details_context_clues_iter.next(), author_details_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc) &(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
@@ -122,6 +126,24 @@ mod tests {
assert_none!(author_details_context_clues_iter.next()); assert_none!(author_details_context_clues_iter.next());
} }
#[test]
fn test_author_overview_context_clues() {
let mut author_overview_context_clues_iter = AUTHOR_OVERVIEW_CONTEXT_CLUES.iter();
assert_some_eq_x!(
author_overview_context_clues_iter.next(),
&(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc
)
);
assert_some_eq_x!(
author_overview_context_clues_iter.next(),
&(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)
);
assert_none!(author_overview_context_clues_iter.next());
}
#[test] #[test]
fn test_add_author_search_results_context_clues() { fn test_add_author_search_results_context_clues() {
let mut add_author_search_results_context_clues_iter = let mut add_author_search_results_context_clues_iter =
@@ -440,6 +462,16 @@ mod tests {
assert_some_eq_x!(context_clues, &EDITION_DETAILS_CONTEXT_CLUES); assert_some_eq_x!(context_clues, &EDITION_DETAILS_CONTEXT_CLUES);
} }
#[test]
fn test_readarr_context_clue_provider_author_overview_block() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveReadarrBlock::AuthorOverview.into());
let context_clues = ReadarrContextClueProvider::get_context_clues(&mut app);
assert_some_eq_x!(context_clues, &AUTHOR_OVERVIEW_CONTEXT_CLUES);
}
#[test] #[test]
fn test_readarr_context_clue_provider_authors_block() { fn test_readarr_context_clue_provider_authors_block() {
let mut app = App::test_default(); let mut app = App::test_default();
@@ -1,3 +1,4 @@
use super::author_overview_handler::AuthorOverviewHandler;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handlers::readarr_handlers::history::history_sorting_options; use crate::handlers::readarr_handlers::history::history_sorting_options;
@@ -5,13 +6,14 @@ use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
use crate::matches_key; use crate::matches_key;
use crate::models::readarr_models::{Book, ReadarrHistoryItem, ReadarrRelease}; use crate::models::readarr_models::{Book, ReadarrHistoryItem, ReadarrRelease};
use crate::models::servarr_data::readarr::modals::AuthorOverviewModal;
use crate::models::servarr_data::readarr::readarr_data::{ use crate::models::servarr_data::readarr::readarr_data::{
AUTHOR_DETAILS_BLOCKS, ActiveReadarrBlock, DELETE_BOOK_SELECTION_BLOCKS, AUTHOR_DETAILS_BLOCKS, ActiveReadarrBlock, DELETE_BOOK_SELECTION_BLOCKS,
EDIT_AUTHOR_SELECTION_BLOCKS, EDIT_AUTHOR_SELECTION_BLOCKS,
}; };
use crate::models::servarr_models::ReleaseDownloadBody; use crate::models::servarr_models::ReleaseDownloadBody;
use crate::models::stateful_table::SortOption; use crate::models::stateful_table::SortOption;
use crate::models::{BlockSelectionState, Route}; use crate::models::{BlockSelectionState, Route, ScrollableText};
use crate::network::readarr_network::ReadarrEvent; use crate::network::readarr_network::ReadarrEvent;
use serde_json::Number; use serde_json::Number;
@@ -34,10 +36,36 @@ impl AuthorDetailsHandler<'_, '_> {
fn extract_book_id(&self) -> i64 { fn extract_book_id(&self) -> i64 {
self.app.data.readarr_data.books.current_selection().id self.app.data.readarr_data.books.current_selection().id
} }
fn build_author_overview_modal(&mut self) {
let overview = self
.app
.data
.readarr_data
.authors
.current_selection()
.overview
.clone()
.unwrap_or_default();
self.app.data.readarr_data.author_overview_modal = Some(AuthorOverviewModal {
overview: ScrollableText::with_string(overview),
});
}
} }
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for AuthorDetailsHandler<'a, 'b> { impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for AuthorDetailsHandler<'a, 'b> {
fn handle(&mut self) { fn handle(&mut self) {
if AuthorOverviewHandler::accepts(self.active_readarr_block) {
return AuthorOverviewHandler::new(
self.key,
self.app,
self.active_readarr_block,
self._context,
)
.handle();
}
let books_table_handling_config = let books_table_handling_config =
TableHandlingConfig::new(ActiveReadarrBlock::AuthorDetails.into()) TableHandlingConfig::new(ActiveReadarrBlock::AuthorDetails.into())
.searching_block(ActiveReadarrBlock::SearchBooks.into()) .searching_block(ActiveReadarrBlock::SearchBooks.into())
@@ -78,7 +106,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for AuthorDetailsHandle
} }
fn accepts(active_block: ActiveReadarrBlock) -> bool { fn accepts(active_block: ActiveReadarrBlock) -> bool {
AUTHOR_DETAILS_BLOCKS.contains(&active_block) AuthorOverviewHandler::accepts(active_block) || AUTHOR_DETAILS_BLOCKS.contains(&active_block)
} }
fn ignore_special_keys(&self) -> bool { fn ignore_special_keys(&self) -> bool {
@@ -294,6 +322,12 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for AuthorDetailsHandle
self.app.data.readarr_data.selected_block = self.app.data.readarr_data.selected_block =
BlockSelectionState::new(EDIT_AUTHOR_SELECTION_BLOCKS); BlockSelectionState::new(EDIT_AUTHOR_SELECTION_BLOCKS);
} }
_ if matches_key!(view, key) => {
self.build_author_overview_modal();
self
.app
.push_navigation_stack(ActiveReadarrBlock::AuthorOverview.into());
}
_ if matches_key!(toggle_monitoring, key) _ if matches_key!(toggle_monitoring, key)
&& !self.app.data.readarr_data.books.is_empty() => && !self.app.data.readarr_data.books.is_empty() =>
{ {
@@ -15,9 +15,10 @@ mod tests {
use crate::models::HorizontallyScrollableText; use crate::models::HorizontallyScrollableText;
use crate::models::readarr_models::{ReadarrHistoryItem, ReadarrRelease}; use crate::models::readarr_models::{ReadarrHistoryItem, ReadarrRelease};
use crate::models::servarr_data::readarr::readarr_data::{ use crate::models::servarr_data::readarr::readarr_data::{
AUTHOR_DETAILS_BLOCKS, ActiveReadarrBlock, AUTHOR_DETAILS_BLOCKS, AUTHOR_OVERVIEW_BLOCKS, ActiveReadarrBlock,
}; };
use crate::models::servarr_models::{Quality, QualityWrapper}; use crate::models::servarr_models::{Quality, QualityWrapper};
use crate::test_handler_delegation;
mod test_handle_delete { mod test_handle_delete {
use super::*; use super::*;
@@ -465,6 +466,7 @@ mod tests {
use crate::assert_navigation_pushed; use crate::assert_navigation_pushed;
use crate::handlers::KeyEventHandler; use crate::handlers::KeyEventHandler;
use crate::handlers::readarr_handlers::library::author_details_handler::AuthorDetailsHandler; use crate::handlers::readarr_handlers::library::author_details_handler::AuthorDetailsHandler;
use crate::handlers::readarr_handlers::library::author_overview_handler::AuthorOverviewHandler;
use crate::models::readarr_models::Author; use crate::models::readarr_models::Author;
use crate::models::servarr_data::readarr::readarr_data::{ use crate::models::servarr_data::readarr::readarr_data::{
ActiveReadarrBlock, EDIT_AUTHOR_SELECTION_BLOCKS, ActiveReadarrBlock, EDIT_AUTHOR_SELECTION_BLOCKS,
@@ -473,7 +475,7 @@ mod tests {
use crate::network::readarr_network::ReadarrEvent; use crate::network::readarr_network::ReadarrEvent;
use crate::network::readarr_network::readarr_network_test_utils::test_utils::torrent_release; use crate::network::readarr_network::readarr_network_test_utils::test_utils::torrent_release;
use crate::{assert_modal_absent, assert_modal_present, assert_navigation_popped}; use crate::{assert_modal_absent, assert_modal_present, assert_navigation_popped};
use pretty_assertions::assert_eq; use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest; use rstest::rstest;
#[rstest] #[rstest]
@@ -538,6 +540,161 @@ mod tests {
assert_modal_absent!(app.data.readarr_data.edit_author_modal); assert_modal_absent!(app.data.readarr_data.edit_author_modal);
} }
#[test]
fn test_author_details_view_key_opens_the_author_overview() {
let mut app = App::test_default_fully_populated();
app.data.readarr_data.author_overview_modal = None;
app.push_navigation_stack(ActiveReadarrBlock::AuthorDetails.into());
AuthorDetailsHandler::new(
DEFAULT_KEYBINDINGS.view.key,
&mut app,
ActiveReadarrBlock::AuthorDetails,
None,
)
.handle();
assert_navigation_pushed!(app, ActiveReadarrBlock::AuthorOverview.into());
assert_modal_present!(app.data.readarr_data.author_overview_modal);
}
#[test]
fn test_author_details_view_key_builds_the_overview_from_the_selected_author() {
let mut app = App::test_default_fully_populated();
app.data.readarr_data.author_overview_modal = None;
app.push_navigation_stack(ActiveReadarrBlock::AuthorDetails.into());
AuthorDetailsHandler::new(
DEFAULT_KEYBINDINGS.view.key,
&mut app,
ActiveReadarrBlock::AuthorDetails,
None,
)
.handle();
let overview = &app
.data
.readarr_data
.author_overview_modal
.as_ref()
.unwrap()
.overview;
assert_eq!(overview.offset, 0);
assert_eq!(
overview.get_text(),
app
.data
.readarr_data
.authors
.current_selection()
.overview
.clone()
.unwrap()
);
}
#[test]
fn test_author_details_view_key_for_an_author_with_no_overview() {
let mut app = App::test_default_fully_populated();
app.data.readarr_data.author_overview_modal = None;
let mut author = app.data.readarr_data.authors.current_selection().clone();
author.overview = None;
app.data.readarr_data.authors.set_items(vec![author]);
app.push_navigation_stack(ActiveReadarrBlock::AuthorDetails.into());
AuthorDetailsHandler::new(
DEFAULT_KEYBINDINGS.view.key,
&mut app,
ActiveReadarrBlock::AuthorDetails,
None,
)
.handle();
assert_navigation_pushed!(app, ActiveReadarrBlock::AuthorOverview.into());
assert_str_eq!(
app
.data
.readarr_data
.author_overview_modal
.as_ref()
.unwrap()
.overview
.get_text(),
""
);
}
#[test]
fn test_author_details_view_key_reopens_the_author_overview_at_the_top() {
let mut app = App::test_default_fully_populated();
app.data.readarr_data.author_overview_modal = None;
app.push_navigation_stack(ActiveReadarrBlock::AuthorDetails.into());
AuthorDetailsHandler::new(
DEFAULT_KEYBINDINGS.view.key,
&mut app,
ActiveReadarrBlock::AuthorDetails,
None,
)
.handle();
AuthorOverviewHandler::new(
DEFAULT_KEYBINDINGS.end.key,
&mut app,
ActiveReadarrBlock::AuthorOverview,
None,
)
.handle();
AuthorOverviewHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveReadarrBlock::AuthorOverview,
None,
)
.handle();
AuthorDetailsHandler::new(
DEFAULT_KEYBINDINGS.view.key,
&mut app,
ActiveReadarrBlock::AuthorDetails,
None,
)
.handle();
assert_navigation_pushed!(app, ActiveReadarrBlock::AuthorOverview.into());
assert_eq!(
app
.data
.readarr_data
.author_overview_modal
.as_ref()
.unwrap()
.overview
.offset,
0
);
}
#[test]
fn test_author_details_view_key_no_op_when_not_ready() {
let mut app = App::test_default_fully_populated();
app.data.readarr_data.author_overview_modal = None;
app.is_loading = true;
app.push_navigation_stack(ActiveReadarrBlock::AuthorDetails.into());
AuthorDetailsHandler::new(
DEFAULT_KEYBINDINGS.view.key,
&mut app,
ActiveReadarrBlock::AuthorDetails,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveReadarrBlock::AuthorDetails.into()
);
assert_modal_absent!(app.data.readarr_data.author_overview_modal);
}
#[test] #[test]
fn test_author_details_toggle_monitoring_key() { fn test_author_details_toggle_monitoring_key() {
let mut app = App::test_default_fully_populated(); let mut app = App::test_default_fully_populated();
@@ -828,8 +985,11 @@ mod tests {
#[test] #[test]
fn test_author_details_handler_accepts() { fn test_author_details_handler_accepts() {
let mut author_details_blocks = AUTHOR_DETAILS_BLOCKS.to_vec();
author_details_blocks.extend(AUTHOR_OVERVIEW_BLOCKS);
ActiveReadarrBlock::iter().for_each(|readarr_block| { ActiveReadarrBlock::iter().for_each(|readarr_block| {
if AUTHOR_DETAILS_BLOCKS.contains(&readarr_block) { if author_details_blocks.contains(&readarr_block) {
assert!( assert!(
AuthorDetailsHandler::accepts(readarr_block), AuthorDetailsHandler::accepts(readarr_block),
"{readarr_block} is not accepted by the AuthorDetailsHandler" "{readarr_block} is not accepted by the AuthorDetailsHandler"
@@ -840,6 +1000,15 @@ mod tests {
}); });
} }
#[test]
fn test_delegates_author_overview_blocks_to_author_overview_handler() {
test_handler_delegation!(
AuthorDetailsHandler,
ActiveReadarrBlock::AuthorDetails,
ActiveReadarrBlock::AuthorOverview
);
}
#[test] #[test]
fn test_extract_author_id() { fn test_extract_author_id() {
let mut app = App::test_default_fully_populated(); let mut app = App::test_default_fully_populated();
@@ -0,0 +1,111 @@
use crate::app::App;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::matches_key;
use crate::models::servarr_data::readarr::readarr_data::{
AUTHOR_OVERVIEW_BLOCKS, ActiveReadarrBlock,
};
use crate::models::{Route, Scrollable, ScrollableText};
#[cfg(test)]
#[path = "author_overview_handler_tests.rs"]
mod author_overview_handler_tests;
pub(super) struct AuthorOverviewHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_readarr_block: ActiveReadarrBlock,
_context: Option<ActiveReadarrBlock>,
}
impl AuthorOverviewHandler<'_, '_> {
fn overview(&mut self) -> &mut ScrollableText {
&mut self
.app
.data
.readarr_data
.author_overview_modal
.as_mut()
.expect("Author overview modal is undefined")
.overview
}
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for AuthorOverviewHandler<'a, 'b> {
fn accepts(active_block: ActiveReadarrBlock) -> bool {
AUTHOR_OVERVIEW_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<ActiveReadarrBlock>,
) -> AuthorOverviewHandler<'a, 'b> {
AuthorOverviewHandler {
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;
}
self.app.data.readarr_data.author_overview_modal.is_some()
}
fn handle_scroll_up(&mut self) {
self.overview().scroll_up();
}
fn handle_scroll_down(&mut self) {
self.overview().scroll_down();
}
fn handle_home(&mut self) {
self.overview().scroll_to_top();
}
fn handle_end(&mut self) {
self.overview().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.author_overview_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()
}
}
@@ -0,0 +1,393 @@
#[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::author_overview_handler::AuthorOverviewHandler;
use crate::models::ScrollableText;
use crate::models::servarr_data::readarr::modals::AuthorOverviewModal;
use crate::models::servarr_data::readarr::readarr_data::{
AUTHOR_OVERVIEW_BLOCKS, ActiveReadarrBlock,
};
mod test_handle_scroll_up_and_down {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_author_overview_scroll_up() {
let mut app = app_with_author_overview();
set_offset(&mut app, 2);
AuthorOverviewHandler::new(
DEFAULT_KEYBINDINGS.up.key,
&mut app,
ActiveReadarrBlock::AuthorOverview,
None,
)
.handle();
assert_eq!(overview_offset(&app), 1);
}
#[test]
fn test_author_overview_scroll_up_no_op_when_not_ready() {
let mut app = app_with_author_overview();
set_offset(&mut app, 2);
app.is_loading = true;
AuthorOverviewHandler::new(
DEFAULT_KEYBINDINGS.up.key,
&mut app,
ActiveReadarrBlock::AuthorOverview,
None,
)
.handle();
assert_eq!(overview_offset(&app), 2);
}
#[test]
fn test_author_overview_scroll_down() {
let mut app = app_with_author_overview();
AuthorOverviewHandler::new(
DEFAULT_KEYBINDINGS.down.key,
&mut app,
ActiveReadarrBlock::AuthorOverview,
None,
)
.handle();
assert_eq!(overview_offset(&app), 1);
}
#[test]
fn test_author_overview_scroll_down_no_op_when_not_ready() {
let mut app = app_with_author_overview();
app.is_loading = true;
AuthorOverviewHandler::new(
DEFAULT_KEYBINDINGS.down.key,
&mut app,
ActiveReadarrBlock::AuthorOverview,
None,
)
.handle();
assert_eq!(overview_offset(&app), 0);
}
}
mod test_handle_home_end {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_author_overview_home() {
let mut app = app_with_author_overview();
set_offset(&mut app, 3);
AuthorOverviewHandler::new(
DEFAULT_KEYBINDINGS.home.key,
&mut app,
ActiveReadarrBlock::AuthorOverview,
None,
)
.handle();
assert_eq!(overview_offset(&app), 0);
}
#[test]
fn test_author_overview_end() {
let mut app = app_with_author_overview();
AuthorOverviewHandler::new(
DEFAULT_KEYBINDINGS.end.key,
&mut app,
ActiveReadarrBlock::AuthorOverview,
None,
)
.handle();
assert_eq!(overview_offset(&app), 5);
}
#[test]
fn test_author_overview_home_no_op_when_not_ready() {
let mut app = app_with_author_overview();
set_offset(&mut app, 3);
app.is_loading = true;
AuthorOverviewHandler::new(
DEFAULT_KEYBINDINGS.home.key,
&mut app,
ActiveReadarrBlock::AuthorOverview,
None,
)
.handle();
assert_eq!(overview_offset(&app), 3);
}
#[test]
fn test_author_overview_end_no_op_when_not_ready() {
let mut app = app_with_author_overview();
app.is_loading = true;
AuthorOverviewHandler::new(
DEFAULT_KEYBINDINGS.end.key,
&mut app,
ActiveReadarrBlock::AuthorOverview,
None,
)
.handle();
assert_eq!(overview_offset(&app), 0);
}
}
mod test_handle_esc {
use super::*;
use crate::{assert_modal_absent, assert_navigation_popped};
#[test]
fn test_author_overview_esc_tears_down_the_author_overview_modal() {
let mut app = app_with_author_overview();
AuthorOverviewHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveReadarrBlock::AuthorOverview,
None,
)
.handle();
assert_navigation_popped!(app, ActiveReadarrBlock::AuthorDetails.into());
assert_modal_absent!(app.data.readarr_data.author_overview_modal);
}
#[test]
fn test_author_overview_esc_tears_down_the_modal_even_when_loading() {
let mut app = app_with_author_overview();
app.is_loading = true;
AuthorOverviewHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveReadarrBlock::AuthorOverview,
None,
)
.handle();
assert_navigation_popped!(app, ActiveReadarrBlock::AuthorDetails.into());
assert_modal_absent!(app.data.readarr_data.author_overview_modal);
}
}
mod test_handle_key_char {
use super::*;
use crate::assert_navigation_pushed;
#[test]
fn test_author_overview_refresh_key() {
let mut app = app_with_author_overview();
app.is_routing = false;
AuthorOverviewHandler::new(
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
ActiveReadarrBlock::AuthorOverview,
None,
)
.handle();
assert_navigation_pushed!(app, ActiveReadarrBlock::AuthorOverview.into());
assert!(app.is_routing);
}
#[test]
fn test_author_overview_refresh_key_no_op_when_not_ready() {
let mut app = app_with_author_overview();
app.is_loading = true;
app.is_routing = false;
AuthorOverviewHandler::new(
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
ActiveReadarrBlock::AuthorOverview,
None,
)
.handle();
assert!(!app.is_routing);
}
}
#[test]
fn test_scrolling_the_overview_does_not_mutate_the_author_record() {
let mut app = app_with_author_overview();
let overview_before = app
.data
.readarr_data
.authors
.current_selection()
.overview
.clone();
AuthorOverviewHandler::new(
DEFAULT_KEYBINDINGS.down.key,
&mut app,
ActiveReadarrBlock::AuthorOverview,
None,
)
.handle();
assert_eq!(overview_offset(&app), 1);
assert_eq!(
app.data.readarr_data.authors.current_selection().overview,
overview_before
);
}
#[test]
fn test_author_overview_handler_accepts() {
ActiveReadarrBlock::iter().for_each(|active_readarr_block| {
if AUTHOR_OVERVIEW_BLOCKS.contains(&active_readarr_block) {
assert!(
AuthorOverviewHandler::accepts(active_readarr_block),
"{active_readarr_block} is not accepted by the AuthorOverviewHandler"
);
} else {
assert!(
!AuthorOverviewHandler::accepts(active_readarr_block),
"{active_readarr_block} is wrongly accepted by the AuthorOverviewHandler"
);
}
});
}
#[test]
fn test_author_overview_handler_dispatches_no_network_events() {
let mut app = app_with_author_overview();
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,
] {
AuthorOverviewHandler::new(key, &mut app, ActiveReadarrBlock::AuthorOverview, None).handle();
assert_none!(app.data.readarr_data.prompt_confirm_action);
assert!(!app.data.readarr_data.prompt_confirm);
}
}
#[test]
fn test_author_overview_handler_is_not_ready_when_loading() {
let mut app = app_with_author_overview();
app.is_loading = true;
let handler = AuthorOverviewHandler::new(
DEFAULT_KEYBINDINGS.up.key,
&mut app,
ActiveReadarrBlock::AuthorOverview,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_author_overview_handler_is_not_ready_when_the_modal_is_absent() {
let mut app = app_with_author_overview();
app.data.readarr_data.author_overview_modal = None;
let handler = AuthorOverviewHandler::new(
DEFAULT_KEYBINDINGS.up.key,
&mut app,
ActiveReadarrBlock::AuthorOverview,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_author_overview_handler_is_ready() {
let mut app = app_with_author_overview();
let handler = AuthorOverviewHandler::new(
DEFAULT_KEYBINDINGS.up.key,
&mut app,
ActiveReadarrBlock::AuthorOverview,
None,
);
assert!(handler.is_ready());
}
#[test]
fn test_author_overview_handler_ignores_special_keys_for_textbox_input() {
let mut app = app_with_author_overview();
app.ignore_special_keys_for_textbox_input = true;
let handler = AuthorOverviewHandler::new(
DEFAULT_KEYBINDINGS.up.key,
&mut app,
ActiveReadarrBlock::AuthorOverview,
None,
);
assert!(handler.ignore_special_keys());
}
fn app_with_author_overview() -> App<'static> {
let mut app = App::test_default_fully_populated();
app.data.readarr_data.author_overview_modal = Some(AuthorOverviewModal {
overview: ScrollableText::with_string(
"some interesting description of the author\r\n\
\r\n\
He was born in Madison, Wisconsin: a city he has never really left.\r\n\
\r\n\
His first novel took him seven years to finish.\r\n"
.to_owned(),
),
});
app.push_navigation_stack(ActiveReadarrBlock::AuthorDetails.into());
app.push_navigation_stack(ActiveReadarrBlock::AuthorOverview.into());
app
}
fn overview_offset(app: &App<'_>) -> u16 {
app
.data
.readarr_data
.author_overview_modal
.as_ref()
.unwrap()
.overview
.offset
}
fn set_offset(app: &mut App<'_>, offset: u16) {
app
.data
.readarr_data
.author_overview_modal
.as_mut()
.unwrap()
.overview
.offset = offset;
}
}
@@ -14,9 +14,9 @@ mod tests {
use crate::models::HorizontallyScrollableText; use crate::models::HorizontallyScrollableText;
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, AUTHOR_OVERVIEW_BLOCKS, ActiveReadarrBlock,
DELETE_AUTHOR_BLOCKS, DELETE_BOOK_BLOCKS, EDIT_AUTHOR_BLOCKS, EDITION_DETAILS_BLOCKS, BOOK_DETAILS_BLOCKS, DELETE_AUTHOR_BLOCKS, DELETE_BOOK_BLOCKS, EDIT_AUTHOR_BLOCKS,
LIBRARY_BLOCKS, EDITION_DETAILS_BLOCKS, LIBRARY_BLOCKS,
}; };
use crate::test_handler_delegation; use crate::test_handler_delegation;
@@ -931,6 +931,7 @@ mod tests {
let mut library_handler_blocks = Vec::new(); let mut library_handler_blocks = Vec::new();
library_handler_blocks.extend(LIBRARY_BLOCKS); library_handler_blocks.extend(LIBRARY_BLOCKS);
library_handler_blocks.extend(AUTHOR_DETAILS_BLOCKS); library_handler_blocks.extend(AUTHOR_DETAILS_BLOCKS);
library_handler_blocks.extend(AUTHOR_OVERVIEW_BLOCKS);
library_handler_blocks.extend(BOOK_DETAILS_BLOCKS); library_handler_blocks.extend(BOOK_DETAILS_BLOCKS);
library_handler_blocks.extend(EDITION_DETAILS_BLOCKS); library_handler_blocks.extend(EDITION_DETAILS_BLOCKS);
library_handler_blocks.extend(ADD_AUTHOR_BLOCKS); library_handler_blocks.extend(ADD_AUTHOR_BLOCKS);
@@ -21,6 +21,7 @@ use crate::models::Route;
mod add_author_handler; mod add_author_handler;
mod author_details_handler; mod author_details_handler;
mod author_overview_handler;
mod book_details_handler; mod book_details_handler;
mod delete_author_handler; mod delete_author_handler;
mod delete_book_handler; mod delete_book_handler;
@@ -276,3 +276,9 @@ impl Default for BookDetailsModal {
pub struct EditionDetailsModal { pub struct EditionDetailsModal {
pub edition_details: ScrollableText, pub edition_details: ScrollableText,
} }
#[derive(Default)]
#[cfg_attr(test, derive(Debug))]
pub struct AuthorOverviewModal {
pub overview: ScrollableText,
}
@@ -7,8 +7,8 @@ mod tests {
use crate::models::readarr_models::{Author, MonitorType, NewItemMonitorType}; use crate::models::readarr_models::{Author, MonitorType, NewItemMonitorType};
use crate::models::servarr_data::modals::EditIndexerModal; use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_data::readarr::modals::{ use crate::models::servarr_data::readarr::modals::{
AddAuthorModal, AddReadarrRootFolderModal, BookDetailsModal, EditAuthorModal, AddAuthorModal, AddReadarrRootFolderModal, AuthorOverviewModal, BookDetailsModal,
EditionDetailsModal, EditAuthorModal, EditionDetailsModal,
}; };
use crate::models::servarr_data::readarr::readarr_data::{ActiveReadarrBlock, ReadarrData}; use crate::models::servarr_data::readarr::readarr_data::{ActiveReadarrBlock, ReadarrData};
use crate::models::servarr_models::{Indexer, IndexerField, RootFolder}; use crate::models::servarr_models::{Indexer, IndexerField, RootFolder};
@@ -331,4 +331,11 @@ mod tests {
assert_is_empty!(edition_details_modal.edition_details); assert_is_empty!(edition_details_modal.edition_details);
} }
#[test]
fn test_author_overview_modal_default() {
let author_overview_modal = AuthorOverviewModal::default();
assert_is_empty!(author_overview_modal.overview);
}
} }
@@ -1,6 +1,8 @@
use serde_json::Number; use serde_json::Number;
use super::modals::{AddAuthorModal, AddReadarrRootFolderModal, BookDetailsModal, EditAuthorModal}; use super::modals::{
AddAuthorModal, AddReadarrRootFolderModal, AuthorOverviewModal, BookDetailsModal, EditAuthorModal,
};
use crate::app::context_clues::{ use crate::app::context_clues::{
BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
@@ -55,6 +57,7 @@ pub struct ReadarrData<'a> {
pub add_searched_authors: Option<StatefulTable<AddAuthorSearchResult>>, pub add_searched_authors: Option<StatefulTable<AddAuthorSearchResult>>,
pub author_history: StatefulTable<ReadarrHistoryItem>, pub author_history: StatefulTable<ReadarrHistoryItem>,
pub author_info_tabs: TabState, pub author_info_tabs: TabState,
pub author_overview_modal: Option<AuthorOverviewModal>,
pub author_releases: StatefulTable<ReadarrRelease>, pub author_releases: StatefulTable<ReadarrRelease>,
pub authors: StatefulTable<Author>, pub authors: StatefulTable<Author>,
pub blocklist: StatefulTable<BlocklistItem>, pub blocklist: StatefulTable<BlocklistItem>,
@@ -141,6 +144,7 @@ impl<'a> Default for ReadarrData<'a> {
add_root_folder_modal: None, add_root_folder_modal: None,
add_searched_authors: None, add_searched_authors: None,
author_history: StatefulTable::default(), author_history: StatefulTable::default(),
author_overview_modal: None,
author_releases: StatefulTable::default(), author_releases: StatefulTable::default(),
authors: StatefulTable::default(), authors: StatefulTable::default(),
blocklist: StatefulTable::default(), blocklist: StatefulTable::default(),
@@ -262,7 +266,14 @@ impl ReadarrData<'_> {
author_name: "Test Author".into(), author_name: "Test Author".into(),
foreign_author_id: "test-foreign-id".to_owned(), foreign_author_id: "test-foreign-id".to_owned(),
status: AuthorStatus::Continuing, status: AuthorStatus::Continuing,
overview: Some("some interesting description of the author".to_owned()), overview: Some(
"some interesting description of the author\r\n\
\r\n\
He was born in Madison, Wisconsin: a city he has never really left.\r\n\
\r\n\
His first novel took him seven years to finish.\r\n"
.to_owned(),
),
path: "/nfs/books/Test Author".to_owned(), path: "/nfs/books/Test Author".to_owned(),
quality_profile_id: 1, quality_profile_id: 1,
metadata_profile_id: 1, metadata_profile_id: 1,
@@ -551,9 +562,14 @@ impl ReadarrData<'_> {
let mut indexer_test_all_results = StatefulTable::default(); let mut indexer_test_all_results = StatefulTable::default();
indexer_test_all_results.set_items(vec![indexer_test_result()]); indexer_test_all_results.set_items(vec![indexer_test_result()]);
let author_overview_modal = AuthorOverviewModal {
overview: ScrollableText::with_string(author.overview.clone().unwrap_or_default()),
};
let mut readarr_data = ReadarrData { let mut readarr_data = ReadarrData {
add_author_modal: Some(add_author_modal), add_author_modal: Some(add_author_modal),
add_root_folder_modal: Some(add_root_folder_modal), add_root_folder_modal: Some(add_root_folder_modal),
author_overview_modal: Some(author_overview_modal),
book_details_modal: Some(book_details_modal), book_details_modal: Some(book_details_modal),
delete_files: true, delete_files: true,
disk_space_vec: vec![diskspace()], disk_space_vec: vec![diskspace()],
@@ -647,6 +663,7 @@ pub enum ActiveReadarrBlock {
AuthorHistory, AuthorHistory,
AuthorHistoryDetails, AuthorHistoryDetails,
AuthorHistorySortPrompt, AuthorHistorySortPrompt,
AuthorOverview,
AuthorsSortPrompt, AuthorsSortPrompt,
AutomaticallySearchAuthorPrompt, AutomaticallySearchAuthorPrompt,
AutomaticallySearchBookPrompt, AutomaticallySearchBookPrompt,
@@ -770,6 +787,8 @@ pub static AUTHOR_DETAILS_BLOCKS: [ActiveReadarrBlock; 15] = [
ActiveReadarrBlock::UpdateAndScanAuthorPrompt, ActiveReadarrBlock::UpdateAndScanAuthorPrompt,
]; ];
pub static AUTHOR_OVERVIEW_BLOCKS: [ActiveReadarrBlock; 1] = [ActiveReadarrBlock::AuthorOverview];
pub static BOOK_DETAILS_BLOCKS: [ActiveReadarrBlock; 16] = [ pub static BOOK_DETAILS_BLOCKS: [ActiveReadarrBlock; 16] = [
ActiveReadarrBlock::BookDetails, ActiveReadarrBlock::BookDetails,
ActiveReadarrBlock::BookHistory, ActiveReadarrBlock::BookHistory,
@@ -11,13 +11,14 @@ mod tests {
use crate::models::readarr_models::{Book, ReadarrHistoryItem, ReadarrRelease}; use crate::models::readarr_models::{Book, ReadarrHistoryItem, ReadarrRelease};
use crate::models::servarr_data::readarr::readarr_data::{ use crate::models::servarr_data::readarr::readarr_data::{
ADD_AUTHOR_BLOCKS, ADD_AUTHOR_SELECTION_BLOCKS, ADD_ROOT_FOLDER_BLOCKS, ADD_AUTHOR_BLOCKS, ADD_AUTHOR_SELECTION_BLOCKS, ADD_ROOT_FOLDER_BLOCKS,
ADD_ROOT_FOLDER_SELECTION_BLOCKS, AUTHOR_DETAILS_BLOCKS, ActiveReadarrBlock, BLOCKLIST_BLOCKS, ADD_ROOT_FOLDER_SELECTION_BLOCKS, AUTHOR_DETAILS_BLOCKS, AUTHOR_OVERVIEW_BLOCKS,
BOOK_DETAILS_BLOCKS, DELETE_AUTHOR_BLOCKS, DELETE_AUTHOR_SELECTION_BLOCKS, DELETE_BOOK_BLOCKS, ActiveReadarrBlock, BLOCKLIST_BLOCKS, BOOK_DETAILS_BLOCKS, DELETE_AUTHOR_BLOCKS,
DELETE_BOOK_SELECTION_BLOCKS, DOWNLOADS_BLOCKS, EDIT_AUTHOR_BLOCKS, DELETE_AUTHOR_SELECTION_BLOCKS, DELETE_BOOK_BLOCKS, DELETE_BOOK_SELECTION_BLOCKS,
EDIT_AUTHOR_SELECTION_BLOCKS, EDIT_INDEXER_BLOCKS, EDIT_INDEXER_NZB_SELECTION_BLOCKS, DOWNLOADS_BLOCKS, EDIT_AUTHOR_BLOCKS, EDIT_AUTHOR_SELECTION_BLOCKS, EDIT_INDEXER_BLOCKS,
EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, EDITION_DETAILS_BLOCKS, HISTORY_BLOCKS, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
INDEXER_SETTINGS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS, LIBRARY_BLOCKS, EDITION_DETAILS_BLOCKS, HISTORY_BLOCKS, INDEXER_SETTINGS_BLOCKS,
ROOT_FOLDERS_BLOCKS, ReadarrData, SYSTEM_DETAILS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS, LIBRARY_BLOCKS, ROOT_FOLDERS_BLOCKS,
ReadarrData, SYSTEM_DETAILS_BLOCKS,
}; };
use crate::models::{BlockSelectionState, Route}; use crate::models::{BlockSelectionState, Route};
use bimap::BiMap; use bimap::BiMap;
@@ -152,6 +153,7 @@ mod tests {
assert_none!(readarr_data.add_root_folder_modal); assert_none!(readarr_data.add_root_folder_modal);
assert_none!(readarr_data.add_searched_authors); assert_none!(readarr_data.add_searched_authors);
assert_is_empty!(readarr_data.author_history); assert_is_empty!(readarr_data.author_history);
assert_none!(readarr_data.author_overview_modal);
assert_is_empty!(readarr_data.author_releases); assert_is_empty!(readarr_data.author_releases);
assert_is_empty!(readarr_data.authors); assert_is_empty!(readarr_data.authors);
assert_is_empty!(readarr_data.blocklist); assert_is_empty!(readarr_data.blocklist);
@@ -361,6 +363,17 @@ mod tests {
assert!(EDITION_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::BookEditionDetails)); assert!(EDITION_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::BookEditionDetails));
} }
#[test]
fn test_author_details_blocks_excludes_author_overview() {
assert!(!AUTHOR_DETAILS_BLOCKS.contains(&ActiveReadarrBlock::AuthorOverview));
}
#[test]
fn test_author_overview_blocks_contents() {
assert_eq!(AUTHOR_OVERVIEW_BLOCKS.len(), 1);
assert!(AUTHOR_OVERVIEW_BLOCKS.contains(&ActiveReadarrBlock::AuthorOverview));
}
#[test] #[test]
fn test_blocklist_blocks_contents() { fn test_blocklist_blocks_contents() {
assert_eq!(BLOCKLIST_BLOCKS.len(), 5); assert_eq!(BLOCKLIST_BLOCKS.len(), 5);
@@ -13,6 +13,7 @@ use crate::models::readarr_models::{Book, ReadarrHistoryItem, ReadarrRelease};
use crate::models::servarr_data::readarr::readarr_data::{ use crate::models::servarr_data::readarr::readarr_data::{
AUTHOR_DETAILS_BLOCKS, ActiveReadarrBlock, AUTHOR_DETAILS_BLOCKS, ActiveReadarrBlock,
}; };
use crate::ui::readarr_ui::library::author_overview_ui::AuthorOverviewUi;
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::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;
@@ -45,6 +46,7 @@ impl DrawUi for AuthorDetailsUi {
}; };
BookDetailsUi::accepts(route) BookDetailsUi::accepts(route)
|| DeleteBookUi::accepts(route) || DeleteBookUi::accepts(route)
|| AuthorOverviewUi::accepts(route)
|| AUTHOR_DETAILS_BLOCKS.contains(&active_readarr_block) || AUTHOR_DETAILS_BLOCKS.contains(&active_readarr_block)
} }
@@ -134,6 +136,10 @@ impl DrawUi for AuthorDetailsUi {
if BookDetailsUi::accepts(route) { if BookDetailsUi::accepts(route) {
BookDetailsUi::draw(f, app, _area); BookDetailsUi::draw(f, app, _area);
} }
if AuthorOverviewUi::accepts(route) {
AuthorOverviewUi::draw(f, app, _area);
}
} }
} }
} }
@@ -7,8 +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, DELETE_BOOK_BLOCKS, AUTHOR_DETAILS_BLOCKS, AUTHOR_OVERVIEW_BLOCKS, ActiveReadarrBlock, BOOK_DETAILS_BLOCKS,
EDITION_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::{
@@ -22,6 +22,7 @@ mod tests {
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); author_details_blocks.extend(DELETE_BOOK_BLOCKS);
author_details_blocks.extend(AUTHOR_OVERVIEW_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) {
@@ -149,6 +150,7 @@ mod tests {
} }
mod snapshot_tests { mod snapshot_tests {
use pretty_assertions::assert_str_eq;
use rstest::rstest; use rstest::rstest;
use crate::app::App; use crate::app::App;
@@ -337,6 +339,46 @@ mod tests {
insta::assert_snapshot!(output); insta::assert_snapshot!(output);
} }
#[test]
fn test_author_details_ui_renders_author_overview_over_author_details() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveReadarrBlock::AuthorDetails.into());
app.push_navigation_stack(ActiveReadarrBlock::AuthorOverview.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
AuthorDetailsUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
#[test]
fn test_scrolling_the_author_overview_does_not_scroll_the_author_details_background() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveReadarrBlock::AuthorDetails.into());
let before = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
AuthorDetailsUi::draw(f, app, f.area());
});
app
.data
.readarr_data
.author_overview_modal
.as_mut()
.unwrap()
.overview
.offset = 3;
let after = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
AuthorDetailsUi::draw(f, app, f.area());
});
assert_str_eq!(before, after);
assert_contains!(
before,
"Overview: some interesting description of the author"
);
}
#[test] #[test]
fn test_author_details_ui_renders_delete_book_over_author_details() { fn test_author_details_ui_renders_delete_book_over_author_details() {
let mut app = App::test_default_fully_populated(); let mut app = App::test_default_fully_populated();
@@ -0,0 +1,87 @@
use crate::app::App;
use crate::models::Route;
use crate::models::readarr_models::Author;
use crate::models::servarr_data::readarr::readarr_data::AUTHOR_OVERVIEW_BLOCKS;
use crate::ui::styles::{primary_style, unmonitored_style};
use crate::ui::utils::title_block_centered;
use crate::ui::widgets::loading_block::LoadingBlock;
use crate::ui::widgets::popup::Size;
use crate::ui::{DrawUi, draw_popup};
use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::text::{Line, Span, Text};
use ratatui::widgets::{Paragraph, Wrap};
#[cfg(test)]
#[path = "author_overview_ui_tests.rs"]
mod author_overview_ui_tests;
pub(super) struct AuthorOverviewUi;
impl DrawUi for AuthorOverviewUi {
fn accepts(route: Route) -> bool {
let Route::Readarr(active_readarr_block, _) = route else {
return false;
};
AUTHOR_OVERVIEW_BLOCKS.contains(&active_readarr_block)
}
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) {
if app.data.readarr_data.author_overview_modal.is_some() {
let draw_author_overview_popup = |f: &mut Frame<'_>, app: &mut App<'_>, popup_area: Rect| {
draw_author_overview(f, app, popup_area);
};
draw_popup(f, app, draw_author_overview_popup, Size::Large);
}
}
}
fn draw_author_overview(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
let block = title_block_centered("Overview");
match app.data.readarr_data.author_overview_modal.as_ref() {
Some(author_overview_modal) if !app.is_loading => {
let style = if app.data.readarr_data.authors.is_empty() {
primary_style()
} else {
style_from_author(app.data.readarr_data.authors.current_selection())
};
let overview = &author_overview_modal.overview;
let text = Text::from(
overview
.items
.iter()
.map(|line| overview_line(line, style))
.collect::<Vec<Line<'static>>>(),
);
let paragraph = Paragraph::new(text)
.block(block)
.wrap(Wrap { trim: false })
.scroll((overview.offset, 0));
f.render_widget(paragraph, area);
}
_ => f.render_widget(
LoadingBlock::new(
app.is_loading || app.data.readarr_data.author_overview_modal.is_none(),
block,
),
area,
),
}
}
fn style_from_author(author: &Author) -> Style {
if !author.monitored {
return unmonitored_style();
}
primary_style()
}
fn overview_line(line: &str, style: Style) -> Line<'static> {
Line::from(Span::styled(line.trim_end().to_owned(), style))
}
@@ -0,0 +1,205 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use ratatui::style::Modifier;
use ratatui::text::Line;
use strum::IntoEnumIterator;
use crate::app::App;
use crate::models::readarr_models::Author;
use crate::models::servarr_data::readarr::readarr_data::{
AUTHOR_OVERVIEW_BLOCKS, ActiveReadarrBlock,
};
use crate::ui::DrawUi;
use crate::ui::readarr_ui::library::author_overview_ui::{
AuthorOverviewUi, overview_line, style_from_author,
};
use crate::ui::styles::{primary_style, unmonitored_style};
use crate::ui::ui_test_utils::test_utils::render_to_string_with_app;
#[test]
fn test_author_overview_ui_accepts() {
ActiveReadarrBlock::iter().for_each(|active_readarr_block| {
if AUTHOR_OVERVIEW_BLOCKS.contains(&active_readarr_block) {
assert!(AuthorOverviewUi::accepts(active_readarr_block.into()));
} else {
assert!(!AuthorOverviewUi::accepts(active_readarr_block.into()));
}
});
}
#[test]
fn test_style_from_author_monitored() {
let author = Author {
monitored: true,
..Author::default()
};
let style = style_from_author(&author);
assert_eq!(style, primary_style());
}
#[test]
fn test_style_from_author_unmonitored() {
let author = Author {
monitored: false,
..Author::default()
};
let style = style_from_author(&author);
assert_eq!(style, unmonitored_style());
}
#[test]
fn test_overview_line_does_not_split_a_label_from_a_colon_in_the_prose() {
let line = overview_line(
"He was born in Madison, Wisconsin: a city he has never really left.",
primary_style(),
);
assert_eq!(
span_contents(&line),
vec!["He was born in Madison, Wisconsin: a city he has never really left."]
);
}
#[test]
fn test_overview_line_renders_the_prose_without_bolding_it() {
let line = overview_line("Overview: a prose line", primary_style());
assert_eq!(line.spans.len(), 1);
assert!(!line.spans[0].style.add_modifier.contains(Modifier::BOLD));
}
#[test]
fn test_overview_line_applies_the_given_style() {
let line = overview_line("an unmonitored author", unmonitored_style());
assert_eq!(line.spans[0].style.fg, unmonitored_style().fg);
}
#[test]
fn test_overview_line_trims_a_trailing_carriage_return() {
let line = overview_line(
"His first novel took him seven years to finish.\r",
primary_style(),
);
assert_eq!(
span_contents(&line),
vec!["His first novel took him seven years to finish."]
);
}
#[test]
fn test_overview_line_renders_a_carriage_return_only_line_as_blank() {
let line = overview_line("\r", primary_style());
assert_eq!(span_contents(&line), vec![""]);
}
fn span_contents(line: &Line<'_>) -> Vec<String> {
line
.spans
.iter()
.map(|span| span.content.to_string())
.collect()
}
mod snapshot_tests {
use crate::models::ScrollableText;
use crate::models::servarr_data::readarr::modals::AuthorOverviewModal;
use crate::models::stateful_table::StatefulTable;
use crate::ui::ui_test_utils::test_utils::TerminalSize;
use super::*;
#[test]
fn test_author_overview_ui_renders() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveReadarrBlock::AuthorOverview.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
AuthorOverviewUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
#[test]
fn test_author_overview_ui_renders_loading() {
let mut app = App::test_default_fully_populated();
app.is_loading = true;
app.push_navigation_stack(ActiveReadarrBlock::AuthorOverview.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
AuthorOverviewUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
#[test]
fn test_author_overview_ui_renders_scrolled() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveReadarrBlock::AuthorOverview.into());
app
.data
.readarr_data
.author_overview_modal
.as_mut()
.unwrap()
.overview
.offset = 2;
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
AuthorOverviewUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
#[test]
fn test_author_overview_ui_renders_an_author_with_no_overview() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveReadarrBlock::AuthorOverview.into());
app.data.readarr_data.author_overview_modal = Some(AuthorOverviewModal {
overview: ScrollableText::with_string(String::new()),
});
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
AuthorOverviewUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
#[test]
fn test_author_overview_ui_renders_nothing_when_the_author_overview_modal_is_undefined() {
let mut app = App::test_default_fully_populated();
app.push_navigation_stack(ActiveReadarrBlock::AuthorOverview.into());
app.data.readarr_data.author_overview_modal = None;
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
AuthorOverviewUi::draw(f, app, f.area());
});
insta::assert_snapshot!(output);
}
#[test]
fn test_author_overview_ui_renders_with_zero_authors() {
let mut app = App::test_default_fully_populated();
app.data.readarr_data.authors = StatefulTable::default();
app.push_navigation_stack(ActiveReadarrBlock::AuthorOverview.into());
let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| {
AuthorOverviewUi::draw(f, app, f.area());
});
assert_contains!(output, "some interesting description of the author");
}
}
}
@@ -4,9 +4,9 @@ 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, AUTHOR_OVERVIEW_BLOCKS, ActiveReadarrBlock,
DELETE_AUTHOR_BLOCKS, DELETE_BOOK_BLOCKS, EDIT_AUTHOR_BLOCKS, EDITION_DETAILS_BLOCKS, BOOK_DETAILS_BLOCKS, DELETE_AUTHOR_BLOCKS, DELETE_BOOK_BLOCKS, EDIT_AUTHOR_BLOCKS,
LIBRARY_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};
@@ -18,6 +18,7 @@ mod tests {
fn test_library_ui_accepts() { fn test_library_ui_accepts() {
let mut blocks = LIBRARY_BLOCKS.to_vec(); let mut blocks = LIBRARY_BLOCKS.to_vec();
blocks.extend(AUTHOR_DETAILS_BLOCKS); blocks.extend(AUTHOR_DETAILS_BLOCKS);
blocks.extend(AUTHOR_OVERVIEW_BLOCKS);
blocks.extend(BOOK_DETAILS_BLOCKS); blocks.extend(BOOK_DETAILS_BLOCKS);
blocks.extend(EDITION_DETAILS_BLOCKS); blocks.extend(EDITION_DETAILS_BLOCKS);
blocks.extend(ADD_AUTHOR_BLOCKS); blocks.extend(ADD_AUTHOR_BLOCKS);
+1
View File
@@ -30,6 +30,7 @@ use crate::{
mod add_author_ui; mod add_author_ui;
mod author_details_ui; mod author_details_ui;
mod author_overview_ui;
mod book_details_ui; mod book_details_ui;
mod delete_author_ui; mod delete_author_ui;
mod delete_book_ui; mod delete_book_ui;
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ │╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ─────────────╭─────────────────────────────────── Details ───────────────────────────────────╮───────────────────────────────╮│ │╭ Author Details ─────────────╭─────────────────────────────────── Details ───────────────────────────────────╮───────────────────────────────╮│
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ │╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ │╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ───────────────────────╭──────────────── Automatic Author Search ────────────────╮───────────────────────────────────────────╮│ │╭ Author Details ───────────────────────╭──────────────── Automatic Author Search ────────────────╮───────────────────────────────────────────╮│
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ───────────────────────╭──────────────── Automatic Author Search ────────────────╮───────────────────────────────────────────╮│ │╭ Author Details ───────────────────────╭──────────────── Automatic Author Search ────────────────╮───────────────────────────────────────────╮│
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ───────────────────────╭──────────────── Automatic Author Search ────────────────╮───────────────────────────────────────────╮│ │╭ Author Details ───────────────────────╭──────────────── Automatic Author Search ────────────────╮───────────────────────────────────────────╮│
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ │╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ │╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ ╭───────────────── Download Rejected Release ──────────────────╮ │ │ ╭───────────────── Download Rejected Release ──────────────────╮ │
│╭ Author Details ──────────────────────│ Do you really want to download the rejected release: Test │───────────────────────────────────────╮│ │╭ Author Details ──────────────────────│ Do you really want to download the rejected release: Test │───────────────────────────────────────╮│
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ │╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ │╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ │╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ │╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ │╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ │╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ───────────────────────╭──────────────────── Update and Scan ────────────────────╮───────────────────────────────────────────╮│ │╭ Author Details ───────────────────────╭──────────────────── Update and Scan ────────────────────╮───────────────────────────────────────────╮│
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ───────────────────────╭──────────────────── Update and Scan ────────────────────╮───────────────────────────────────────────╮│ │╭ Author Details ───────────────────────╭──────────────────── Update and Scan ────────────────────╮───────────────────────────────────────────╮│
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ───────────────────────╭──────────────────── Update and Scan ────────────────────╮───────────────────────────────────────────╮│ │╭ Author Details ───────────────────────╭──────────────────── Update and Scan ────────────────────╮───────────────────────────────────────────╮│
@@ -0,0 +1,52 @@
---
source: src/ui/readarr_ui/library/author_details_ui_tests.rs
expression: output
---
╭ Test Author ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│Author: Test Author │
│Status: Continuing │
│Genres: sci╭─────────────────────────────────────────────────────── Overview ───────────────────────────────────────────────────────╮ │
│Rating: N/A│some interesting description of the author │ │
│Path: /nfs/│ │ │
│Quality Pro│He was born in Madison, Wisconsin: a city he has never really left. │ │
│Metadata Pr│ │ │
│Monitored: │His first novel took him seven years to finish. │ │
│Overview: s│ │took him │
│seven years│ │ │
│ │ │ │
│ │ │ │
│╭ Author D│ │───────────╮│
││ Books │ H│ │ ││
││──────────│ │───────────││
││ Monitor│ │ ││
││=> 🏷 │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ │ │ ││
││ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ││
││ ││
││ ││
│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ───────────────────────╭──────────────── Automatic Author Search ────────────────╮───────────────────────────────────────────╮│ │╭ Author Details ───────────────────────╭──────────────── Automatic Author Search ────────────────╮───────────────────────────────────────────╮│
@@ -14,8 +14,8 @@ expression: output
│Qual│ │ │ │Qual│ │ │
│Meta│ │ │ │Meta│ │ │
│Moni│ │ │ │Moni│ │ │
│Over│ │ │ │Over│ │im │
│ │ │ │ │seve│ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│╭ A│ │─────╮│ │╭ A│ │─────╮│
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ───────────────────────╭────────────────────── Delete Book ──────────────────────╮───────────────────────────────────────────╮│ │╭ Author Details ───────────────────────╭────────────────────── Delete Book ──────────────────────╮───────────────────────────────────────────╮│
@@ -17,7 +17,7 @@ expression: output
│Books: 3 │ │Books: 3 │
│Files: 2/3 │ │Files: 2/3 │
│Size on Disk: 3.30 GB │ │Size on Disk: 3.30 GB │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ │╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
││ Books │ History │ Author Search ││ ││ Books │ History │ Author Search ││
││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ───────────────────────╭──────────────────── Update and Scan ────────────────────╮───────────────────────────────────────────╮│ │╭ Author Details ───────────────────────╭──────────────────── Update and Scan ────────────────────╮───────────────────────────────────────────╮│
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ │╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ │╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ─────────────╭─────────────────────────────────── Details ───────────────────────────────────╮───────────────────────────────╮│ │╭ Author Details ─────────────╭─────────────────────────────────── Details ───────────────────────────────────╮───────────────────────────────╮│
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ │╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ │╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ │╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ │╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│
@@ -0,0 +1,48 @@
---
source: src/ui/readarr_ui/library/author_overview_ui_tests.rs
expression: output
---
╭─────────────────────────────────────────────────────── Overview ───────────────────────────────────────────────────────╮
│some interesting description of the author │
│ │
│He was born in Madison, Wisconsin: a city he has never really left. │
│ │
│His first novel took him seven years to finish. │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
source: src/ui/readarr_ui/library/author_overview_ui_tests.rs
expression: output
---
╭─────────────────────────────────────────────────────── Overview ───────────────────────────────────────────────────────╮
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
source: src/ui/readarr_ui/library/author_overview_ui_tests.rs
expression: output
---
╭─────────────────────────────────────────────────────── Overview ───────────────────────────────────────────────────────╮
│ │
│ │
│ Loading ... │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,48 @@
---
source: src/ui/readarr_ui/library/author_overview_ui_tests.rs
expression: output
---
╭─────────────────────────────────────────────────────── Overview ───────────────────────────────────────────────────────╮
│He was born in Madison, Wisconsin: a city he has never really left. │
│ │
│His first novel took him seven years to finish. │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: St│ │ │ │Quality Profile: St│ │ │
│Metadata Profile: S│ │ │ │Metadata Profile: S│ │ │
│Monitored: Yes │ │ │ │Monitored: Yes │ │ │
│Overview: some inte│ │ │ │Overview: some inte│ │st novel took him │
│ │ │ │ │seven years to fini│ │ │
│ │ │ │ │ │ │ │
│ │ ╭───╮ │ │ │ │ ╭───╮ │ │
│╭ Author Details │ Monitored: │ ✔ │ │────────────────────╮│ │╭ Author Details │ Monitored: │ ✔ │ │────────────────────╮│
@@ -14,8 +14,8 @@ expression: output
│Quality Profile: Standard │ │Quality Profile: Standard │
│Metadata Profile: Standard │ │Metadata Profile: Standard │
│Monitored: Yes │ │Monitored: Yes │
│Overview: some interesting description of the author │ │Overview: some interesting description of the authorHe was born in Madison, Wisconsin: a city he has never really left.His first novel took him │
│ │ │seven years to finish. │
│ │ │ │
│ │ │ │
│╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ │╭ Author Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│